domma-cms 1.10.15 → 1.10.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/admin/css/admin.css +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/index.html +2 -2
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/lib/collection-editor-arrange.js +1 -1
  8. package/admin/js/lib/collection-entries-arrange.js +1 -1
  9. package/admin/js/lib/form-submissions-arrange.js +1 -1
  10. package/admin/js/lib/hotkeys-ui.js +7 -16
  11. package/admin/js/lib/hotkeys.js +1 -1
  12. package/admin/js/lib/media-arrange.js +1 -1
  13. package/admin/js/lib/media-picker.js +17 -5
  14. package/admin/js/lib/media-upload.js +1 -0
  15. package/admin/js/lib/palette-commands.js +1 -0
  16. package/admin/js/templates/collection-entries.html +4 -0
  17. package/admin/js/templates/form-submissions.html +3 -0
  18. package/admin/js/templates/media.html +19 -10
  19. package/admin/js/views/collection-entries.js +7 -7
  20. package/admin/js/views/form-editor.js +6 -6
  21. package/admin/js/views/form-submissions.js +2 -2
  22. package/admin/js/views/media.js +3 -3
  23. package/docs/DommaFeatures.md +5 -4
  24. package/docs/api-reference.md +6 -1
  25. package/docs/configuration.md +57 -10
  26. package/docs/guides/multi-page-forms.md +1 -1
  27. package/docs/markdown-shortcodes.md +3 -2
  28. package/docs/plugin-development.md +3 -0
  29. package/package.json +2 -2
  30. package/public/css/forms.css +1 -1
  31. package/public/css/site.css +1 -1
  32. package/public/js/collection-browser.js +2 -2
  33. package/public/js/collection-range.mjs +1 -0
  34. package/public/js/form-files.mjs +1 -0
  35. package/public/js/form-logic-engine.js +1 -1
  36. package/public/js/forms.js +2 -2
  37. package/public/js/site.js +1 -1
  38. package/server/routes/api/forms.js +211 -60
  39. package/server/routes/api/media.js +25 -2
  40. package/server/server.js +8 -2
  41. package/server/services/collections.js +3 -0
  42. package/server/services/email.js +15 -4
  43. package/server/services/forms.js +11 -3
  44. package/server/services/markdown.js +20 -9
  45. package/server/services/mediaTypes.js +22 -0
  46. package/server/templates/page.html +8 -6
