domma-cms 1.14.0 → 1.15.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.15.0] - 2026-10-29
4
+
5
+ - The "Reduce motion" control has its own Site Settings tab, beside Breadcrumbs: show it, its label, and what a new visitor gets (follow their device, full motion, or calm). Placement puts it in the footer as before (a switch or a checkbox), as a floating button in a chosen corner (offsets, size and corners, Search's limits), or at the end of the navbar. A floating button sharing a corner with Search, Back to top or the theme switcher stacks clear of it; it defaults to the bottom left, clear of both. Stored as `site.json` `motion` (`server/services/motionSettings.js`); a site without it keeps reading `footer.showMotionToggle` / `motionDefault` / `motionControl`, which Site Settings still writes as a mirror, so nothing changes until the tab is saved. The visitor's choice is still the one `domma:reduced_motion` preference.
6
+
3
7
  ## [1.14.0] - 2026-10-29
4
8
 
5
9
  - Page editor: **Highlight text** is on the Insert and Wrap menus (Content) - the [text] dialog with a gradient and a shine already chosen, its highlight choices first and the colours shown in the preview. Right-click a `[text]` tag for one-click Highlight, sweeping Highlight, Light up as you read, Rise in word by word, and Plain.
@@ -1,3 +1,3 @@
1
- export const TAB_FIELDS={general:["title","tagline","siteUrl","adminHome","spacerSize","bodyClass"],branding:["brandText","brandLogo","brandIcon","brandUrl","adminBrandTitle","adminBrandLogo","adminBrandIcon"],favicon:["favicon"],seo:["seoTitle","seoSeparator","seoDescription"],footer:["footerCopyright","footerLayout","footerBlurb","footerBlocks","footerMotionToggle","footerMotionDefault","footerMotionControl","footerClass","footerSocialClass","footerLook","footerCtaText","footerCtaHighlight","footerCtaUrl","footerCtaSub","footerWatermark","footerStatus","footerFacts","footerTagline","footerEmail","footerAddress","footerGradFrom","footerGradTo","socialTwitter","socialFacebook","socialInstagram","socialLinkedin","socialGithub","socialYoutube"],email:["smtpHost","smtpPort","smtpUser","smtpPass","smtpSecure","smtpInsecureTLS","smtpFromAddress","smtpFromName"],pageNavigation:["bttEnabled","bttSmooth","bttLabel","bttThreshold","bttPosition","bttOffset","bttBottomOffset","gtbEnabled","gtbSmooth","gtbLabel","gtbThreshold","gtbPosition","gtbOffset","gtbBottomOffset"],cookies:["ccEnabled","ccMessage","ccAcceptAll","ccRejectAll","ccCustomize","ccSavePrefs","ccPrivacyText","ccPrivacyUrl","ccCookieText","ccCookieUrl","ccPosition","ccLayout","ccTheme","ccShowFunctional","ccShowAnalytics","ccShowMarketing","ccVersion","ccSettingsLink","ccSettingsLinkText"],analytics:["gaId"],breadcrumbs:["bcEnabled","bcHomeLabel","bcPosition","bcOffsetX","bcOffsetY"]},SECRET_FIELDS=["smtpPass"],BREADCRUMB_POSITIONS=["TL","TR","BL","BR"];export function footerBlockNames(t){return[...new Set(String(t??"").split(/[\s,]+/).map(r=>r.trim().toLowerCase()).filter(Boolean))]}export function gaIdError(t){const r=o(t).trim();return!r||/^G-[A-Z0-9]{4,20}$/i.test(r)?"":"A measurement ID looks like G-XXXXXXXXXX (Google Analytics > Admin > Data streams)."}export const FOOTER_LAYOUTS=["auto","columns","row"],FOOTER_LOOKS=["","showcase"],FOOTER_FACTS_MAX=4;const l=/^(\/|#|https?:\/\/|mailto:)/,m=/^#[0-9a-f]{6}$/i;export function footerShowcaseErrors(t){const r=[],a=o(t.footerCtaUrl).trim();a&&!l.test(a)&&r.push("The footer call to action link must start with /, #, https:// or mailto: (Footer tab).");for(const[e,i]of[["from",t.footerGradFrom],["to",t.footerGradTo]]){const n=o(i).trim();n&&!m.test(n)&&r.push(`The footer gradient "${e}" colour must look like #f26b3a (Footer tab).`)}return r}const o=(t,r="")=>t==null?r:String(t),s=(t,r)=>{const a=Number.parseInt(t,10);return Number.isFinite(a)?a:r};export function homeValue(t){return/^\/[A-Za-z0-9\-_/]*$/.test(t||"")&&String(t).replace(/\/+$/,"")||"/"}export function formFromSettings(t={}){const r=t.backToTop||{},a=t.goToBottom||{},e=t.cookieConsent||{},i=t.breadcrumbs||{};return{title:o(t.title),tagline:o(t.tagline),siteUrl:o(t.baseUrl),adminHome:homeValue(t.adminHome),spacerSize:o(t.layoutOptions?.spacerSize??40),bodyClass:o(t.bodyClass),brandText:o(t.brand?.text),brandLogo:o(t.brand?.logo),brandIcon:o(t.brand?.icon),brandUrl:o(t.brand?.url,"/"),favicon:o(t.favicon),adminBrandTitle:o(t.adminBrand?.title),adminBrandLogo:o(t.adminBrand?.logo),adminBrandIcon:o(t.adminBrand?.icon),seoTitle:o(t.seo?.defaultTitle),seoSeparator:o(t.seo?.titleSeparator," | "),seoDescription:o(t.seo?.defaultDescription),footerCopyright:o(t.footer?.copyright),footerLayout:FOOTER_LAYOUTS.includes(t.footer?.layout)?t.footer.layout:"auto",footerBlurb:o(t.footer?.blurb),footerBlocks:Array.isArray(t.footer?.blocks)?t.footer.blocks.filter(n=>typeof n=="string").join(", "):"",footerMotionToggle:t.footer?.showMotionToggle!==!1,footerMotionDefault:["off","on"].includes(t.footer?.motionDefault)?t.footer.motionDefault:"system",footerMotionControl:t.footer?.motionControl==="checkbox"?"checkbox":"switch",footerClass:o(t.footer?.class),footerSocialClass:o(t.footer?.socialClass),footerLook:FOOTER_LOOKS.includes(t.footer?.look)?t.footer.look:"",footerCtaText:o(t.footer?.cta?.text),footerCtaHighlight:o(t.footer?.cta?.highlight),footerCtaUrl:o(t.footer?.cta?.url),footerCtaSub:o(t.footer?.cta?.sub),footerWatermark:o(t.footer?.watermark),footerStatus:o(t.footer?.status),footerFacts:Array.isArray(t.footer?.facts)?t.footer.facts.filter(n=>typeof n=="string").join(`
2
- `):"",footerTagline:o(t.footer?.tagline),footerEmail:o(t.footer?.email),footerAddress:o(t.footer?.address),footerGradFrom:o(t.footer?.gradient?.from),footerGradTo:o(t.footer?.gradient?.to),socialTwitter:o(t.social?.twitter),socialFacebook:o(t.social?.facebook),socialInstagram:o(t.social?.instagram),socialLinkedin:o(t.social?.linkedin),socialGithub:o(t.social?.github),socialYoutube:o(t.social?.youtube),smtpHost:o(t.smtp?.host),smtpPort:o(t.smtp?.port||587),smtpUser:o(t.smtp?.user),smtpPass:"",smtpSecure:t.smtp?.secure===!0,smtpInsecureTLS:t.smtp?.allowInsecureTLS===!0,smtpFromAddress:o(t.smtp?.fromAddress),smtpFromName:o(t.smtp?.fromName),bttEnabled:r.enabled!==!1,bttSmooth:r.smooth!==!1,bttLabel:o(r.label),bttThreshold:o(r.scrollThreshold??300),bttPosition:o(r.position,"bottom-right"),bttOffset:o(r.offset??32),bttBottomOffset:o(r.bottomOffset??r.offset??32),gtbEnabled:a.enabled===!0,gtbSmooth:a.smooth!==!1,gtbLabel:o(a.label),gtbThreshold:o(a.scrollThreshold??300),gtbPosition:o(a.position,"bottom-right"),gtbOffset:o(a.offset??32),gtbBottomOffset:o(a.bottomOffset??a.offset??32),ccEnabled:e.enabled!==!1,ccMessage:o(e.message),ccAcceptAll:o(e.acceptAllText,"Accept All"),ccRejectAll:o(e.rejectAllText,"Reject All"),ccCustomize:o(e.customizeText,"Customize"),ccSavePrefs:o(e.savePreferencesText,"Save Preferences"),ccPrivacyText:o(e.privacyPolicyText,"Privacy Policy"),ccPrivacyUrl:o(e.privacyPolicyUrl),ccCookieText:o(e.cookiePolicyText,"Cookie Policy"),ccCookieUrl:o(e.cookiePolicyUrl),ccPosition:o(e.position,"bottom"),ccLayout:o(e.layout,"bar"),ccTheme:o(e.theme,"dark"),ccShowFunctional:e.showFunctional!==!1,ccShowAnalytics:e.showAnalytics!==!1,ccShowMarketing:e.showMarketing!==!1,ccVersion:o(e.consentVersion,"1.0"),ccSettingsLink:e.showSettingsLink!==!1,ccSettingsLinkText:o(e.settingsLinkText,"Cookie settings"),gaId:o(t.googleAnalytics?.measurementId),bcEnabled:i.enabled===!0,bcHomeLabel:o(i.homeLabel,"Home"),bcPosition:BREADCRUMB_POSITIONS.includes(i.position)?i.position:"TL",bcOffsetX:o(i.offsetX??8),bcOffsetY:o(i.offsetY??8)}}export function payloadFromForm(t,r={},{adminHomeReady:a=!0}={}){const e=i=>o(i).trim();return{title:e(t.title),tagline:e(t.tagline),baseUrl:e(t.siteUrl).replace(/\/+$/,""),...a&&{adminHome:e(t.adminHome)||"/"},layoutOptions:{spacerSize:s(t.spacerSize,40)||40},bodyClass:e(t.bodyClass),brand:{...r.brand,text:e(t.brandText),logo:e(t.brandLogo),icon:e(t.brandIcon),url:e(t.brandUrl)||"/"},favicon:e(t.favicon),adminBrand:{...r.adminBrand,title:e(t.adminBrandTitle),logo:e(t.adminBrandLogo),icon:e(t.adminBrandIcon)},seo:{defaultTitle:e(t.seoTitle),titleSeparator:o(t.seoSeparator)||" | ",defaultDescription:e(t.seoDescription)},footer:{copyright:e(t.footerCopyright),layout:FOOTER_LAYOUTS.includes(t.footerLayout)?t.footerLayout:"auto",blurb:e(t.footerBlurb),blocks:footerBlockNames(t.footerBlocks),showMotionToggle:t.footerMotionToggle===!0,links:r.footer?.links||[],motionDefault:["off","on"].includes(t.footerMotionDefault)?t.footerMotionDefault:"system",motionControl:t.footerMotionControl==="checkbox"?"checkbox":"switch",class:e(t.footerClass),socialClass:e(t.footerSocialClass),look:FOOTER_LOOKS.includes(t.footerLook)?t.footerLook:"",cta:{text:e(t.footerCtaText),highlight:e(t.footerCtaHighlight),url:e(t.footerCtaUrl),sub:e(t.footerCtaSub)},watermark:e(t.footerWatermark),status:e(t.footerStatus),facts:String(t.footerFacts??"").split(`
3
- `).map(i=>i.trim()).filter(Boolean).slice(0,FOOTER_FACTS_MAX),tagline:e(t.footerTagline),email:e(t.footerEmail),address:e(t.footerAddress),gradient:{from:e(t.footerGradFrom),to:e(t.footerGradTo)}},social:{twitter:e(t.socialTwitter),facebook:e(t.socialFacebook),instagram:e(t.socialInstagram),linkedin:e(t.socialLinkedin),github:e(t.socialGithub),youtube:e(t.socialYoutube)},smtp:{host:e(t.smtpHost),port:s(t.smtpPort,587)||587,user:e(t.smtpUser),pass:o(t.smtpPass),secure:t.smtpSecure===!0,allowInsecureTLS:t.smtpInsecureTLS===!0,fromAddress:e(t.smtpFromAddress),fromName:e(t.smtpFromName)},backToTop:{enabled:t.bttEnabled===!0,scrollThreshold:s(t.bttThreshold,300),position:o(t.bttPosition)||"bottom-right",offset:s(t.bttOffset,32),bottomOffset:s(t.bttBottomOffset,32),label:e(t.bttLabel),smooth:t.bttSmooth===!0},goToBottom:{enabled:t.gtbEnabled===!0,scrollThreshold:s(t.gtbThreshold,300),position:o(t.gtbPosition)||"bottom-right",offset:s(t.gtbOffset,32),bottomOffset:s(t.gtbBottomOffset,32),label:e(t.gtbLabel),smooth:t.gtbSmooth===!0},cookieConsent:{enabled:t.ccEnabled===!0,message:e(t.ccMessage),acceptAllText:e(t.ccAcceptAll),rejectAllText:e(t.ccRejectAll),customizeText:e(t.ccCustomize),savePreferencesText:e(t.ccSavePrefs),privacyPolicyText:e(t.ccPrivacyText),privacyPolicyUrl:e(t.ccPrivacyUrl),cookiePolicyText:e(t.ccCookieText),cookiePolicyUrl:e(t.ccCookieUrl),position:o(t.ccPosition)||"bottom",layout:o(t.ccLayout)||"bar",theme:o(t.ccTheme)||"dark",showFunctional:t.ccShowFunctional===!0,showAnalytics:t.ccShowAnalytics===!0,showMarketing:t.ccShowMarketing===!0,consentVersion:e(t.ccVersion)||"1.0",showSettingsLink:t.ccSettingsLink===!0,settingsLinkText:e(t.ccSettingsLinkText)||"Cookie settings"},googleAnalytics:{measurementId:e(t.gaId).toUpperCase()},breadcrumbs:{enabled:t.bcEnabled===!0,homeLabel:e(t.bcHomeLabel)||"Home",position:BREADCRUMB_POSITIONS.includes(t.bcPosition)?t.bcPosition:"TL",offsetX:s(t.bcOffsetX,8),offsetY:s(t.bcOffsetY,8)}}}export function siteUrlError(t){const r=o(t).trim().replace(/\/+$/,"");return r?/^https?:\/\//i.test(r)?/^https?:\/\/([a-z0-9.\-]+|\[[0-9a-f:.]+\])(:\d{1,5})?$/i.test(r)?"":"Just the address, like https://example.com - no page path after it.":"Start with https:// (or http://).":""}const c=t=>typeof t=="boolean"?t:o(t);export function snapshot(t){return JSON.stringify(Object.keys(t).sort().map(r=>[r,c(t[r])]))}export function changedTabs(t,r){const a=[];for(const[e,i]of Object.entries(TAB_FIELDS))i.some(n=>c(t[n])!==c(r[n]))&&a.push(e);return a}export function draftOf(t){return Object.fromEntries(Object.entries(t).filter(([r])=>!SECRET_FIELDS.includes(r)))}export function settingsProblems(t){const r=[];return o(t.smtpHost).trim()?o(t.smtpFromAddress).trim()||r.push({tone:"warn",tab:"email",text:"No From address - mail has no sender and many servers refuse it."}):r.push({tone:"danger",tab:"email",text:"No SMTP host - mail goes to a throwaway test inbox and reaches no one (password resets, form notifications)."}),(t.ccEnabled===!0||o(t.gaId).trim())&&!o(t.ccPrivacyUrl).trim()&&r.push({tone:"warn",tab:"cookies",text:"The cookie banner is on but links to no privacy policy."}),r}export function describeCacheKey(t){const r=/^page:(.*?):m\d+:role([^:]*):/.exec(String(t||""));return r?{path:r[1]||"/",role:r[2]||"anon",page:!0}:{path:String(t||""),role:"",page:!1}}export function expiresIn(t,r=Date.now()){if(t==null)return"no expiry";const a=t-r;return a<=0?"expired":a<6e4?`${Math.round(a/1e3)}s`:a<36e5?`${Math.round(a/6e4)}m`:`${Math.round(a/36e5)}h`}
1
+ export const TAB_FIELDS={general:["title","tagline","siteUrl","adminHome","spacerSize","bodyClass"],branding:["brandText","brandLogo","brandIcon","brandUrl","adminBrandTitle","adminBrandLogo","adminBrandIcon"],favicon:["favicon"],seo:["seoTitle","seoSeparator","seoDescription"],footer:["footerCopyright","footerLayout","footerBlurb","footerBlocks","footerClass","footerSocialClass","footerLook","footerCtaText","footerCtaHighlight","footerCtaUrl","footerCtaSub","footerWatermark","footerStatus","footerFacts","footerTagline","footerEmail","footerAddress","footerGradFrom","footerGradTo","socialTwitter","socialFacebook","socialInstagram","socialLinkedin","socialGithub","socialYoutube"],email:["smtpHost","smtpPort","smtpUser","smtpPass","smtpSecure","smtpInsecureTLS","smtpFromAddress","smtpFromName"],pageNavigation:["bttEnabled","bttSmooth","bttLabel","bttThreshold","bttPosition","bttOffset","bttBottomOffset","gtbEnabled","gtbSmooth","gtbLabel","gtbThreshold","gtbPosition","gtbOffset","gtbBottomOffset"],cookies:["ccEnabled","ccMessage","ccAcceptAll","ccRejectAll","ccCustomize","ccSavePrefs","ccPrivacyText","ccPrivacyUrl","ccCookieText","ccCookieUrl","ccPosition","ccLayout","ccTheme","ccShowFunctional","ccShowAnalytics","ccShowMarketing","ccVersion","ccSettingsLink","ccSettingsLinkText"],analytics:["gaId"],breadcrumbs:["bcEnabled","bcHomeLabel","bcPosition","bcOffsetX","bcOffsetY"],motion:["mtEnabled","mtLabel","mtDefault","mtControl","mtPlacement","mtTriggerStyle","mtPosition","mtOffsetX","mtOffsetY","mtIconSize","mtIconRadius","mtClass"]},SECRET_FIELDS=["smtpPass"],BREADCRUMB_POSITIONS=["TL","TR","BL","BR"],MOTION_PLACEMENTS=["footer","floating","navbar"],MOTION_LABEL="Reduce motion";const u=["system","off","on"],c=(t,r,i,e)=>{const n=Number.parseInt(t,10);return Number.isFinite(n)?Math.min(e,Math.max(i,n)):r};export function motionFromSettings(t={}){const r=t.motion&&typeof t.motion=="object"?t.motion:{},i=t.footer||{},e=b=>r[b]!==void 0&&r[b]!==null,n=e("default")?r.default:i.motionDefault,s=e("control")?r.control:i.motionControl,m=c(r.iconSize,52,32,96);return{mtEnabled:(e("enabled")?r.enabled:i.showMotionToggle)!==!1,mtLabel:typeof r.label=="string"&&r.label.trim()?r.label.trim():MOTION_LABEL,mtDefault:u.includes(n)?n:"system",mtControl:s==="checkbox"?"checkbox":"switch",mtPlacement:MOTION_PLACEMENTS.includes(r.placement)?r.placement:"footer",mtTriggerStyle:r.triggerStyle==="icon-text"?"icon-text":"icon",mtPosition:BREADCRUMB_POSITIONS.includes(r.position)?r.position:"BL",mtOffsetX:o(c(r.offsetX,32,0,500)),mtOffsetY:o(c(r.offsetY,32,0,500)),mtIconSize:o(m),mtIconRadius:o(c(r.iconRadius,26,0,Math.floor(m/2))),mtClass:o(r.class)}}export function motionFromForm(t){const r=c(t.mtIconSize,52,32,96);return{enabled:t.mtEnabled===!0,default:u.includes(t.mtDefault)?t.mtDefault:"system",placement:MOTION_PLACEMENTS.includes(t.mtPlacement)?t.mtPlacement:"footer",control:t.mtControl==="checkbox"?"checkbox":"switch",triggerStyle:t.mtTriggerStyle==="icon-text"?"icon-text":"icon",label:o(t.mtLabel).trim().replace(/\s+/g," ").slice(0,40)||MOTION_LABEL,position:BREADCRUMB_POSITIONS.includes(t.mtPosition)?t.mtPosition:"BL",offsetX:c(t.mtOffsetX,32,0,500),offsetY:c(t.mtOffsetY,32,0,500),iconSize:r,iconRadius:c(t.mtIconRadius,26,0,Math.floor(r/2)),class:o(t.mtClass).trim()}}export function footerBlockNames(t){return[...new Set(String(t??"").split(/[\s,]+/).map(r=>r.trim().toLowerCase()).filter(Boolean))]}export function gaIdError(t){const r=o(t).trim();return!r||/^G-[A-Z0-9]{4,20}$/i.test(r)?"":"A measurement ID looks like G-XXXXXXXXXX (Google Analytics > Admin > Data streams)."}export const FOOTER_LAYOUTS=["auto","columns","row"],FOOTER_LOOKS=["","showcase"],FOOTER_FACTS_MAX=4;const d=/^(\/|#|https?:\/\/|mailto:)/,f=/^#[0-9a-f]{6}$/i;export function footerShowcaseErrors(t){const r=[],i=o(t.footerCtaUrl).trim();i&&!d.test(i)&&r.push("The footer call to action link must start with /, #, https:// or mailto: (Footer tab).");for(const[e,n]of[["from",t.footerGradFrom],["to",t.footerGradTo]]){const s=o(n).trim();s&&!f.test(s)&&r.push(`The footer gradient "${e}" colour must look like #f26b3a (Footer tab).`)}return r}const o=(t,r="")=>t==null?r:String(t),a=(t,r)=>{const i=Number.parseInt(t,10);return Number.isFinite(i)?i:r};export function homeValue(t){return/^\/[A-Za-z0-9\-_/]*$/.test(t||"")&&String(t).replace(/\/+$/,"")||"/"}export function formFromSettings(t={}){const r=t.backToTop||{},i=t.goToBottom||{},e=t.cookieConsent||{},n=t.breadcrumbs||{};return{title:o(t.title),tagline:o(t.tagline),siteUrl:o(t.baseUrl),adminHome:homeValue(t.adminHome),spacerSize:o(t.layoutOptions?.spacerSize??40),bodyClass:o(t.bodyClass),brandText:o(t.brand?.text),brandLogo:o(t.brand?.logo),brandIcon:o(t.brand?.icon),brandUrl:o(t.brand?.url,"/"),favicon:o(t.favicon),adminBrandTitle:o(t.adminBrand?.title),adminBrandLogo:o(t.adminBrand?.logo),adminBrandIcon:o(t.adminBrand?.icon),seoTitle:o(t.seo?.defaultTitle),seoSeparator:o(t.seo?.titleSeparator," | "),seoDescription:o(t.seo?.defaultDescription),footerCopyright:o(t.footer?.copyright),footerLayout:FOOTER_LAYOUTS.includes(t.footer?.layout)?t.footer.layout:"auto",footerBlurb:o(t.footer?.blurb),footerBlocks:Array.isArray(t.footer?.blocks)?t.footer.blocks.filter(s=>typeof s=="string").join(", "):"",footerClass:o(t.footer?.class),footerSocialClass:o(t.footer?.socialClass),footerLook:FOOTER_LOOKS.includes(t.footer?.look)?t.footer.look:"",footerCtaText:o(t.footer?.cta?.text),footerCtaHighlight:o(t.footer?.cta?.highlight),footerCtaUrl:o(t.footer?.cta?.url),footerCtaSub:o(t.footer?.cta?.sub),footerWatermark:o(t.footer?.watermark),footerStatus:o(t.footer?.status),footerFacts:Array.isArray(t.footer?.facts)?t.footer.facts.filter(s=>typeof s=="string").join(`
2
+ `):"",footerTagline:o(t.footer?.tagline),footerEmail:o(t.footer?.email),footerAddress:o(t.footer?.address),footerGradFrom:o(t.footer?.gradient?.from),footerGradTo:o(t.footer?.gradient?.to),socialTwitter:o(t.social?.twitter),socialFacebook:o(t.social?.facebook),socialInstagram:o(t.social?.instagram),socialLinkedin:o(t.social?.linkedin),socialGithub:o(t.social?.github),socialYoutube:o(t.social?.youtube),smtpHost:o(t.smtp?.host),smtpPort:o(t.smtp?.port||587),smtpUser:o(t.smtp?.user),smtpPass:"",smtpSecure:t.smtp?.secure===!0,smtpInsecureTLS:t.smtp?.allowInsecureTLS===!0,smtpFromAddress:o(t.smtp?.fromAddress),smtpFromName:o(t.smtp?.fromName),bttEnabled:r.enabled!==!1,bttSmooth:r.smooth!==!1,bttLabel:o(r.label),bttThreshold:o(r.scrollThreshold??300),bttPosition:o(r.position,"bottom-right"),bttOffset:o(r.offset??32),bttBottomOffset:o(r.bottomOffset??r.offset??32),gtbEnabled:i.enabled===!0,gtbSmooth:i.smooth!==!1,gtbLabel:o(i.label),gtbThreshold:o(i.scrollThreshold??300),gtbPosition:o(i.position,"bottom-right"),gtbOffset:o(i.offset??32),gtbBottomOffset:o(i.bottomOffset??i.offset??32),ccEnabled:e.enabled!==!1,ccMessage:o(e.message),ccAcceptAll:o(e.acceptAllText,"Accept All"),ccRejectAll:o(e.rejectAllText,"Reject All"),ccCustomize:o(e.customizeText,"Customize"),ccSavePrefs:o(e.savePreferencesText,"Save Preferences"),ccPrivacyText:o(e.privacyPolicyText,"Privacy Policy"),ccPrivacyUrl:o(e.privacyPolicyUrl),ccCookieText:o(e.cookiePolicyText,"Cookie Policy"),ccCookieUrl:o(e.cookiePolicyUrl),ccPosition:o(e.position,"bottom"),ccLayout:o(e.layout,"bar"),ccTheme:o(e.theme,"dark"),ccShowFunctional:e.showFunctional!==!1,ccShowAnalytics:e.showAnalytics!==!1,ccShowMarketing:e.showMarketing!==!1,ccVersion:o(e.consentVersion,"1.0"),ccSettingsLink:e.showSettingsLink!==!1,ccSettingsLinkText:o(e.settingsLinkText,"Cookie settings"),gaId:o(t.googleAnalytics?.measurementId),bcEnabled:n.enabled===!0,bcHomeLabel:o(n.homeLabel,"Home"),bcPosition:BREADCRUMB_POSITIONS.includes(n.position)?n.position:"TL",bcOffsetX:o(n.offsetX??8),bcOffsetY:o(n.offsetY??8),...motionFromSettings(t)}}export function payloadFromForm(t,r={},{adminHomeReady:i=!0}={}){const e=s=>o(s).trim(),n=motionFromForm(t);return{title:e(t.title),tagline:e(t.tagline),baseUrl:e(t.siteUrl).replace(/\/+$/,""),...i&&{adminHome:e(t.adminHome)||"/"},layoutOptions:{spacerSize:a(t.spacerSize,40)||40},bodyClass:e(t.bodyClass),brand:{...r.brand,text:e(t.brandText),logo:e(t.brandLogo),icon:e(t.brandIcon),url:e(t.brandUrl)||"/"},favicon:e(t.favicon),adminBrand:{...r.adminBrand,title:e(t.adminBrandTitle),logo:e(t.adminBrandLogo),icon:e(t.adminBrandIcon)},seo:{defaultTitle:e(t.seoTitle),titleSeparator:o(t.seoSeparator)||" | ",defaultDescription:e(t.seoDescription)},footer:{copyright:e(t.footerCopyright),layout:FOOTER_LAYOUTS.includes(t.footerLayout)?t.footerLayout:"auto",blurb:e(t.footerBlurb),blocks:footerBlockNames(t.footerBlocks),links:r.footer?.links||[],showMotionToggle:n.enabled,motionDefault:n.default,motionControl:n.control,class:e(t.footerClass),socialClass:e(t.footerSocialClass),look:FOOTER_LOOKS.includes(t.footerLook)?t.footerLook:"",cta:{text:e(t.footerCtaText),highlight:e(t.footerCtaHighlight),url:e(t.footerCtaUrl),sub:e(t.footerCtaSub)},watermark:e(t.footerWatermark),status:e(t.footerStatus),facts:String(t.footerFacts??"").split(`
3
+ `).map(s=>s.trim()).filter(Boolean).slice(0,FOOTER_FACTS_MAX),tagline:e(t.footerTagline),email:e(t.footerEmail),address:e(t.footerAddress),gradient:{from:e(t.footerGradFrom),to:e(t.footerGradTo)}},social:{twitter:e(t.socialTwitter),facebook:e(t.socialFacebook),instagram:e(t.socialInstagram),linkedin:e(t.socialLinkedin),github:e(t.socialGithub),youtube:e(t.socialYoutube)},smtp:{host:e(t.smtpHost),port:a(t.smtpPort,587)||587,user:e(t.smtpUser),pass:o(t.smtpPass),secure:t.smtpSecure===!0,allowInsecureTLS:t.smtpInsecureTLS===!0,fromAddress:e(t.smtpFromAddress),fromName:e(t.smtpFromName)},backToTop:{enabled:t.bttEnabled===!0,scrollThreshold:a(t.bttThreshold,300),position:o(t.bttPosition)||"bottom-right",offset:a(t.bttOffset,32),bottomOffset:a(t.bttBottomOffset,32),label:e(t.bttLabel),smooth:t.bttSmooth===!0},goToBottom:{enabled:t.gtbEnabled===!0,scrollThreshold:a(t.gtbThreshold,300),position:o(t.gtbPosition)||"bottom-right",offset:a(t.gtbOffset,32),bottomOffset:a(t.gtbBottomOffset,32),label:e(t.gtbLabel),smooth:t.gtbSmooth===!0},cookieConsent:{enabled:t.ccEnabled===!0,message:e(t.ccMessage),acceptAllText:e(t.ccAcceptAll),rejectAllText:e(t.ccRejectAll),customizeText:e(t.ccCustomize),savePreferencesText:e(t.ccSavePrefs),privacyPolicyText:e(t.ccPrivacyText),privacyPolicyUrl:e(t.ccPrivacyUrl),cookiePolicyText:e(t.ccCookieText),cookiePolicyUrl:e(t.ccCookieUrl),position:o(t.ccPosition)||"bottom",layout:o(t.ccLayout)||"bar",theme:o(t.ccTheme)||"dark",showFunctional:t.ccShowFunctional===!0,showAnalytics:t.ccShowAnalytics===!0,showMarketing:t.ccShowMarketing===!0,consentVersion:e(t.ccVersion)||"1.0",showSettingsLink:t.ccSettingsLink===!0,settingsLinkText:e(t.ccSettingsLinkText)||"Cookie settings"},googleAnalytics:{measurementId:e(t.gaId).toUpperCase()},breadcrumbs:{enabled:t.bcEnabled===!0,homeLabel:e(t.bcHomeLabel)||"Home",position:BREADCRUMB_POSITIONS.includes(t.bcPosition)?t.bcPosition:"TL",offsetX:a(t.bcOffsetX,8),offsetY:a(t.bcOffsetY,8)},motion:n}}export function siteUrlError(t){const r=o(t).trim().replace(/\/+$/,"");return r?/^https?:\/\//i.test(r)?/^https?:\/\/([a-z0-9.\-]+|\[[0-9a-f:.]+\])(:\d{1,5})?$/i.test(r)?"":"Just the address, like https://example.com - no page path after it.":"Start with https:// (or http://).":""}const l=t=>typeof t=="boolean"?t:o(t);export function snapshot(t){return JSON.stringify(Object.keys(t).sort().map(r=>[r,l(t[r])]))}export function changedTabs(t,r){const i=[];for(const[e,n]of Object.entries(TAB_FIELDS))n.some(s=>l(t[s])!==l(r[s]))&&i.push(e);return i}export function draftOf(t){return Object.fromEntries(Object.entries(t).filter(([r])=>!SECRET_FIELDS.includes(r)))}const p={footerMotionToggle:"mtEnabled",footerMotionDefault:"mtDefault",footerMotionControl:"mtControl"};export function formFromDraft(t,r={}){const i={...r};for(const[n,s]of Object.entries(p))n in i&&!(s in i)&&(i[s]=i[n]),delete i[n];const e={...t};for(const[n,s]of Object.entries(i))n in e&&!SECRET_FIELDS.includes(n)&&(e[n]=s);return e}export function settingsProblems(t){const r=[];return o(t.smtpHost).trim()?o(t.smtpFromAddress).trim()||r.push({tone:"warn",tab:"email",text:"No From address - mail has no sender and many servers refuse it."}):r.push({tone:"danger",tab:"email",text:"No SMTP host - mail goes to a throwaway test inbox and reaches no one (password resets, form notifications)."}),(t.ccEnabled===!0||o(t.gaId).trim())&&!o(t.ccPrivacyUrl).trim()&&r.push({tone:"warn",tab:"cookies",text:"The cookie banner is on but links to no privacy policy."}),r}export function describeCacheKey(t){const r=/^page:(.*?):m\d+:role([^:]*):/.exec(String(t||""));return r?{path:r[1]||"/",role:r[2]||"anon",page:!0}:{path:String(t||""),role:"",page:!1}}export function expiresIn(t,r=Date.now()){if(t==null)return"no expiry";const i=t-r;return i<=0?"expired":i<6e4?`${Math.round(i/1e3)}s`:i<36e5?`${Math.round(i/6e4)}m`:`${Math.round(i/36e5)}h`}
@@ -42,10 +42,21 @@
42
42
  </tr>
43
43
  <tr>
44
44
  <td>Footer</td>
45
- <td>Copyright text, layout, blurb, social links, and whether visitors get a "Reduce motion"
46
- switch. Footer links are menus - see <a href="#/docs/usage/navigation">Navigation</a>.
45
+ <td>Copyright text, layout, blurb, footer blocks and social links.
46
+ Footer links are menus - see <a href="#/docs/usage/navigation">Navigation</a>.
47
47
  The <a href="#showcase-footer">Showcase footer</a> card adds a big call to action and more.</td>
48
48
  </tr>
49
+ <tr>
50
+ <td>Reduce motion</td>
51
+ <td>The accessibility control that lets visitors turn animations off: whether it is shown,
52
+ its label, and what a visitor who has not chosen gets (follow their device, full motion,
53
+ or calm). <strong>Placement</strong> puts it in the footer (a switch or a checkbox), as a
54
+ floating button in a corner you pick - with its size, corners and distance from the
55
+ edges - or at the end of the navbar (floating where there is no navbar, or on a phone
56
+ when the navbar shows only its menu button). A floating button sharing a corner with Search,
57
+ Back to top or the theme switcher stacks clear of it. Hiding the control does not take
58
+ the choice away from visitors who already made it.</td>
59
+ </tr>
49
60
  <tr>
50
61
  <td>Email</td>
51
62
  <td>The SMTP server the site sends mail through (form notifications, password resets,
@@ -291,7 +291,7 @@
291
291
  </div>
292
292
  <div class="form-group">
293
293
  <label>Staggered entrance
294
- <span data-help-title="Staggered entrance" data-help="The items arrive one after another when the menu is drawn, like a row of cards revealing. A dropdown or footer column staggers in on its own when it opens. Visitors who have asked for less motion (their device setting, or the site's motion switch) see the items straight away."></span></label>
294
+ <span data-help-title="Staggered entrance" data-help="The items arrive one after another when the menu is drawn, like a row of cards revealing. A dropdown or footer column staggers in on its own when it opens. Visitors who have asked for less motion (their device setting, or the site's Reduce motion control) see the items straight away."></span></label>
295
295
  <label style="display:flex;align-items:center;gap:.5rem;font-weight:400;cursor:pointer">
296
296
  <input type="checkbox" id="me-entrance-enabled" name="entranceEnabled" style="width:auto;margin:0">
297
297
  Bring the items in one after another
@@ -25,6 +25,7 @@
25
25
  <button type="button" class="tab-item">Cookie consent <i class="stx-dot" data-bind-hidden="!tabs.value.cookies" title="Changed"></i></button>
26
26
  <button type="button" class="tab-item">Analytics <i class="stx-dot" data-bind-hidden="!tabs.value.analytics" title="Changed"></i></button>
27
27
  <button type="button" class="tab-item">Breadcrumbs <i class="stx-dot" data-bind-hidden="!tabs.value.breadcrumbs" title="Changed"></i></button>
28
+ <button type="button" class="tab-item">Reduce motion <i class="stx-dot" data-bind-hidden="!tabs.value.motion" title="Changed"></i></button>
28
29
  <button type="button" class="tab-item">Cache</button>
29
30
  </div>
30
31
 
@@ -203,28 +204,8 @@
203
204
  <span data-help="Blocks (Content › Blocks) shown side by side at the top of the footer on every page, by name, in order - for example wp-footer-1, wp-footer-2. Import from WordPress puts a site's footer widget areas here. Leave it empty for none; a name that is not a block is left out."></span></label>
204
205
  <input id="stx-footer-blocks" type="text" class="form-input" maxlength="400" placeholder="wp-footer-1, wp-footer-2" data-model="footerBlocks.value">
205
206
  </div>
206
- <div class="stx-wide stx-setting">
207
- <label class="form-label" for="stx-motion">Show a “Reduce motion” switch
208
- <span data-help="An accessibility switch in the footer that lets visitors turn animations off. Turning this off only hides the switch - visitors who already chose it, and anyone whose browser asks for reduced motion, still get a calmer site."></span></label>
209
- <input id="stx-motion" type="checkbox" class="form-switch" data-model="footerMotionToggle.value">
210
- </div>
211
- <div class="stx-setting">
212
- <label class="form-label" for="stx-motion-default">Reduced motion for new visitors
213
- <span data-help="What a visitor who has not used the switch gets. Follow their device honours the system setting (the accessible default). Off shows every animation until they ask for less; On starts calm. A visitor's own choice always wins."></span></label>
214
- <select id="stx-motion-default" class="form-input" data-model="footerMotionDefault.value">
215
- <option value="system">Follow their device</option>
216
- <option value="off">Off - full motion</option>
217
- <option value="on">On - calm</option>
218
- </select>
219
- </div>
220
- <div class="stx-setting">
221
- <label class="form-label" for="stx-motion-control">The control
222
- <span data-help="How the Reduce motion control looks in the footer."></span></label>
223
- <select id="stx-motion-control" class="form-input" data-model="footerMotionControl.value">
224
- <option value="switch">A switch</option>
225
- <option value="checkbox">A checkbox</option>
226
- </select>
227
- </div>
207
+ <p class="stx-wide stx-hint">The “Reduce motion” control now has a tab of its own:
208
+ <button type="button" class="stx-inline-link" data-on-click="goTab('motion')">Reduce motion</button>.</p>
228
209
  <div>
229
210
  <label class="form-label" for="stx-footer-class">Footer CSS classes
230
211
  <span data-help-title="Footer CSS classes" data-help="Your own class names, separated by spaces, added to the site footer. Style them in Custom CSS. The footer's menus take classes of their own in Menus."></span></label>
@@ -584,6 +565,91 @@
584
565
  </div>
585
566
  </div>
586
567
 
568
+ <!-- Reduce motion -->
569
+ <div class="tab-panel">
570
+ <div class="stx-card-columns">
571
+ <div class="stx-card">
572
+ <div class="stx-card-title"><span data-icon="pause" data-icon-size="16" aria-hidden="true"></span>The control
573
+ <span data-help-title="Reduce motion" data-help="An accessibility control that lets visitors turn animations off. Turning it off only hides the control - visitors who already chose it, and anyone whose browser asks for reduced motion, still get a calmer site."></span></div>
574
+ <div class="stx-grid">
575
+ <div class="stx-wide stx-setting"><label class="form-label" for="stx-mt-on">Show it</label>
576
+ <input id="stx-mt-on" type="checkbox" class="form-switch" data-model="mtEnabled.value"></div>
577
+ <div class="stx-wide"><label class="form-label" for="stx-mt-label">Label
578
+ <span data-help="The words on the control, and what a screen reader announces. Up to 40 characters."></span></label>
579
+ <input id="stx-mt-label" type="text" class="form-input" maxlength="40" placeholder="Reduce motion" data-model="mtLabel.value"></div>
580
+ <div class="stx-wide stx-setting">
581
+ <label class="form-label" for="stx-motion-default">Reduced motion for new visitors
582
+ <span data-help="What a visitor who has not used the control gets. Follow their device honours the system setting (the accessible default). Off shows every animation until they ask for less; On starts calm. A visitor's own choice always wins."></span></label>
583
+ <select id="stx-motion-default" class="form-input" data-model="mtDefault.value">
584
+ <option value="system">Follow their device</option>
585
+ <option value="off">Off - full motion</option>
586
+ <option value="on">On - calm</option>
587
+ </select>
588
+ </div>
589
+ <div class="stx-wide stx-setting" data-bind-hidden="mtPlacement.value !== 'footer'">
590
+ <label class="form-label" for="stx-motion-control">Footer style
591
+ <span data-help="How the Reduce motion control looks in the footer."></span></label>
592
+ <select id="stx-motion-control" class="form-input" data-model="mtControl.value">
593
+ <option value="switch">A switch</option>
594
+ <option value="checkbox">A checkbox</option>
595
+ </select>
596
+ </div>
597
+ <div class="stx-wide">
598
+ <label class="form-label" for="stx-mt-class">CSS classes
599
+ <span data-help="Your own class names, separated by spaces, added to the control wherever it sits. Style them in Custom CSS."></span></label>
600
+ <input id="stx-mt-class" type="text" class="form-input" placeholder="motion-quiet" autocomplete="off" spellcheck="false" data-model="mtClass.value">
601
+ </div>
602
+ </div>
603
+ </div>
604
+ <div class="stx-card">
605
+ <div class="stx-card-title"><span data-icon="layout" data-icon-size="16" aria-hidden="true"></span>Placement
606
+ <span data-help-title="Placement" data-help="Footer puts it at the foot of every page, as it always was. Floating button is a disc fixed to a corner of the window, kept clear of Search, Back to top and the theme switcher. Navbar adds it at the end of the navbar - on a page with no navbar, or on a phone when the navbar shows only its menu button, it floats instead."></span></div>
607
+ <div class="stx-grid">
608
+ <div class="stx-wide">
609
+ <span class="form-label">Where it goes</span>
610
+ <div class="stx-segments" role="group" aria-label="Where it goes">
611
+ <button type="button" data-on-click="motionPlacement('footer')" data-bind-class="mtPlacement.value === 'footer' && 'is-on'">Footer</button>
612
+ <button type="button" data-on-click="motionPlacement('floating')" data-bind-class="mtPlacement.value === 'floating' && 'is-on'">Floating button</button>
613
+ <button type="button" data-on-click="motionPlacement('navbar')" data-bind-class="mtPlacement.value === 'navbar' && 'is-on'">Navbar</button>
614
+ </div>
615
+ </div>
616
+ <div class="stx-setting stx-wide" data-bind-hidden="mtPlacement.value === 'footer'">
617
+ <label class="form-label" for="stx-mt-style">Button
618
+ <span data-help="An icon on its own (the label is still read out by screen readers), or the icon with the label beside it."></span></label>
619
+ <select id="stx-mt-style" class="form-input" data-model="mtTriggerStyle.value">
620
+ <option value="icon">Icon</option>
621
+ <option value="icon-text">Icon and label</option>
622
+ </select>
623
+ </div>
624
+ <div class="stx-wide" data-bind-hidden="mtPlacement.value !== 'floating'">
625
+ <span class="form-label">Corner</span>
626
+ <div class="stx-corners" role="group" aria-label="Corner">
627
+ <button type="button" data-on-click="motionCorner('TL')" data-bind-class="mtPosition.value === 'TL' && 'is-on'">↖ Top left</button>
628
+ <button type="button" data-on-click="motionCorner('TR')" data-bind-class="mtPosition.value === 'TR' && 'is-on'">Top right ↗</button>
629
+ <button type="button" data-on-click="motionCorner('BL')" data-bind-class="mtPosition.value === 'BL' && 'is-on'">↙ Bottom left</button>
630
+ <button type="button" data-on-click="motionCorner('BR')" data-bind-class="mtPosition.value === 'BR' && 'is-on'">Bottom right ↘</button>
631
+ </div>
632
+ </div>
633
+ <div class="stx-pair stx-wide" data-bind-hidden="mtPlacement.value !== 'floating'">
634
+ <div><label class="form-label" for="stx-mt-x">From the side (px)</label>
635
+ <input id="stx-mt-x" type="number" class="form-input" min="0" max="500" placeholder="32" data-model="mtOffsetX.value"></div>
636
+ <div><label class="form-label" for="stx-mt-y">From the top/bottom (px)</label>
637
+ <input id="stx-mt-y" type="number" class="form-input" min="0" max="500" placeholder="32" data-model="mtOffsetY.value"></div>
638
+ </div>
639
+ <div class="stx-pair stx-wide" data-bind-hidden="mtPlacement.value !== 'floating' || mtTriggerStyle.value !== 'icon'">
640
+ <div><label class="form-label" for="stx-mt-size">Size <span class="stx-readout" data-bind-text="mtSizeText.value"></span>
641
+ <span data-help="How big the disc is (32-96 px). The glyph inside scales with it. 44 px and up is comfortable to tap on a phone."></span></label>
642
+ <input id="stx-mt-size" type="range" min="32" max="96" step="2" data-model="mtIconSize.value"></div>
643
+ <div><label class="form-label" for="stx-mt-radius">Corners <span class="stx-readout" data-bind-text="mtRadiusText.value"></span>
644
+ <span data-help="0 is a square; the top of the range is a circle, and it is always half the size - so it moves when the size does."></span></label>
645
+ <input id="stx-mt-radius" type="range" min="0" step="1" data-model="mtIconRadius.value"></div>
646
+ </div>
647
+ </div>
648
+ <p class="stx-hint" data-bind-hidden="mtPlacement.value !== 'floating'">If Search, Back to top or the theme switcher sits in the same corner, the disc stacks clear of it, further from the edge.</p>
649
+ </div>
650
+ </div>
651
+ </div>
652
+
587
653
  <!-- Cache -->
588
654
  <div class="tab-panel">
589
655
  <div class="stx-card">
@@ -662,6 +728,14 @@
662
728
  .stx-corners button:nth-child(-n+2) { border-bottom: 1px solid var(--dm-border); }
663
729
  .stx-corners button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
664
730
  .stx-corners button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
731
+ .stx-segments { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
732
+ .stx-segments button { border: 0; background: none; color: var(--dm-text); padding: .45rem .8rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
733
+ .stx-segments button + button { border-left: 1px solid var(--dm-border); }
734
+ .stx-segments button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
735
+ .stx-segments button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
736
+ .stx-readout { color: var(--dm-text-muted); font-weight: 400; font-variant-numeric: tabular-nums; }
737
+ .stx-pair input[type="range"] { width: 100%; }
738
+ .stx-inline-link { background: none; border: 0; padding: 0; font: inherit; color: var(--dm-primary); text-decoration: underline; cursor: pointer; }
665
739
  .stx-chips { display: flex; gap: .35rem; flex-wrap: wrap; }
666
740
  .stx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
667
741
  background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
@@ -1,11 +1,11 @@
1
- import{api as B,apiRequest as C}from"../api.js";import{placeTools as Ie}from"../lib/sidebar-grouping.js";import{addBannerAction as me}from"../lib/plugin-chrome.js";import{helpPopovers as Ae}from"../lib/help-popover.js";import{openMenuAt as Y,sidebarCount as fe}from"../lib/tool-kit.js";import{changedTabs as Pe,describeCacheKey as J,draftOf as $e,expiresIn as je,footerShowcaseErrors as Be,formFromSettings as De,gaIdError as he,homeValue as He,payloadFromForm as qe,siteUrlError as ve,settingsProblems as Fe,snapshot as be,TAB_FIELDS as Ue}from"../lib/site-settings-arrange.js";import{pickMedia as _e}from"../lib/media-picker.js";import{faviconGenerator as Ke}from"../lib/favicon-generator.js";import{makeIconInput as Ne}from"../lib/shortcode-modal.js";import{fmtDateTime as ze}from"../lib/dates.js";const Q=/^\/settings(\?.*)?$/,ge="#/settings",w="settings.draft",Ge=`
1
+ import{api as D,apiRequest as k}from"../api.js";import{placeTools as $e}from"../lib/sidebar-grouping.js";import{addBannerAction as fe}from"../lib/plugin-chrome.js";import{helpPopovers as Ae}from"../lib/help-popover.js";import{openMenuAt as V,sidebarCount as he}from"../lib/tool-kit.js";import{changedTabs as De,describeCacheKey as W,draftOf as Be,expiresIn as He,footerShowcaseErrors as je,formFromDraft as qe,formFromSettings as Fe,gaIdError as ve,homeValue as Ue,payloadFromForm as _e,siteUrlError as be,settingsProblems as Ke,snapshot as ge,TAB_FIELDS as ze}from"../lib/site-settings-arrange.js";import{pickMedia as Ne}from"../lib/media-picker.js";import{faviconGenerator as Ge}from"../lib/favicon-generator.js";import{makeIconInput as Ve}from"../lib/shortcode-modal.js";import{fmtDateTime as We}from"../lib/dates.js";const Y=/^\/settings(\?.*)?$/,ye="#/settings",y="settings.draft",Ye=`
2
2
  <div class="stx-problem">
3
3
  <span class="stx-chip" data-bind-class="tone === 'danger' ? 'is-bad' : 'is-warn'">{{label}}</span>
4
4
  <span>{{text}}</span>
5
5
  <button type="button" class="btn btn-sm btn-ghost" data-go="{{tab}}">Go there</button>
6
- </div>`,Ve=`
6
+ </div>`,Je=`
7
7
  <div class="stx-row" data-id="{{id}}" tabindex="0">
8
8
  <span class="stx-main"><strong>{{path}}</strong><small>{{meta}}</small></span>
9
9
  <span class="stx-chip">{{expires}}</span>
10
10
  <button type="button" class="stx-kebab" aria-label="Actions for {{path}}" title="Actions">\u22EE</button>
11
- </div>`,We=["general","branding","favicon","seo","footer","email","pageNavigation","cookies","analytics","breadcrumbs","cache"];async function Ye(l,u){const[r,k]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),n=new Map([["/","Dashboard (built in)"]]),i=(v,d)=>{for(const o of v||[]){if(!o||o.hidden||o.restricted)continue;const m=typeof o.url=="string"?o.url:"";m.startsWith("#/")&&m.length>2&&!n.has(m.slice(1))&&n.set(m.slice(1),`${d?`${d} \u203A `:""}${o.text||m}`),i(o.items,o.url?d:o.text||d)}};i(Ie(r?.items||[],k||[]),""),n.has(u)||n.set(u,`${u} (not in the sidebar)`),l.replaceChildren();for(const[v,d]of n){const o=document.createElement("option");o.value=v,o.textContent=d,l.appendChild(o)}}let f=null,ye=!1;function Je(){ye||!window.R?.use||(ye=!0,R.use((l,u,r)=>!(Q.test(u?.path||"")&&!Q.test(l?.path||""))||!f?.dirty()?r():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(n=>{n?r():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:l}={})=>{if(f&&!Q.test(l?.path||"")){const u=f;f=null,u.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(l){if(Je(),f){const e=f;f=null,e.dispose()}const u=[],r=e=>(u.push(e),e);let k=!1;const n=l.get(0).querySelector(".stx")||l.get(0),i=()=>!k&&n.isConnected,v=E.loader(l.get(0),{type:"dots"});let d;try{d=await B.settings.get()}catch(e){v.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(v.destroy(),!n.isConnected)return;const o=M.observable(d||{}),m=De(d||{}),s={};for(const[e,t]of Object.entries(m))s[e]=M.observable(t);const D=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].value])),X=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].peek()])),b=M.observable(m),q=M.computed(()=>be(b.value)),x=M.computed(()=>be(D())!==q.value),T=M.observable(!1),L=M.observable(""),Z=M.observable(!1);B.plugins.policy().then(e=>{i()&&(Z.value=e?.signedOnly===!1)}).catch(()=>{});const F=M.observable(!1),ee=M.computed(()=>{const e=new Set(Pe(D(),b.value));return Object.fromEntries(Object.keys(Ue).map(t=>[t,e.has(t)]))}),U=M.observable([]);r(M.effect(()=>{U.value=Fe(D()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const te=M.observable(""),O=M.observable(!1),A=M.observable(""),P=M.observable(""),p=M.observable(null),_=M.observable([]),K=M.observable(""),N=M.observable(!0),z=M.observable(""),ae=M.observable([]);r(M.effect(()=>{const e=K.value.trim().toLowerCase(),t=Date.now();ae.value=[..._.value].reverse().filter(a=>!e||String(a.key).toLowerCase().includes(e)||(a.tags||[]).some(c=>String(c).toLowerCase().includes(e))).map(a=>{const c=J(a.key);return{id:a.key,path:c.path,meta:[c.page?`as ${c.role}`:"not a page",(a.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:je(a.expiresAt,t)}})}));const se=Ke(s.favicon,s.brandLogo,i),we={...se.data,...s,saving:T,dirty:x,tabs:ee,problems:U,testTo:te,testing:O,testResult:A,testTone:P,cacheQuery:K,cacheLoading:N,cacheError:z,cacheRows:ae,errorText:L,problemsText:M.computed(()=>U.value.length?"yes":""),siteUrlError:M.computed(()=>ve(s.siteUrl.value)),gaIdError:M.computed(()=>he(s.gaId.value)),gaForcesBanner:M.computed(()=>!!String(s.gaId.value||"").trim()&&s.ccEnabled.value!==!0),noSmtp:M.computed(()=>!String(s.smtpHost.value||"").trim()),mayAllowInsecureTLS:Z,dirtyEmail:M.computed(()=>ee.value.email),seoLength:M.computed(()=>{const e=String(s.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cacheOn:M.computed(()=>p.value?.enabled===!0),cacheState:M.computed(()=>{const e=p.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=p.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=p.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>p.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>K.value.trim()?"Nothing matches.":p.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function G(){if(T.peek())return;const e=X(),t=b.peek(),a=ve(e.siteUrl);if(a){E.toast(`Site URL: ${a}`,{type:"error"});return}const c=he(e.gaId);if(c){E.toast(`Google Analytics: ${c}`,{type:"error"});return}const y=Be(e)[0];if(y){E.toast(y,{type:"error"});return}T.value=!0,L.value="";const W=String(e.title).trim()!==String(t.title).trim();try{await B.settings.save(qe(e,o.peek(),{adminHomeReady:F.peek()}));const j=await B.settings.get().catch(()=>null);if(!i())return;j&&(o.value=j);const Re={...e,smtpPass:""};s.smtpPass.value="",b.value=Re;try{S.remove?S.remove(w):S.set(w,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(W||["adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(pe=>e[pe]!==t[pe]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),fe(ge),E.toast("Settings saved.",{type:"success"})}catch(j){L.value=j.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{T.value=!1}}async function ke(){await E.confirm("Throw away your unsaved settings?")&&(ne(b.peek()),L.value="")}function ne(e){for(const[t,a]of Object.entries(e))s[t]&&(s[t].value=a)}async function Ee(){if(!O.peek()){O.value=!0,P.value="",A.value="Sending\u2026";try{const e=await C("/settings/test-email",{method:"POST",body:JSON.stringify({to:te.peek().trim()||void 0})});if(!i())return;A.value=e?.message||"Test email sent.",P.value="is-ok"}catch(e){if(!i())return;A.value=e.message||"Could not send the test email.",P.value="is-bad"}finally{O.value=!1}}}async function $(){N.value=!0;try{const[e,t]=await Promise.all([C("/cache/status").catch(()=>null),C("/cache/keys")]);if(!i())return;p.value=e,_.value=Array.isArray(t?.entries)?t.entries:[],z.value=""}catch(e){if(!i())return;z.value=`Could not read the cache: ${e.message||e}`}finally{N.value=!1}}async function Se(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await C("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}i()&&await $()}}async function Me(){const e=p.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{p.value=await C("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),fe(ge)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}i()&&await $()}}const oe=e=>_.peek().find(t=>t.key===e);function Ce(e){const t=oe(e);if(!t)return[];const a=J(t.key);return[{header:a.path},{label:"Open the page",icon:"external-link",disabled:!a.page,action:()=>window.open(a.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...a.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(a.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}n.querySelector(".stx-problems").innerHTML=Ge;const g=n.querySelector(".stx-list");g.innerHTML=Ve;const xe=E.tabs(n.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Te=M.applyBindings(we,n,{methods:{...se.methods,save:G,discard:ke,sendTest:Ee,refreshCache:$,clearCache:Se,toggleCache:Me,pickBrandImage:async e=>{const t=await _e({title:"Choose a branding image",current:s[e].peek()});t&&i()&&(s[e].value=t)},clearBrandImage:e=>{s[e].value=""},corner:e=>{s.bcPosition.value=e}}});r(()=>Te.dispose());for(const e of n.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,a=Ne("Choose an icon",s[t].peek());a.input.id=`stx-${t}`,a.input.classList.add("form-input");const c=()=>{s[t].value=a.input.value};a.input.addEventListener("input",c),r(()=>a.input.removeEventListener("input",c)),r(M.effect(()=>{a.input.value=s[t].value})),e.appendChild(a.el)}r(Ae(n,{position:"bottom"}));const Le=e=>{const t=e.target.closest("[data-go]");if(!t)return;const a=We.indexOf(t.dataset.go);a>=0&&xe.activate(a)};n.querySelector(".stx-problems").addEventListener("click",Le);const V=n.querySelector("#stx-admin-home");Ye(V,He(s.adminHome.peek())).then(()=>{i()&&(V.value=s.adminHome.peek(),F.value=!0)}).catch(()=>{});const Oe=E.contextMenu(g,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Ce(e.dataset.id)});r(()=>Oe?.destroy?.());const re=e=>{const t=oe(e.dataset.id),a=t&&J(t.key);a?.page?window.open(a.path,"_blank","noopener"):Y(e)},ie=e=>{const t=e.target.closest(".stx-row");if(!t)return;const a=e.target.closest(".stx-kebab");if(a){Y(a);return}re(t)},ce=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Y(t)):e.key==="Enter"&&e.target===t&&re(t))};g.addEventListener("click",ie),g.addEventListener("keydown",ce),r(()=>{g.removeEventListener("click",ie),g.removeEventListener("keydown",ce)}),me(l,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),me(l,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>G()});const le=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),G())};document.addEventListener("keydown",le,!0),r(()=>document.removeEventListener("keydown",le,!0));const ue=()=>{if(x.peek())try{S.set(w,{form:$e(X()),base:q.peek(),at:new Date().toISOString()})}catch{}},de=e=>{x.peek()&&(ue(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",de),r(()=>window.removeEventListener("beforeunload",de)),f={dirty:()=>x.peek(),dispose(){k=!0,ue(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(n),$();let h=null;try{h=S.get(w)}catch{}if(h?.form){try{S.remove?S.remove(w):S.set(w,null)}catch{}const e=h.base&&h.base!==q.peek(),t=h.at?` (${ze(h.at)})`:"",a=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;if(await E.confirm(a)&&i()){const c={...b.peek()};for(const[y,W]of Object.entries(h.form))y in c&&y!=="smtpPass"&&(c[y]=W);ne(c),F.peek()&&(V.value=s.adminHome.peek())}}}};
11
+ </div>`,we=["general","branding","favicon","seo","footer","email","pageNavigation","cookies","analytics","breadcrumbs","motion","cache"];async function Qe(c,u){const[o,w]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),s=new Map([["/","Dashboard (built in)"]]),i=(v,d)=>{for(const r of v||[]){if(!r||r.hidden||r.restricted)continue;const p=typeof r.url=="string"?r.url:"";p.startsWith("#/")&&p.length>2&&!s.has(p.slice(1))&&s.set(p.slice(1),`${d?`${d} \u203A `:""}${r.text||p}`),i(r.items,r.url?d:r.text||d)}};i($e(o?.items||[],w||[]),""),s.has(u)||s.set(u,`${u} (not in the sidebar)`),c.replaceChildren();for(const[v,d]of s){const r=document.createElement("option");r.value=v,r.textContent=d,c.appendChild(r)}}let f=null,ke=!1;function Xe(){ke||!window.R?.use||(ke=!0,R.use((c,u,o)=>!(Y.test(u?.path||"")&&!Y.test(c?.path||""))||!f?.dirty()?o():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(s=>{s?o():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:c}={})=>{if(f&&!Y.test(c?.path||"")){const u=f;f=null,u.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(c){if(Xe(),f){const e=f;f=null,e.dispose()}const u=[],o=e=>(u.push(e),e);let w=!1;const s=c.get(0).querySelector(".stx")||c.get(0),i=()=>!w&&s.isConnected,v=E.loader(c.get(0),{type:"dots"});let d;try{d=await D.settings.get()}catch(e){v.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(v.destroy(),!s.isConnected)return;const r=M.observable(d||{}),p=Fe(d||{}),n={};for(const[e,t]of Object.entries(p))n[e]=M.observable(t);const B=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),J=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),b=M.observable(p),j=M.computed(()=>ge(b.value)),x=M.computed(()=>ge(B())!==j.value),C=M.observable(!1),T=M.observable(""),Q=M.observable(!1);D.plugins.policy().then(e=>{i()&&(Q.value=e?.signedOnly===!1)}).catch(()=>{});const q=M.observable(!1),X=M.computed(()=>{const e=new Set(De(B(),b.value));return Object.fromEntries(Object.keys(ze).map(t=>[t,e.has(t)]))}),F=M.observable([]);o(M.effect(()=>{F.value=Ke(B()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const Z=M.observable(""),L=M.observable(!1),O=M.observable(""),P=M.observable(""),m=M.observable(null),U=M.observable([]),_=M.observable(""),K=M.observable(!0),z=M.observable(""),ee=M.observable([]);o(M.effect(()=>{const e=_.value.trim().toLowerCase(),t=Date.now();ee.value=[...U.value].reverse().filter(a=>!e||String(a.key).toLowerCase().includes(e)||(a.tags||[]).some(l=>String(l).toLowerCase().includes(e))).map(a=>{const l=W(a.key);return{id:a.key,path:l.path,meta:[l.page?`as ${l.role}`:"not a page",(a.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:He(a.expiresAt,t)}})}));const te=Ge(n.favicon,n.brandLogo,i),Ee={...te.data,...n,saving:C,dirty:x,tabs:X,problems:F,testTo:Z,testing:L,testResult:O,testTone:P,cacheQuery:_,cacheLoading:K,cacheError:z,cacheRows:ee,errorText:T,problemsText:M.computed(()=>F.value.length?"yes":""),siteUrlError:M.computed(()=>be(n.siteUrl.value)),gaIdError:M.computed(()=>ve(n.gaId.value)),gaForcesBanner:M.computed(()=>!!String(n.gaId.value||"").trim()&&n.ccEnabled.value!==!0),noSmtp:M.computed(()=>!String(n.smtpHost.value||"").trim()),mtSizeText:M.computed(()=>`${n.mtIconSize.value} px`),mtRadiusText:M.computed(()=>`${n.mtIconRadius.value} px`),mayAllowInsecureTLS:Q,dirtyEmail:M.computed(()=>X.value.email),seoLength:M.computed(()=>{const e=String(n.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cacheOn:M.computed(()=>m.value?.enabled===!0),cacheState:M.computed(()=>{const e=m.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=m.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=m.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>m.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>_.value.trim()?"Nothing matches.":m.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function N(){if(C.peek())return;const e=J(),t=b.peek(),a=be(e.siteUrl);if(a){E.toast(`Site URL: ${a}`,{type:"error"});return}const l=ve(e.gaId);if(l){E.toast(`Google Analytics: ${l}`,{type:"error"});return}const me=je(e)[0];if(me){E.toast(me,{type:"error"});return}C.value=!0,T.value="";const Oe=String(e.title).trim()!==String(t.title).trim();try{await D.settings.save(_e(e,r.peek(),{adminHomeReady:q.peek()}));const A=await D.settings.get().catch(()=>null);if(!i())return;A&&(r.value=A);const Pe={...e,smtpPass:""};n.smtpPass.value="",b.value=Pe;try{S.remove?S.remove(y):S.set(y,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(Oe||["adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(pe=>e[pe]!==t[pe]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),he(ye),E.toast("Settings saved.",{type:"success"})}catch(A){T.value=A.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{C.value=!1}}async function Se(){await E.confirm("Throw away your unsaved settings?")&&(ae(b.peek()),T.value="")}function ae(e){for(const[t,a]of Object.entries(e))n[t]&&(n[t].value=a)}async function Me(){if(!L.peek()){L.value=!0,P.value="",O.value="Sending\u2026";try{const e=await k("/settings/test-email",{method:"POST",body:JSON.stringify({to:Z.peek().trim()||void 0})});if(!i())return;O.value=e?.message||"Test email sent.",P.value="is-ok"}catch(e){if(!i())return;O.value=e.message||"Could not send the test email.",P.value="is-bad"}finally{L.value=!1}}}async function $(){K.value=!0;try{const[e,t]=await Promise.all([k("/cache/status").catch(()=>null),k("/cache/keys")]);if(!i())return;m.value=e,U.value=Array.isArray(t?.entries)?t.entries:[],z.value=""}catch(e){if(!i())return;z.value=`Could not read the cache: ${e.message||e}`}finally{K.value=!1}}async function xe(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await k("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}i()&&await $()}}async function Ce(){const e=m.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{m.value=await k("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),he(ye)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}i()&&await $()}}const ne=e=>U.peek().find(t=>t.key===e);function Te(e){const t=ne(e);if(!t)return[];const a=W(t.key);return[{header:a.path},{label:"Open the page",icon:"external-link",disabled:!a.page,action:()=>window.open(a.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...a.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(a.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}s.querySelector(".stx-problems").innerHTML=Ye;const g=s.querySelector(".stx-list");g.innerHTML=Je;const se=E.tabs(s.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Ie=M.applyBindings(Ee,s,{methods:{...te.methods,save:N,discard:Se,sendTest:Me,refreshCache:$,clearCache:xe,toggleCache:Ce,pickBrandImage:async e=>{const t=await Ne({title:"Choose a branding image",current:n[e].peek()});t&&i()&&(n[e].value=t)},clearBrandImage:e=>{n[e].value=""},corner:e=>{n.bcPosition.value=e},motionCorner:e=>{n.mtPosition.value=e},motionPlacement:e=>{n.mtPlacement.value=e},goTab:e=>{const t=we.indexOf(e);t>=0&&se.activate(t)}}}),oe=s.querySelector("#stx-mt-radius");o(M.effect(()=>{const e=Math.floor((Number.parseInt(n.mtIconSize.value,10)||52)/2);oe.max=String(e);const t=Number.parseInt(n.mtIconRadius.peek(),10)||0;t>e?n.mtIconRadius.value=String(e):oe.value=String(t)})),o(()=>Ie.dispose());for(const e of s.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,a=Ve("Choose an icon",n[t].peek());a.input.id=`stx-${t}`,a.input.classList.add("form-input");const l=()=>{n[t].value=a.input.value};a.input.addEventListener("input",l),o(()=>a.input.removeEventListener("input",l)),o(M.effect(()=>{a.input.value=n[t].value})),e.appendChild(a.el)}o(Ae(s,{position:"bottom"}));const Re=e=>{const t=e.target.closest("[data-go]");if(!t)return;const a=we.indexOf(t.dataset.go);a>=0&&se.activate(a)};s.querySelector(".stx-problems").addEventListener("click",Re);const G=s.querySelector("#stx-admin-home");Qe(G,Ue(n.adminHome.peek())).then(()=>{i()&&(G.value=n.adminHome.peek(),q.value=!0)}).catch(()=>{});const Le=E.contextMenu(g,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Te(e.dataset.id)});o(()=>Le?.destroy?.());const re=e=>{const t=ne(e.dataset.id),a=t&&W(t.key);a?.page?window.open(a.path,"_blank","noopener"):V(e)},ie=e=>{const t=e.target.closest(".stx-row");if(!t)return;const a=e.target.closest(".stx-kebab");if(a){V(a);return}re(t)},ce=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),V(t)):e.key==="Enter"&&e.target===t&&re(t))};g.addEventListener("click",ie),g.addEventListener("keydown",ce),o(()=>{g.removeEventListener("click",ie),g.removeEventListener("keydown",ce)}),fe(c,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),fe(c,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>N()});const le=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),N())};document.addEventListener("keydown",le,!0),o(()=>document.removeEventListener("keydown",le,!0));const ue=()=>{if(x.peek())try{S.set(y,{form:Be(J()),base:j.peek(),at:new Date().toISOString()})}catch{}},de=e=>{x.peek()&&(ue(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",de),o(()=>window.removeEventListener("beforeunload",de)),f={dirty:()=>x.peek(),dispose(){w=!0,ue(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(s),$();let h=null;try{h=S.get(y)}catch{}if(h?.form){try{S.remove?S.remove(y):S.set(y,null)}catch{}const e=h.base&&h.base!==j.peek(),t=h.at?` (${We(h.at)})`:"",a=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;await E.confirm(a)&&i()&&(ae(qe(b.peek(),h.form)),q.peek()&&(G.value=n.adminHome.peek()))}}};
@@ -1099,8 +1099,9 @@ Where: `server/services/presetCollections.js` (`PRESETS`, `seedAll()`), `server/
1099
1099
  - **Site Settings** - One screen with one Save covering nine tabs:
1100
1100
  - General: title, tagline, Site URL, admin home, default spacer size.
1101
1101
  - SEO defaults: default title, separator, meta description.
1102
- - Footer: copyright, a "Reduce motion" switch, social icons for X, Facebook, Instagram, LinkedIn, GitHub and YouTube.
1102
+ - Footer: copyright, layout, blurb, footer blocks, social icons for X, Facebook, Instagram, LinkedIn, GitHub and YouTube.
1103
1103
  - Showcase footer (Footer tab): `footer.look: showcase` (glowing top line, more room), `footer.cta` (a big call to action, some words in a gradient, optional link and sub-line), `footer.watermark` (a huge faint name behind the footer that drifts on scroll), `footer.email` / `footer.address` under the brand, `footer.status` (green dot) and up to four `footer.facts`, `footer.tagline` in the bottom bar, `footer.gradient` `{from, to}`. Columns layout only; each part shows when it has words. Drawn by `public/js/footer-showcase.mjs` + `footer-showcase.css`.
1104
+ - Reduce motion: on/off, label, the default for new visitors; in the footer (switch or checkbox), a floating button (corner, offsets, size, corners) or the navbar.
1104
1105
  - Email: SMTP host, port, user, password and TLS; From address and name; send a test email.
1105
1106
  - Back to top: on/off, label, threshold, position, offsets, smooth scrolling.
1106
1107
  - Cookie consent: message, position, layout, colours, categories, policy links, button text, consent version.
@@ -137,7 +137,6 @@ The main site-wide configuration. This file is editable from the **Settings** ad
137
137
  "copyright": "© 2026 My Company",
138
138
  "layout": "auto",
139
139
  "blurb": "",
140
- "showMotionToggle": true,
141
140
  "links": [
142
141
  { "text": "Privacy Policy", "url": "/privacy" }
143
142
  ]
@@ -195,7 +194,8 @@ The main site-wide configuration. This file is editable from the **Settings** ad
195
194
  | `theme`, `adminTheme`, `fontFamily`, `fontSize`, `autoTheme`, `baseTheme` | **Mirror only.** These moved to `config/theme.json` (see below) and are rewritten here on every theme save so plugins and fleet tooling that read them keep working. Editing them here has no effect. |
196
195
  | `smtp` | SMTP connection details for outgoing email (password is stripped from GET responses) - see **Outgoing email** below |
197
196
  | `social` | Social profile URLs; empty strings are ignored by the frontend |
198
- | `footer` | Footer copyright, legacy link list, and the "Reduce motion" switch - see below |
197
+ | `footer` | Footer copyright, layout, blocks and classes - see below |
198
+ | `motion` | The "Reduce motion" control: shown, default, footer/floating/navbar placement - see **Reduce motion** below |
199
199
  | `backToTop` | Scroll-to-top button configuration |
200
200
  | `cookieConsent` | Cookie consent banner configuration |
201
201
  | `googleAnalytics` | `{measurementId: "G-…"}` - Google Analytics 4, set at Site Settings > Analytics. See **Google Analytics** below |
@@ -285,15 +285,60 @@ the host is empty you get, in order of how likely you are to see it:
285
285
  | `layout` | string | `auto` (default), `columns` or `row`. **Columns**: the brand (the navbar's text, logo or icon), the blurb and the social links beside one column per group of the `footer-primary` menu (a top-level item with sub-items; its text is the heading, a link only if it has a URL), over a bottom bar of copyright, the `footer-legal` menu and the motion control. **Row**: everything on one wrapped row, as before. **Auto**: columns when the footer menu has groups, else a row. Site Settings > Footer > Layout. |
286
286
  | `blurb` | string | A sentence or two under the brand in the columns layout. Plain text. |
287
287
  | `blocks` | array | Block names (`content/blocks/<name>.html`) drawn side by side at the top of the footer on every page, in order - up to 8, lower-case letters, digits and hyphens. Rendered by the server (`buildFooterBlocks()` in `renderer.js`, both render paths) as `[block template="…" /]`, so sanitised like a page; a name that is not a block is left out. Site Settings > Footer > Footer blocks; Import from WordPress adds a site's footer widget areas here. |
288
- | `showMotionToggle` | boolean | Whether the footer shows the "Reduce motion" accessibility switch. **Defaults to `true`** - omit the key and the switch appears, so sites that predate this setting are unchanged. Editable from Site Settings > Footer. |
288
+ | `showMotionToggle`, `motionDefault`, `motionControl` | boolean, string, string | **Mirror of `motion`** (below) - `enabled`, `default` and `control`. Still honoured key by key where `motion` says nothing, so a site that predates the block renders as before; Site Settings rewrites them on every save for older readers and fleet tooling. A save that sends only these keys moves the `motion` block with them. |
289
289
  | `class` | string | Your own CSS classes on the site footer (`#site-footer`). Site Settings > Footer. |
290
290
  | `socialClass` | string | Your own CSS classes on every social media icon link in the footer. |
291
291
  | `links` | array | Legacy single footer link list. Migrated to `config/menus/` on first boot (see [Multi-menu system](#multi-menu-system)); kept only so the migration can read it. |
292
292
 
293
- Setting `showMotionToggle: false` hides the control, not the capability. A visitor who already
294
- flipped the switch keeps their stored `domma:reduced_motion` preference, and anyone whose browser
295
- sends `prefers-reduced-motion: reduce` still gets the calmer site - they just lose the ability to
296
- override it from the footer.
293
+ ### Reduce motion
294
+
295
+ The accessibility control that lets visitors turn animations off - **Site Settings > Reduce motion**.
296
+ `motion` in `site.json`, normalised by `server/services/motionSettings.js` (whitelisted and clamped: it is
297
+ written into every page as `window.__CMS_SITE__.motion`, on both render paths) and drawn by
298
+ `public/js/motion-control.mjs`.
299
+
300
+ ```json
301
+ "motion": {
302
+ "enabled": true,
303
+ "default": "system",
304
+ "placement": "footer",
305
+ "control": "switch",
306
+ "triggerStyle": "icon",
307
+ "label": "Reduce motion",
308
+ "position": "BL",
309
+ "offsetX": 32,
310
+ "offsetY": 32,
311
+ "iconSize": 52,
312
+ "iconRadius": 26,
313
+ "class": ""
314
+ }
315
+ ```
316
+
317
+ | `motion` field | Type | Description |
318
+ |---|---|---|
319
+ | `enabled` | boolean | Show the control. **Defaults to `true`**. |
320
+ | `default` | string | Reduced motion for a visitor who has not chosen: `system` (follow their device, the default), `off` (full motion) or `on` (calm). Their own choice always wins. Feeds the `{{motionDefault}}` bootstrap in `page.html`. |
321
+ | `placement` | string | `footer` (default - the bottom bar in the columns layout), `floating` (a disc fixed to a corner of the window) or `navbar` (a button at the end of the navbar; on a page with no navbar, or on a phone when the navbar is set to show only its menu button, it floats instead). |
322
+ | `control` | string | The footer's control: `switch` (default) or `checkbox`. |
323
+ | `triggerStyle` | string | Floating and navbar: `icon` (default; the label is still the accessible name) or `icon-text`. |
324
+ | `label` | string | The words on the control - trimmed, at most 40 characters. Default `Reduce motion`. |
325
+ | `position` | string | Floating: the corner - `TL`, `TR`, `BL` (default) or `BR`. |
326
+ | `offsetX`, `offsetY` | number | Floating: px from the side and from the top/bottom, 0-500 (default 32). |
327
+ | `iconSize`, `iconRadius` | number | Floating, icon only: the disc's size (32-96 px, default 52) and corner radius (0 to half the size, default 26) - Search's limits. |
328
+ | `class` | string | Your own CSS classes on the control, wherever it sits. |
329
+
330
+ With no `motion` block (or a key missing from it) the footer keys above decide, so every existing site
331
+ keeps its footer switch with no migration. The defaults are what the footer switch always was.
332
+
333
+ The floating disc defaults to the bottom left, clear of Search (right-centre) and Back to top
334
+ (bottom right). Put it in a corner something else already holds - Search's disc, Back to top or Go to
335
+ bottom, the theme switcher's disc, the breadcrumb pill - and it stacks clear of it, further from the edge;
336
+ the other control does not move. It never animates itself.
337
+
338
+ Hiding the control (`enabled: false`) hides the control, not the capability. A visitor who already
339
+ used it keeps their stored `domma:reduced_motion` preference (the one stored preference, whatever the
340
+ placement), and anyone whose browser sends `prefers-reduced-motion: reduce` still gets the calmer
341
+ site - they just lose the ability to override it.
297
342
 
298
343
  ### SEO and indexing
299
344
 
@@ -788,7 +833,7 @@ navbar, footer, `[menu]` and floating overlays (not the admin sidebar).
788
833
 
789
834
  Items are numbered per list, so a dropdown or footer column staggers in on its own when it opens;
790
835
  items past the twentieth share its delay. Visitors who asked for less motion (the OS setting or the
791
- footer's motion switch) see the items at once.
836
+ Reduce motion control) see the items at once.
792
837
 
793
838
  ### Item highlighting
794
839
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -0,0 +1 @@
1
+ .dm-motion-fab{position:fixed;z-index:9997;width:52px;height:52px;border-radius:50%;border:1px solid var(--dm-border, rgba(0, 0, 0, .12));background:var(--dm-surface, #fff);color:var(--dm-text, #111);box-shadow:0 4px 16px #00000024;display:flex;align-items:center;justify-content:center;gap:8px;padding:0;cursor:pointer;white-space:nowrap;font:inherit;transition:none;animation:none}.dm-motion-fab:hover,.dm-motion-fab[aria-pressed=true]{background:var(--dm-primary, #5b8cff);border-color:var(--dm-primary, #5b8cff);color:var(--dm-primary-text, #fff)}.dm-motion-fab:focus-visible,.dm-motion-nav:focus-visible{outline:2px solid var(--dm-primary, #5b8cff);outline-offset:2px}.dm-motion-fab .dm-motion-icon,.dm-motion-fab svg{width:var(--dm-motion-glyph, 20px);height:var(--dm-motion-glyph, 20px);flex-shrink:0;pointer-events:none}.dm-motion-fab--pill{width:auto;height:44px;border-radius:999px;padding:0 18px 0 14px}.dm-motion-fab-label{font-size:.875rem;font-weight:500;pointer-events:none}.dm-motion-nav{display:inline-flex;align-items:center;gap:6px;background:none;border:0;padding:6px;border-radius:var(--dm-radius, 6px);color:inherit;cursor:pointer;font:inherit;font-size:var(--dm-font-size-sm, 14px);transition:none}.dm-motion-nav:hover{background:var(--dm-hover-bg, rgba(128, 128, 128, .14))}.dm-motion-nav[aria-pressed=true]{background:color-mix(in srgb,currentColor 16%,transparent)}.dm-motion-nav svg,.dm-motion-nav .dm-motion-icon{width:18px;height:18px;flex-shrink:0;pointer-events:none}@media print{.dm-motion-fab{display:none}}
@@ -0,0 +1 @@
1
+ import{applyClasses as f}from"/public/js/css-classes.mjs";const x="(prefers-reduced-motion: reduce)";export function reducedMotionOn(t={}){const e=globalThis.S?.get?.("reduced_motion");return e!=null?!!e:t.default==="on"?!0:t.default==="off"?!1:!!(window.matchMedia&&window.matchMedia(x).matches)}function s(t){globalThis.S?.set?.("reduced_motion",t),window.location.reload()}function C(t){const e=document.createElement("span");return e.className="dm-motion-icon",e.setAttribute("data-icon",t?"play":"pause"),e.setAttribute("aria-hidden","true"),e}function v(t,e){const i=t.control==="checkbox",o=document.createElement("input");o.type="checkbox",o.className=i?"footer-motion-checkbox":"form-switch-input",o.id="dm-motion-switch",o.checked=e,o.addEventListener("change",()=>s(o.checked));const n=document.createElement("span");n.className=i?"footer-motion-label":"form-switch-label",n.textContent=t.label;const a=document.createElement("label");return a.className=i?"footer-motion-check":"form-switch footer-motion-switch",a.title=t.label,a.append(o,n),a}function p(t,e,i,o){const n=document.createElement("button");if(n.type="button",n.className=i,n.id="dm-motion-toggle",n.setAttribute("aria-pressed",String(e)),n.setAttribute("aria-label",t.label),n.title=e?`${t.label}: on`:`${t.label}: off`,n.appendChild(C(e)),t.triggerStyle==="icon-text"){const a=document.createElement("span");a.className=o,a.textContent=t.label,n.appendChild(a)}return n.addEventListener("click",()=>s(!e)),n}const h=[".site-search-floating-trigger","#back-to-top-btn","#go-to-bottom-btn",".dm-theme-switcher:not(.dm-ts-inline)","nav.dm-breadcrumbs"].join(","),r=12;function E(t){const e=t.getBoundingClientRect();if(getComputedStyle(t).opacity!=="0"||t.offsetParent!==null)return e;const i=Math.min(e.top,t.offsetTop),o=Math.min(e.left,t.offsetLeft);return{top:i,left:o,right:Math.max(e.right,t.offsetLeft+t.offsetWidth),bottom:Math.max(e.bottom,t.offsetTop+t.offsetHeight),width:e.width,height:e.height}}const S=(t,e)=>t.left<e.right+r&&e.left<t.right+r&&t.top<e.bottom+r&&e.top<t.bottom+r;function g(t,e){const i=e.position==="TL"||e.position==="BL",o=e.position==="TL"||e.position==="TR";t.style.left=i?`${e.offsetX}px`:"auto",t.style.right=i?"auto":`${e.offsetX}px`;let n=e.offsetY;const a=()=>{t.style.top=o?`${n}px`:"auto",t.style.bottom=o?"auto":`${n}px`};a();const l=[...document.querySelectorAll(h)].filter(c=>c!==t&&getComputedStyle(c).position==="fixed"&&getComputedStyle(c).display!=="none");for(let c=0;c<l.length+1;c++){const w=t.getBoundingClientRect(),d=l.map(E).find(m=>m.width&&m.height&&S(w,m));if(!d)break;n=Math.round(o?d.bottom+r:window.innerHeight-d.top+r),a()}}function b(t,e){const i=t.triggerStyle==="icon-text",o=p(t,e,`dm-motion-fab${i?" dm-motion-fab--pill":""}`,"dm-motion-fab-label");return i||(o.style.width=`${t.iconSize}px`,o.style.height=`${t.iconSize}px`,o.style.borderRadius=`${t.iconRadius}px`,o.style.setProperty("--dm-motion-glyph",`${Math.round(t.iconSize*20/52)}px`)),f(o,t.class),o}function y(t,e){const i=()=>{t.isConnected&&g(t,e)};let o=0;const n=()=>{cancelAnimationFrame(o),o=requestAnimationFrame(i)};return new MutationObserver(a=>{a.some(l=>[...l.addedNodes].some(c=>c.nodeType===1&&c.matches?.(h)))&&n()}).observe(document.body,{childList:!0}),window.addEventListener("resize",n),window.addEventListener("load",n,{once:!0}),i}const u=t=>{window.Domma?.icons?.scan&&window.Domma.icons.scan(t)};function k(t,e){const i=b(t,e);return document.body.appendChild(i),y(i,t)(),i}function L(t,e,i){let o=t.querySelector(".navbar-actions");o||(o=document.createElement("div"),o.className="navbar-actions",(t.querySelector(".navbar-container")||t).appendChild(o));const n=p(e,i,"dm-motion-nav","dm-motion-nav-label");f(n,e.class);let a=null,l=null;const c=()=>{if(getComputedStyle(o).display!=="none"){a?.remove(),n.isConnected||(o.appendChild(n),u(n));return}n.remove(),a||(a=b(e,i),l=y(a,e)),a.isConnected||(document.body.appendChild(a),u(a),l())};return c(),window.addEventListener("resize",c),n.isConnected?n:a}export function mountMotionControl(t,e={}){if(!t||t.enabled===!1||document.getElementById("dm-motion-toggle")||document.getElementById("dm-motion-switch"))return null;const i=reducedMotionOn(t);let o=null;if(t.placement==="footer"){if(!e.footer)return null;o=v(t,i),f(o,t.class),e.footer.appendChild(o)}else t.placement==="navbar"&&e.navbar?o=L(e.navbar,t,i):o=k(t,i);return u(o),o}
package/public/js/site.js CHANGED
@@ -1 +1 @@
1
- import{applyMobileMenu as ae,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 X}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{applyMenuLook as pe}from"/public/js/menu-cards.mjs?v=20261029-glider-layout";import{initPageMotion as fe}from"/public/js/page-motion.mjs?v=20261029-page-motion";import{applyFooterShowcase as he}from"/public/js/footer-showcase.mjs?v=20261029-footer-showcase";import{initFloatingMenus as ge}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-forms-files-total";import{buildWizardSteps as ye}from"/public/js/form-steps.mjs";import{applyFormClasses as te,applySubmitButtons as ve,applyWizardButtons as be}from"/public/js/form-submit.mjs";import{captureFileFields as oe,destroyDropzones as we,fileBlueprint as Ce,restoreFileFields as Se,setFileLimits as xe,splitFiles as ke,submitBody as Le,totalTooLarge as $e,wireFileFields as Ee}from"/public/js/form-files.mjs";document.addEventListener("click",u=>{const i=u.target?.closest?.(".dm-banner__dismiss");i&&(u.preventDefault(),i.closest(".dm-banner")?.remove())}),$(()=>{re(document);const u=window.__CMS_NAV__||{},i=window.__CMS_SITE__||{};if(i.autoTheme?.enabled){let t=function(e){const n=(e||"07:00").split(":");return+n[0]*60+(+n[1]||0)},s=function(){const e=new Date,n=e.getHours()*60+e.getMinutes();return n>=t(l.dayStart)&&n<t(l.nightStart)?l.dayTheme:l.nightTheme},o=function(e){const n=l.apply&&l.apply[e]||{theme:e,custom:""};Domma.theme.set(n.theme);for(const m of[...document.body.classList])m.startsWith("dm-custom-")&&document.body.classList.remove(m);n.custom&&document.body.classList.add(n.custom)};const l=i.autoTheme;o(s()),setInterval(()=>o(s()),6e4)}const L=$("#site-navbar");if(L.length&&u.brand){let s=function(p,v=0){return Array.isArray(p)?p.filter(f=>!(f&&(f.type==="spacer"||f.type==="theme-switcher"))).filter(f=>!(f&&f.type==="separator"&&v===0)).map(f=>f&&f.type==="separator"?{divider:!0}:Array.isArray(f?.items)?{...f,items:s(f.items,v+1)}:f):p};const l={...u.brand},t=l.size&&l.size!=="md"?` navbar-brand-${l.size}`:"";if(l.logo||l.icon||l.tagline){let p="";l.logo?p+=`<img src="${l.logo}" class="navbar-brand-logo" alt="${l.text||""}">`:l.icon&&(p+=`<span data-icon="${l.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),l.text&&(p+=`<span class="navbar-brand-text${t}">${l.text}</span>`),l.tagline&&(p+=`<small class="navbar-brand-tagline">${l.tagline}</small>`),l.html=p}else t&&l.text&&(l.html=`<span class="navbar-brand-text${t}">${l.text}</span>`);const o=s(u.items||[]),e=u.position==="floating",n=u.orientation==="vertical";L.get(0).querySelector(":scope > .cms-nojs-links")?.remove(),Domma.elements.navbar("#site-navbar",{brand:l,items:o,variant:u.variant||"dark",position:e?"static":u.position||"sticky",collapsible:!0,appearOnHover:!!u.appearOnHover}),Domma.icons.scan("#site-navbar");const m=L.get(0);ae(m,u.mobile);const b=()=>{if(!n)return;const v=m.querySelector('a[aria-current="page"]')?.closest(".navbar-dropdown");v&&!v.classList.contains("open")&&v.querySelector(":scope > .navbar-dropdown-toggle")?.click()};m.classList.add(`dm-nav-align-${ue(u)||"centre"}`),n&&L.get(0).classList.add("dm-nav-vertical",u.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const a=u.dock||{};n&&!e&&Ae(m,u.side==="right"),!n&&a.hideOnScroll&&Te(m),V(m,u),X(m,u),P(m,u.class),e&&(le(L.get(0),u),ge(L.get(0).parentNode||document));const c=m.querySelector(".navbar-toggle"),g=m.querySelector(".navbar-collapse");if(c&&g){let p=null;const v=()=>{g.classList.contains("show")&&c.click()},f=()=>{p&&(p.remove(),p=null),document.body.classList.remove("dm-drawer-open")},d=()=>{p||(p=document.createElement("div"),p.className="dm-navbar-backdrop",p.addEventListener("click",v),document.body.appendChild(p),document.body.classList.add("dm-drawer-open"))};c.addEventListener("click",()=>{requestAnimationFrame(()=>{g.classList.contains("show")?(b(),d()):f()})}),g.addEventListener("click",y=>{y.target.closest("a")&&requestAnimationFrame(f)}),document.addEventListener("keydown",y=>{y.key==="Escape"&&g.classList.contains("show")&&v()})}const w=m.querySelector(".navbar-nav");if(w){const p=Array.from(w.children);let v=0;for(const f of u.items||[])if(!(f&&f.type==="separator")){if(f&&f.type==="spacer"){const d=document.createElement("li"),y=["xs","sm","md","lg","xl","flex"].includes(f.size)?f.size:"md";d.className=`dm-menu-spacer dm-menu-spacer--${y}`,d.setAttribute("aria-hidden","true"),w.insertBefore(d,p[v]||null);continue}if(f&&f.type==="theme-switcher"){const d=document.createElement("li");d.className="dm-menu-theme-switcher";const y=document.createElement("span");y.dataset.themeSwitcher="menu",y.dataset.position="inline",d.appendChild(y),w.insertBefore(d,p[v]||null);continue}v++}window.CMSThemeSwitcher?.scan(w)}m.querySelectorAll("[data-path]").forEach(p=>{const v=p.dataset.path.split(".").map(Number),f=v.length>1,d=v.reduce((C,D)=>C?.items?.[D],{items:o});if(!d)return;d.colour&&G(p,d.colour),d.pill&&!f&&ce(p,d.pill),de(p,d),P(p.closest("li"),d.class);const y=U(d.badge);y&&p.appendChild(y),K(d.url,location.pathname)&&(p.classList.add("active"),p.setAttribute("aria-current","page"))}),pe(m,u,o),b()}const h=$("#site-footer");if(h.length){let e=function(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},n=function(r){return e(r).replace(/'/g,"&#39;")},m=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';if(r&&r.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md"}" aria-hidden="true"></li>`;if(r&&r.type==="theme-switcher")return'<li class="dm-menu-theme-switcher"><span data-theme-switcher="menu" data-position="inline"></span></li>';const N=n(r.url||"#"),k=e(r.text||""),M=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(m).join("")}</ul>`:"",j=me(r),ne=[j.hover&&` data-menu-item-hover="${n(j.hover)}"`,j.active&&` data-menu-item-active="${n(j.active)}"`].filter(Boolean).join(""),se=j.accent?` style="--dm-menu-highlight:${n(j.accent)}"`:"",J=Y(r.class).join(" ");return`<li${J?` class="${n(J)}"`:""}><a href="${N}"${ne}${se}>${k}</a>${M}</li>`},a=function(){if(!i.footer)return"";const r=Object.keys(t).filter(k=>l[k]);if(!r.length)return"";const N=Y(i.footer.socialClass).join(" ");return`<div class="footer-social">${r.map(k=>{const{label:M,svg:j}=t[k];return`<a href="${l[k]}" target="_blank" rel="noopener noreferrer" aria-label="${M}" class="footer-social-link${N?` ${n(N)}`:""}">${j}</a>`}).join("")}</div>`},p=function(r){if(!r||r.type==="separator"||r.type==="spacer")return"";if(r.type==="theme-switcher")return`<li class="cms-footer-col">${m(r).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const N=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(m).join("")}</ul>`:"",k=w(r)?`<span class="cms-footer-col-title">${e(r.text||"")}</span>`:`<a class="cms-footer-col-title" href="${n(r.url||"#")}">${e(r.text||"")}</a>`,M=["cms-footer-col",...Y(r.class)].join(" ");return`<li class="${n(M)}">${k}${N}</li>`},d=function(r,N,k=!1){return(r||[]).forEach(M=>{M&&(M.type==="separator"||M.type==="spacer"||M.type==="theme-switcher")||(k&&w(M)||N.push(M),Array.isArray(M.items)&&M.items.length&&d(M.items,N))}),N};const l=i.social||{},t={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 o=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},b=(()=>{const r=i.cookieConsent;if(!r||r.showSettingsLink===!1)return"";const N=k=>(k||[]).some(M=>M&&(M.url==="#cookie-settings"||N(M.items)));return N(o.primary)||N(o.legal)?"":`<li class="cms-cookie-settings"><a href="#cookie-settings">${e(r.settingsLinkText||"Cookie settings")}</a></li>`})(),c=["columns","row"].includes(i.footer?.layout)?i.footer.layout:"auto",g=i.footer?.look==="showcase"||c==="columns"||c==="auto"&&o.primary.some(r=>Array.isArray(r?.items)&&r.items.length),w=r=>g&&!r.url&&Array.isArray(r.items)&&r.items.length>0;if(g){const r=u.brand||{},N=r.text||i.title||"",k=r.logo?`<img src="${n(r.logo)}" class="cms-footer-brand-logo" alt="">`:r.icon?'<span class="cms-footer-brand-icon"></span>':"",M=String(i.footer?.blurb||"").trim();s='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',s+=`<div class="cms-footer-brand">${N||k?`<a class="cms-footer-brand-name" href="${n(r.url||"/")}">${k}<span>${e(N)}</span></a>`:""}${M?`<p class="cms-footer-blurb">${e(M)}</p>`:""}${a()}</div>`,o.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${o.primary.map(p).join("")}</ul></nav>`),s+='</div><div class="cms-footer-bottom">',o.copyright&&(s+=`<p class="cms-footer-copyright">${e(o.copyright)}</p>`),(o.legal.length||b)&&(s+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${o.legal.map(m).join("")}${b}</ul></nav>`),s+="</div>"}else o.copyright&&(s+=`<p>${e(o.copyright)}</p>`),o.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary"><ul>${o.primary.map(m).join("")}</ul></nav>`),(o.legal.length||b)&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${o.legal.map(m).join("")}${b}</ul></nav>`),s+=a();s+="</div>",h.get(0).querySelector(":scope > .cms-nojs-links")?.remove();const v=h.get(0).querySelector(":scope > .cms-footer-blocks");v?.remove(),h.html(s),v&&h.get(0).prepend(v),window.CMSThemeSwitcher?.scan(h.get(0));const f=h.get(0).querySelector(".cms-footer-brand-icon");f&&u.brand?.icon&&f.setAttribute("data-icon",u.brand.icon),P(h.get(0),i.footer?.class),g&&he(h.get(0),i);const y=[...d(o.primary,[],!0),...d(o.legal,[])];h.get(0).querySelectorAll(".footer-nav a").forEach((r,N)=>{const k=y[N];if(!k)return;k.colour&&G(r,k.colour);const M=U(k.badge);M&&r.appendChild(M),K(k.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),V(h.get(0).querySelector(".footer-nav--primary"),{highlight:o.primaryHighlight}),V(h.get(0).querySelector(".footer-nav--legal"),{highlight:o.legalHighlight}),X(h.get(0).querySelector(".footer-nav--primary"),{entrance:o.primaryEntrance}),X(h.get(0).querySelector(".footer-nav--legal"),{entrance:o.legalEntrance}),P(h.get(0).querySelector(".footer-nav--primary"),o.primaryClass),P(h.get(0).querySelector(".footer-nav--legal"),o.legalClass);const D=i.footer?.showMotionToggle!==!1,z=S.get("reduced_motion"),B=i.footer?.motionDefault,O=z!=null?!!z:B==="on"?!0:B==="off"?!1:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),A=i.footer?.motionControl==="checkbox",I=h.get(0).querySelector(".cms-footer-bottom")||h.get(0).querySelector(".footer-inner");if(D&&I){const r=document.createElement("input");r.type="checkbox",r.className=A?"footer-motion-checkbox":"form-switch-input",r.id="dm-motion-switch",r.checked=O,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const N=document.createElement("span");N.className=A?"footer-motion-label":"form-switch-label",N.textContent="Reduce motion";const k=document.createElement("label");k.className=A?"footer-motion-check":"form-switch footer-motion-switch",k.title="Reduce motion",k.appendChild(r),k.appendChild(N),I.appendChild(k)}}const q=document.getElementById("site-sidebar");q&&_e(q),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const x=$(".page-body");if(x.length){x.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),x.find(".tabs").each(function(){Domma.elements.tabs(this)}),x.find(".carousel").each(function(){const t={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"},s=a=>{const c=String(a||"").trim();return c?t[c.toLowerCase()]?`var(${t[c.toLowerCase()]})`:CSS.supports("color",c)?c:"":""},o=s(this.dataset.background),e=s(this.dataset.arrowBackground),n=s(this.dataset.arrowColor);o&&(this.style.setProperty("--dm-carousel-background",o),this.classList.add("dm-carousel-custom-background")),e&&this.style.setProperty("--dm-carousel-arrow-background",e),n&&this.style.setProperty("--dm-carousel-arrow-color",n);const m=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${m}`);const b={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 a=Number(this.dataset.height);Number.isFinite(a)&&a>0&&(b.height=a)}if(this.dataset.animationDuration){const a=parseInt(this.dataset.animationDuration,10);Number.isFinite(a)&&(b.animationDuration=a)}this.dataset.animationEasing&&(b.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(a=>a.cloneNode(!0)),this._dmCarouselOpts=b),this._dmCarousel=Domma.elements.carousel(this,b),ie(this)}),x.find(".dm-countdown").each(function(){const t={autoStart:!0},s=String(this.dataset.format||"").trim(),o=String(this.dataset.to||"").trim(),e=/^(\d{4})-(\d{2})-(\d{2})$/.exec(o),n=e?new Date(+e[1],+e[2]-1,+e[3]).getTime():o?new Date(o).getTime():NaN;Number.isFinite(n)?t.duration=Math.max(0,n-Date.now()):this.dataset.duration&&(t.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const m=/^dd:/i.test(s);t.format=m?"hh:mm:ss":s||(t.duration>=36e5?"hh:mm:ss":"mm:ss");const b=this,a=g=>String(g).padStart(2,"0"),c=g=>{const w=Math.max(0,Math.floor(g/1e3)),p=b.querySelector(".dm-timer-time");p&&(p.textContent=`${a(Math.floor(w/86400))}:${a(Math.floor(w%86400/3600))}:${a(Math.floor(w%3600/60))}:${a(w%60)}`)};m&&(t.onTick=c),Domma.elements.timer(this,t),m&&c(t.duration||0)}),x.find("[data-tooltip]:not(.dm-range-slider)").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),x.find(".dm-progression").each(function(){const t=Array.from(this.querySelectorAll(".dm-progression-item")).map(e=>{const n={title:e.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:e.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return e.dataset.date&&(n.date=e.dataset.date),e.dataset.status&&(n.status=e.dataset.status),e.dataset.eventIcon&&(n.icon=e.dataset.eventIcon),e.dataset.entryId&&(n._entryId=e.dataset.entryId),n}),s=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",o=Domma.elements.progression(this,{layout:s,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...t.length?{items:t}:{}});if((this.dataset.mode||"timeline")==="timeline"){const e={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((n,m)=>{const b=t[m];if(!b)return;const a=n.querySelector(".dm-progression-title");if(b.icon&&a){const g=document.createElement("span");g.dataset.icon=b.icon,g.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",g.setAttribute("aria-hidden","true"),a.prepend(g)}const c=e[b.status];if(c&&a){const g=document.createElement("span");g.className=`badge badge-${c.tone} cms-timeline-status`,g.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const w=document.createElement("span");w.dataset.icon=c.icon,w.dataset.iconSize="14",w.setAttribute("aria-hidden","true"),g.append(w,document.createTextNode(c.label)),a.after(g)}})}Domma.icons.scan(this),t.length&&(this._dmCtx={instance:o,items:t})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}const l=matchMedia("(prefers-reduced-motion: reduce)").matches||document.documentElement.classList.contains("dm-reduced-motion");document.querySelectorAll(".page-body .row[data-reveal]").forEach(t=>{const s=t.dataset.revealMode||"stagger",o=t.dataset.revealAnimation||"slide-up",e=parseInt(t.dataset.revealDuration,10)||(o.startsWith("swoosh")?700:400),n=parseInt(t.dataset.revealStagger,10)||(o.startsWith("swoosh")?90:60),m=parseInt(t.dataset.revealDelay,10)||0,b=t.dataset.revealDirection==="rtl",a=t.dataset.revealReplay==="true",c=Array.from(t.children);if(l&&o.startsWith("swoosh"))return;const g={"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)",swoosh:"translateY(36px) scale(0.96)"},w=d=>o!=="swoosh-sides"?g[o]||"":c.length>1&&d===0?"translateX(-64px) rotate(-1.5deg)":c.length>1&&d===c.length-1?"translateX(64px) rotate(1.5deg)":"translateY(40px) scale(0.94)",p=o.startsWith("swoosh"),v=p?"cubic-bezier(.2,.8,.2,1)":"ease",f=(d,y)=>{d.style.opacity="0",d.style.transform=w(y),p&&(d.style.filter="blur(10px)")};c.forEach((d,y)=>{f(d,y),d.style.transition=`opacity ${e}ms ${v}, transform ${e}ms ${v}`+(p?`, filter ${e}ms ${v}`:"");const C=b?c.length-1-y:y;d.style.transitionDelay=s==="stagger"?`${m+C*n}ms`:`${m}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const d=new IntersectionObserver(y=>{y.forEach(C=>{if(C.isIntersecting){if(C.target.offsetWidth,C.target.style.opacity="1",C.target.style.transform="",p&&(C.target.style.filter=""),!a){d.unobserve(C.target);const D=C.target;D.addEventListener("transitionend",function z(B){B.target!==D||B.propertyName!=="opacity"||(D.removeEventListener("transitionend",z),D.style.transition="",D.style.transitionDelay="")})}}else a&&C.boundingClientRect.top>0&&f(C.target,c.indexOf(C.target))})},{threshold:.1});c.forEach(y=>d.observe(y))}))}),fe(document.querySelector(".page-body")||document),x.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),x.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,s=document.getElementById(t);if(!s)return;const o=E.slideover({title:s.dataset.soTitle||"",size:s.dataset.soSize||"md",position:s.dataset.soPosition||"right"});s.style.display="",o.setContent(s),o.open()})})}if(typeof $.setup=="function"){const l=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(t=>{try{const s=W(t.dataset.config);Object.assign(l,s)}catch{}}),Object.keys(l).length>0){const t={};for(const[s,o]of Object.entries(l)){const e=o?.events?.click,{confirm:n,toast:m,alert:b,prompt:a,...c}=e||{};if(n||m||b||a){$(s).on("click",async function(N){if(N.preventDefault(),n&&!await E.confirm(n))return;let k=null;if(!(a&&(k=await E.prompt(a,{inputPlaceholder:c.promptPlaceholder||"",inputValue:c.promptDefault||""}),k===null))){{const M=$(c.target||this);c.toggleClass&&M.toggleClass(c.toggleClass),c.addClass&&M.addClass(c.addClass),c.removeClass&&M.removeClass(c.removeClass),k!==null&&(c.setText&&M.text(k),c.setVal&&M.val(k),c.setAttr&&M.attr(c.setAttr,k))}c.href&&(window.location.href=c.href),m&&E.toast(m,{type:c.toastType||"success"}),b&&E.alert(b)}});const{target:w,toggleClass:p,addClass:v,removeClass:f,setText:d,setVal:y,setAttr:C,href:D,toastType:z,promptPlaceholder:B,promptDefault:O,...A}=c,I={...o.events};Object.keys(A).length?I.click=w?{target:w,...A}:A:delete I.click;const r={...o,events:I};Object.keys(I).length||delete r.events,Object.keys(r).length&&(t[s]=r)}else t[s]=o}$.setup(t)}}x.length&&R(x.get(0))}),window.wireCTAButtons=R;function R(u){u.querySelectorAll(".dm-cta-trigger").forEach(i=>{i.__ctaWired||(i.__ctaWired=!0,i.addEventListener("click",async()=>{const L=i.dataset.action,h=i.dataset.entry,q=i.dataset.confirm;let x=S.get("auth_token");if(!x){E.toast("Please log in to perform this action.",{type:"warning"});return}if(q&&!await E.confirm(q))return;const l=Array.from(i.childNodes).map(s=>s.cloneNode(!0));i.disabled=!0,i.textContent="Running\u2026";const t=s=>fetch(`/api/actions/${L}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(i.dataset.scope?{entryId:h,scope:i.dataset.scope}:{entryId:h})});try{let s=await t(x);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const n=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(n.ok){const{token:m}=await n.json();S.set("auth_token",m),x=m,s=await t(x)}}}const o=await s.json().catch(()=>({}));if(!s.ok||o.success===!1)throw new Error(o.error||o.message||`Error ${s.status}`);E.toast(o.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{i.disabled=!1,i.textContent="",l.forEach(s=>i.appendChild(s)),Domma.icons.scan(i)}}))})}(function(){const i=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!i.length)return;const L=S.get("auth_token");function h(x,l){const t=document.createElement("div");t.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(l){s.appendChild(document.createTextNode("Please "));const o=document.createElement("a");o.href="/login",o.textContent="sign in",s.appendChild(o),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=x;return t.appendChild(s),t}function q(x,l){const t=document.createElement("template");t.innerHTML=l||"",x.replaceWith(t.content)}i.forEach(async x=>{const l=x.dataset.collectionAttrs;if(!l)return;if(!L){x.replaceChildren(h("",!0));return}const t=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:l})});try{let s=L,o=await t(s);if(o.status===401){const n=S.get("auth_refresh_token");if(n){const m=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:n})});if(m.ok){const{token:b}=await m.json();S.set("auth_token",b),s=b,o=await t(s)}}}if(!o.ok){const{error:n}=await o.json().catch(()=>({}));x.replaceChildren(h(n||"Unable to load entries.",!1));return}const{html:e}=await o.json();q(x,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{x.replaceChildren(h("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 i=document.querySelectorAll("[data-collection-table]");!i.length||typeof T>"u"||!T.create||i.forEach(L=>{let h;try{h=W(L.dataset.payload)}catch{return}const{columns:q,rows:x,search:l,sortable:t,exportable:s,pageSize:o,empty:e,ctaConfig:n,slug:m,exportCfg:b}=h;if(!q?.length)return;if(n){const g=q.findIndex(w=>w.key==="_cta");g!==-1&&(q[g]={key:"_cta",title:"",render:(w,p)=>{const v=document.createElement("button");if(v.className=`btn btn-${n.style||"primary"} dm-cta-trigger`,v.dataset.action=n.action||"",n.scope&&(v.dataset.scope=n.scope),v.dataset.entry=p._entryId||"",n.confirm&&(v.dataset.confirm=n.confirm),n.icon){const f=document.createElement("span");f.dataset.icon=n.icon,v.appendChild(f),v.appendChild(document.createTextNode(" "))}return v.appendChild(document.createTextNode(n.label||"Run")),v}})}const a="col-table-"+Math.random().toString(36).slice(2,7),c=document.createElement("div");c.id=a,L.replaceChildren(c),T.create("#"+a,{data:x,columns:q,search:l,sortable:t,exportable:s,pageSize:o,emptyMessage:e}),n&&R(c),L._dmTable={columns:q,rows:x,slug:m,exportCfg:b,render(g){T.create("#"+a,{data:g,columns:q,search:l,sortable:t,exportable:s,pageSize:o,emptyMessage:e}),n&&R(c)}}})})(),(function(){const i=document.querySelectorAll("[data-form-inline]");if(!i.length||typeof F>"u")return;function L(t){const s=t&&t.logic;return!!(s&&(s.visibility||s.requirement))}function h(t,s){const o={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const n=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),m=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":n?"chooser":e.type,b={...e.formConfig||{}};b.span==="full"&&s&&(b.span=s);const a={type:m,required:L(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},...b}};e.minLength!==void 0&&(a.minLength=e.minLength),e.maxLength!==void 0&&(a.maxLength=e.maxLength),e.min!==void 0&&(a.min=e.min),e.max!==void 0&&(a.max=e.max),n&&(e.variant&&(a.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(a.multiple=!0),e.density&&(a.density=e.density),e.columns&&(a.columns=e.columns),e.accent&&(a.accent=e.accent),e.accentStyle&&(a.accentStyle=e.accentStyle),e.glow&&(a.glow=!0),e.glowColour&&(a.glowColour=e.glowColour),e.shadow&&(a.shadow=e.shadow),e.shadowColour&&(a.shadowColour=e.shadowColour)),o[e.name]=e.type==="file"?Ce(e,a):a}),o}function q(t){const s={};return(t||[]).forEach(o=>{if(!(!o.name||o.type==="page-break"||o.type==="spacer")&&(o.type==="select"||o.type==="multiselect")&&o.required){const e=(o.options||[])[0];e&&(s[o.name]=typeof e=="object"?e.value:e)}}),s}function x(t,s){(s||[]).forEach(o=>{if(o.type!=="date"||!o.name)return;const e=t.querySelector(`[name="${o.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function l(t,s,o){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=s,e.style.cssText=o?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}i.forEach(t=>{let s;try{s=W(t.dataset.formInline)}catch{return}t.querySelector(":scope > .cms-form-embed-summary")?.remove();const o=s.fields||[],e=s.settings||{},n=Number(e.columns)===3?3:2,m=e.layout||"grid",b=o.some(f=>f.type==="page-break"),a=new Map;xe(t,s.fileLimits,a),Ee(t,o);const c=async f=>{if(t.dataset.triggerBlocked==="true"){const d=t._formLogicRuntime,y=d&&d.submitBlock?d.submitBlock().reasons:[];l(t,y.join(" ")||"This form cannot be submitted yet.",!0);const C=new Error("Blocked by a form trigger");throw C.formSubmitFailed=!0,C}try{const d=t.querySelector(".fb-form-honeypot"),y=d?.querySelector('input[type="text"]')||null,C=d?.querySelector('[name="_t"]')||null,D=Object.assign({},f);delete D._t,d?.dataset.hpName&&delete D[d.dataset.hpName],y!==null&&(D._hp=y.value),C!==null&&(D._t=C.value);const z=new Map(a);oe(t,z);const B=ke(D,o,z),O=$e(t,B.files);if(O){l(t,O,!0);const I=new Error(O);throw I.formSubmitFailed=!0,I}const A=await H.post(`/api/forms/submit/${s.slug}`,B.files.size?Le(B.values,B.files).body:B.values);if(A&&A.error){l(t,A.error,!0);const I=new Error(A.error);throw I.formSubmitFailed=!0,I}if(A?.redirect){window.location.href=A.redirect;return}for(we(t);t.firstChild;)t.removeChild(t.firstChild);l(t,A?.message||e.successMessage||"Thank you for your submission.",!1)}catch(d){throw d.formSubmitFailed||l(t,d.message||"Submission failed. Please try again.",!0),d}},g=Date.now();function w(f){const d=f.querySelector("form");if(!d||d.querySelector(".fb-form-honeypot"))return;const y=document.createElement("div");y.className="fb-form-honeypot",y.setAttribute("aria-hidden","true");const C=document.createElement("input");C.name=d.querySelector('[name="website"]')?"_website":"website",y.dataset.hpName=C.name,C.type="text",C.tabIndex=-1,C.autocomplete="url",C.placeholder="https://",y.appendChild(C);const D=document.createElement("input");D.name="_t",D.type="hidden",D.value=g,y.appendChild(D),d.appendChild(y)}function p(f){if(!window.FormLogicEngine||!o.some(y=>y.logic||Array.isArray(y.triggers)&&y.triggers.length))return null;const d=new window.FormLogicEngine.FormLogicRuntime(s,t,f?{baseValues:f}:void 0);if(d.init(),t._formLogicRuntime=d,t._formLogicObserved)return d;if(t._formLogicObserved=!0,t.parentNode&&typeof MutationObserver<"u"){const y=new MutationObserver(function(C){for(const D of C)for(const z of D.removedNodes)if(z===t||z.nodeType===1&&z.contains&&z.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),y.disconnect();return}});y.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=y}return d}let v=!1;if(t.addEventListener("dm-form-end",f=>{const d=f.detail?.validation;if(d&&!d.valid){Z(t,d.missing),ee(d);return}v||(v=!0,c(f.detail?.values||{}).catch(()=>{}).finally(()=>{v=!1}))}),b&&F.wizard){const f=ye(o,s.title),d=f.map(A=>({title:A.title,description:A.description,fields:h(A.fields,n)})),y=f.map(A=>A.fields);let C=0,D=null,z=null;const B=A=>{const I=window.FormLogicEngine;if(!I||!I.validateFields)return;const r=I.validateFields(t,y[C]||[]);r.valid||(A.preventDefault(),A.stopImmediatePropagation(),Z(t,r.missing),ee(r))},O=()=>{const A=y[C]||o;be(t,e),te(t,e,A),x(t,A),Q(t,A),Se(t,a),e.honeypot!==!1&&w(t),D&&D.destroy(),D=p(z?.getStepData?z.getStepData():void 0),t.querySelectorAll(".wizard-next, .wizard-submit").forEach(I=>I.addEventListener("click",B,!0))};z=F.wizard(t,{schema:{steps:d},layout:m,columns:n,finishText:e.submitText||"Finish",onSubmit:c,onStepChange:A=>{oe(t,a),C=A,setTimeout(O,0)}}),t.addEventListener("dm-form-trigger",A=>{if(A.detail?.type!=="goto-step"||!z?.goToStep)return;const I=Number(A.detail.action.step);!Number.isFinite(I)||I<0||I>=d.length||I!==C&&z.goToStep(I)}),Promise.resolve(z).then(O)}else if(F.render){const f=F.render(t,h(o,n),q(o),{submitText:e.submitText||"Submit",layout:m,columns:n,onSubmit:c});Promise.resolve(f).then(function(){ve(t,e,m),te(t,e,o),x(t,o),Q(t,o),e.honeypot!==!1&&w(t),p()})}})})();function Ae(u,i){const L=document.body,h=L.classList,q=new Set((L.dataset.railSite||"").split(" ").filter(Boolean)),x=()=>window.matchMedia("(min-width: 993px)").matches;u.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(n=>{const m=n.textContent.trim();m&&(n.setAttribute("title",m),n.querySelector("svg, [data-icon]")||(n.dataset.initial=m.charAt(0).toUpperCase()))});const l=document.createElement("button");l.type="button",l.className="dm-nav-collapse-toggle";const t=()=>{const n=h.contains("dm-vnav-collapsed");l.setAttribute("aria-pressed",String(n)),l.setAttribute("aria-label",n?"Expand menu":"Collapse menu"),l.title=l.getAttribute("aria-label");const m=i?"chevrons-left":"chevrons-right",b=i?"chevrons-right":"chevrons-left";l.innerHTML=`<span data-icon="${n?m:b}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(l)},s=n=>{h.toggle("dm-vnav-collapsed",n),S.set("nav_collapsed",n),t()};l.addEventListener("click",()=>s(!h.contains("dm-vnav-collapsed"))),t();const o=u.querySelector(".navbar-container")||u;o.insertBefore(l,o.firstChild?.nextSibling||null);const e=n=>{h.toggle("dm-vnav-autohide",n),h.toggle("dm-vnav-collapsible",!n&&q.has("collapsible")),n?h.remove("dm-vnav-collapsed"):q.has("collapsible")&&S.get("nav_collapsed")===!0&&h.add("dm-vnav-collapsed"),u.classList.remove("dm-nav-revealed"),t()};u.addEventListener("pointerdown",n=>{h.contains("dm-vnav-autohide")&&n.pointerType!=="mouse"&&!u.classList.contains("dm-nav-revealed")&&(u.classList.add("dm-nav-revealed"),n.preventDefault())}),document.addEventListener("pointerdown",n=>{u.contains(n.target)||u.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(u,{items:()=>{const n=h.contains("dm-vnav-autohide"),m=h.contains("dm-vnav-collapsed"),b=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:m?"Expand menu":"Collapse to icons",icon:m?"maximize":"minimize",disabled:!x()||n||!q.has("collapsible"),action:()=>s(!m)},{label:n?"Keep on screen":"Hide when not in use",icon:n?"eye":"eye-off",disabled:!x(),action:()=>{S.set("nav_autohide",!n),e(!n)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!b,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(q.has("autohide")),h.toggle("dm-vnav-collapsed",!q.has("autohide")&&q.has("start-collapsed")),t()}}]}})}function Te(u){u.classList.add("dm-nav-scroll-hide");let i=window.scrollY,L=!1;window.addEventListener("scroll",()=>{L||(L=!0,requestAnimationFrame(()=>{L=!1;const h=window.scrollY,q=u.matches(":focus-within")||u.querySelector(".navbar-dropdown.open, .navbar-collapse.show");h>i&&h>u.offsetHeight&&!q?u.classList.add("dm-nav-scrolled-away"):h<i&&u.classList.remove("dm-nav-scrolled-away"),i=h}))},{passive:!0})}function _e(u){let i={};try{i=JSON.parse(u.dataset.contents||"{}")}catch{}const L=u.closest(".site-main"),h=document.querySelector(".site-content"),q=new Set(String(i.exclude||"").split(",").map(a=>a.trim().toLowerCase()).filter(Boolean)),x=a=>!a.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),l=a=>(h?[...h.querySelectorAll(a)]:[]).filter(x).filter(c=>!q.has(c.textContent.trim().toLowerCase())),t=i.headings==="h2"?l("h2"):i.headings==="h3"?l("h3"):i.headings==="h2h3"?l("h2, h3"):l("h2").length?l("h2"):l("h3");if(!t.length){L?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(a=>a.id)),o=document.createElement("nav");o.className="dm-contents",o.setAttribute("aria-label",i.title||"Page contents"),i.hover&&(o.dataset.menuHover=i.hover),i.selected&&(o.dataset.menuActive=i.selected),i.accent&&o.style.setProperty("--dm-menu-highlight",i.accent.startsWith("#")?i.accent:`var(--dm-${i.accent})`),i.font&&(o.style.fontFamily=i.font),i.fontSize&&(o.style.fontSize=i.fontSize),i.fontWeight&&(o.style.fontWeight=i.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const n=document.createElement("p");n.className="dm-contents-title",n.textContent=i.title||"",e.appendChild(n),o.appendChild(e);const m=document.createElement("ol");m.className="dm-contents-list"+(i.numbered?" is-numbered":"");const b=[];for(const a of t){if(!a.id){const w=(a.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let p=w;for(let v=2;s.has(p);v++)p=`${w}-${v}`;s.add(p),a.id=p}const c=document.createElement("li");c.className=`dm-contents-item dm-contents-${a.tagName.toLowerCase()}`;const g=document.createElement("a");g.href=`#${a.id}`,g.textContent=a.textContent.trim(),c.appendChild(g),m.appendChild(c),b.push([a,g])}if(o.appendChild(m),u.replaceChildren(o),i.scrollSpy!==!1&&"IntersectionObserver"in window){const a=new Map,c=()=>{const w=b.find(([p])=>a.get(p))||null;b.forEach(([,p])=>{const v=w&&p===w[1];p.classList.toggle("is-active",!!v),v?p.setAttribute("aria-current","location"):p.removeAttribute("aria-current")})},g=new IntersectionObserver(w=>{w.forEach(p=>a.set(p.target,p.isIntersecting)),c()},{rootMargin:"-80px 0px -60% 0px"});b.forEach(([w])=>g.observe(w))}if(L?.classList.contains("contents-collapsible")){const a=document.createElement("button");a.type="button",a.className="dm-contents-toggle";const c=()=>{const g=L.classList.contains("contents-collapsed");a.setAttribute("aria-expanded",String(!g)),a.setAttribute("aria-label",g?"Show the contents":"Hide the contents"),a.title=a.getAttribute("aria-label"),a.innerHTML=`<span data-icon="${g?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(a)};a.addEventListener("click",()=>{S.set("contents_collapsed",L.classList.toggle("contents-collapsed")),c()}),c(),e.insertBefore(a,e.firstChild)}if(L?.classList.contains("contents-floating")&&i.movable){o.classList.add("is-movable");const a=S.get("contents_position");a&&Number.isFinite(a.x)&&Object.assign(u.style,{left:`${a.x}px`,top:`${a.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",c=>{if(c.target.closest("button"))return;const g=u.getBoundingClientRect(),w=c.clientX-g.left,p=c.clientY-g.top,v=d=>{const y=Math.max(0,Math.min(window.innerWidth-g.width,d.clientX-w)),C=Math.max(0,Math.min(window.innerHeight-40,d.clientY-p));Object.assign(u.style,{left:`${y}px`,top:`${C}px`,right:"auto",bottom:"auto"})},f=()=>{document.removeEventListener("pointermove",v),S.set("contents_position",{x:parseInt(u.style.left,10),y:parseInt(u.style.top,10)})};document.addEventListener("pointermove",v),document.addEventListener("pointerup",f,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(u,{items:()=>{const a=L?.classList.contains("contents-collapsible"),c=L?.classList.contains("contents-collapsed"),g=b.find(([,w])=>w.classList.contains("is-active"));return[{header:"Contents"},...a?[{label:c?"Show the contents":"Collapse to a button",icon:c?"list":"minimize",action:()=>u.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!g,action:async()=>{const w=`${location.origin}${location.pathname}#${g[0].id}`;try{await _.copyToClipboard(w),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"})}]}}),L?.classList.contains("contents-autohide")&&(u.addEventListener("pointerdown",a=>{a.pointerType!=="mouse"&&!u.classList.contains("is-revealed")&&(u.classList.add("is-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{u.contains(a.target)||u.classList.remove("is-revealed")}))}
1
+ import{applyMobileMenu as oe,initCarouselPopup as ne}from"/public/js/mobile-ui.mjs?v=20261016-mobile-options";import{installSmoothCarets as se}from"/public/js/smooth-caret.js";import{decodeBase64Json as W}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as J,applyColour as U,applyPill as ae,applyFloat as ie,applyHighlight as V,applyItemHighlight as re,itemHighlightAttrs as ce,isCurrentItem as G,menuAlign as le,applyClasses as j,cssClasses as Y,applyEntrance as X}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{applyMenuLook as de}from"/public/js/menu-cards.mjs?v=20261029-glider-layout";import{initPageMotion as me}from"/public/js/page-motion.mjs?v=20261029-page-motion";import{applyFooterShowcase as ue}from"/public/js/footer-showcase.mjs?v=20261029-footer-showcase";import{initFloatingMenus as pe}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as K,markInvalidFields as Q,showValidationPopup as Z}from"/public/js/forms.js?v=20261025-forms-files-total";import{buildWizardSteps as fe}from"/public/js/form-steps.mjs";import{mountMotionControl as he}from"/public/js/motion-control.mjs?v=20261029-motion-under-switcher";import{applyFormClasses as ee,applySubmitButtons as ge,applyWizardButtons as ye}from"/public/js/form-submit.mjs";import{captureFileFields as te,destroyDropzones as ve,fileBlueprint as be,restoreFileFields as we,setFileLimits as Ce,splitFiles as Se,submitBody as xe,totalTooLarge as ke,wireFileFields as Le}from"/public/js/form-files.mjs";document.addEventListener("click",u=>{const r=u.target?.closest?.(".dm-banner__dismiss");r&&(u.preventDefault(),r.closest(".dm-banner")?.remove())}),$(()=>{se(document);const u=window.__CMS_NAV__||{},r=window.__CMS_SITE__||{};if(r.autoTheme?.enabled){let s=function(o){const c=(o||"07:00").split(":");return+c[0]*60+(+c[1]||0)},n=function(){const o=new Date,c=o.getHours()*60+o.getMinutes();return c>=s(t.dayStart)&&c<s(t.nightStart)?t.dayTheme:t.nightTheme},e=function(o){const c=t.apply&&t.apply[o]||{theme:o,custom:""};Domma.theme.set(c.theme);for(const f of[...document.body.classList])f.startsWith("dm-custom-")&&document.body.classList.remove(f);c.custom&&document.body.classList.add(c.custom)};const t=r.autoTheme;e(n()),setInterval(()=>e(n()),6e4)}const L=$("#site-navbar");if(L.length&&u.brand){let n=function(m,w=0){return Array.isArray(m)?m.filter(d=>!(d&&(d.type==="spacer"||d.type==="theme-switcher"))).filter(d=>!(d&&d.type==="separator"&&w===0)).map(d=>d&&d.type==="separator"?{divider:!0}:Array.isArray(d?.items)?{...d,items:n(d.items,w+1)}:d):m};const t={...u.brand},s=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let m="";t.logo?m+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(m+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(m+=`<span class="navbar-brand-text${s}">${t.text}</span>`),t.tagline&&(m+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=m}else s&&t.text&&(t.html=`<span class="navbar-brand-text${s}">${t.text}</span>`);const e=n(u.items||[]),o=u.position==="floating",c=u.orientation==="vertical";L.get(0).querySelector(":scope > .cms-nojs-links")?.remove(),Domma.elements.navbar("#site-navbar",{brand:t,items:e,variant:u.variant||"dark",position:o?"static":u.position||"sticky",collapsible:!0,appearOnHover:!!u.appearOnHover}),Domma.icons.scan("#site-navbar");const f=L.get(0);oe(f,u.mobile);const a=()=>{if(!c)return;const w=f.querySelector('a[aria-current="page"]')?.closest(".navbar-dropdown");w&&!w.classList.contains("open")&&w.querySelector(":scope > .navbar-dropdown-toggle")?.click()};f.classList.add(`dm-nav-align-${le(u)||"centre"}`),c&&L.get(0).classList.add("dm-nav-vertical",u.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const h=u.dock||{};c&&!o&&$e(f,u.side==="right"),!c&&h.hideOnScroll&&Ee(f),V(f,u),X(f,u),j(f,u.class),o&&(ie(L.get(0),u),pe(L.get(0).parentNode||document));const l=f.querySelector(".navbar-toggle"),y=f.querySelector(".navbar-collapse");if(l&&y){let m=null;const w=()=>{y.classList.contains("show")&&l.click()},d=()=>{m&&(m.remove(),m=null),document.body.classList.remove("dm-drawer-open")},p=()=>{m||(m=document.createElement("div"),m.className="dm-navbar-backdrop",m.addEventListener("click",w),document.body.appendChild(m),document.body.classList.add("dm-drawer-open"))};l.addEventListener("click",()=>{requestAnimationFrame(()=>{y.classList.contains("show")?(a(),p()):d()})}),y.addEventListener("click",v=>{v.target.closest("a")&&requestAnimationFrame(d)}),document.addEventListener("keydown",v=>{v.key==="Escape"&&y.classList.contains("show")&&w()})}const C=f.querySelector(".navbar-nav");if(C){const m=Array.from(C.children);let w=0;for(const d of u.items||[])if(!(d&&d.type==="separator")){if(d&&d.type==="spacer"){const p=document.createElement("li"),v=["xs","sm","md","lg","xl","flex"].includes(d.size)?d.size:"md";p.className=`dm-menu-spacer dm-menu-spacer--${v}`,p.setAttribute("aria-hidden","true"),C.insertBefore(p,m[w]||null);continue}if(d&&d.type==="theme-switcher"){const p=document.createElement("li");p.className="dm-menu-theme-switcher";const v=document.createElement("span");v.dataset.themeSwitcher="menu",v.dataset.position="inline",p.appendChild(v),C.insertBefore(p,m[w]||null);continue}w++}window.CMSThemeSwitcher?.scan(C)}f.querySelectorAll("[data-path]").forEach(m=>{const w=m.dataset.path.split(".").map(Number),d=w.length>1,p=w.reduce((A,i)=>A?.items?.[i],{items:e});if(!p)return;p.colour&&U(m,p.colour),p.pill&&!d&&ae(m,p.pill),re(m,p),j(m.closest("li"),p.class);const v=J(p.badge);v&&m.appendChild(v),G(p.url,location.pathname)&&(m.classList.add("active"),m.setAttribute("aria-current","page"))}),de(f,u,e),a()}let k=null;const x=$("#site-footer");if(x.length){let o=function(i){return String(i??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},c=function(i){return o(i).replace(/'/g,"&#39;")},f=function(i){if(i&&i.type==="separator")return'<li class="footer-sep"><hr></li>';if(i&&i.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(i.size)?i.size:"md"}" aria-hidden="true"></li>`;if(i&&i.type==="theme-switcher")return'<li class="dm-menu-theme-switcher"><span data-theme-switcher="menu" data-position="inline"></span></li>';const M=c(i.url||"#"),q=o(i.text||""),g=Array.isArray(i.items)&&i.items.length?`<ul>${i.items.map(f).join("")}</ul>`:"",D=ce(i),N=[D.hover&&` data-menu-item-hover="${c(D.hover)}"`,D.active&&` data-menu-item-active="${c(D.active)}"`].filter(Boolean).join(""),O=D.accent?` style="--dm-menu-highlight:${c(D.accent)}"`:"",R=Y(i.class).join(" ");return`<li${R?` class="${c(R)}"`:""}><a href="${M}"${N}${O}>${q}</a>${g}</li>`},h=function(){if(!r.footer)return"";const i=Object.keys(s).filter(q=>t[q]);if(!i.length)return"";const M=Y(r.footer.socialClass).join(" ");return`<div class="footer-social">${i.map(q=>{const{label:g,svg:D}=s[q];return`<a href="${t[q]}" target="_blank" rel="noopener noreferrer" aria-label="${g}" class="footer-social-link${M?` ${c(M)}`:""}">${D}</a>`}).join("")}</div>`},m=function(i){if(!i||i.type==="separator"||i.type==="spacer")return"";if(i.type==="theme-switcher")return`<li class="cms-footer-col">${f(i).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const M=Array.isArray(i.items)&&i.items.length?`<ul>${i.items.map(f).join("")}</ul>`:"",q=C(i)?`<span class="cms-footer-col-title">${o(i.text||"")}</span>`:`<a class="cms-footer-col-title" href="${c(i.url||"#")}">${o(i.text||"")}</a>`,g=["cms-footer-col",...Y(i.class)].join(" ");return`<li class="${c(g)}">${q}${M}</li>`},p=function(i,M,q=!1){return(i||[]).forEach(g=>{g&&(g.type==="separator"||g.type==="spacer"||g.type==="theme-switcher")||(q&&C(g)||M.push(g),Array.isArray(g.items)&&g.items.length&&p(g.items,M))}),M};const t=r.social||{},s={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 n='<div class="footer-inner container">';const e=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},a=(()=>{const i=r.cookieConsent;if(!i||i.showSettingsLink===!1)return"";const M=q=>(q||[]).some(g=>g&&(g.url==="#cookie-settings"||M(g.items)));return M(e.primary)||M(e.legal)?"":`<li class="cms-cookie-settings"><a href="#cookie-settings">${o(i.settingsLinkText||"Cookie settings")}</a></li>`})(),l=["columns","row"].includes(r.footer?.layout)?r.footer.layout:"auto",y=r.footer?.look==="showcase"||l==="columns"||l==="auto"&&e.primary.some(i=>Array.isArray(i?.items)&&i.items.length),C=i=>y&&!i.url&&Array.isArray(i.items)&&i.items.length>0;if(y){const i=u.brand||{},M=i.text||r.title||"",q=i.logo?`<img src="${c(i.logo)}" class="cms-footer-brand-logo" alt="">`:i.icon?'<span class="cms-footer-brand-icon"></span>':"",g=String(r.footer?.blurb||"").trim();n='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',n+=`<div class="cms-footer-brand">${M||q?`<a class="cms-footer-brand-name" href="${c(i.url||"/")}">${q}<span>${o(M)}</span></a>`:""}${g?`<p class="cms-footer-blurb">${o(g)}</p>`:""}${h()}</div>`,e.primary.length&&(n+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${e.primary.map(m).join("")}</ul></nav>`),n+='</div><div class="cms-footer-bottom">',e.copyright&&(n+=`<p class="cms-footer-copyright">${o(e.copyright)}</p>`),(e.legal.length||a)&&(n+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${e.legal.map(f).join("")}${a}</ul></nav>`),n+="</div>"}else e.copyright&&(n+=`<p>${o(e.copyright)}</p>`),e.primary.length&&(n+=`<nav class="footer-nav footer-nav--primary"><ul>${e.primary.map(f).join("")}</ul></nav>`),(e.legal.length||a)&&(n+=`<nav class="footer-nav footer-nav--legal"><ul>${e.legal.map(f).join("")}${a}</ul></nav>`),n+=h();n+="</div>",x.get(0).querySelector(":scope > .cms-nojs-links")?.remove();const w=x.get(0).querySelector(":scope > .cms-footer-blocks");w?.remove(),x.html(n),w&&x.get(0).prepend(w),window.CMSThemeSwitcher?.scan(x.get(0));const d=x.get(0).querySelector(".cms-footer-brand-icon");d&&u.brand?.icon&&d.setAttribute("data-icon",u.brand.icon),j(x.get(0),r.footer?.class),y&&ue(x.get(0),r);const v=[...p(e.primary,[],!0),...p(e.legal,[])];x.get(0).querySelectorAll(".footer-nav a").forEach((i,M)=>{const q=v[M];if(!q)return;q.colour&&U(i,q.colour);const g=J(q.badge);g&&i.appendChild(g),G(q.url,location.pathname)&&(i.classList.add("is-active"),i.setAttribute("aria-current","page"))}),V(x.get(0).querySelector(".footer-nav--primary"),{highlight:e.primaryHighlight}),V(x.get(0).querySelector(".footer-nav--legal"),{highlight:e.legalHighlight}),X(x.get(0).querySelector(".footer-nav--primary"),{entrance:e.primaryEntrance}),X(x.get(0).querySelector(".footer-nav--legal"),{entrance:e.legalEntrance}),j(x.get(0).querySelector(".footer-nav--primary"),e.primaryClass),j(x.get(0).querySelector(".footer-nav--legal"),e.legalClass),k=x.get(0).querySelector(".cms-footer-bottom")||x.get(0).querySelector(".footer-inner")}he(r.motion||{enabled:r.footer?.showMotionToggle!==!1,default:r.footer?.motionDefault,placement:"footer",control:r.footer?.motionControl,label:"Reduce motion"},{footer:k,navbar:document.querySelector("#site-navbar .navbar-container")?.closest("#site-navbar")||null});const I=document.getElementById("site-sidebar");I&&Ae(I),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const b=$(".page-body");if(b.length){b.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),b.find(".tabs").each(function(){Domma.elements.tabs(this)}),b.find(".carousel").each(function(){const s={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"},n=h=>{const l=String(h||"").trim();return l?s[l.toLowerCase()]?`var(${s[l.toLowerCase()]})`:CSS.supports("color",l)?l:"":""},e=n(this.dataset.background),o=n(this.dataset.arrowBackground),c=n(this.dataset.arrowColor);e&&(this.style.setProperty("--dm-carousel-background",e),this.classList.add("dm-carousel-custom-background")),o&&this.style.setProperty("--dm-carousel-arrow-background",o),c&&this.style.setProperty("--dm-carousel-arrow-color",c);const f=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${f}`);const a={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&&(a.height=h)}if(this.dataset.animationDuration){const h=parseInt(this.dataset.animationDuration,10);Number.isFinite(h)&&(a.animationDuration=h)}this.dataset.animationEasing&&(a.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(h=>h.cloneNode(!0)),this._dmCarouselOpts=a),this._dmCarousel=Domma.elements.carousel(this,a),ne(this)}),b.find(".dm-countdown").each(function(){const s={autoStart:!0},n=String(this.dataset.format||"").trim(),e=String(this.dataset.to||"").trim(),o=/^(\d{4})-(\d{2})-(\d{2})$/.exec(e),c=o?new Date(+o[1],+o[2]-1,+o[3]).getTime():e?new Date(e).getTime():NaN;Number.isFinite(c)?s.duration=Math.max(0,c-Date.now()):this.dataset.duration&&(s.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const f=/^dd:/i.test(n);s.format=f?"hh:mm:ss":n||(s.duration>=36e5?"hh:mm:ss":"mm:ss");const a=this,h=y=>String(y).padStart(2,"0"),l=y=>{const C=Math.max(0,Math.floor(y/1e3)),m=a.querySelector(".dm-timer-time");m&&(m.textContent=`${h(Math.floor(C/86400))}:${h(Math.floor(C%86400/3600))}:${h(Math.floor(C%3600/60))}:${h(C%60)}`)};f&&(s.onTick=l),Domma.elements.timer(this,s),f&&l(s.duration||0)}),b.find("[data-tooltip]:not(.dm-range-slider)").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),b.find(".dm-progression").each(function(){const s=Array.from(this.querySelectorAll(".dm-progression-item")).map(o=>{const c={title:o.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:o.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return o.dataset.date&&(c.date=o.dataset.date),o.dataset.status&&(c.status=o.dataset.status),o.dataset.eventIcon&&(c.icon=o.dataset.eventIcon),o.dataset.entryId&&(c._entryId=o.dataset.entryId),c}),n=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",e=Domma.elements.progression(this,{layout:n,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...s.length?{items:s}:{}});if((this.dataset.mode||"timeline")==="timeline"){const o={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((c,f)=>{const a=s[f];if(!a)return;const h=c.querySelector(".dm-progression-title");if(a.icon&&h){const y=document.createElement("span");y.dataset.icon=a.icon,y.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",y.setAttribute("aria-hidden","true"),h.prepend(y)}const l=o[a.status];if(l&&h){const y=document.createElement("span");y.className=`badge badge-${l.tone} cms-timeline-status`,y.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const C=document.createElement("span");C.dataset.icon=l.icon,C.dataset.iconSize="14",C.setAttribute("aria-hidden","true"),y.append(C,document.createTextNode(l.label)),h.after(y)}})}Domma.icons.scan(this),s.length&&(this._dmCtx={instance:e,items:s})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}const t=matchMedia("(prefers-reduced-motion: reduce)").matches||document.documentElement.classList.contains("dm-reduced-motion");document.querySelectorAll(".page-body .row[data-reveal]").forEach(s=>{const n=s.dataset.revealMode||"stagger",e=s.dataset.revealAnimation||"slide-up",o=parseInt(s.dataset.revealDuration,10)||(e.startsWith("swoosh")?700:400),c=parseInt(s.dataset.revealStagger,10)||(e.startsWith("swoosh")?90:60),f=parseInt(s.dataset.revealDelay,10)||0,a=s.dataset.revealDirection==="rtl",h=s.dataset.revealReplay==="true",l=Array.from(s.children);if(t&&e.startsWith("swoosh"))return;const y={"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)",swoosh:"translateY(36px) scale(0.96)"},C=p=>e!=="swoosh-sides"?y[e]||"":l.length>1&&p===0?"translateX(-64px) rotate(-1.5deg)":l.length>1&&p===l.length-1?"translateX(64px) rotate(1.5deg)":"translateY(40px) scale(0.94)",m=e.startsWith("swoosh"),w=m?"cubic-bezier(.2,.8,.2,1)":"ease",d=(p,v)=>{p.style.opacity="0",p.style.transform=C(v),m&&(p.style.filter="blur(10px)")};l.forEach((p,v)=>{d(p,v),p.style.transition=`opacity ${o}ms ${w}, transform ${o}ms ${w}`+(m?`, filter ${o}ms ${w}`:"");const A=a?l.length-1-v:v;p.style.transitionDelay=n==="stagger"?`${f+A*c}ms`:`${f}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const p=new IntersectionObserver(v=>{v.forEach(A=>{if(A.isIntersecting){if(A.target.offsetWidth,A.target.style.opacity="1",A.target.style.transform="",m&&(A.target.style.filter=""),!h){p.unobserve(A.target);const i=A.target;i.addEventListener("transitionend",function M(q){q.target!==i||q.propertyName!=="opacity"||(i.removeEventListener("transitionend",M),i.style.transition="",i.style.transitionDelay="")})}}else h&&A.boundingClientRect.top>0&&d(A.target,l.indexOf(A.target))})},{threshold:.1});l.forEach(v=>p.observe(v))}))}),me(document.querySelector(".page-body")||document),b.find(".card[data-collapsible]").each(function(){const s=this.querySelector(".card-header");s&&s.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),b.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const s=this.dataset.soTarget,n=document.getElementById(s);if(!n)return;const e=E.slideover({title:n.dataset.soTitle||"",size:n.dataset.soSize||"md",position:n.dataset.soPosition||"right"});n.style.display="",e.setContent(n),e.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(s=>{try{const n=W(s.dataset.config);Object.assign(t,n)}catch{}}),Object.keys(t).length>0){const s={};for(const[n,e]of Object.entries(t)){const o=e?.events?.click,{confirm:c,toast:f,alert:a,prompt:h,...l}=o||{};if(c||f||a||h){$(n).on("click",async function(R){if(R.preventDefault(),c&&!await E.confirm(c))return;let z=null;if(!(h&&(z=await E.prompt(h,{inputPlaceholder:l.promptPlaceholder||"",inputValue:l.promptDefault||""}),z===null))){{const B=$(l.target||this);l.toggleClass&&B.toggleClass(l.toggleClass),l.addClass&&B.addClass(l.addClass),l.removeClass&&B.removeClass(l.removeClass),z!==null&&(l.setText&&B.text(z),l.setVal&&B.val(z),l.setAttr&&B.attr(l.setAttr,z))}l.href&&(window.location.href=l.href),f&&E.toast(f,{type:l.toastType||"success"}),a&&E.alert(a)}});const{target:C,toggleClass:m,addClass:w,removeClass:d,setText:p,setVal:v,setAttr:A,href:i,toastType:M,promptPlaceholder:q,promptDefault:g,...D}=l,N={...e.events};Object.keys(D).length?N.click=C?{target:C,...D}:D:delete N.click;const O={...e,events:N};Object.keys(N).length||delete O.events,Object.keys(O).length&&(s[n]=O)}else s[n]=e}$.setup(s)}}b.length&&P(b.get(0))}),window.wireCTAButtons=P;function P(u){u.querySelectorAll(".dm-cta-trigger").forEach(r=>{r.__ctaWired||(r.__ctaWired=!0,r.addEventListener("click",async()=>{const L=r.dataset.action,k=r.dataset.entry,x=r.dataset.confirm;let I=S.get("auth_token");if(!I){E.toast("Please log in to perform this action.",{type:"warning"});return}if(x&&!await E.confirm(x))return;const b=Array.from(r.childNodes).map(s=>s.cloneNode(!0));r.disabled=!0,r.textContent="Running\u2026";const t=s=>fetch(`/api/actions/${L}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(r.dataset.scope?{entryId:k,scope:r.dataset.scope}:{entryId:k})});try{let s=await t(I);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const o=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(o.ok){const{token:c}=await o.json();S.set("auth_token",c),I=c,s=await t(I)}}}const n=await s.json().catch(()=>({}));if(!s.ok||n.success===!1)throw new Error(n.error||n.message||`Error ${s.status}`);E.toast(n.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{r.disabled=!1,r.textContent="",b.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 L=S.get("auth_token");function k(I,b){const t=document.createElement("div");t.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(b){s.appendChild(document.createTextNode("Please "));const n=document.createElement("a");n.href="/login",n.textContent="sign in",s.appendChild(n),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=I;return t.appendChild(s),t}function x(I,b){const t=document.createElement("template");t.innerHTML=b||"",I.replaceWith(t.content)}r.forEach(async I=>{const b=I.dataset.collectionAttrs;if(!b)return;if(!L){I.replaceChildren(k("",!0));return}const t=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:b})});try{let s=L,n=await t(s);if(n.status===401){const o=S.get("auth_refresh_token");if(o){const c=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:o})});if(c.ok){const{token:f}=await c.json();S.set("auth_token",f),s=f,n=await t(s)}}}if(!n.ok){const{error:o}=await n.json().catch(()=>({}));I.replaceChildren(k(o||"Unable to load entries.",!1));return}const{html:e}=await n.json();x(I,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{I.replaceChildren(k("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),P(document.body)},100)})(),(function(){const r=document.querySelectorAll("[data-collection-table]");!r.length||typeof T>"u"||!T.create||r.forEach(L=>{let k;try{k=W(L.dataset.payload)}catch{return}const{columns:x,rows:I,search:b,sortable:t,exportable:s,pageSize:n,empty:e,ctaConfig:o,slug:c,exportCfg:f}=k;if(!x?.length)return;if(o){const l=x.findIndex(y=>y.key==="_cta");l!==-1&&(x[l]={key:"_cta",title:"",render:(y,C)=>{const m=document.createElement("button");if(m.className=`btn btn-${o.style||"primary"} dm-cta-trigger`,m.dataset.action=o.action||"",o.scope&&(m.dataset.scope=o.scope),m.dataset.entry=C._entryId||"",o.confirm&&(m.dataset.confirm=o.confirm),o.icon){const w=document.createElement("span");w.dataset.icon=o.icon,m.appendChild(w),m.appendChild(document.createTextNode(" "))}return m.appendChild(document.createTextNode(o.label||"Run")),m}})}const a="col-table-"+Math.random().toString(36).slice(2,7),h=document.createElement("div");h.id=a,L.replaceChildren(h),T.create("#"+a,{data:I,columns:x,search:b,sortable:t,exportable:s,pageSize:n,emptyMessage:e}),o&&P(h),L._dmTable={columns:x,rows:I,slug:c,exportCfg:f,render(l){T.create("#"+a,{data:l,columns:x,search:b,sortable:t,exportable:s,pageSize:n,emptyMessage:e}),o&&P(h)}}})})(),(function(){const r=document.querySelectorAll("[data-form-inline]");if(!r.length||typeof F>"u")return;function L(t){const s=t&&t.logic;return!!(s&&(s.visibility||s.requirement))}function k(t,s){const n={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const o=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),c=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":o?"chooser":e.type,f={...e.formConfig||{}};f.span==="full"&&s&&(f.span=s);const a={type:c,required:L(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},...f}};e.minLength!==void 0&&(a.minLength=e.minLength),e.maxLength!==void 0&&(a.maxLength=e.maxLength),e.min!==void 0&&(a.min=e.min),e.max!==void 0&&(a.max=e.max),o&&(e.variant&&(a.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(a.multiple=!0),e.density&&(a.density=e.density),e.columns&&(a.columns=e.columns),e.accent&&(a.accent=e.accent),e.accentStyle&&(a.accentStyle=e.accentStyle),e.glow&&(a.glow=!0),e.glowColour&&(a.glowColour=e.glowColour),e.shadow&&(a.shadow=e.shadow),e.shadowColour&&(a.shadowColour=e.shadowColour)),n[e.name]=e.type==="file"?be(e,a):a}),n}function x(t){const s={};return(t||[]).forEach(n=>{if(!(!n.name||n.type==="page-break"||n.type==="spacer")&&(n.type==="select"||n.type==="multiselect")&&n.required){const e=(n.options||[])[0];e&&(s[n.name]=typeof e=="object"?e.value:e)}}),s}function I(t,s){(s||[]).forEach(n=>{if(n.type!=="date"||!n.name)return;const e=t.querySelector(`[name="${n.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function b(t,s,n){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=s,e.style.cssText=n?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}r.forEach(t=>{let s;try{s=W(t.dataset.formInline)}catch{return}t.querySelector(":scope > .cms-form-embed-summary")?.remove();const n=s.fields||[],e=s.settings||{},o=Number(e.columns)===3?3:2,c=e.layout||"grid",f=n.some(w=>w.type==="page-break"),a=new Map;Ce(t,s.fileLimits,a),Le(t,n);const h=async w=>{if(t.dataset.triggerBlocked==="true"){const d=t._formLogicRuntime,p=d&&d.submitBlock?d.submitBlock().reasons:[];b(t,p.join(" ")||"This form cannot be submitted yet.",!0);const v=new Error("Blocked by a form trigger");throw v.formSubmitFailed=!0,v}try{const d=t.querySelector(".fb-form-honeypot"),p=d?.querySelector('input[type="text"]')||null,v=d?.querySelector('[name="_t"]')||null,A=Object.assign({},w);delete A._t,d?.dataset.hpName&&delete A[d.dataset.hpName],p!==null&&(A._hp=p.value),v!==null&&(A._t=v.value);const i=new Map(a);te(t,i);const M=Se(A,n,i),q=ke(t,M.files);if(q){b(t,q,!0);const D=new Error(q);throw D.formSubmitFailed=!0,D}const g=await H.post(`/api/forms/submit/${s.slug}`,M.files.size?xe(M.values,M.files).body:M.values);if(g&&g.error){b(t,g.error,!0);const D=new Error(g.error);throw D.formSubmitFailed=!0,D}if(g?.redirect){window.location.href=g.redirect;return}for(ve(t);t.firstChild;)t.removeChild(t.firstChild);b(t,g?.message||e.successMessage||"Thank you for your submission.",!1)}catch(d){throw d.formSubmitFailed||b(t,d.message||"Submission failed. Please try again.",!0),d}},l=Date.now();function y(w){const d=w.querySelector("form");if(!d||d.querySelector(".fb-form-honeypot"))return;const p=document.createElement("div");p.className="fb-form-honeypot",p.setAttribute("aria-hidden","true");const v=document.createElement("input");v.name=d.querySelector('[name="website"]')?"_website":"website",p.dataset.hpName=v.name,v.type="text",v.tabIndex=-1,v.autocomplete="url",v.placeholder="https://",p.appendChild(v);const A=document.createElement("input");A.name="_t",A.type="hidden",A.value=l,p.appendChild(A),d.appendChild(p)}function C(w){if(!window.FormLogicEngine||!n.some(p=>p.logic||Array.isArray(p.triggers)&&p.triggers.length))return null;const d=new window.FormLogicEngine.FormLogicRuntime(s,t,w?{baseValues:w}:void 0);if(d.init(),t._formLogicRuntime=d,t._formLogicObserved)return d;if(t._formLogicObserved=!0,t.parentNode&&typeof MutationObserver<"u"){const p=new MutationObserver(function(v){for(const A of v)for(const i of A.removedNodes)if(i===t||i.nodeType===1&&i.contains&&i.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),p.disconnect();return}});p.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=p}return d}let m=!1;if(t.addEventListener("dm-form-end",w=>{const d=w.detail?.validation;if(d&&!d.valid){Q(t,d.missing),Z(d);return}m||(m=!0,h(w.detail?.values||{}).catch(()=>{}).finally(()=>{m=!1}))}),f&&F.wizard){const w=fe(n,s.title),d=w.map(g=>({title:g.title,description:g.description,fields:k(g.fields,o)})),p=w.map(g=>g.fields);let v=0,A=null,i=null;const M=g=>{const D=window.FormLogicEngine;if(!D||!D.validateFields)return;const N=D.validateFields(t,p[v]||[]);N.valid||(g.preventDefault(),g.stopImmediatePropagation(),Q(t,N.missing),Z(N))},q=()=>{const g=p[v]||n;ye(t,e),ee(t,e,g),I(t,g),K(t,g),we(t,a),e.honeypot!==!1&&y(t),A&&A.destroy(),A=C(i?.getStepData?i.getStepData():void 0),t.querySelectorAll(".wizard-next, .wizard-submit").forEach(D=>D.addEventListener("click",M,!0))};i=F.wizard(t,{schema:{steps:d},layout:c,columns:o,finishText:e.submitText||"Finish",onSubmit:h,onStepChange:g=>{te(t,a),v=g,setTimeout(q,0)}}),t.addEventListener("dm-form-trigger",g=>{if(g.detail?.type!=="goto-step"||!i?.goToStep)return;const D=Number(g.detail.action.step);!Number.isFinite(D)||D<0||D>=d.length||D!==v&&i.goToStep(D)}),Promise.resolve(i).then(q)}else if(F.render){const w=F.render(t,k(n,o),x(n),{submitText:e.submitText||"Submit",layout:c,columns:o,onSubmit:h});Promise.resolve(w).then(function(){ge(t,e,c),ee(t,e,n),I(t,n),K(t,n),e.honeypot!==!1&&y(t),C()})}})})();function $e(u,r){const L=document.body,k=L.classList,x=new Set((L.dataset.railSite||"").split(" ").filter(Boolean)),I=()=>window.matchMedia("(min-width: 993px)").matches;u.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(o=>{const c=o.textContent.trim();c&&(o.setAttribute("title",c),o.querySelector("svg, [data-icon]")||(o.dataset.initial=c.charAt(0).toUpperCase()))});const b=document.createElement("button");b.type="button",b.className="dm-nav-collapse-toggle";const t=()=>{const o=k.contains("dm-vnav-collapsed");b.setAttribute("aria-pressed",String(o)),b.setAttribute("aria-label",o?"Expand menu":"Collapse menu"),b.title=b.getAttribute("aria-label");const c=r?"chevrons-left":"chevrons-right",f=r?"chevrons-right":"chevrons-left";b.innerHTML=`<span data-icon="${o?c:f}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(b)},s=o=>{k.toggle("dm-vnav-collapsed",o),S.set("nav_collapsed",o),t()};b.addEventListener("click",()=>s(!k.contains("dm-vnav-collapsed"))),t();const n=u.querySelector(".navbar-container")||u;n.insertBefore(b,n.firstChild?.nextSibling||null);const e=o=>{k.toggle("dm-vnav-autohide",o),k.toggle("dm-vnav-collapsible",!o&&x.has("collapsible")),o?k.remove("dm-vnav-collapsed"):x.has("collapsible")&&S.get("nav_collapsed")===!0&&k.add("dm-vnav-collapsed"),u.classList.remove("dm-nav-revealed"),t()};u.addEventListener("pointerdown",o=>{k.contains("dm-vnav-autohide")&&o.pointerType!=="mouse"&&!u.classList.contains("dm-nav-revealed")&&(u.classList.add("dm-nav-revealed"),o.preventDefault())}),document.addEventListener("pointerdown",o=>{u.contains(o.target)||u.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(u,{items:()=>{const o=k.contains("dm-vnav-autohide"),c=k.contains("dm-vnav-collapsed"),f=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:c?"Expand menu":"Collapse to icons",icon:c?"maximize":"minimize",disabled:!I()||o||!x.has("collapsible"),action:()=>s(!c)},{label:o?"Keep on screen":"Hide when not in use",icon:o?"eye":"eye-off",disabled:!I(),action:()=>{S.set("nav_autohide",!o),e(!o)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!f,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(x.has("autohide")),k.toggle("dm-vnav-collapsed",!x.has("autohide")&&x.has("start-collapsed")),t()}}]}})}function Ee(u){u.classList.add("dm-nav-scroll-hide");let r=window.scrollY,L=!1;window.addEventListener("scroll",()=>{L||(L=!0,requestAnimationFrame(()=>{L=!1;const k=window.scrollY,x=u.matches(":focus-within")||u.querySelector(".navbar-dropdown.open, .navbar-collapse.show");k>r&&k>u.offsetHeight&&!x?u.classList.add("dm-nav-scrolled-away"):k<r&&u.classList.remove("dm-nav-scrolled-away"),r=k}))},{passive:!0})}function Ae(u){let r={};try{r=JSON.parse(u.dataset.contents||"{}")}catch{}const L=u.closest(".site-main"),k=document.querySelector(".site-content"),x=new Set(String(r.exclude||"").split(",").map(a=>a.trim().toLowerCase()).filter(Boolean)),I=a=>!a.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),b=a=>(k?[...k.querySelectorAll(a)]:[]).filter(I).filter(h=>!x.has(h.textContent.trim().toLowerCase())),t=r.headings==="h2"?b("h2"):r.headings==="h3"?b("h3"):r.headings==="h2h3"?b("h2, h3"):b("h2").length?b("h2"):b("h3");if(!t.length){L?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(a=>a.id)),n=document.createElement("nav");n.className="dm-contents",n.setAttribute("aria-label",r.title||"Page contents"),r.hover&&(n.dataset.menuHover=r.hover),r.selected&&(n.dataset.menuActive=r.selected),r.accent&&n.style.setProperty("--dm-menu-highlight",r.accent.startsWith("#")?r.accent:`var(--dm-${r.accent})`),r.font&&(n.style.fontFamily=r.font),r.fontSize&&(n.style.fontSize=r.fontSize),r.fontWeight&&(n.style.fontWeight=r.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const o=document.createElement("p");o.className="dm-contents-title",o.textContent=r.title||"",e.appendChild(o),n.appendChild(e);const c=document.createElement("ol");c.className="dm-contents-list"+(r.numbered?" is-numbered":"");const f=[];for(const a of t){if(!a.id){const y=(a.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let C=y;for(let m=2;s.has(C);m++)C=`${y}-${m}`;s.add(C),a.id=C}const h=document.createElement("li");h.className=`dm-contents-item dm-contents-${a.tagName.toLowerCase()}`;const l=document.createElement("a");l.href=`#${a.id}`,l.textContent=a.textContent.trim(),h.appendChild(l),c.appendChild(h),f.push([a,l])}if(n.appendChild(c),u.replaceChildren(n),r.scrollSpy!==!1&&"IntersectionObserver"in window){const a=new Map,h=()=>{const y=f.find(([C])=>a.get(C))||null;f.forEach(([,C])=>{const m=y&&C===y[1];C.classList.toggle("is-active",!!m),m?C.setAttribute("aria-current","location"):C.removeAttribute("aria-current")})},l=new IntersectionObserver(y=>{y.forEach(C=>a.set(C.target,C.isIntersecting)),h()},{rootMargin:"-80px 0px -60% 0px"});f.forEach(([y])=>l.observe(y))}if(L?.classList.contains("contents-collapsible")){const a=document.createElement("button");a.type="button",a.className="dm-contents-toggle";const h=()=>{const l=L.classList.contains("contents-collapsed");a.setAttribute("aria-expanded",String(!l)),a.setAttribute("aria-label",l?"Show the contents":"Hide the contents"),a.title=a.getAttribute("aria-label"),a.innerHTML=`<span data-icon="${l?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(a)};a.addEventListener("click",()=>{S.set("contents_collapsed",L.classList.toggle("contents-collapsed")),h()}),h(),e.insertBefore(a,e.firstChild)}if(L?.classList.contains("contents-floating")&&r.movable){n.classList.add("is-movable");const a=S.get("contents_position");a&&Number.isFinite(a.x)&&Object.assign(u.style,{left:`${a.x}px`,top:`${a.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",h=>{if(h.target.closest("button"))return;const l=u.getBoundingClientRect(),y=h.clientX-l.left,C=h.clientY-l.top,m=d=>{const p=Math.max(0,Math.min(window.innerWidth-l.width,d.clientX-y)),v=Math.max(0,Math.min(window.innerHeight-40,d.clientY-C));Object.assign(u.style,{left:`${p}px`,top:`${v}px`,right:"auto",bottom:"auto"})},w=()=>{document.removeEventListener("pointermove",m),S.set("contents_position",{x:parseInt(u.style.left,10),y:parseInt(u.style.top,10)})};document.addEventListener("pointermove",m),document.addEventListener("pointerup",w,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(u,{items:()=>{const a=L?.classList.contains("contents-collapsible"),h=L?.classList.contains("contents-collapsed"),l=f.find(([,y])=>y.classList.contains("is-active"));return[{header:"Contents"},...a?[{label:h?"Show the contents":"Collapse to a button",icon:h?"list":"minimize",action:()=>u.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!l,action:async()=>{const y=`${location.origin}${location.pathname}#${l[0].id}`;try{await _.copyToClipboard(y),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"})}]}}),L?.classList.contains("contents-autohide")&&(u.addEventListener("pointerdown",a=>{a.pointerType!=="mouse"&&!u.classList.contains("is-revealed")&&(u.classList.add("is-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{u.contains(a.target)||u.classList.remove("is-revealed")}))}
@@ -20,6 +20,7 @@ import {fileURLToPath} from 'url';
20
20
  import * as cache from '../../services/cache/index.js';
21
21
  import {normaliseAdminPath} from '../../services/roles.js';
22
22
  import {cssClassProblems} from '../../../public/js/css-classes.mjs';
23
+ import {motionFooterMirror, normaliseMotionSettings} from '../../services/motionSettings.js';
23
24
  import {invalidateMeasurementId, measurementIdOf, normaliseMeasurementId} from '../../services/googleAnalytics.js';
24
25
 
25
26
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -124,7 +125,9 @@ export async function settingsRoutes(fastify) {
124
125
  // Designer classes on <body>, every page (Site Settings > Appearance).
125
126
  'bodyClass',
126
127
  // Google Analytics (Site Settings > Analytics) - services/googleAnalytics.js.
127
- 'googleAnalytics'
128
+ 'googleAnalytics',
129
+ // The "Reduce motion" control (Site Settings > Reduce motion) - services/motionSettings.js.
130
+ 'motion'
128
131
  ]);
129
132
  const unknownKeys = Object.keys(data).filter(k => !ALLOWED_KEYS.has(k));
130
133
  if (unknownKeys.length > 0) {
@@ -141,7 +144,7 @@ export async function settingsRoutes(fastify) {
141
144
  }
142
145
  // Designer classes - the same rules as menus and forms (css-classes.mjs).
143
146
  for (const [label, value] of [['Body CSS classes', data.bodyClass], ['Footer CSS classes', data.footer?.class],
144
- ['Social link CSS classes', data.footer?.socialClass]]) {
147
+ ['Social link CSS classes', data.footer?.socialClass], ['Reduce motion CSS classes', data.motion?.class]]) {
145
148
  const problems = cssClassProblems(value);
146
149
  if (problems.length) return reply.status(400).send({error: `${label}: ${problems[0]}.`});
147
150
  }
@@ -184,6 +187,24 @@ export async function settingsRoutes(fastify) {
184
187
  merged[k] = v;
185
188
  }
186
189
  }
190
+ // The Reduce motion control: stored whitelisted, with the footer keys it used to live
191
+ // under kept in step for older readers. A client that still writes only those footer
192
+ // keys (fleet tooling) moves the block with them, so its change is not overruled.
193
+ if ('motion' in data) {
194
+ if (data.motion !== null && (typeof data.motion !== 'object' || Array.isArray(data.motion))) {
195
+ return reply.status(400).send({error: 'Reduce motion: send an object.'});
196
+ }
197
+ merged.motion = normaliseMotionSettings(merged.motion, merged.footer);
198
+ merged.footer = {...merged.footer, ...motionFooterMirror(merged.motion)};
199
+ } else if (merged.motion && data.footer && typeof data.footer === 'object') {
200
+ const legacy = {showMotionToggle: 'enabled', motionDefault: 'default', motionControl: 'control'};
201
+ const moved = Object.entries(legacy).filter(([k]) => k in data.footer);
202
+ if (moved.length) {
203
+ const next = {...merged.motion};
204
+ for (const [k, key] of moved) next[key] = k === 'showMotionToggle' ? data.footer[k] !== false : data.footer[k];
205
+ merged.motion = normaliseMotionSettings(next, merged.footer);
206
+ }
207
+ }
187
208
  // Never erase smtp.pass with the empty string that the GET endpoint injects for safety
188
209
  if (merged.smtp && merged.smtp.pass === '' && existing.smtp?.pass) {
189
210
  merged.smtp = {...merged.smtp, pass: existing.smtp.pass};
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The "Reduce motion" control - `site.json` `motion` (Site Settings > Reduce motion).
3
+ *
4
+ * One pure normaliser, shared by the renderer (both render paths: the page's
5
+ * bootstrap default and the object site.js draws the control from) and the
6
+ * settings route (what is stored). Whitelisted and clamped rather than merged,
7
+ * because the result is written into the source of every public page.
8
+ *
9
+ * Until it had a tab of its own the control lived in the footer settings: `footer.showMotionToggle`,
10
+ * `footer.motionDefault` and `footer.motionControl`. A site with no `motion`
11
+ * block - or one missing a key - falls back to those, key by key, so every
12
+ * existing site renders exactly as before with no migration. The admin keeps
13
+ * writing them as a mirror for older readers and fleet tooling.
14
+ *
15
+ * No I/O here; tested in tests/services/motion-settings.test.js.
16
+ */
17
+ import {cssClasses} from '../../public/js/css-classes.mjs';
18
+
19
+ /** Today's behaviour: shown, in the footer, as a switch, following the device. */
20
+ export const MOTION_DEFAULTS = Object.freeze({
21
+ enabled: true,
22
+ default: 'system',
23
+ placement: 'footer',
24
+ control: 'switch',
25
+ triggerStyle: 'icon',
26
+ label: 'Reduce motion',
27
+ // Bottom left: clear of Search (right-centre) and Back to top (bottom right)
28
+ // at their defaults. public/js/motion-control.mjs stacks it clear of them
29
+ // anyway when a site moves one into the same corner.
30
+ position: 'BL',
31
+ offsetX: 32,
32
+ offsetY: 32,
33
+ iconSize: 52,
34
+ iconRadius: 26,
35
+ class: ''
36
+ });
37
+
38
+ export const MOTION_DEFAULT_CHOICES = ['system', 'off', 'on'];
39
+ export const MOTION_PLACEMENTS = ['footer', 'floating', 'navbar'];
40
+ export const MOTION_CONTROLS = ['switch', 'checkbox'];
41
+ export const MOTION_TRIGGER_STYLES = ['icon', 'icon-text'];
42
+ /** The breadcrumb corners - the Placement card's corner picker. */
43
+ export const MOTION_POSITIONS = ['TL', 'TR', 'BL', 'BR'];
44
+ export const MOTION_LABEL_MAX = 40;
45
+ /** The same limits as Search's floating disc (services/search.js). */
46
+ export const MOTION_ICON_SIZE_MIN = 32;
47
+ export const MOTION_ICON_SIZE_MAX = 96;
48
+ export const MOTION_OFFSET_MAX = 500;
49
+
50
+ const pick = (value, list, fallback) => (list.includes(value) ? value : fallback);
51
+
52
+ function num(value, fallback, min, max) {
53
+ const n = typeof value === 'number' ? value : (typeof value === 'string' && value.trim() ? Number(value) : NaN);
54
+ if (!Number.isFinite(n)) return fallback;
55
+ return Math.min(max, Math.max(min, Math.round(n)));
56
+ }
57
+
58
+ /**
59
+ * The settings a page and the admin act on.
60
+ *
61
+ * @param {object} [raw] - site.json `motion` (absent or partial is fine)
62
+ * @param {object} [footer] - site.json `footer`, for the keys the control used to live under
63
+ * @returns {object} every key of MOTION_DEFAULTS, nothing else
64
+ */
65
+ export function normaliseMotionSettings(raw, footer) {
66
+ const m = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
67
+ const f = footer && typeof footer === 'object' ? footer : {};
68
+ const has = (k) => m[k] !== undefined && m[k] !== null;
69
+
70
+ const enabledRaw = has('enabled') ? m.enabled : f.showMotionToggle;
71
+ const defaultRaw = has('default') ? m.default : f.motionDefault;
72
+ const controlRaw = has('control') ? m.control : f.motionControl;
73
+
74
+ const label = typeof m.label === 'string' && m.label.trim()
75
+ ? m.label.trim().replace(/\s+/g, ' ').slice(0, MOTION_LABEL_MAX)
76
+ : MOTION_DEFAULTS.label;
77
+
78
+ // The radius is clamped against the size (half of it is already a circle) - Search's rule.
79
+ const iconSize = num(m.iconSize, MOTION_DEFAULTS.iconSize, MOTION_ICON_SIZE_MIN, MOTION_ICON_SIZE_MAX);
80
+ const maxRadius = Math.floor(iconSize / 2);
81
+
82
+ return {
83
+ // Absent means shown, as footer.showMotionToggle always did.
84
+ enabled: enabledRaw !== false,
85
+ default: pick(defaultRaw, MOTION_DEFAULT_CHOICES, MOTION_DEFAULTS.default),
86
+ placement: pick(m.placement, MOTION_PLACEMENTS, MOTION_DEFAULTS.placement),
87
+ control: pick(controlRaw, MOTION_CONTROLS, MOTION_DEFAULTS.control),
88
+ triggerStyle: pick(m.triggerStyle, MOTION_TRIGGER_STYLES, MOTION_DEFAULTS.triggerStyle),
89
+ label,
90
+ position: pick(m.position, MOTION_POSITIONS, MOTION_DEFAULTS.position),
91
+ offsetX: num(m.offsetX, MOTION_DEFAULTS.offsetX, 0, MOTION_OFFSET_MAX),
92
+ offsetY: num(m.offsetY, MOTION_DEFAULTS.offsetY, 0, MOTION_OFFSET_MAX),
93
+ iconSize,
94
+ iconRadius: Math.min(maxRadius, num(m.iconRadius, MOTION_DEFAULTS.iconRadius, 0, maxRadius)),
95
+ class: cssClasses(typeof m.class === 'string' ? m.class : '').join(' ')
96
+ };
97
+ }
98
+
99
+ /** The settings for a site.json. */
100
+ export function motionSettingsOf(site) {
101
+ return normaliseMotionSettings(site?.motion, site?.footer);
102
+ }
103
+
104
+ /** The footer keys older readers look at, kept in step with a `motion` block. */
105
+ export function motionFooterMirror(settings) {
106
+ return {showMotionToggle: settings.enabled, motionDefault: settings.default, motionControl: settings.control};
107
+ }
@@ -16,6 +16,7 @@ import {getProjectForPage} from './projects.js';
16
16
  import {resolveContextMenusForPage} from './contextMenus.js';
17
17
  import {buildCustomThemeStyleTag, buildOverrideStyleTag, getThemeTokenMap, listThemeIds, loadThemeConfig, resolveThemeClasses} from './themeSettings.js';
18
18
  import {getSearchSettings} from './search.js';
19
+ import {motionSettingsOf} from './motionSettings.js';
19
20
  import {isToolEnabled} from './tools.js';
20
21
  import {consentSettingsFor, measurementIdOf} from './googleAnalytics.js';
21
22
  import {fmtDateTime} from './dates.js';
@@ -209,10 +210,30 @@ export async function buildFooterBlocks(site, ctx = {}) {
209
210
  return cols.length ? `<div class="cms-footer-blocks">${cols.join('')}</div>` : '';
210
211
  }
211
212
 
213
+ const MOTION_ASSET_V = '20261029-motion-under-switcher';
214
+
212
215
  /** The site's reduced-motion default for visitors who have not chosen: 'system', 'off' or 'on'. */
213
216
  function motionDefaultOf(site) {
214
- const d = site?.footer?.motionDefault;
215
- return d === 'off' || d === 'on' ? d : 'system';
217
+ return motionSettingsOf(site).default;
218
+ }
219
+
220
+ /**
221
+ * The "Reduce motion" control (site.json `motion`, Site Settings > Reduce motion):
222
+ * the whitelisted settings for window.__CMS_SITE__.motion - site.js draws the
223
+ * control from them (public/js/motion-control.mjs) - and its stylesheet. The
224
+ * old footer keys still count where `motion` says nothing (motionSettings.js).
225
+ *
226
+ * Both render paths need this - the standing trap in this file.
227
+ *
228
+ * @param {object} site - site.json
229
+ * @returns {{siteFields: {motion: object}, headTag: string}}
230
+ */
231
+ export function buildMotionAssets(site) {
232
+ const motion = motionSettingsOf(site);
233
+ return {
234
+ siteFields: {motion},
235
+ headTag: motion.enabled ? `<link rel="stylesheet" href="/public/css/motion.css?v=${MOTION_ASSET_V}">` : ''
236
+ };
216
237
  }
217
238
 
218
239
  /** The override a page's [navbar] tag left in its html (the first one), or null. */
@@ -450,6 +471,7 @@ export async function renderPage(page, opts = {}) {
450
471
 
451
472
  const themeView = await buildThemeView(page);
452
473
  const searchAssets = buildSearchAssets();
474
+ const motionAssets = buildMotionAssets(site);
453
475
  const footerBlocksHtml = preset.footer !== false ? await buildFooterBlocks(site, {user: opts.user || null, urlPath: menuCtx.urlPath, project: menuCtx.project, tags: opts.cacheTags}) : '';
454
476
  const switcherAssets = buildThemeSwitcherAssets(themeView.cfg, {
455
477
  html: `${page.html || ''}${overlayMenus}${footerBlocksHtml}`,
@@ -547,6 +569,8 @@ export async function renderPage(page, opts = {}) {
547
569
  footerScript: footerJson ? `window.__CMS_FOOTER__ = ${footerJson};` : '',
548
570
  ctxMenusScript: ctxMenusScript,
549
571
  ctxMenusModule: ctxMenusModule,
572
+ // Every `<` escaped, not only `</script>`: a label holding `<!--<script` would
573
+ // otherwise put the parser in script-data-escaped state and swallow the page.
550
574
  siteJson: JSON.stringify(Object.assign(
551
575
  {
552
576
  title: site.title,
@@ -561,13 +585,14 @@ export async function renderPage(page, opts = {}) {
561
585
  site.backToTop?.enabled ? { backToTop: site.backToTop } : {},
562
586
  site.goToBottom?.enabled ? { goToBottom: site.goToBottom } : {},
563
587
  consentAssets.siteFields,
588
+ motionAssets.siteFields,
564
589
  autoTheme ? {autoTheme} : {}
565
- )).replace(/<\/script>/gi, '<\\/script>'),
590
+ )).replace(/</g, '\\u003c'),
566
591
  headInject: [injection.head, navbarFontLink].filter(Boolean).join('\n'),
567
592
  // Theme overrides land BEFORE content/custom.css: the overrides are
568
593
  // generated, custom.css is hand-written, and the hand-written one stays
569
594
  // the last word.
570
- headInjectLate: [injection.headLate, searchAssets.headTag, switcherAssets.headTag, themeView.customThemeStyleTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
595
+ headInjectLate: [injection.headLate, searchAssets.headTag, motionAssets.headTag, switcherAssets.headTag, themeView.customThemeStyleTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
571
596
  .filter(Boolean).join('\n'),
572
597
  bodyEndInject: [
573
598
  // Draft banner - first, so it is the topmost fixed element and a
@@ -1256,6 +1281,7 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
1256
1281
  // other is the standing trap in this file.
1257
1282
  const themeView = await buildThemeView({});
1258
1283
  const searchAssets = buildSearchAssets();
1284
+ const motionAssets = buildMotionAssets(site);
1259
1285
  const footerBlocksHtml = await buildFooterBlocks(site, {user, urlPath: seoMeta.urlPath || '/', project: menuCtx.project ?? null});
1260
1286
  const switcherAssets = buildThemeSwitcherAssets(themeView.cfg, {
1261
1287
  html: `${renderedFragment || ''}${overlayMenus}${footerBlocksHtml}`,
@@ -1340,6 +1366,7 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
1340
1366
  footerScript: footerJson ? `window.__CMS_FOOTER__ = ${footerJson};` : '',
1341
1367
  ctxMenusScript: ctxMenusScript,
1342
1368
  ctxMenusModule: ctxMenusModule,
1369
+ // Every `<` escaped, as on the page path above.
1343
1370
  siteJson: JSON.stringify({
1344
1371
  title: site.title,
1345
1372
  footer: site.footer
@@ -1349,13 +1376,14 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
1349
1376
  }
1350
1377
  : site.footer,
1351
1378
  social: site.social || null,
1379
+ ...motionAssets.siteFields,
1352
1380
  ...(bare ? {} : consentAssets.siteFields)
1353
- }).replace(/<\/script>/gi, '<\\/script>'),
1381
+ }).replace(/</g, '\\u003c'),
1354
1382
  headInject: [bare ? '' : injection.head, navbarFontLink].filter(Boolean).join('\n'),
1355
1383
  // Theme overrides land BEFORE content/custom.css: the overrides are
1356
1384
  // generated, custom.css is hand-written, and the hand-written one stays
1357
1385
  // the last word.
1358
- headInjectLate: [bare ? '' : injection.headLate, searchAssets.headTag, switcherAssets.headTag, themeView.customThemeStyleTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
1386
+ headInjectLate: [bare ? '' : injection.headLate, searchAssets.headTag, motionAssets.headTag, switcherAssets.headTag, themeView.customThemeStyleTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
1359
1387
  .filter(Boolean).join('\n'),
1360
1388
  bodyEndInject: [
1361
1389
  overlayMenus,
@@ -112,7 +112,7 @@
112
112
  _stored = JSON.parse(localStorage.getItem('domma:reduced_motion'));
113
113
  } catch (e) {
114
114
  }
115
- // No choice of the visitor's yet: the site's default (Site Settings > Footer) -
115
+ // No choice of the visitor's yet: the site's default (Site Settings > Reduce motion) -
116
116
  // 'on' or 'off' decide it; 'system' (or nothing) leaves it to the device.
117
117
  if (_stored === null || _stored === undefined) {
118
118
  var _def = '{{motionDefault}}';
@@ -188,7 +188,7 @@
188
188
  <script src="/public/js/form-logic-engine.js?v=20261025-dropzone-disable" defer></script>
189
189
 
190
190
  <!-- Site initialisation -->
191
- <script src="/public/js/site.js?v=20261029-glider-layout" type="module"></script>
191
+ <script src="/public/js/site.js?v=20261029-reduce-motion" type="module"></script>
192
192
  <script src="/public/js/collection-context.js?v=20261010-utf8-payloads" type="module"></script>
193
193
  {{ctxMenusModule}}
194
194