package/public/js/site.js CHANGED
@@ -1 +1 @@
1
- import{applyMobileMenu as ae,initCarouselPopup as se}from"/public/js/mobile-ui.mjs?v=20261016-mobile-options";import{installSmoothCarets as ie}from"/public/js/smooth-caret.js";import{decodeBase64Json as W}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as U,applyColour as G,applyPill as re,applyFloat as ce,applyHighlight as V,applyItemHighlight as le,itemHighlightAttrs as de,isCurrentItem as K,menuAlign as me,applyClasses as P,cssClasses as Y,applyEntrance as J}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{initFloatingMenus as ue}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as Q,markInvalidFields as Z,showValidationPopup as ee}from"/public/js/forms.js?v=20261015-smooth-caret";import{buildWizardSteps as pe}from"/public/js/form-steps.mjs";import{applyFormClasses as te,applySubmitButtons as fe,applyWizardButtons as he}from"/public/js/form-submit.mjs";document.addEventListener("click",p=>{const r=p.target?.closest?.(".dm-banner__dismiss");r&&(p.preventDefault(),r.closest(".dm-banner")?.remove())}),$(()=>{ie(document);const p=window.__CMS_NAV__||{},r=window.__CMS_SITE__||{};if(r.autoTheme?.enabled){let o=function(e){const a=(e||"07:00").split(":");return+a[0]*60+(+a[1]||0)},s=function(){const e=new Date,a=e.getHours()*60+e.getMinutes();return a>=o(n.dayStart)&&a<o(n.nightStart)?n.dayTheme:n.nightTheme},t=function(e){const a=n.apply&&n.apply[e]||{theme:e,custom:""};Domma.theme.set(a.theme);for(const l of[...document.body.classList])l.startsWith("dm-custom-")&&document.body.classList.remove(l);a.custom&&document.body.classList.add(a.custom)};const n=r.autoTheme;t(s()),setInterval(()=>t(s()),6e4)}const x=$("#site-navbar");if(x.length&&p.brand){let s=function(f,u=0){return Array.isArray(f)?f.filter(m=>!(m&&(m.type==="spacer"||m.type==="theme-switcher"))).filter(m=>!(m&&m.type==="separator"&&u===0)).map(m=>m&&m.type==="separator"?{divider:!0}:Array.isArray(m?.items)?{...m,items:s(m.items,u+1)}:m):f};const n={...p.brand},o=n.size&&n.size!=="md"?` navbar-brand-${n.size}`:"";if(n.logo||n.icon||n.tagline){let f="";n.logo?f+=`<img src="${n.logo}" class="navbar-brand-logo" alt="${n.text||""}">`:n.icon&&(f+=`<span data-icon="${n.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),n.text&&(f+=`<span class="navbar-brand-text${o}">${n.text}</span>`),n.tagline&&(f+=`<small class="navbar-brand-tagline">${n.tagline}</small>`),n.html=f}else o&&n.text&&(n.html=`<span class="navbar-brand-text${o}">${n.text}</span>`);const t=s(p.items||[]),e=p.position==="floating",a=p.orientation==="vertical";x.get(0).querySelector(":scope > .cms-nojs-links")?.remove(),Domma.elements.navbar("#site-navbar",{brand:n,items:t,variant:p.variant||"dark",position:e?"static":p.position||"sticky",collapsible:!0,appearOnHover:!!p.appearOnHover}),Domma.icons.scan("#site-navbar");const l=x.get(0);ae(l,p.mobile);const h=()=>{if(!a)return;const u=l.querySelector('a[aria-current="page"]')?.closest(".navbar-dropdown");u&&!u.classList.contains("open")&&u.querySelector(":scope > .navbar-dropdown-toggle")?.click()};l.classList.add(`dm-nav-align-${me(p)||"centre"}`),a&&x.get(0).classList.add("dm-nav-vertical",p.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const i=p.dock||{};a&&!e&&ge(l,p.side==="right"),!a&&i.hideOnScroll&&ye(l),V(l,p),J(l,p),P(l,p.class),e&&(ce(x.get(0),p),ue(x.get(0).parentNode||document));const c=l.querySelector(".navbar-toggle"),g=l.querySelector(".navbar-collapse");if(c&&g){let f=null;const u=()=>{g.classList.contains("show")&&c.click()},m=()=>{f&&(f.remove(),f=null),document.body.classList.remove("dm-drawer-open")},v=()=>{f||(f=document.createElement("div"),f.className="dm-navbar-backdrop",f.addEventListener("click",u),document.body.appendChild(f),document.body.classList.add("dm-drawer-open"))};c.addEventListener("click",()=>{requestAnimationFrame(()=>{g.classList.contains("show")?(h(),v()):m()})}),g.addEventListener("click",C=>{C.target.closest("a")&&requestAnimationFrame(m)}),document.addEventListener("keydown",C=>{C.key==="Escape"&&g.classList.contains("show")&&u()})}const b=l.querySelector(".navbar-nav");if(b){const f=Array.from(b.children);let u=0;for(const m of p.items||[])if(!(m&&m.type==="separator")){if(m&&m.type==="spacer"){const v=document.createElement("li"),C=["xs","sm","md","lg","xl","flex"].includes(m.size)?m.size:"md";v.className=`dm-menu-spacer dm-menu-spacer--${C}`,v.setAttribute("aria-hidden","true"),b.insertBefore(v,f[u]||null);continue}if(m&&m.type==="theme-switcher"){const v=document.createElement("li");v.className="dm-menu-theme-switcher";const C=document.createElement("span");C.dataset.themeSwitcher="menu",C.dataset.position="inline",v.appendChild(C),b.insertBefore(v,f[u]||null);continue}u++}window.CMSThemeSwitcher?.scan(b)}l.querySelectorAll("[data-path]").forEach(f=>{const u=f.dataset.path.split(".").map(Number),m=u.length>1,v=u.reduce((D,q)=>D?.items?.[q],{items:t});if(!v)return;v.colour&&G(f,v.colour),v.pill&&!m&&re(f,v.pill),le(f,v),P(f.closest("li"),v.class);const C=U(v.badge);C&&f.appendChild(C),K(v.url,location.pathname)&&(f.classList.add("active"),f.setAttribute("aria-current","page"))}),h()}const y=$("#site-footer");if(y.length){let e=function(d){return String(d??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},a=function(d){return e(d).replace(/'/g,"&#39;")},l=function(d){if(d&&d.type==="separator")return'<li class="footer-sep"><hr></li>';if(d&&d.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(d.size)?d.size:"md"}" aria-hidden="true"></li>`;if(d&&d.type==="theme-switcher")return'<li class="dm-menu-theme-switcher"><span data-theme-switcher="menu" data-position="inline"></span></li>';const M=a(d.url||"#"),L=e(d.text||""),A=Array.isArray(d.items)&&d.items.length?`<ul>${d.items.map(l).join("")}</ul>`:"",B=de(d),oe=[B.hover&&` data-menu-item-hover="${a(B.hover)}"`,B.active&&` data-menu-item-active="${a(B.active)}"`].filter(Boolean).join(""),ne=B.accent?` style="--dm-menu-highlight:${a(B.accent)}"`:"",X=Y(d.class).join(" ");return`<li${X?` class="${a(X)}"`:""}><a href="${M}"${oe}${ne}>${L}</a>${A}</li>`},i=function(){if(!r.footer)return"";const d=Object.keys(o).filter(L=>n[L]);if(!d.length)return"";const M=Y(r.footer.socialClass).join(" ");return`<div class="footer-social">${d.map(L=>{const{label:A,svg:B}=o[L];return`<a href="${n[L]}" target="_blank" rel="noopener noreferrer" aria-label="${A}" class="footer-social-link${M?` ${a(M)}`:""}">${B}</a>`}).join("")}</div>`},f=function(d){if(!d||d.type==="separator"||d.type==="spacer")return"";if(d.type==="theme-switcher")return`<li class="cms-footer-col">${l(d).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const M=Array.isArray(d.items)&&d.items.length?`<ul>${d.items.map(l).join("")}</ul>`:"",L=b(d)?`<span class="cms-footer-col-title">${e(d.text||"")}</span>`:`<a class="cms-footer-col-title" href="${a(d.url||"#")}">${e(d.text||"")}</a>`,A=["cms-footer-col",...Y(d.class)].join(" ");return`<li class="${a(A)}">${L}${M}</li>`},v=function(d,M,L=!1){return(d||[]).forEach(A=>{A&&(A.type==="separator"||A.type==="spacer"||A.type==="theme-switcher")||(L&&b(A)||M.push(A),Array.isArray(A.items)&&A.items.length&&v(A.items,M))}),M};const n=r.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let s='<div class="footer-inner container">';const t=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},h=(()=>{const d=r.cookieConsent;if(!d||d.showSettingsLink===!1)return"";const M=L=>(L||[]).some(A=>A&&(A.url==="#cookie-settings"||M(A.items)));return M(t.primary)||M(t.legal)?"":`<li class="cms-cookie-settings"><a href="#cookie-settings">${e(d.settingsLinkText||"Cookie settings")}</a></li>`})(),c=["columns","row"].includes(r.footer?.layout)?r.footer.layout:"auto",g=c==="columns"||c==="auto"&&t.primary.some(d=>Array.isArray(d?.items)&&d.items.length),b=d=>g&&!d.url&&Array.isArray(d.items)&&d.items.length>0;if(g){const d=p.brand||{},M=d.text||r.title||"",L=d.logo?`<img src="${a(d.logo)}" class="cms-footer-brand-logo" alt="">`:d.icon?'<span class="cms-footer-brand-icon"></span>':"",A=String(r.footer?.blurb||"").trim();s='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',s+=`<div class="cms-footer-brand">${M||L?`<a class="cms-footer-brand-name" href="${a(d.url||"/")}">${L}<span>${e(M)}</span></a>`:""}${A?`<p class="cms-footer-blurb">${e(A)}</p>`:""}${i()}</div>`,t.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${t.primary.map(f).join("")}</ul></nav>`),s+='</div><div class="cms-footer-bottom">',t.copyright&&(s+=`<p class="cms-footer-copyright">${e(t.copyright)}</p>`),(t.legal.length||h)&&(s+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${t.legal.map(l).join("")}${h}</ul></nav>`),s+="</div>"}else t.copyright&&(s+=`<p>${e(t.copyright)}</p>`),t.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary"><ul>${t.primary.map(l).join("")}</ul></nav>`),(t.legal.length||h)&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${t.legal.map(l).join("")}${h}</ul></nav>`),s+=i();s+="</div>",y.get(0).querySelector(":scope > .cms-nojs-links")?.remove();const u=y.get(0).querySelector(":scope > .cms-footer-blocks");u?.remove(),y.html(s),u&&y.get(0).prepend(u),window.CMSThemeSwitcher?.scan(y.get(0));const m=y.get(0).querySelector(".cms-footer-brand-icon");m&&p.brand?.icon&&m.setAttribute("data-icon",p.brand.icon),P(y.get(0),r.footer?.class);const C=[...v(t.primary,[],!0),...v(t.legal,[])];y.get(0).querySelectorAll(".footer-nav a").forEach((d,M)=>{const L=C[M];if(!L)return;L.colour&&G(d,L.colour);const A=U(L.badge);A&&d.appendChild(A),K(L.url,location.pathname)&&(d.classList.add("is-active"),d.setAttribute("aria-current","page"))}),V(y.get(0).querySelector(".footer-nav--primary"),{highlight:t.primaryHighlight}),V(y.get(0).querySelector(".footer-nav--legal"),{highlight:t.legalHighlight}),J(y.get(0).querySelector(".footer-nav--primary"),{entrance:t.primaryEntrance}),J(y.get(0).querySelector(".footer-nav--legal"),{entrance:t.legalEntrance}),P(y.get(0).querySelector(".footer-nav--primary"),t.primaryClass),P(y.get(0).querySelector(".footer-nav--legal"),t.legalClass);const q=r.footer?.showMotionToggle!==!1,z=S.get("reduced_motion"),j=r.footer?.motionDefault,I=z!=null?!!z:j==="on"?!0:j==="off"?!1:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),N=r.footer?.motionControl==="checkbox",O=y.get(0).querySelector(".cms-footer-bottom")||y.get(0).querySelector(".footer-inner");if(q&&O){const d=document.createElement("input");d.type="checkbox",d.className=N?"footer-motion-checkbox":"form-switch-input",d.id="dm-motion-switch",d.checked=I,d.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const M=document.createElement("span");M.className=N?"footer-motion-label":"form-switch-label",M.textContent="Reduce motion";const L=document.createElement("label");L.className=N?"footer-motion-check":"form-switch footer-motion-switch",L.title="Reduce motion",L.appendChild(d),L.appendChild(M),O.appendChild(L)}}const k=document.getElementById("site-sidebar");k&&ve(k),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const w=$(".page-body");if(w.length){w.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),w.find(".tabs").each(function(){Domma.elements.tabs(this)}),w.find(".carousel").each(function(){const n={primary:"--dm-primary",secondary:"--dm-secondary",accent:"--dm-primary",text:"--dm-text",surface:"--dm-surface",background:"--dm-background",success:"--dm-success",warning:"--dm-warning",danger:"--dm-danger",info:"--dm-info"},o=h=>{const i=String(h||"").trim();return i?n[i.toLowerCase()]?`var(${n[i.toLowerCase()]})`:CSS.supports("color",i)?i:"":""},s=o(this.dataset.background),t=o(this.dataset.arrowBackground),e=o(this.dataset.arrowColor);s&&(this.style.setProperty("--dm-carousel-background",s),this.classList.add("dm-carousel-custom-background")),t&&this.style.setProperty("--dm-carousel-arrow-background",t),e&&this.style.setProperty("--dm-carousel-arrow-color",e);const a=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${a}`);const l={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide",heightMode:this.dataset.heightMode==="average"?"average":"crop"};if(this.dataset.height){const h=Number(this.dataset.height);Number.isFinite(h)&&h>0&&(l.height=h)}if(this.dataset.animationDuration){const h=parseInt(this.dataset.animationDuration,10);Number.isFinite(h)&&(l.animationDuration=h)}this.dataset.animationEasing&&(l.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(h=>h.cloneNode(!0)),this._dmCarouselOpts=l),this._dmCarousel=Domma.elements.carousel(this,l),se(this)}),w.find(".dm-countdown").each(function(){const n={autoStart:!0},o=String(this.dataset.format||"").trim(),s=String(this.dataset.to||"").trim(),t=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s),e=t?new Date(+t[1],+t[2]-1,+t[3]).getTime():s?new Date(s).getTime():NaN;Number.isFinite(e)?n.duration=Math.max(0,e-Date.now()):this.dataset.duration&&(n.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const a=/^dd:/i.test(o);n.format=a?"hh:mm:ss":o||(n.duration>=36e5?"hh:mm:ss":"mm:ss");const l=this,h=c=>String(c).padStart(2,"0"),i=c=>{const g=Math.max(0,Math.floor(c/1e3)),b=l.querySelector(".dm-timer-time");b&&(b.textContent=`${h(Math.floor(g/86400))}:${h(Math.floor(g%86400/3600))}:${h(Math.floor(g%3600/60))}:${h(g%60)}`)};a&&(n.onTick=i),Domma.elements.timer(this,n),a&&i(n.duration||0)}),w.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),w.find(".dm-progression").each(function(){const n=Array.from(this.querySelectorAll(".dm-progression-item")).map(t=>{const e={title:t.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:t.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return t.dataset.date&&(e.date=t.dataset.date),t.dataset.status&&(e.status=t.dataset.status),t.dataset.eventIcon&&(e.icon=t.dataset.eventIcon),t.dataset.entryId&&(e._entryId=t.dataset.entryId),e}),o=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",s=Domma.elements.progression(this,{layout:o,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...n.length?{items:n}:{}});if((this.dataset.mode||"timeline")==="timeline"){const t={completed:{label:"Completed",icon:"check-circle",tone:"success"},planned:{label:"Planned",icon:"circle",tone:"secondary"},"in-progress":{label:"In progress",icon:"clock",tone:"primary"},blocked:{label:"Blocked",icon:"alert-circle",tone:"danger"}};this.querySelectorAll(".dm-progression-item").forEach((e,a)=>{const l=n[a];if(!l)return;const h=e.querySelector(".dm-progression-title");if(l.icon&&h){const c=document.createElement("span");c.dataset.icon=l.icon,c.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",c.setAttribute("aria-hidden","true"),h.prepend(c)}const i=t[l.status];if(i&&h){const c=document.createElement("span");c.className=`badge badge-${i.tone} cms-timeline-status`,c.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const g=document.createElement("span");g.dataset.icon=i.icon,g.dataset.iconSize="14",g.setAttribute("aria-hidden","true"),c.append(g,document.createTextNode(i.label)),h.after(c)}})}Domma.icons.scan(this),n.length&&(this._dmCtx={instance:s,items:n})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(n=>{const o=n.dataset.revealMode||"stagger",s=n.dataset.revealAnimation||"slide-up",t=parseInt(n.dataset.revealDuration,10)||400,e=parseInt(n.dataset.revealStagger,10)||60,a=parseInt(n.dataset.revealDelay,10)||0,l=n.dataset.revealDirection==="rtl",h=Array.from(n.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};h.forEach((c,g)=>{c.style.opacity="0",c.style.transform=i[s]||"",c.style.transition=`opacity ${t}ms ease, transform ${t}ms ease`;const b=l?h.length-1-g:g;c.style.transitionDelay=o==="stagger"?`${a+b*e}ms`:`${a}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const c=new IntersectionObserver(g=>{g.forEach(b=>{b.isIntersecting&&(b.target.offsetWidth,b.target.style.opacity="1",b.target.style.transform="none",c.unobserve(b.target))})},{threshold:.1});h.forEach(g=>c.observe(g))}))}),w.find(".card[data-collapsible]").each(function(){const n=this.querySelector(".card-header");n&&n.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),w.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const n=this.dataset.soTarget,o=document.getElementById(n);if(!o)return;const s=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",s.setContent(o),s.open()})})}if(typeof $.setup=="function"){const n=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const s=W(o.dataset.config);Object.assign(n,s)}catch{}}),Object.keys(n).length>0){const o={};for(const[s,t]of Object.entries(n)){const e=t?.events?.click,{confirm:a,toast:l,alert:h,prompt:i,...c}=e||{};a||l||h||i?($(s).on("click",async function(b){if(b.preventDefault(),a&&!await E.confirm(a))return;let f=null;if(!(i&&(f=await E.prompt(i,{inputPlaceholder:c.promptPlaceholder||"",inputValue:c.promptDefault||""}),f===null))){if(c.target){const u=$(c.target);c.toggleClass&&u.toggleClass(c.toggleClass),c.addClass&&u.addClass(c.addClass),c.removeClass&&u.removeClass(c.removeClass),f!==null&&(c.setText&&u.text(f),c.setVal&&u.val(f),c.setAttr&&u.attr(c.setAttr,f))}c.href&&(window.location.href=c.href),l&&E.toast(l,{type:c.toastType||"success"}),h&&E.alert(h)}}),Object.keys(c).length&&(o[s]={...t,events:{...t.events,click:c}})):o[s]=t}$.setup(o)}}w.length&&R(w.get(0))}),window.wireCTAButtons=R;function R(p){p.querySelectorAll(".dm-cta-trigger").forEach(r=>{r.__ctaWired||(r.__ctaWired=!0,r.addEventListener("click",async()=>{const x=r.dataset.action,y=r.dataset.entry,k=r.dataset.confirm;let w=S.get("auth_token");if(!w){E.toast("Please log in to perform this action.",{type:"warning"});return}if(k&&!await E.confirm(k))return;const n=Array.from(r.childNodes).map(s=>s.cloneNode(!0));r.disabled=!0,r.textContent="Running\u2026";const o=s=>fetch(`/api/actions/${x}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(r.dataset.scope?{entryId:y,scope:r.dataset.scope}:{entryId:y})});try{let s=await o(w);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const a=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(a.ok){const{token:l}=await a.json();S.set("auth_token",l),w=l,s=await o(w)}}}const t=await s.json().catch(()=>({}));if(!s.ok||t.success===!1)throw new Error(t.error||t.message||`Error ${s.status}`);E.toast(t.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{r.disabled=!1,r.textContent="",n.forEach(s=>r.appendChild(s)),Domma.icons.scan(r)}}))})}(function(){const r=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!r.length)return;const x=S.get("auth_token");function y(w,n){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(n){s.appendChild(document.createTextNode("Please "));const t=document.createElement("a");t.href="/login",t.textContent="sign in",s.appendChild(t),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=w;return o.appendChild(s),o}function k(w,n){const o=document.createElement("template");o.innerHTML=n||"",w.replaceWith(o.content)}r.forEach(async w=>{const n=w.dataset.collectionAttrs;if(!n)return;if(!x){w.replaceChildren(y("",!0));return}const o=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:n})});try{let s=x,t=await o(s);if(t.status===401){const a=S.get("auth_refresh_token");if(a){const l=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:a})});if(l.ok){const{token:h}=await l.json();S.set("auth_token",h),s=h,t=await o(s)}}}if(!t.ok){const{error:a}=await t.json().catch(()=>({}));w.replaceChildren(y(a||"Unable to load entries.",!1));return}const{html:e}=await t.json();k(w,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{w.replaceChildren(y("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),R(document.body)},100)})(),(function(){const r=document.querySelectorAll("[data-collection-table]");!r.length||typeof T>"u"||!T.create||r.forEach(x=>{let y;try{y=W(x.dataset.payload)}catch{return}const{columns:k,rows:w,search:n,sortable:o,exportable:s,pageSize:t,empty:e,ctaConfig:a,slug:l,exportCfg:h}=y;if(!k?.length)return;if(a){const g=k.findIndex(b=>b.key==="_cta");g!==-1&&(k[g]={key:"_cta",title:"",render:(b,f)=>{const u=document.createElement("button");if(u.className=`btn btn-${a.style||"primary"} dm-cta-trigger`,u.dataset.action=a.action||"",a.scope&&(u.dataset.scope=a.scope),u.dataset.entry=f._entryId||"",a.confirm&&(u.dataset.confirm=a.confirm),a.icon){const m=document.createElement("span");m.dataset.icon=a.icon,u.appendChild(m),u.appendChild(document.createTextNode(" "))}return u.appendChild(document.createTextNode(a.label||"Run")),u}})}const i="col-table-"+Math.random().toString(36).slice(2,7),c=document.createElement("div");c.id=i,x.replaceChildren(c),T.create("#"+i,{data:w,columns:k,search:n,sortable:o,exportable:s,pageSize:t,emptyMessage:e}),a&&R(c),x._dmTable={columns:k,rows:w,slug:l,exportCfg:h,render(g){T.create("#"+i,{data:g,columns:k,search:n,sortable:o,exportable:s,pageSize:t,emptyMessage:e}),a&&R(c)}}})})(),(function(){const r=document.querySelectorAll("[data-form-inline]");if(!r.length||typeof F>"u")return;function x(o){const s=o&&o.logic;return!!(s&&(s.visibility||s.requirement))}function y(o,s){const t={};return(o||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const a=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),l=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":a?"chooser":e.type,h={...e.formConfig||{}};h.span==="full"&&s&&(h.span=s);const i={type:l,required:x(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...h}};e.minLength!==void 0&&(i.minLength=e.minLength),e.maxLength!==void 0&&(i.maxLength=e.maxLength),e.min!==void 0&&(i.min=e.min),e.max!==void 0&&(i.max=e.max),a&&(e.variant&&(i.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(i.multiple=!0),e.density&&(i.density=e.density),e.columns&&(i.columns=e.columns),e.accent&&(i.accent=e.accent),e.accentStyle&&(i.accentStyle=e.accentStyle),e.glow&&(i.glow=!0),e.glowColour&&(i.glowColour=e.glowColour),e.shadow&&(i.shadow=e.shadow),e.shadowColour&&(i.shadowColour=e.shadowColour)),t[e.name]=i}),t}function k(o){const s={};return(o||[]).forEach(t=>{if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(s[t.name]=typeof e=="object"?e.value:e)}}),s}function w(o,s){(s||[]).forEach(t=>{if(t.type!=="date"||!t.name)return;const e=o.querySelector(`[name="${t.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function n(o,s,t){let e=o.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",o.appendChild(e)),e.textContent=s,e.style.cssText=t?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}r.forEach(o=>{let s;try{s=W(o.dataset.formInline)}catch{return}o.querySelector(":scope > .cms-form-embed-summary")?.remove();const t=s.fields||[],e=s.settings||{},a=Number(e.columns)===3?3:2,l=e.layout||"grid",h=t.some(u=>u.type==="page-break"),i=async u=>{if(o.dataset.triggerBlocked==="true"){const m=o._formLogicRuntime,v=m&&m.submitBlock?m.submitBlock().reasons:[];n(o,v.join(" ")||"This form cannot be submitted yet.",!0);const C=new Error("Blocked by a form trigger");throw C.formSubmitFailed=!0,C}try{const m=o.querySelector(".fb-form-honeypot"),v=m?.querySelector('input[type="text"]')||null,C=m?.querySelector('[name="_t"]')||null,D=Object.assign({},u);delete D._t,m?.dataset.hpName&&delete D[m.dataset.hpName],v!==null&&(D._hp=v.value),C!==null&&(D._t=C.value);const q=await H.post(`/api/forms/submit/${s.slug}`,D);if(q&&q.error){n(o,q.error,!0);const z=new Error(q.error);throw z.formSubmitFailed=!0,z}if(q?.redirect){window.location.href=q.redirect;return}for(;o.firstChild;)o.removeChild(o.firstChild);n(o,q?.message||e.successMessage||"Thank you for your submission.",!1)}catch(m){throw m.formSubmitFailed||n(o,m.message||"Submission failed. Please try again.",!0),m}},c=Date.now();function g(u){const m=u.querySelector("form");if(!m||m.querySelector(".fb-form-honeypot"))return;const v=document.createElement("div");v.className="fb-form-honeypot",v.setAttribute("aria-hidden","true");const C=document.createElement("input");C.name=m.querySelector('[name="website"]')?"_website":"website",v.dataset.hpName=C.name,C.type="text",C.tabIndex=-1,C.autocomplete="url",C.placeholder="https://",v.appendChild(C);const D=document.createElement("input");D.name="_t",D.type="hidden",D.value=c,v.appendChild(D),m.appendChild(v)}function b(u){if(!window.FormLogicEngine||!t.some(v=>v.logic||Array.isArray(v.triggers)&&v.triggers.length))return null;const m=new window.FormLogicEngine.FormLogicRuntime(s,o,u?{baseValues:u}:void 0);if(m.init(),o._formLogicRuntime=m,o._formLogicObserved)return m;if(o._formLogicObserved=!0,o.parentNode&&typeof MutationObserver<"u"){const v=new MutationObserver(function(C){for(const D of C)for(const q of D.removedNodes)if(q===o||q.nodeType===1&&q.contains&&q.contains(o)){o._formLogicRuntime&&o._formLogicRuntime.destroy(),v.disconnect();return}});v.observe(o.parentNode,{childList:!0,subtree:!1}),o._formLogicObserver=v}return m}let f=!1;if(o.addEventListener("dm-form-end",u=>{const m=u.detail?.validation;if(m&&!m.valid){Z(o,m.missing),ee(m);return}f||(f=!0,i(u.detail?.values||{}).catch(()=>{}).finally(()=>{f=!1}))}),h&&F.wizard){const u=pe(t,s.title),m=u.map(I=>({title:I.title,description:I.description,fields:y(I.fields,a)})),v=u.map(I=>I.fields);let C=0,D=null,q=null;const z=I=>{const N=window.FormLogicEngine;if(!N||!N.validateFields)return;const O=N.validateFields(o,v[C]||[]);O.valid||(I.preventDefault(),I.stopImmediatePropagation(),Z(o,O.missing),ee(O))},j=()=>{const I=v[C]||t;he(o,e),te(o,e,I),w(o,I),Q(o,I),e.honeypot!==!1&&g(o),D&&D.destroy(),D=b(q?.getStepData?q.getStepData():void 0),o.querySelectorAll(".wizard-next, .wizard-submit").forEach(N=>N.addEventListener("click",z,!0))};q=F.wizard(o,{schema:{steps:m},layout:l,columns:a,finishText:e.submitText||"Finish",onSubmit:i,onStepChange:I=>{C=I,setTimeout(j,0)}}),o.addEventListener("dm-form-trigger",I=>{if(I.detail?.type!=="goto-step"||!q?.goToStep)return;const N=Number(I.detail.action.step);!Number.isFinite(N)||N<0||N>=m.length||N!==C&&q.goToStep(N)}),Promise.resolve(q).then(j)}else if(F.render){const u=F.render(o,y(t,a),k(t),{submitText:e.submitText||"Submit",layout:l,columns:a,onSubmit:i});Promise.resolve(u).then(function(){fe(o,e,l),te(o,e,t),w(o,t),Q(o,t),e.honeypot!==!1&&g(o),b()})}})})();function ge(p,r){const x=document.body,y=x.classList,k=new Set((x.dataset.railSite||"").split(" ").filter(Boolean)),w=()=>window.matchMedia("(min-width: 993px)").matches;p.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(a=>{const l=a.textContent.trim();l&&(a.setAttribute("title",l),a.querySelector("svg, [data-icon]")||(a.dataset.initial=l.charAt(0).toUpperCase()))});const n=document.createElement("button");n.type="button",n.className="dm-nav-collapse-toggle";const o=()=>{const a=y.contains("dm-vnav-collapsed");n.setAttribute("aria-pressed",String(a)),n.setAttribute("aria-label",a?"Expand menu":"Collapse menu"),n.title=n.getAttribute("aria-label");const l=r?"chevrons-left":"chevrons-right",h=r?"chevrons-right":"chevrons-left";n.innerHTML=`<span data-icon="${a?l:h}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(n)},s=a=>{y.toggle("dm-vnav-collapsed",a),S.set("nav_collapsed",a),o()};n.addEventListener("click",()=>s(!y.contains("dm-vnav-collapsed"))),o();const t=p.querySelector(".navbar-container")||p;t.insertBefore(n,t.firstChild?.nextSibling||null);const e=a=>{y.toggle("dm-vnav-autohide",a),y.toggle("dm-vnav-collapsible",!a&&k.has("collapsible")),a?y.remove("dm-vnav-collapsed"):k.has("collapsible")&&S.get("nav_collapsed")===!0&&y.add("dm-vnav-collapsed"),p.classList.remove("dm-nav-revealed"),o()};p.addEventListener("pointerdown",a=>{y.contains("dm-vnav-autohide")&&a.pointerType!=="mouse"&&!p.classList.contains("dm-nav-revealed")&&(p.classList.add("dm-nav-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{p.contains(a.target)||p.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(p,{items:()=>{const a=y.contains("dm-vnav-autohide"),l=y.contains("dm-vnav-collapsed"),h=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:l?"Expand menu":"Collapse to icons",icon:l?"maximize":"minimize",disabled:!w()||a||!k.has("collapsible"),action:()=>s(!l)},{label:a?"Keep on screen":"Hide when not in use",icon:a?"eye":"eye-off",disabled:!w(),action:()=>{S.set("nav_autohide",!a),e(!a)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!h,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(k.has("autohide")),y.toggle("dm-vnav-collapsed",!k.has("autohide")&&k.has("start-collapsed")),o()}}]}})}function ye(p){p.classList.add("dm-nav-scroll-hide");let r=window.scrollY,x=!1;window.addEventListener("scroll",()=>{x||(x=!0,requestAnimationFrame(()=>{x=!1;const y=window.scrollY,k=p.matches(":focus-within")||p.querySelector(".navbar-dropdown.open, .navbar-collapse.show");y>r&&y>p.offsetHeight&&!k?p.classList.add("dm-nav-scrolled-away"):y<r&&p.classList.remove("dm-nav-scrolled-away"),r=y}))},{passive:!0})}function ve(p){let r={};try{r=JSON.parse(p.dataset.contents||"{}")}catch{}const x=p.closest(".site-main"),y=document.querySelector(".site-content"),k=new Set(String(r.exclude||"").split(",").map(i=>i.trim().toLowerCase()).filter(Boolean)),w=i=>!i.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),n=i=>(y?[...y.querySelectorAll(i)]:[]).filter(w).filter(c=>!k.has(c.textContent.trim().toLowerCase())),o=r.headings==="h2"?n("h2"):r.headings==="h3"?n("h3"):r.headings==="h2h3"?n("h2, h3"):n("h2").length?n("h2"):n("h3");if(!o.length){x?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(i=>i.id)),t=document.createElement("nav");t.className="dm-contents",t.setAttribute("aria-label",r.title||"Page contents"),r.hover&&(t.dataset.menuHover=r.hover),r.selected&&(t.dataset.menuActive=r.selected),r.accent&&t.style.setProperty("--dm-menu-highlight",r.accent.startsWith("#")?r.accent:`var(--dm-${r.accent})`),r.font&&(t.style.fontFamily=r.font),r.fontSize&&(t.style.fontSize=r.fontSize),r.fontWeight&&(t.style.fontWeight=r.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const a=document.createElement("p");a.className="dm-contents-title",a.textContent=r.title||"",e.appendChild(a),t.appendChild(e);const l=document.createElement("ol");l.className="dm-contents-list"+(r.numbered?" is-numbered":"");const h=[];for(const i of o){if(!i.id){const b=(i.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let f=b;for(let u=2;s.has(f);u++)f=`${b}-${u}`;s.add(f),i.id=f}const c=document.createElement("li");c.className=`dm-contents-item dm-contents-${i.tagName.toLowerCase()}`;const g=document.createElement("a");g.href=`#${i.id}`,g.textContent=i.textContent.trim(),c.appendChild(g),l.appendChild(c),h.push([i,g])}if(t.appendChild(l),p.replaceChildren(t),r.scrollSpy!==!1&&"IntersectionObserver"in window){const i=new Map,c=()=>{const b=h.find(([f])=>i.get(f))||null;h.forEach(([,f])=>{const u=b&&f===b[1];f.classList.toggle("is-active",!!u),u?f.setAttribute("aria-current","location"):f.removeAttribute("aria-current")})},g=new IntersectionObserver(b=>{b.forEach(f=>i.set(f.target,f.isIntersecting)),c()},{rootMargin:"-80px 0px -60% 0px"});h.forEach(([b])=>g.observe(b))}if(x?.classList.contains("contents-collapsible")){const i=document.createElement("button");i.type="button",i.className="dm-contents-toggle";const c=()=>{const g=x.classList.contains("contents-collapsed");i.setAttribute("aria-expanded",String(!g)),i.setAttribute("aria-label",g?"Show the contents":"Hide the contents"),i.title=i.getAttribute("aria-label"),i.innerHTML=`<span data-icon="${g?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(i)};i.addEventListener("click",()=>{S.set("contents_collapsed",x.classList.toggle("contents-collapsed")),c()}),c(),e.insertBefore(i,e.firstChild)}if(x?.classList.contains("contents-floating")&&r.movable){t.classList.add("is-movable");const i=S.get("contents_position");i&&Number.isFinite(i.x)&&Object.assign(p.style,{left:`${i.x}px`,top:`${i.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",c=>{if(c.target.closest("button"))return;const g=p.getBoundingClientRect(),b=c.clientX-g.left,f=c.clientY-g.top,u=v=>{const C=Math.max(0,Math.min(window.innerWidth-g.width,v.clientX-b)),D=Math.max(0,Math.min(window.innerHeight-40,v.clientY-f));Object.assign(p.style,{left:`${C}px`,top:`${D}px`,right:"auto",bottom:"auto"})},m=()=>{document.removeEventListener("pointermove",u),S.set("contents_position",{x:parseInt(p.style.left,10),y:parseInt(p.style.top,10)})};document.addEventListener("pointermove",u),document.addEventListener("pointerup",m,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(p,{items:()=>{const i=x?.classList.contains("contents-collapsible"),c=x?.classList.contains("contents-collapsed"),g=h.find(([,b])=>b.classList.contains("is-active"));return[{header:"Contents"},...i?[{label:c?"Show the contents":"Collapse to a button",icon:c?"list":"minimize",action:()=>p.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!g,action:async()=>{const b=`${location.origin}${location.pathname}#${g[0].id}`;try{await _.copyToClipboard(b),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),x?.classList.contains("contents-autohide")&&(p.addEventListener("pointerdown",i=>{i.pointerType!=="mouse"&&!p.classList.contains("is-revealed")&&(p.classList.add("is-revealed"),i.preventDefault())}),document.addEventListener("pointerdown",i=>{p.contains(i.target)||p.classList.remove("is-revealed")}))}
1
+ import{applyMobileMenu as se,initCarouselPopup as ie}from"/public/js/mobile-ui.mjs?v=20261016-mobile-options";import{installSmoothCarets as re}from"/public/js/smooth-caret.js";import{decodeBase64Json as W}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as U,applyColour as G,applyPill as ce,applyFloat as le,applyHighlight as V,applyItemHighlight as de,itemHighlightAttrs as me,isCurrentItem as K,menuAlign as ue,applyClasses as P,cssClasses as Y,applyEntrance as J}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{initFloatingMenus as pe}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as Q,markInvalidFields as Z,showValidationPopup as ee}from"/public/js/forms.js?v=20261025-form-dropzone";import{buildWizardSteps as fe}from"/public/js/form-steps.mjs";import{applyFormClasses as te,applySubmitButtons as he,applyWizardButtons as ge}from"/public/js/form-submit.mjs";import{captureFileFields as oe,destroyDropzones as ye,fileBlueprint as ve,restoreFileFields as be,splitFiles as we,submitBody as Ce,wireFileFields as Se}from"/public/js/form-files.mjs";document.addEventListener("click",m=>{const c=m.target?.closest?.(".dm-banner__dismiss");c&&(m.preventDefault(),c.closest(".dm-banner")?.remove())}),$(()=>{re(document);const m=window.__CMS_NAV__||{},c=window.__CMS_SITE__||{};if(c.autoTheme?.enabled){let o=function(e){const a=(e||"07:00").split(":");return+a[0]*60+(+a[1]||0)},s=function(){const e=new Date,a=e.getHours()*60+e.getMinutes();return a>=o(n.dayStart)&&a<o(n.nightStart)?n.dayTheme:n.nightTheme},t=function(e){const a=n.apply&&n.apply[e]||{theme:e,custom:""};Domma.theme.set(a.theme);for(const d of[...document.body.classList])d.startsWith("dm-custom-")&&document.body.classList.remove(d);a.custom&&document.body.classList.add(a.custom)};const n=c.autoTheme;t(s()),setInterval(()=>t(s()),6e4)}const x=$("#site-navbar");if(x.length&&m.brand){let s=function(u,y=0){return Array.isArray(u)?u.filter(h=>!(h&&(h.type==="spacer"||h.type==="theme-switcher"))).filter(h=>!(h&&h.type==="separator"&&y===0)).map(h=>h&&h.type==="separator"?{divider:!0}:Array.isArray(h?.items)?{...h,items:s(h.items,y+1)}:h):u};const n={...m.brand},o=n.size&&n.size!=="md"?` navbar-brand-${n.size}`:"";if(n.logo||n.icon||n.tagline){let u="";n.logo?u+=`<img src="${n.logo}" class="navbar-brand-logo" alt="${n.text||""}">`:n.icon&&(u+=`<span data-icon="${n.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),n.text&&(u+=`<span class="navbar-brand-text${o}">${n.text}</span>`),n.tagline&&(u+=`<small class="navbar-brand-tagline">${n.tagline}</small>`),n.html=u}else o&&n.text&&(n.html=`<span class="navbar-brand-text${o}">${n.text}</span>`);const t=s(m.items||[]),e=m.position==="floating",a=m.orientation==="vertical";x.get(0).querySelector(":scope > .cms-nojs-links")?.remove(),Domma.elements.navbar("#site-navbar",{brand:n,items:t,variant:m.variant||"dark",position:e?"static":m.position||"sticky",collapsible:!0,appearOnHover:!!m.appearOnHover}),Domma.icons.scan("#site-navbar");const d=x.get(0);se(d,m.mobile);const p=()=>{if(!a)return;const y=d.querySelector('a[aria-current="page"]')?.closest(".navbar-dropdown");y&&!y.classList.contains("open")&&y.querySelector(":scope > .navbar-dropdown-toggle")?.click()};d.classList.add(`dm-nav-align-${ue(m)||"centre"}`),a&&x.get(0).classList.add("dm-nav-vertical",m.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const i=m.dock||{};a&&!e&&xe(d,m.side==="right"),!a&&i.hideOnScroll&&ke(d),V(d,m),J(d,m),P(d,m.class),e&&(le(x.get(0),m),pe(x.get(0).parentNode||document));const r=d.querySelector(".navbar-toggle"),g=d.querySelector(".navbar-collapse");if(r&&g){let u=null;const y=()=>{g.classList.contains("show")&&r.click()},h=()=>{u&&(u.remove(),u=null),document.body.classList.remove("dm-drawer-open")},f=()=>{u||(u=document.createElement("div"),u.className="dm-navbar-backdrop",u.addEventListener("click",y),document.body.appendChild(u),document.body.classList.add("dm-drawer-open"))};r.addEventListener("click",()=>{requestAnimationFrame(()=>{g.classList.contains("show")?(p(),f()):h()})}),g.addEventListener("click",w=>{w.target.closest("a")&&requestAnimationFrame(h)}),document.addEventListener("keydown",w=>{w.key==="Escape"&&g.classList.contains("show")&&y()})}const b=d.querySelector(".navbar-nav");if(b){const u=Array.from(b.children);let y=0;for(const h of m.items||[])if(!(h&&h.type==="separator")){if(h&&h.type==="spacer"){const f=document.createElement("li"),w=["xs","sm","md","lg","xl","flex"].includes(h.size)?h.size:"md";f.className=`dm-menu-spacer dm-menu-spacer--${w}`,f.setAttribute("aria-hidden","true"),b.insertBefore(f,u[y]||null);continue}if(h&&h.type==="theme-switcher"){const f=document.createElement("li");f.className="dm-menu-theme-switcher";const w=document.createElement("span");w.dataset.themeSwitcher="menu",w.dataset.position="inline",f.appendChild(w),b.insertBefore(f,u[y]||null);continue}y++}window.CMSThemeSwitcher?.scan(b)}d.querySelectorAll("[data-path]").forEach(u=>{const y=u.dataset.path.split(".").map(Number),h=y.length>1,f=y.reduce((A,I)=>A?.items?.[I],{items:t});if(!f)return;f.colour&&G(u,f.colour),f.pill&&!h&&ce(u,f.pill),de(u,f),P(u.closest("li"),f.class);const w=U(f.badge);w&&u.appendChild(w),K(f.url,location.pathname)&&(u.classList.add("active"),u.setAttribute("aria-current","page"))}),p()}const v=$("#site-footer");if(v.length){let e=function(l){return String(l??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},a=function(l){return e(l).replace(/'/g,"&#39;")},d=function(l){if(l&&l.type==="separator")return'<li class="footer-sep"><hr></li>';if(l&&l.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(l.size)?l.size:"md"}" aria-hidden="true"></li>`;if(l&&l.type==="theme-switcher")return'<li class="dm-menu-theme-switcher"><span data-theme-switcher="menu" data-position="inline"></span></li>';const D=a(l.url||"#"),L=e(l.text||""),q=Array.isArray(l.items)&&l.items.length?`<ul>${l.items.map(d).join("")}</ul>`:"",j=me(l),ne=[j.hover&&` data-menu-item-hover="${a(j.hover)}"`,j.active&&` data-menu-item-active="${a(j.active)}"`].filter(Boolean).join(""),ae=j.accent?` style="--dm-menu-highlight:${a(j.accent)}"`:"",X=Y(l.class).join(" ");return`<li${X?` class="${a(X)}"`:""}><a href="${D}"${ne}${ae}>${L}</a>${q}</li>`},i=function(){if(!c.footer)return"";const l=Object.keys(o).filter(L=>n[L]);if(!l.length)return"";const D=Y(c.footer.socialClass).join(" ");return`<div class="footer-social">${l.map(L=>{const{label:q,svg:j}=o[L];return`<a href="${n[L]}" target="_blank" rel="noopener noreferrer" aria-label="${q}" class="footer-social-link${D?` ${a(D)}`:""}">${j}</a>`}).join("")}</div>`},u=function(l){if(!l||l.type==="separator"||l.type==="spacer")return"";if(l.type==="theme-switcher")return`<li class="cms-footer-col">${d(l).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const D=Array.isArray(l.items)&&l.items.length?`<ul>${l.items.map(d).join("")}</ul>`:"",L=b(l)?`<span class="cms-footer-col-title">${e(l.text||"")}</span>`:`<a class="cms-footer-col-title" href="${a(l.url||"#")}">${e(l.text||"")}</a>`,q=["cms-footer-col",...Y(l.class)].join(" ");return`<li class="${a(q)}">${L}${D}</li>`},f=function(l,D,L=!1){return(l||[]).forEach(q=>{q&&(q.type==="separator"||q.type==="spacer"||q.type==="theme-switcher")||(L&&b(q)||D.push(q),Array.isArray(q.items)&&q.items.length&&f(q.items,D))}),D};const n=c.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let s='<div class="footer-inner container">';const t=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},p=(()=>{const l=c.cookieConsent;if(!l||l.showSettingsLink===!1)return"";const D=L=>(L||[]).some(q=>q&&(q.url==="#cookie-settings"||D(q.items)));return D(t.primary)||D(t.legal)?"":`<li class="cms-cookie-settings"><a href="#cookie-settings">${e(l.settingsLinkText||"Cookie settings")}</a></li>`})(),r=["columns","row"].includes(c.footer?.layout)?c.footer.layout:"auto",g=r==="columns"||r==="auto"&&t.primary.some(l=>Array.isArray(l?.items)&&l.items.length),b=l=>g&&!l.url&&Array.isArray(l.items)&&l.items.length>0;if(g){const l=m.brand||{},D=l.text||c.title||"",L=l.logo?`<img src="${a(l.logo)}" class="cms-footer-brand-logo" alt="">`:l.icon?'<span class="cms-footer-brand-icon"></span>':"",q=String(c.footer?.blurb||"").trim();s='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',s+=`<div class="cms-footer-brand">${D||L?`<a class="cms-footer-brand-name" href="${a(l.url||"/")}">${L}<span>${e(D)}</span></a>`:""}${q?`<p class="cms-footer-blurb">${e(q)}</p>`:""}${i()}</div>`,t.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${t.primary.map(u).join("")}</ul></nav>`),s+='</div><div class="cms-footer-bottom">',t.copyright&&(s+=`<p class="cms-footer-copyright">${e(t.copyright)}</p>`),(t.legal.length||p)&&(s+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${t.legal.map(d).join("")}${p}</ul></nav>`),s+="</div>"}else t.copyright&&(s+=`<p>${e(t.copyright)}</p>`),t.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary"><ul>${t.primary.map(d).join("")}</ul></nav>`),(t.legal.length||p)&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${t.legal.map(d).join("")}${p}</ul></nav>`),s+=i();s+="</div>",v.get(0).querySelector(":scope > .cms-nojs-links")?.remove();const y=v.get(0).querySelector(":scope > .cms-footer-blocks");y?.remove(),v.html(s),y&&v.get(0).prepend(y),window.CMSThemeSwitcher?.scan(v.get(0));const h=v.get(0).querySelector(".cms-footer-brand-icon");h&&m.brand?.icon&&h.setAttribute("data-icon",m.brand.icon),P(v.get(0),c.footer?.class);const w=[...f(t.primary,[],!0),...f(t.legal,[])];v.get(0).querySelectorAll(".footer-nav a").forEach((l,D)=>{const L=w[D];if(!L)return;L.colour&&G(l,L.colour);const q=U(L.badge);q&&l.appendChild(q),K(L.url,location.pathname)&&(l.classList.add("is-active"),l.setAttribute("aria-current","page"))}),V(v.get(0).querySelector(".footer-nav--primary"),{highlight:t.primaryHighlight}),V(v.get(0).querySelector(".footer-nav--legal"),{highlight:t.legalHighlight}),J(v.get(0).querySelector(".footer-nav--primary"),{entrance:t.primaryEntrance}),J(v.get(0).querySelector(".footer-nav--legal"),{entrance:t.legalEntrance}),P(v.get(0).querySelector(".footer-nav--primary"),t.primaryClass),P(v.get(0).querySelector(".footer-nav--legal"),t.legalClass);const I=c.footer?.showMotionToggle!==!1,N=S.get("reduced_motion"),O=c.footer?.motionDefault,B=N!=null?!!N:O==="on"?!0:O==="off"?!1:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),M=c.footer?.motionControl==="checkbox",z=v.get(0).querySelector(".cms-footer-bottom")||v.get(0).querySelector(".footer-inner");if(I&&z){const l=document.createElement("input");l.type="checkbox",l.className=M?"footer-motion-checkbox":"form-switch-input",l.id="dm-motion-switch",l.checked=B,l.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const D=document.createElement("span");D.className=M?"footer-motion-label":"form-switch-label",D.textContent="Reduce motion";const L=document.createElement("label");L.className=M?"footer-motion-check":"form-switch footer-motion-switch",L.title="Reduce motion",L.appendChild(l),L.appendChild(D),z.appendChild(L)}}const k=document.getElementById("site-sidebar");k&&Le(k),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const C=$(".page-body");if(C.length){C.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),C.find(".tabs").each(function(){Domma.elements.tabs(this)}),C.find(".carousel").each(function(){const n={primary:"--dm-primary",secondary:"--dm-secondary",accent:"--dm-primary",text:"--dm-text",surface:"--dm-surface",background:"--dm-background",success:"--dm-success",warning:"--dm-warning",danger:"--dm-danger",info:"--dm-info"},o=p=>{const i=String(p||"").trim();return i?n[i.toLowerCase()]?`var(${n[i.toLowerCase()]})`:CSS.supports("color",i)?i:"":""},s=o(this.dataset.background),t=o(this.dataset.arrowBackground),e=o(this.dataset.arrowColor);s&&(this.style.setProperty("--dm-carousel-background",s),this.classList.add("dm-carousel-custom-background")),t&&this.style.setProperty("--dm-carousel-arrow-background",t),e&&this.style.setProperty("--dm-carousel-arrow-color",e);const a=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${a}`);const d={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide",heightMode:this.dataset.heightMode==="average"?"average":"crop"};if(this.dataset.height){const p=Number(this.dataset.height);Number.isFinite(p)&&p>0&&(d.height=p)}if(this.dataset.animationDuration){const p=parseInt(this.dataset.animationDuration,10);Number.isFinite(p)&&(d.animationDuration=p)}this.dataset.animationEasing&&(d.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(p=>p.cloneNode(!0)),this._dmCarouselOpts=d),this._dmCarousel=Domma.elements.carousel(this,d),ie(this)}),C.find(".dm-countdown").each(function(){const n={autoStart:!0},o=String(this.dataset.format||"").trim(),s=String(this.dataset.to||"").trim(),t=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s),e=t?new Date(+t[1],+t[2]-1,+t[3]).getTime():s?new Date(s).getTime():NaN;Number.isFinite(e)?n.duration=Math.max(0,e-Date.now()):this.dataset.duration&&(n.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const a=/^dd:/i.test(o);n.format=a?"hh:mm:ss":o||(n.duration>=36e5?"hh:mm:ss":"mm:ss");const d=this,p=r=>String(r).padStart(2,"0"),i=r=>{const g=Math.max(0,Math.floor(r/1e3)),b=d.querySelector(".dm-timer-time");b&&(b.textContent=`${p(Math.floor(g/86400))}:${p(Math.floor(g%86400/3600))}:${p(Math.floor(g%3600/60))}:${p(g%60)}`)};a&&(n.onTick=i),Domma.elements.timer(this,n),a&&i(n.duration||0)}),C.find("[data-tooltip]:not(.dm-range-slider)").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),C.find(".dm-progression").each(function(){const n=Array.from(this.querySelectorAll(".dm-progression-item")).map(t=>{const e={title:t.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:t.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return t.dataset.date&&(e.date=t.dataset.date),t.dataset.status&&(e.status=t.dataset.status),t.dataset.eventIcon&&(e.icon=t.dataset.eventIcon),t.dataset.entryId&&(e._entryId=t.dataset.entryId),e}),o=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",s=Domma.elements.progression(this,{layout:o,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...n.length?{items:n}:{}});if((this.dataset.mode||"timeline")==="timeline"){const t={completed:{label:"Completed",icon:"check-circle",tone:"success"},planned:{label:"Planned",icon:"circle",tone:"secondary"},"in-progress":{label:"In progress",icon:"clock",tone:"primary"},blocked:{label:"Blocked",icon:"alert-circle",tone:"danger"}};this.querySelectorAll(".dm-progression-item").forEach((e,a)=>{const d=n[a];if(!d)return;const p=e.querySelector(".dm-progression-title");if(d.icon&&p){const r=document.createElement("span");r.dataset.icon=d.icon,r.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",r.setAttribute("aria-hidden","true"),p.prepend(r)}const i=t[d.status];if(i&&p){const r=document.createElement("span");r.className=`badge badge-${i.tone} cms-timeline-status`,r.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const g=document.createElement("span");g.dataset.icon=i.icon,g.dataset.iconSize="14",g.setAttribute("aria-hidden","true"),r.append(g,document.createTextNode(i.label)),p.after(r)}})}Domma.icons.scan(this),n.length&&(this._dmCtx={instance:s,items:n})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(n=>{const o=n.dataset.revealMode||"stagger",s=n.dataset.revealAnimation||"slide-up",t=parseInt(n.dataset.revealDuration,10)||400,e=parseInt(n.dataset.revealStagger,10)||60,a=parseInt(n.dataset.revealDelay,10)||0,d=n.dataset.revealDirection==="rtl",p=Array.from(n.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};p.forEach((r,g)=>{r.style.opacity="0",r.style.transform=i[s]||"",r.style.transition=`opacity ${t}ms ease, transform ${t}ms ease`;const b=d?p.length-1-g:g;r.style.transitionDelay=o==="stagger"?`${a+b*e}ms`:`${a}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const r=new IntersectionObserver(g=>{g.forEach(b=>{b.isIntersecting&&(b.target.offsetWidth,b.target.style.opacity="1",b.target.style.transform="none",r.unobserve(b.target))})},{threshold:.1});p.forEach(g=>r.observe(g))}))}),C.find(".card[data-collapsible]").each(function(){const n=this.querySelector(".card-header");n&&n.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),C.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const n=this.dataset.soTarget,o=document.getElementById(n);if(!o)return;const s=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",s.setContent(o),s.open()})})}if(typeof $.setup=="function"){const n=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const s=W(o.dataset.config);Object.assign(n,s)}catch{}}),Object.keys(n).length>0){const o={};for(const[s,t]of Object.entries(n)){const e=t?.events?.click,{confirm:a,toast:d,alert:p,prompt:i,...r}=e||{};a||d||p||i?($(s).on("click",async function(b){if(b.preventDefault(),a&&!await E.confirm(a))return;let u=null;if(!(i&&(u=await E.prompt(i,{inputPlaceholder:r.promptPlaceholder||"",inputValue:r.promptDefault||""}),u===null))){if(r.target){const y=$(r.target);r.toggleClass&&y.toggleClass(r.toggleClass),r.addClass&&y.addClass(r.addClass),r.removeClass&&y.removeClass(r.removeClass),u!==null&&(r.setText&&y.text(u),r.setVal&&y.val(u),r.setAttr&&y.attr(r.setAttr,u))}r.href&&(window.location.href=r.href),d&&E.toast(d,{type:r.toastType||"success"}),p&&E.alert(p)}}),Object.keys(r).length&&(o[s]={...t,events:{...t.events,click:r}})):o[s]=t}$.setup(o)}}C.length&&R(C.get(0))}),window.wireCTAButtons=R;function R(m){m.querySelectorAll(".dm-cta-trigger").forEach(c=>{c.__ctaWired||(c.__ctaWired=!0,c.addEventListener("click",async()=>{const x=c.dataset.action,v=c.dataset.entry,k=c.dataset.confirm;let C=S.get("auth_token");if(!C){E.toast("Please log in to perform this action.",{type:"warning"});return}if(k&&!await E.confirm(k))return;const n=Array.from(c.childNodes).map(s=>s.cloneNode(!0));c.disabled=!0,c.textContent="Running\u2026";const o=s=>fetch(`/api/actions/${x}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(c.dataset.scope?{entryId:v,scope:c.dataset.scope}:{entryId:v})});try{let s=await o(C);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const a=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(a.ok){const{token:d}=await a.json();S.set("auth_token",d),C=d,s=await o(C)}}}const t=await s.json().catch(()=>({}));if(!s.ok||t.success===!1)throw new Error(t.error||t.message||`Error ${s.status}`);E.toast(t.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{c.disabled=!1,c.textContent="",n.forEach(s=>c.appendChild(s)),Domma.icons.scan(c)}}))})}(function(){const c=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!c.length)return;const x=S.get("auth_token");function v(C,n){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(n){s.appendChild(document.createTextNode("Please "));const t=document.createElement("a");t.href="/login",t.textContent="sign in",s.appendChild(t),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=C;return o.appendChild(s),o}function k(C,n){const o=document.createElement("template");o.innerHTML=n||"",C.replaceWith(o.content)}c.forEach(async C=>{const n=C.dataset.collectionAttrs;if(!n)return;if(!x){C.replaceChildren(v("",!0));return}const o=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:n})});try{let s=x,t=await o(s);if(t.status===401){const a=S.get("auth_refresh_token");if(a){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:a})});if(d.ok){const{token:p}=await d.json();S.set("auth_token",p),s=p,t=await o(s)}}}if(!t.ok){const{error:a}=await t.json().catch(()=>({}));C.replaceChildren(v(a||"Unable to load entries.",!1));return}const{html:e}=await t.json();k(C,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{C.replaceChildren(v("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),R(document.body)},100)})(),(function(){const c=document.querySelectorAll("[data-collection-table]");!c.length||typeof T>"u"||!T.create||c.forEach(x=>{let v;try{v=W(x.dataset.payload)}catch{return}const{columns:k,rows:C,search:n,sortable:o,exportable:s,pageSize:t,empty:e,ctaConfig:a,slug:d,exportCfg:p}=v;if(!k?.length)return;if(a){const g=k.findIndex(b=>b.key==="_cta");g!==-1&&(k[g]={key:"_cta",title:"",render:(b,u)=>{const y=document.createElement("button");if(y.className=`btn btn-${a.style||"primary"} dm-cta-trigger`,y.dataset.action=a.action||"",a.scope&&(y.dataset.scope=a.scope),y.dataset.entry=u._entryId||"",a.confirm&&(y.dataset.confirm=a.confirm),a.icon){const h=document.createElement("span");h.dataset.icon=a.icon,y.appendChild(h),y.appendChild(document.createTextNode(" "))}return y.appendChild(document.createTextNode(a.label||"Run")),y}})}const i="col-table-"+Math.random().toString(36).slice(2,7),r=document.createElement("div");r.id=i,x.replaceChildren(r),T.create("#"+i,{data:C,columns:k,search:n,sortable:o,exportable:s,pageSize:t,emptyMessage:e}),a&&R(r),x._dmTable={columns:k,rows:C,slug:d,exportCfg:p,render(g){T.create("#"+i,{data:g,columns:k,search:n,sortable:o,exportable:s,pageSize:t,emptyMessage:e}),a&&R(r)}}})})(),(function(){const c=document.querySelectorAll("[data-form-inline]");if(!c.length||typeof F>"u")return;function x(o){const s=o&&o.logic;return!!(s&&(s.visibility||s.requirement))}function v(o,s){const t={};return(o||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const a=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),d=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":a?"chooser":e.type,p={...e.formConfig||{}};p.span==="full"&&s&&(p.span=s);const i={type:d,required:x(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...p}};e.minLength!==void 0&&(i.minLength=e.minLength),e.maxLength!==void 0&&(i.maxLength=e.maxLength),e.min!==void 0&&(i.min=e.min),e.max!==void 0&&(i.max=e.max),a&&(e.variant&&(i.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(i.multiple=!0),e.density&&(i.density=e.density),e.columns&&(i.columns=e.columns),e.accent&&(i.accent=e.accent),e.accentStyle&&(i.accentStyle=e.accentStyle),e.glow&&(i.glow=!0),e.glowColour&&(i.glowColour=e.glowColour),e.shadow&&(i.shadow=e.shadow),e.shadowColour&&(i.shadowColour=e.shadowColour)),t[e.name]=e.type==="file"?ve(e,i):i}),t}function k(o){const s={};return(o||[]).forEach(t=>{if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(s[t.name]=typeof e=="object"?e.value:e)}}),s}function C(o,s){(s||[]).forEach(t=>{if(t.type!=="date"||!t.name)return;const e=o.querySelector(`[name="${t.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function n(o,s,t){let e=o.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",o.appendChild(e)),e.textContent=s,e.style.cssText=t?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}c.forEach(o=>{let s;try{s=W(o.dataset.formInline)}catch{return}o.querySelector(":scope > .cms-form-embed-summary")?.remove();const t=s.fields||[],e=s.settings||{},a=Number(e.columns)===3?3:2,d=e.layout||"grid",p=t.some(h=>h.type==="page-break"),i=new Map;Se(o,t);const r=async h=>{if(o.dataset.triggerBlocked==="true"){const f=o._formLogicRuntime,w=f&&f.submitBlock?f.submitBlock().reasons:[];n(o,w.join(" ")||"This form cannot be submitted yet.",!0);const A=new Error("Blocked by a form trigger");throw A.formSubmitFailed=!0,A}try{const f=o.querySelector(".fb-form-honeypot"),w=f?.querySelector('input[type="text"]')||null,A=f?.querySelector('[name="_t"]')||null,I=Object.assign({},h);delete I._t,f?.dataset.hpName&&delete I[f.dataset.hpName],w!==null&&(I._hp=w.value),A!==null&&(I._t=A.value);const N=new Map(i);oe(o,N);const O=we(I,t,N),B=await H.post(`/api/forms/submit/${s.slug}`,O.files.size?Ce(O.values,O.files).body:O.values);if(B&&B.error){n(o,B.error,!0);const M=new Error(B.error);throw M.formSubmitFailed=!0,M}if(B?.redirect){window.location.href=B.redirect;return}for(ye(o);o.firstChild;)o.removeChild(o.firstChild);n(o,B?.message||e.successMessage||"Thank you for your submission.",!1)}catch(f){throw f.formSubmitFailed||n(o,f.message||"Submission failed. Please try again.",!0),f}},g=Date.now();function b(h){const f=h.querySelector("form");if(!f||f.querySelector(".fb-form-honeypot"))return;const w=document.createElement("div");w.className="fb-form-honeypot",w.setAttribute("aria-hidden","true");const A=document.createElement("input");A.name=f.querySelector('[name="website"]')?"_website":"website",w.dataset.hpName=A.name,A.type="text",A.tabIndex=-1,A.autocomplete="url",A.placeholder="https://",w.appendChild(A);const I=document.createElement("input");I.name="_t",I.type="hidden",I.value=g,w.appendChild(I),f.appendChild(w)}function u(h){if(!window.FormLogicEngine||!t.some(w=>w.logic||Array.isArray(w.triggers)&&w.triggers.length))return null;const f=new window.FormLogicEngine.FormLogicRuntime(s,o,h?{baseValues:h}:void 0);if(f.init(),o._formLogicRuntime=f,o._formLogicObserved)return f;if(o._formLogicObserved=!0,o.parentNode&&typeof MutationObserver<"u"){const w=new MutationObserver(function(A){for(const I of A)for(const N of I.removedNodes)if(N===o||N.nodeType===1&&N.contains&&N.contains(o)){o._formLogicRuntime&&o._formLogicRuntime.destroy(),w.disconnect();return}});w.observe(o.parentNode,{childList:!0,subtree:!1}),o._formLogicObserver=w}return f}let y=!1;if(o.addEventListener("dm-form-end",h=>{const f=h.detail?.validation;if(f&&!f.valid){Z(o,f.missing),ee(f);return}y||(y=!0,r(h.detail?.values||{}).catch(()=>{}).finally(()=>{y=!1}))}),p&&F.wizard){const h=fe(t,s.title),f=h.map(M=>({title:M.title,description:M.description,fields:v(M.fields,a)})),w=h.map(M=>M.fields);let A=0,I=null,N=null;const O=M=>{const z=window.FormLogicEngine;if(!z||!z.validateFields)return;const l=z.validateFields(o,w[A]||[]);l.valid||(M.preventDefault(),M.stopImmediatePropagation(),Z(o,l.missing),ee(l))},B=()=>{const M=w[A]||t;ge(o,e),te(o,e,M),C(o,M),Q(o,M),be(o,i),e.honeypot!==!1&&b(o),I&&I.destroy(),I=u(N?.getStepData?N.getStepData():void 0),o.querySelectorAll(".wizard-next, .wizard-submit").forEach(z=>z.addEventListener("click",O,!0))};N=F.wizard(o,{schema:{steps:f},layout:d,columns:a,finishText:e.submitText||"Finish",onSubmit:r,onStepChange:M=>{oe(o,i),A=M,setTimeout(B,0)}}),o.addEventListener("dm-form-trigger",M=>{if(M.detail?.type!=="goto-step"||!N?.goToStep)return;const z=Number(M.detail.action.step);!Number.isFinite(z)||z<0||z>=f.length||z!==A&&N.goToStep(z)}),Promise.resolve(N).then(B)}else if(F.render){const h=F.render(o,v(t,a),k(t),{submitText:e.submitText||"Submit",layout:d,columns:a,onSubmit:r});Promise.resolve(h).then(function(){he(o,e,d),te(o,e,t),C(o,t),Q(o,t),e.honeypot!==!1&&b(o),u()})}})})();function xe(m,c){const x=document.body,v=x.classList,k=new Set((x.dataset.railSite||"").split(" ").filter(Boolean)),C=()=>window.matchMedia("(min-width: 993px)").matches;m.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(a=>{const d=a.textContent.trim();d&&(a.setAttribute("title",d),a.querySelector("svg, [data-icon]")||(a.dataset.initial=d.charAt(0).toUpperCase()))});const n=document.createElement("button");n.type="button",n.className="dm-nav-collapse-toggle";const o=()=>{const a=v.contains("dm-vnav-collapsed");n.setAttribute("aria-pressed",String(a)),n.setAttribute("aria-label",a?"Expand menu":"Collapse menu"),n.title=n.getAttribute("aria-label");const d=c?"chevrons-left":"chevrons-right",p=c?"chevrons-right":"chevrons-left";n.innerHTML=`<span data-icon="${a?d:p}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(n)},s=a=>{v.toggle("dm-vnav-collapsed",a),S.set("nav_collapsed",a),o()};n.addEventListener("click",()=>s(!v.contains("dm-vnav-collapsed"))),o();const t=m.querySelector(".navbar-container")||m;t.insertBefore(n,t.firstChild?.nextSibling||null);const e=a=>{v.toggle("dm-vnav-autohide",a),v.toggle("dm-vnav-collapsible",!a&&k.has("collapsible")),a?v.remove("dm-vnav-collapsed"):k.has("collapsible")&&S.get("nav_collapsed")===!0&&v.add("dm-vnav-collapsed"),m.classList.remove("dm-nav-revealed"),o()};m.addEventListener("pointerdown",a=>{v.contains("dm-vnav-autohide")&&a.pointerType!=="mouse"&&!m.classList.contains("dm-nav-revealed")&&(m.classList.add("dm-nav-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{m.contains(a.target)||m.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(m,{items:()=>{const a=v.contains("dm-vnav-autohide"),d=v.contains("dm-vnav-collapsed"),p=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:d?"Expand menu":"Collapse to icons",icon:d?"maximize":"minimize",disabled:!C()||a||!k.has("collapsible"),action:()=>s(!d)},{label:a?"Keep on screen":"Hide when not in use",icon:a?"eye":"eye-off",disabled:!C(),action:()=>{S.set("nav_autohide",!a),e(!a)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!p,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(k.has("autohide")),v.toggle("dm-vnav-collapsed",!k.has("autohide")&&k.has("start-collapsed")),o()}}]}})}function ke(m){m.classList.add("dm-nav-scroll-hide");let c=window.scrollY,x=!1;window.addEventListener("scroll",()=>{x||(x=!0,requestAnimationFrame(()=>{x=!1;const v=window.scrollY,k=m.matches(":focus-within")||m.querySelector(".navbar-dropdown.open, .navbar-collapse.show");v>c&&v>m.offsetHeight&&!k?m.classList.add("dm-nav-scrolled-away"):v<c&&m.classList.remove("dm-nav-scrolled-away"),c=v}))},{passive:!0})}function Le(m){let c={};try{c=JSON.parse(m.dataset.contents||"{}")}catch{}const x=m.closest(".site-main"),v=document.querySelector(".site-content"),k=new Set(String(c.exclude||"").split(",").map(i=>i.trim().toLowerCase()).filter(Boolean)),C=i=>!i.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),n=i=>(v?[...v.querySelectorAll(i)]:[]).filter(C).filter(r=>!k.has(r.textContent.trim().toLowerCase())),o=c.headings==="h2"?n("h2"):c.headings==="h3"?n("h3"):c.headings==="h2h3"?n("h2, h3"):n("h2").length?n("h2"):n("h3");if(!o.length){x?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(i=>i.id)),t=document.createElement("nav");t.className="dm-contents",t.setAttribute("aria-label",c.title||"Page contents"),c.hover&&(t.dataset.menuHover=c.hover),c.selected&&(t.dataset.menuActive=c.selected),c.accent&&t.style.setProperty("--dm-menu-highlight",c.accent.startsWith("#")?c.accent:`var(--dm-${c.accent})`),c.font&&(t.style.fontFamily=c.font),c.fontSize&&(t.style.fontSize=c.fontSize),c.fontWeight&&(t.style.fontWeight=c.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const a=document.createElement("p");a.className="dm-contents-title",a.textContent=c.title||"",e.appendChild(a),t.appendChild(e);const d=document.createElement("ol");d.className="dm-contents-list"+(c.numbered?" is-numbered":"");const p=[];for(const i of o){if(!i.id){const b=(i.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let u=b;for(let y=2;s.has(u);y++)u=`${b}-${y}`;s.add(u),i.id=u}const r=document.createElement("li");r.className=`dm-contents-item dm-contents-${i.tagName.toLowerCase()}`;const g=document.createElement("a");g.href=`#${i.id}`,g.textContent=i.textContent.trim(),r.appendChild(g),d.appendChild(r),p.push([i,g])}if(t.appendChild(d),m.replaceChildren(t),c.scrollSpy!==!1&&"IntersectionObserver"in window){const i=new Map,r=()=>{const b=p.find(([u])=>i.get(u))||null;p.forEach(([,u])=>{const y=b&&u===b[1];u.classList.toggle("is-active",!!y),y?u.setAttribute("aria-current","location"):u.removeAttribute("aria-current")})},g=new IntersectionObserver(b=>{b.forEach(u=>i.set(u.target,u.isIntersecting)),r()},{rootMargin:"-80px 0px -60% 0px"});p.forEach(([b])=>g.observe(b))}if(x?.classList.contains("contents-collapsible")){const i=document.createElement("button");i.type="button",i.className="dm-contents-toggle";const r=()=>{const g=x.classList.contains("contents-collapsed");i.setAttribute("aria-expanded",String(!g)),i.setAttribute("aria-label",g?"Show the contents":"Hide the contents"),i.title=i.getAttribute("aria-label"),i.innerHTML=`<span data-icon="${g?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(i)};i.addEventListener("click",()=>{S.set("contents_collapsed",x.classList.toggle("contents-collapsed")),r()}),r(),e.insertBefore(i,e.firstChild)}if(x?.classList.contains("contents-floating")&&c.movable){t.classList.add("is-movable");const i=S.get("contents_position");i&&Number.isFinite(i.x)&&Object.assign(m.style,{left:`${i.x}px`,top:`${i.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",r=>{if(r.target.closest("button"))return;const g=m.getBoundingClientRect(),b=r.clientX-g.left,u=r.clientY-g.top,y=f=>{const w=Math.max(0,Math.min(window.innerWidth-g.width,f.clientX-b)),A=Math.max(0,Math.min(window.innerHeight-40,f.clientY-u));Object.assign(m.style,{left:`${w}px`,top:`${A}px`,right:"auto",bottom:"auto"})},h=()=>{document.removeEventListener("pointermove",y),S.set("contents_position",{x:parseInt(m.style.left,10),y:parseInt(m.style.top,10)})};document.addEventListener("pointermove",y),document.addEventListener("pointerup",h,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(m,{items:()=>{const i=x?.classList.contains("contents-collapsible"),r=x?.classList.contains("contents-collapsed"),g=p.find(([,b])=>b.classList.contains("is-active"));return[{header:"Contents"},...i?[{label:r?"Show the contents":"Collapse to a button",icon:r?"list":"minimize",action:()=>m.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!g,action:async()=>{const b=`${location.origin}${location.pathname}#${g[0].id}`;try{await _.copyToClipboard(b),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),x?.classList.contains("contents-autohide")&&(m.addEventListener("pointerdown",i=>{i.pointerType!=="mouse"&&!m.classList.contains("is-revealed")&&(m.classList.add("is-revealed"),i.preventDefault())}),document.addEventListener("pointerdown",i=>{m.contains(i.target)||m.classList.remove("is-revealed")}))}
@@ -27,6 +27,9 @@ import {deleteForm, ensureCollectionForForm, ensureFormsDir, listForms, readForm
27
27
  import {executeAction} from '../../services/actions.js';
28
28
  // The same rules the browser draws the buttons by (a Cancel link must be safe).
29
29
  import {buttonSettingsProblems, formClassProblems, publicButtonSettings} from '../../../public/js/form-submit.mjs';
30
+ import {cleanUploadName, countRefusal, fileRefusal, fileRules, fileValueText, isFileValue} from '../../../public/js/form-files.mjs';
31
+ import {sanitiseCsv} from '../../../public/js/collection-export.mjs';
32
+ import {originOf, trustedOrigin} from '../../services/passwordReset.js';
30
33
  import {createTransport, mailFailure, sendFormEmail, TEST_EMAIL_FAILED} from '../../services/email.js';
31
34
  import {formActionFailed, formTriggerNotice} from '../../services/notification-sources.js';
32
35
  import {
@@ -43,7 +46,8 @@ import {getConfig} from '../../config.js';
43
46
  import {authenticate, requireAdmin, requirePermission} from '../../middleware/auth.js';
44
47
  import {hooks} from '../../services/hooks.js';
45
48
  import {pluginDenialFor} from '../../services/collectionAccess.js';
46
- import {saveMedia} from '../../services/content.js';
49
+ import {deleteMedia, saveMedia} from '../../services/content.js';
50
+ import {servedType} from '../../services/mediaTypes.js';
47
51
  import {v4 as uuidv4} from 'uuid';
48
52
  import {ago, clip, plural} from '../../services/sidebarBadges.js';
49
53
  import {safeFetch} from '../../services/outbound.js';
@@ -110,18 +114,29 @@ async function detectFormCollectionMismatch(form) {
110
114
  * @returns {string}
111
115
  */
112
116
  function safeFilename(raw) {
113
- const cleaned = String(raw || 'upload')
114
- .toLowerCase()
115
- .replace(/[^\w.-]+/g, '-')
116
- .replace(/-+/g, '-')
117
- .replace(/^[-.]+|[-.]+$/g, '')
118
- .slice(0, 200) || 'upload';
119
- const prefix = uuidv4().slice(0, 8);
120
- return `${prefix}-${cleaned}`;
117
+ return `${uuidv4().slice(0, 8)}-${cleanUploadName(raw)}`;
118
+ }
119
+
120
+ /**
121
+ * The kinds of file a form may collect, by the type /media/ will serve the stored name as:
122
+ * images (not SVG), audio, video, PDF, Office and OpenDocument, plain text and CSV,
123
+ * archives. A name with no known type is served as a download. Anything else - above all
124
+ * anything a browser would render as a page on this site's address - is refused.
125
+ */
126
+ const SERVABLE = /^(image\/(?!svg)[\w.+-]+|audio\/[\w.+-]+|video\/[\w.+-]+|text\/(plain|csv)|application\/(pdf|rtf|zip|x-zip-compressed|gzip|x-gzip|x-tar|x-7z-compressed|x-rar-compressed|vnd\.rar|msword|vnd\.ms-[\w.+-]+|vnd\.openxmlformats-officedocument\.[\w.+-]+|vnd\.oasis\.opendocument\.[\w.+-]+|epub\+zip|octet-stream))$/i;
127
+
128
+ /** Why a stored name would be served as something a form may not collect, or null. */
129
+ function servedTypeRefusal(fieldCfg, filename) {
130
+ const served = servedType(cleanUploadName(filename));
131
+ if (!served || SERVABLE.test(served)) return null;
132
+ const label = fieldCfg.label || fieldCfg.name;
133
+ return `"${label}" file type "${served}" cannot be uploaded here.`;
121
134
  }
122
135
 
123
136
  /**
124
137
  * Check a multipart file part against a form's `type: file` field config.
138
+ * The rules live in public/js/form-files.mjs, which the browser's dropzone
139
+ * applies as files are added - one definition, so the two cannot drift.
125
140
  *
126
141
  * @param {object} fieldCfg - The form-field definition
127
142
  * @param {object} part - Multipart part (mimetype, filename)
@@ -129,76 +144,176 @@ function safeFilename(raw) {
129
144
  * @returns {string|null} - Error message, or null if OK
130
145
  */
131
146
  function validateUpload(fieldCfg, part, size) {
132
- const cfg = fieldCfg?.file || {};
133
- const max = Number(cfg.maxSize) || 5 * 1024 * 1024; // 5 MB default
134
- if (size > max) {
135
- return `"${fieldCfg.label || fieldCfg.name}" file is too large (max ${Math.round(max / 1024)} KB).`;
136
- }
137
- const accept = String(cfg.accept || '').trim();
138
- if (accept) {
139
- // Comma-separated list of mime types, allowing wildcards like image/*
140
- const patterns = accept.split(',').map(s => s.trim()).filter(Boolean);
141
- const mime = part.mimetype || '';
142
- const ok = patterns.some(p => {
143
- if (p.endsWith('/*')) return mime.startsWith(p.slice(0, -1));
144
- return p === mime;
145
- });
146
- if (!ok) {
147
- return `"${fieldCfg.label || fieldCfg.name}" file type "${mime}" not allowed (expected: ${accept}).`;
148
- }
149
- }
150
- return null;
147
+ return fileRefusal(fieldCfg, {mimetype: part.mimetype || '', filename: part.filename || '', size})
148
+ || servedTypeRefusal(fieldCfg, part.filename || '');
149
+ }
150
+
151
+ /** What one submission's files may hold in memory together when server.json sets nothing: 50 MB. */
152
+ export const DEFAULT_MAX_SUBMISSION_BYTES = 50 * 1024 * 1024;
153
+
154
+ /**
155
+ * The most bytes one submission's files may hold in memory, all fields together:
156
+ * server.json `uploads.maxSubmissionBytes` (read fresh) when set; otherwise 50 MB or
157
+ * `uploads.maxFileSize`, whichever is larger, so one file the site takes is never
158
+ * refused for the total.
159
+ *
160
+ * @returns {number}
161
+ */
162
+ export function maxSubmissionBytes() {
163
+ let uploads = {};
164
+ try { uploads = getConfig('server')?.uploads || {}; } catch { /* no server.json */ }
165
+ const set = Number(uploads.maxSubmissionBytes);
166
+ if (set > 0) return set;
167
+ return Math.max(DEFAULT_MAX_SUBMISSION_BYTES, Number(uploads.maxFileSize) || 0);
168
+ }
169
+
170
+ /** The refusal for a submission whose files together pass maxSubmissionBytes(). */
171
+ export function submissionTooLarge(limit) {
172
+ const mb = limit / (1024 * 1024);
173
+ const size = mb >= 1 ? `${Math.round(mb * 10) / 10} MB` : `${Math.round(limit / 1024)} KB`;
174
+ return `The files sent are too large together (at most ${size} in one submission). Send fewer or smaller files.`;
151
175
  }
152
176
 
153
177
  /**
154
178
  * Drain a multipart request into a JSON-shaped body. Text parts become string
155
- * values; file parts are validated against the matching form field, saved to
156
- * `content/media/`, and replaced by a `{ url, name, size, mime }` reference.
179
+ * values; file parts are checked against the matching form field and held in
180
+ * memory, and the field's value becomes its reference - `{url, name, size, mime}`
181
+ * for a single field, a list of them for a `multiple` one - with the url filled
182
+ * in by saveFormFiles() once the whole submission has passed.
157
183
  *
158
- * Throws on validation failure (caller handles HTTP 400). Files are streamed
159
- * into memory up to fastify-multipart's configured limit - fine for the
160
- * resume-pdf / portrait-photo scale; large uploads should still use the admin
161
- * media flow.
184
+ * Throws on validation failure (caller handles HTTP 400): any one file a field
185
+ * would refuse, or more files than a field takes (countRefusal), refuses the
186
+ * submission. Files are held in memory up to fastify-multipart's configured
187
+ * limit, at most fileRules().maxFiles per field, and at most maxSubmissionBytes()
188
+ * for all of them together - the request is refused the moment the running
189
+ * total passes it, before the rest is read. Fine for the resume-pdf /
190
+ * portrait-photo scale; large uploads should still use the admin media flow.
162
191
  *
163
192
  * @param {import('fastify').FastifyRequest} request
164
193
  * @param {object} form
165
- * @returns {Promise<object>}
194
+ * @returns {Promise<{body: object, pending: Map<string, {buffer: Buffer, filename: string}[]>}>}
166
195
  */
167
196
  async function parseMultipartForm(request, form) {
168
197
  const out = {};
198
+ const pending = new Map(); // field name -> [{buffer, filename}], saved by saveFormFiles()
169
199
  const fileFields = new Map(
170
200
  (form.fields || []).filter(f => f.type === 'file').map(f => [f.name, f])
171
201
  );
202
+ const drain = async (part) => { for await (const _ of part.file) { /* drain */ } };
203
+ const limit = maxSubmissionBytes();
204
+ let held = 0; // bytes of every file part so far, all fields
172
205
 
173
206
  for await (const part of request.parts()) {
174
207
  if (part.type === 'file') {
175
208
  const fieldCfg = fileFields.get(part.fieldname);
176
- if (!fieldCfg) {
177
- // Unknown file field - drain and skip rather than 400, so a
178
- // form that gains a file input later doesn't reject older
179
- // submissions in-flight from a stale page.
180
- for await (const _ of part.file) { /* drain */ }
209
+ const list = pending.get(part.fieldname) || [];
210
+ const rules = fieldCfg ? fileRules(fieldCfg) : null;
211
+ // An unknown file field, or a second file for one that holds one: drain and
212
+ // skip rather than 400, so a form that gains a file input later doesn't reject
213
+ // older submissions in-flight from a stale page.
214
+ if (!fieldCfg || (!rules.multiple && list.length)) {
215
+ await drain(part);
181
216
  continue;
182
217
  }
218
+ // More than a multiple field takes refuses the submission, before the
219
+ // extra file is read into memory.
220
+ if (list.length >= rules.maxFiles) {
221
+ await drain(part);
222
+ throw new Error(countRefusal(fieldCfg, list.length + 1));
223
+ }
183
224
  const chunks = [];
184
- for await (const chunk of part.file) chunks.push(chunk);
225
+ let over = false;
226
+ for await (const chunk of part.file) {
227
+ if (over) continue; // the rest of this part is drained, not held
228
+ held += chunk.length;
229
+ if (held > limit) {
230
+ // Stop holding at once: drop what is read, then refuse
231
+ over = true;
232
+ chunks.length = 0;
233
+ pending.clear();
234
+ continue;
235
+ }
236
+ chunks.push(chunk);
237
+ }
238
+ if (over) throw new Error(submissionTooLarge(limit));
185
239
  const buffer = Buffer.concat(chunks);
186
- const err = validateUpload(fieldCfg, part, buffer.length);
240
+ // Over the server's own upload cap the part arrives cut short, not refused
241
+ const size = part.file.truncated ? Infinity : buffer.length;
242
+ const err = validateUpload(fieldCfg, part, size);
187
243
  if (err) throw new Error(err);
188
244
  if (buffer.length === 0) continue; // empty file input - skip
189
- const saved = await saveMedia(safeFilename(part.filename), buffer);
190
- out[part.fieldname] = {
191
- url: saved.url,
192
- name: part.filename,
193
- size: buffer.length,
194
- mime: part.mimetype
195
- };
196
- } else {
197
- // Text field - last value wins on duplicates (browser-standard).
245
+ list.push({buffer, filename: part.filename});
246
+ pending.set(part.fieldname, list);
247
+ const ref = {url: '', name: part.filename, size: buffer.length, mime: part.mimetype};
248
+ if (rules.multiple) (Array.isArray(out[part.fieldname]) ? out[part.fieldname] : (out[part.fieldname] = [])).push(ref);
249
+ else out[part.fieldname] = ref;
250
+ } else if (!fileFields.has(part.fieldname)) {
251
+ // Text field - last value wins on duplicates (browser-standard). A file
252
+ // field's value only ever comes from its file parts (fileFieldsOnly).
198
253
  out[part.fieldname] = part.value;
199
254
  }
200
255
  }
201
- return out;
256
+ return {body: out, pending};
257
+ }
258
+
259
+ /**
260
+ * A file field's value is only ever what this server made of the files it was
261
+ * sent: whatever a JSON body (or a text part) says one holds - a reference to
262
+ * any address - is dropped, so an entry never links to a file nobody uploaded.
263
+ *
264
+ * @param {object} form
265
+ * @param {object} body - mutated
266
+ * @param {Map<string, unknown>|null} pending
267
+ */
268
+ function fileFieldsOnly(form, body, pending) {
269
+ if (!body || typeof body !== 'object') return;
270
+ for (const f of form.fields || []) {
271
+ if (f.type !== 'file' || !f.name || pending?.has(f.name)) continue;
272
+ if (body[f.name] !== undefined && body[f.name] !== '') body[f.name] = '';
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Write the files of a submission that passed every check, into the reference objects
278
+ * parseMultipartForm() left in `body`. Only the fields that will be kept are written -
279
+ * nothing a bot, a rate-limited or refused submission or a switched-off field sent
280
+ * reaches content/media/.
281
+ *
282
+ * If one cannot be written, the ones already written for this submission are removed
283
+ * and the error is thrown - a submission keeps all its files or none.
284
+ *
285
+ * @param {Map<string, {buffer: Buffer, filename: string}[]>} pending
286
+ * @param {object} body
287
+ * @param {Set<string>} keep - field names the submission keeps
288
+ * @returns {Promise<string[]>} the media file names written (for removeFormFiles)
289
+ */
290
+ async function saveFormFiles(pending, body, keep) {
291
+ const written = [];
292
+ try {
293
+ for (const [name, files] of pending) {
294
+ if (!keep.has(name)) {
295
+ delete body[name];
296
+ continue;
297
+ }
298
+ const refs = Array.isArray(body[name]) ? body[name] : [body[name]];
299
+ for (let i = 0; i < files.length; i++) {
300
+ const saved = await saveMedia(safeFilename(files[i].filename), files[i].buffer);
301
+ written.push(saved.name);
302
+ refs[i].url = saved.url;
303
+ }
304
+ }
305
+ } catch (err) {
306
+ await removeFormFiles(written);
307
+ throw err;
308
+ }
309
+ return written;
310
+ }
311
+
312
+ /** Remove a submission's written files again (it was not stored after all). Never throws. */
313
+ async function removeFormFiles(names) {
314
+ for (const name of names || []) {
315
+ try { await deleteMedia(name); } catch { /* already gone */ }
316
+ }
202
317
  }
203
318
 
204
319
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -223,17 +338,34 @@ function isRateLimited(slug, ip, limitPerMinute) {
223
338
  return false;
224
339
  }
225
340
 
226
- function submissionsToCSV(form, entries) {
341
+ /** A stored answer as plain text: lists joined with "; ", objects as JSON. */
342
+ function answerCsvText(raw) {
343
+ if (raw == null) return '';
344
+ if (Array.isArray(raw)) return raw.map(v => (v && typeof v === 'object' ? JSON.stringify(v) : String(v))).join('; ');
345
+ if (typeof raw === 'object') return JSON.stringify(raw);
346
+ return String(raw);
347
+ }
348
+
349
+ /**
350
+ * One CSV cell: quoted, inner quotes doubled, and a leading =, +, -, @, TAB or CR
351
+ * neutralised (sanitiseCsv, as the public collection export does) - a visitor's
352
+ * answer must not run as a formula in the spreadsheet an admin opens.
353
+ */
354
+ function csvCell(text) {
355
+ return `"${sanitiseCsv(String(text ?? '')).replace(/"/g, '""')}"`;
356
+ }
357
+
358
+ export function submissionsToCSV(form, entries) {
227
359
  const fields = (form.fields || []).filter(f => f.type !== 'page-break' && f.type !== 'spacer');
228
- const headers = [...fields.map(f => `"${f.label || f.name}"`), '"Date"'];
360
+ const headers = [...fields.map(f => csvCell(f.label || f.name)), csvCell('Date')];
229
361
  const rows = entries.map(e => {
230
362
  const cols = fields.map(f => {
231
363
  const raw = e.data?.[f.name] ?? '';
232
- const str = Array.isArray(raw) ? raw.join('; ') : String(raw);
233
- const val = str.replace(/"/g, '""');
234
- return `"${val}"`;
364
+ // Files: "cv.pdf (/media/…)", several joined with "; " like any list. Anything
365
+ // else in the column (an older text answer) is written as it is.
366
+ return csvCell(isFileValue(raw) ? fileValueText(raw) : answerCsvText(raw));
235
367
  });
236
- cols.push(`"${e.meta?.createdAt || ''}"`);
368
+ cols.push(csvCell(e.meta?.createdAt || ''));
237
369
  return cols.join(',');
238
370
  });
239
371
  return [headers.join(','), ...rows].join('\n');
@@ -783,13 +915,15 @@ export async function formsRoutes(fastify) {
783
915
  // then their { url, name, size, mime } reference object lands in `body`
784
916
  // under the field name. Downstream code sees a uniform JSON-shaped body.
785
917
  let body = request.body || {};
918
+ let pendingFiles = null;
786
919
  if (request.isMultipart && request.isMultipart()) {
787
920
  try {
788
- body = await parseMultipartForm(request, form);
921
+ ({body, pending: pendingFiles} = await parseMultipartForm(request, form));
789
922
  } catch (err) {
790
923
  return reply.status(400).send({ error: err.message });
791
924
  }
792
925
  }
926
+ fileFieldsOnly(form, body, pendingFiles);
793
927
  const settings = form.settings || {};
794
928
 
795
929
  // Best-effort auth - never reject, just enrich.
@@ -892,6 +1026,17 @@ export async function formsRoutes(fastify) {
892
1026
  return reply.status(429).send({ error: 'Too many submissions. Please try again later.' });
893
1027
  }
894
1028
 
1029
+ // The checks are done: only now do a submission's files reach content/media/
1030
+ let writtenFiles = [];
1031
+ if (pendingFiles?.size) {
1032
+ try {
1033
+ writtenFiles = await saveFormFiles(pendingFiles, body, record ? visibleFieldNames : new Set());
1034
+ } catch (err) {
1035
+ fastify.log.warn(`[forms] Saving the files of a "${slug}" submission failed: ${err.message}`);
1036
+ return reply.status(500).send({error: 'Your files could not be saved, so the submission was not sent. Please try again.'});
1037
+ }
1038
+ }
1039
+
895
1040
  // Build submission data - only include visible fields
896
1041
  const data = {};
897
1042
  const excused = [];
@@ -945,12 +1090,15 @@ export async function formsRoutes(fastify) {
945
1090
  // storage is explicitly disabled with no slug. Fail loudly
946
1091
  // rather than silently lose submissions.
947
1092
  fastify.log.warn(`[forms] Submission for "${slug}" had no target collection "${targetSlug}"`);
1093
+ await removeFormFiles(writtenFiles);
948
1094
  return reply.status(500).send({
949
1095
  error: `Submission not saved - target collection "${targetSlug}" does not exist. Please contact the site administrator.`
950
1096
  });
951
1097
  }
952
1098
  } catch (err) {
953
1099
  fastify.log.warn(`[forms] Collection write failed for "${slug}" → "${targetSlug}": ${err.message}`);
1100
+ // Not stored, so its files are not kept either
1101
+ await removeFormFiles(writtenFiles);
954
1102
  // Distinguish validation errors (user-actionable) from other
955
1103
  // failures (admin needs to look) so the message helps the right
956
1104
  // person. Validation errors typically start with "Validation failed:".
@@ -974,7 +1122,10 @@ export async function formsRoutes(fastify) {
974
1122
  subject: `${emailAction.subjectPrefix || `[${form.title}]`} New submission`,
975
1123
  formTitle: form.title,
976
1124
  fields: form.fields,
977
- data
1125
+ data,
1126
+ // File links in the email are absolute: site.json baseUrl, else an address an
1127
+ // admin has signed in at (trustedOrigin - never just this request's Host)
1128
+ baseUrl: await trustedOrigin(originOf(request)).catch(() => '')
978
1129
  });
979
1130
  } catch (err) {
980
1131
  fastify.log.warn(`[forms] Email send failed for "${slug}": ${err.message}`);