domma-cms 1.14.0 → 1.16.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/admin/css/admin.css +1 -1
  3. package/admin/js/lib/admin-brand.js +1 -1
  4. package/admin/js/lib/google-analytics-panel.js +9 -0
  5. package/admin/js/lib/google-business-callback.js +1 -0
  6. package/admin/js/lib/google-business-import.js +3 -0
  7. package/admin/js/lib/google-business-panel.js +32 -0
  8. package/admin/js/lib/seo-defaults.js +1 -0
  9. package/admin/js/lib/sidebar-renderer.js +5 -5
  10. package/admin/js/lib/site-search-section.js +1 -0
  11. package/admin/js/lib/site-settings-arrange.js +3 -3
  12. package/admin/js/lib/topbar-account.js +1 -1
  13. package/admin/js/lib/topbar-cache.js +6 -0
  14. package/admin/js/lib/topbar-notifications.js +4 -4
  15. package/admin/js/templates/docs/usage-site-settings.html +20 -7
  16. package/admin/js/templates/menu-editor.html +1 -1
  17. package/admin/js/templates/settings.html +104 -63
  18. package/admin/js/views/analytics.js +8 -6
  19. package/admin/js/views/search.js +2 -2
  20. package/admin/js/views/seo.js +50 -42
  21. package/admin/js/views/settings.js +3 -3
  22. package/docs/DommaFeatures.md +3 -2
  23. package/docs/configuration.md +53 -8
  24. package/docs/google-business-profile.md +44 -0
  25. package/docs/screenshots/google-business-profile-review.png +0 -0
  26. package/docs/screenshots/notifications-cache.png +0 -0
  27. package/docs/screenshots/site-settings-search.png +0 -0
  28. package/package.json +1 -1
  29. package/public/css/motion.css +1 -0
  30. package/public/js/motion-control.mjs +1 -0
  31. package/public/js/site.js +1 -1
  32. package/server/routes/api/analytics.js +19 -0
  33. package/server/routes/api/google-business.js +64 -0
  34. package/server/routes/api/seo.js +14 -3
  35. package/server/routes/api/settings.js +23 -2
  36. package/server/services/adminBrand.js +1 -0
  37. package/server/services/googleBusiness.js +225 -0
  38. package/server/services/motionSettings.js +107 -0
  39. package/server/services/renderer.js +36 -6
  40. package/server/templates/page.html +2 -2
@@ -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
 
@@ -0,0 +1,44 @@
1
+ # Google Business Profile
2
+
3
+ Open SEO and choose its settings cog. Site defaults and the Google Business Profile connection are here. Google Analytics has its own section in the Analytics tool's settings.
4
+
5
+ ## Set up Google access
6
+
7
+ Use a Google Cloud project approved for Business Profile API access. Approval is a Google process and must be completed before the connection can retrieve locations. Follow [Google's prerequisites](https://developers.google.com/my-business/content/prereqs).
8
+
9
+ 1. Enable the My Business Account Management API and My Business Business Information API in the approved project.
10
+ 2. Configure the OAuth consent screen. If the app is in testing, add the Google account that manages the business as a test user.
11
+ 3. Create an OAuth client of type Web application.
12
+ 4. Copy the exact return address shown under Google app setup in SEO settings into the client's Authorized redirect URIs. It ends with `/api/seo/google-business/callback`.
13
+ 5. Enter the client ID and client secret, then choose Connect Google. Sign in with an owner or manager of the Business Profile and grant access.
14
+
15
+ Google requires the `business.manage` scope to read these resources. This version uses the API to read business details and revoke its own connection. See [Google's OAuth guide](https://developers.google.com/my-business/content/implement-oauth) and [basic setup](https://developers.google.com/my-business/content/basic-setup).
16
+
17
+ The return address uses the trusted Site URL. Open the admin at that address when connecting. HTTPS is required, with HTTP allowed for localhost development.
18
+
19
+ ## Review and import
20
+
21
+ Choose the business account, then the location. More accounts and More locations load subsequent result pages. Choose Review details to fetch the selected location from Google.
22
+
23
+ Each proposed field appears beside its current value. Empty fields are selected by default. Select any replacements you want, then choose Apply selected details to form. Save SEO settings to publish the resulting structured data on the website.
24
+
25
+ The import can offer the business name, website, phone, public storefront address, Google Maps profile link, LocalBusiness type and structured data switch. Existing logo, email and profile links are retained. Missing Google fields do not clear existing website values. Review address fields before publishing them, especially for a service business.
26
+
27
+ Google listing edits, review replies, posts and automatic synchronisation are outside this version. Imported details are a reviewed copy and can be edited in SEO afterwards.
28
+
29
+ ## Reconnect and disconnect
30
+
31
+ Reconnect Google renews authorization. An empty client secret field keeps the saved secret when the client ID is unchanged. Google may ask you to grant access again after an app in testing expires or access is revoked.
32
+
33
+ Disconnect removes this site's saved credentials and attempts to revoke the Google token. Previously imported details remain in SEO. If Google cannot be reached for revocation, the screen explains how to remove the app from your Google account permissions.
34
+
35
+ The encrypted connection and its encryption key live under `content/sessions/google-business/`, which is excluded from site repositories. Keep this private runtime directory in protected backups if you want connections to survive a restore. The key and credential files are readable only by their owner. Tokens and client secrets are never returned by the status API, placed in browser drafts, or stored in `site.json`.
36
+
37
+ ## Troubleshooting
38
+
39
+ - Access refused: check Google's API approval, enable both APIs and confirm the Google account manages the business.
40
+ - No locations: select another account or use the Google account that owns or manages the listing.
41
+ - Redirect mismatch: use the exact return address shown in SEO and the Web application OAuth client type.
42
+ - Sign-in expired: choose Connect again in the same browser. Sign-in tickets last ten minutes and can be used once.
43
+ - Access revoked: reconnect to grant Google access again.
44
+ - Request limit: wait before retrying. Google applies project quotas.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.14.0",
3
+ "version": "1.16.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 ne,initCarouselPopup as ae}from"/public/js/mobile-ui.mjs?v=20261016-mobile-options";import{installSmoothCarets as se}from"/public/js/smooth-caret.js";import{decodeBase64Json as V}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as U,applyColour as G,applyPill as ie,applyFloat as re,applyHighlight as Y,applyItemHighlight as ce,itemHighlightAttrs as le,isCurrentItem as K,menuAlign as de,applyClasses as P,cssClasses as X,applyEntrance as J}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{applyMenuLook as me}from"/public/js/menu-cards.mjs?v=20261029-glider-layout";import{initPageMotion as ue}from"/public/js/page-motion.mjs?v=20261029-page-motion";import{applyFooterShowcase as pe}from"/public/js/footer-showcase.mjs?v=20261029-footer-showcase";import{initFloatingMenus as fe}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 he}from"/public/js/form-steps.mjs";import{mountMotionControl as ge}from"/public/js/motion-control.mjs?v=20261029-motion-under-switcher";import{applyFormClasses as te,applySubmitButtons as ye,applyWizardButtons as ve}from"/public/js/form-submit.mjs";import{captureFileFields as oe,destroyDropzones as be,fileBlueprint as we,restoreFileFields as Ce,setFileLimits as Se,splitFiles as xe,submitBody as ke,totalTooLarge as Le,wireFileFields as $e}from"/public/js/form-files.mjs";document.addEventListener("click",m=>{const i=m.target?.closest?.(".dm-banner__dismiss");i&&(m.preventDefault(),i.closest(".dm-banner")?.remove())}),$(()=>{se(document);const m=window.__CMS_NAV__||{},i=window.__CMS_SITE__||{},M=i.brandingEnabled!==!1&&m.brand?.enabled!==!1;if(i.autoTheme?.enabled){let o=function(c){const d=(c||"07:00").split(":");return+d[0]*60+(+d[1]||0)},e=function(){const c=new Date,d=c.getHours()*60+c.getMinutes();return d>=o(s.dayStart)&&d<o(s.nightStart)?s.dayTheme:s.nightTheme},t=function(c){const d=s.apply&&s.apply[c]||{theme:c,custom:""};Domma.theme.set(d.theme);for(const n of[...document.body.classList])n.startsWith("dm-custom-")&&document.body.classList.remove(n);d.custom&&document.body.classList.add(d.custom)};const s=i.autoTheme;t(e()),setInterval(()=>t(e()),6e4)}const L=$("#site-navbar");if(L.length&&m.brand){let e=function(u,f=0){return Array.isArray(u)?u.filter(h=>!(h&&(h.type==="spacer"||h.type==="theme-switcher"))).filter(h=>!(h&&h.type==="separator"&&f===0)).map(h=>h&&h.type==="separator"?{divider:!0}:Array.isArray(h?.items)?{...h,items:e(h.items,f+1)}:h):u};const s={...m.brand},o=s.size&&s.size!=="md"?` navbar-brand-${s.size}`:"";if(s.logo||s.icon||s.tagline){let u="";s.logo?u+=`<img src="${s.logo}" class="navbar-brand-logo" alt="${s.text||""}">`:s.icon&&(u+=`<span data-icon="${s.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),s.text&&(u+=`<span class="navbar-brand-text${o}">${s.text}</span>`),s.tagline&&(u+=`<small class="navbar-brand-tagline">${s.tagline}</small>`),s.html=u}else o&&s.text&&(s.html=`<span class="navbar-brand-text${o}">${s.text}</span>`);const t=e(m.items||[]),c=m.position==="floating",d=m.orientation==="vertical";L.get(0).querySelector(":scope > .cms-nojs-links")?.remove(),Domma.elements.navbar("#site-navbar",{brand:M?s:null,items:t,variant:m.variant||"dark",position:c?"static":m.position||"sticky",collapsible:!0,appearOnHover:!!m.appearOnHover}),Domma.icons.scan("#site-navbar");const n=L.get(0);ne(n,m.mobile);const y=()=>{if(!d)return;const f=n.querySelector('a[aria-current="page"]')?.closest(".navbar-dropdown");f&&!f.classList.contains("open")&&f.querySelector(":scope > .navbar-dropdown-toggle")?.click()};n.classList.add(`dm-nav-align-${de(m)||"centre"}`),d&&L.get(0).classList.add("dm-nav-vertical",m.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const g=m.dock||{};d&&!c&&Ee(n,m.side==="right"),!d&&g.hideOnScroll&&Ae(n),Y(n,m),J(n,m),P(n,m.class),c&&(re(L.get(0),m),fe(L.get(0).parentNode||document));const l=n.querySelector(".navbar-toggle"),b=n.querySelector(".navbar-collapse");if(l&&b){let u=null;const f=()=>{b.classList.contains("show")&&l.click()},h=()=>{u&&(u.remove(),u=null),document.body.classList.remove("dm-drawer-open")},p=()=>{u||(u=document.createElement("div"),u.className="dm-navbar-backdrop",u.addEventListener("click",f),document.body.appendChild(u),document.body.classList.add("dm-drawer-open"))};l.addEventListener("click",()=>{requestAnimationFrame(()=>{b.classList.contains("show")?(y(),p()):h()})}),b.addEventListener("click",k=>{k.target.closest("a")&&requestAnimationFrame(h)}),document.addEventListener("keydown",k=>{k.key==="Escape"&&b.classList.contains("show")&&f()})}const w=n.querySelector(".navbar-nav");if(w){const u=Array.from(w.children);let f=0;for(const h of m.items||[])if(!(h&&h.type==="separator")){if(h&&h.type==="spacer"){const p=document.createElement("li"),k=["xs","sm","md","lg","xl","flex"].includes(h.size)?h.size:"md";p.className=`dm-menu-spacer dm-menu-spacer--${k}`,p.setAttribute("aria-hidden","true"),w.insertBefore(p,u[f]||null);continue}if(h&&h.type==="theme-switcher"){const p=document.createElement("li");p.className="dm-menu-theme-switcher";const k=document.createElement("span");k.dataset.themeSwitcher="menu",k.dataset.position="inline",p.appendChild(k),w.insertBefore(p,u[f]||null);continue}f++}window.CMSThemeSwitcher?.scan(w)}n.querySelectorAll("[data-path]").forEach(u=>{const f=u.dataset.path.split(".").map(Number),h=f.length>1,p=f.reduce((q,r)=>q?.items?.[r],{items:t});if(!p)return;p.colour&&G(u,p.colour),p.pill&&!h&&ie(u,p.pill),ce(u,p),P(u.closest("li"),p.class);const k=U(p.badge);k&&u.appendChild(k),K(p.url,location.pathname)&&(u.classList.add("active"),u.setAttribute("aria-current","page"))}),me(n,m,t),y()}let I=null;const C=$("#site-footer");if(C.length){let c=function(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},d=function(r){return c(r).replace(/'/g,"&#39;")},n=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 D=d(r.url||"#"),v=c(r.text||""),x=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(n).join("")}</ul>`:"",N=le(r),O=[N.hover&&` data-menu-item-hover="${d(N.hover)}"`,N.active&&` data-menu-item-active="${d(N.active)}"`].filter(Boolean).join(""),j=N.accent?` style="--dm-menu-highlight:${d(N.accent)}"`:"",W=X(r.class).join(" ");return`<li${W?` class="${d(W)}"`:""}><a href="${D}"${O}${j}>${v}</a>${x}</li>`},g=function(){if(!i.footer)return"";const r=Object.keys(o).filter(v=>s[v]);if(!r.length)return"";const D=X(i.footer.socialClass).join(" ");return`<div class="footer-social">${r.map(v=>{const{label:x,svg:N}=o[v];return`<a href="${s[v]}" target="_blank" rel="noopener noreferrer" aria-label="${x}" class="footer-social-link${D?` ${d(D)}`:""}">${N}</a>`}).join("")}</div>`},u=function(r){if(!r||r.type==="separator"||r.type==="spacer")return"";if(r.type==="theme-switcher")return`<li class="cms-footer-col">${n(r).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const D=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(n).join("")}</ul>`:"",v=w(r)?`<span class="cms-footer-col-title">${c(r.text||"")}</span>`:`<a class="cms-footer-col-title" href="${d(r.url||"#")}">${c(r.text||"")}</a>`,x=["cms-footer-col",...X(r.class)].join(" ");return`<li class="${d(x)}">${v}${D}</li>`},p=function(r,D,v=!1){return(r||[]).forEach(x=>{x&&(x.type==="separator"||x.type==="spacer"||x.type==="theme-switcher")||(v&&w(x)||D.push(x),Array.isArray(x.items)&&x.items.length&&p(x.items,D))}),D};const s=i.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let e='<div class="footer-inner container">';const t=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},y=(()=>{const r=i.cookieConsent;if(!r||r.showSettingsLink===!1)return"";const D=v=>(v||[]).some(x=>x&&(x.url==="#cookie-settings"||D(x.items)));return D(t.primary)||D(t.legal)?"":`<li class="cms-cookie-settings"><a href="#cookie-settings">${c(r.settingsLinkText||"Cookie settings")}</a></li>`})(),l=["columns","row"].includes(i.footer?.layout)?i.footer.layout:"auto",b=i.footer?.look==="showcase"||l==="columns"||l==="auto"&&t.primary.some(r=>Array.isArray(r?.items)&&r.items.length),w=r=>b&&!r.url&&Array.isArray(r.items)&&r.items.length>0;if(b){const r=m.brand||{},D=M&&(r.text||i.title)||"",v=M?r.logo?`<img src="${d(r.logo)}" class="cms-footer-brand-logo" alt="">`:r.icon?'<span class="cms-footer-brand-icon"></span>':"":"",x=String(i.footer?.blurb||"").trim();e='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',e+=`<div class="cms-footer-brand">${D||v?`<a class="cms-footer-brand-name" href="${d(r.url||"/")}">${v}<span>${c(D)}</span></a>`:""}${x?`<p class="cms-footer-blurb">${c(x)}</p>`:""}${g()}</div>`,t.primary.length&&(e+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${t.primary.map(u).join("")}</ul></nav>`),e+='</div><div class="cms-footer-bottom">',t.copyright&&(e+=`<p class="cms-footer-copyright">${c(t.copyright)}</p>`),(t.legal.length||y)&&(e+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${t.legal.map(n).join("")}${y}</ul></nav>`),e+="</div>"}else t.copyright&&(e+=`<p>${c(t.copyright)}</p>`),t.primary.length&&(e+=`<nav class="footer-nav footer-nav--primary"><ul>${t.primary.map(n).join("")}</ul></nav>`),(t.legal.length||y)&&(e+=`<nav class="footer-nav footer-nav--legal"><ul>${t.legal.map(n).join("")}${y}</ul></nav>`),e+=g();e+="</div>",C.get(0).querySelector(":scope > .cms-nojs-links")?.remove();const f=C.get(0).querySelector(":scope > .cms-footer-blocks");f?.remove(),C.html(e),f&&C.get(0).prepend(f),window.CMSThemeSwitcher?.scan(C.get(0));const h=C.get(0).querySelector(".cms-footer-brand-icon");h&&m.brand?.icon&&h.setAttribute("data-icon",m.brand.icon),P(C.get(0),i.footer?.class),b&&pe(C.get(0),i);const k=[...p(t.primary,[],!0),...p(t.legal,[])];C.get(0).querySelectorAll(".footer-nav a").forEach((r,D)=>{const v=k[D];if(!v)return;v.colour&&G(r,v.colour);const x=U(v.badge);x&&r.appendChild(x),K(v.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),Y(C.get(0).querySelector(".footer-nav--primary"),{highlight:t.primaryHighlight}),Y(C.get(0).querySelector(".footer-nav--legal"),{highlight:t.legalHighlight}),J(C.get(0).querySelector(".footer-nav--primary"),{entrance:t.primaryEntrance}),J(C.get(0).querySelector(".footer-nav--legal"),{entrance:t.legalEntrance}),P(C.get(0).querySelector(".footer-nav--primary"),t.primaryClass),P(C.get(0).querySelector(".footer-nav--legal"),t.legalClass),I=C.get(0).querySelector(".cms-footer-bottom")||C.get(0).querySelector(".footer-inner")}ge(i.motion||{enabled:i.footer?.showMotionToggle!==!1,default:i.footer?.motionDefault,placement:"footer",control:i.footer?.motionControl,label:"Reduce motion"},{footer:I,navbar:document.querySelector("#site-navbar .navbar-container")?.closest("#site-navbar")||null});const A=document.getElementById("site-sidebar");A&&Te(A),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const a=$(".page-body");if(a.length){a.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),a.find(".tabs").each(function(){Domma.elements.tabs(this)}),a.find(".carousel").each(function(){const o={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"},e=g=>{const l=String(g||"").trim();return l?o[l.toLowerCase()]?`var(${o[l.toLowerCase()]})`:CSS.supports("color",l)?l:"":""},t=e(this.dataset.background),c=e(this.dataset.arrowBackground),d=e(this.dataset.arrowColor);t&&(this.style.setProperty("--dm-carousel-background",t),this.classList.add("dm-carousel-custom-background")),c&&this.style.setProperty("--dm-carousel-arrow-background",c),d&&this.style.setProperty("--dm-carousel-arrow-color",d);const n=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${n}`);const y={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 g=Number(this.dataset.height);Number.isFinite(g)&&g>0&&(y.height=g)}if(this.dataset.animationDuration){const g=parseInt(this.dataset.animationDuration,10);Number.isFinite(g)&&(y.animationDuration=g)}this.dataset.animationEasing&&(y.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(g=>g.cloneNode(!0)),this._dmCarouselOpts=y),this._dmCarousel=Domma.elements.carousel(this,y),ae(this)}),a.find(".dm-countdown").each(function(){const o={autoStart:!0},e=String(this.dataset.format||"").trim(),t=String(this.dataset.to||"").trim(),c=/^(\d{4})-(\d{2})-(\d{2})$/.exec(t),d=c?new Date(+c[1],+c[2]-1,+c[3]).getTime():t?new Date(t).getTime():NaN;Number.isFinite(d)?o.duration=Math.max(0,d-Date.now()):this.dataset.duration&&(o.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const n=/^dd:/i.test(e);o.format=n?"hh:mm:ss":e||(o.duration>=36e5?"hh:mm:ss":"mm:ss");const y=this,g=b=>String(b).padStart(2,"0"),l=b=>{const w=Math.max(0,Math.floor(b/1e3)),u=y.querySelector(".dm-timer-time");u&&(u.textContent=`${g(Math.floor(w/86400))}:${g(Math.floor(w%86400/3600))}:${g(Math.floor(w%3600/60))}:${g(w%60)}`)};n&&(o.onTick=l),Domma.elements.timer(this,o),n&&l(o.duration||0)}),a.find("[data-tooltip]:not(.dm-range-slider)").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),a.find(".dm-progression").each(function(){const o=Array.from(this.querySelectorAll(".dm-progression-item")).map(c=>{const d={title:c.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:c.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return c.dataset.date&&(d.date=c.dataset.date),c.dataset.status&&(d.status=c.dataset.status),c.dataset.eventIcon&&(d.icon=c.dataset.eventIcon),c.dataset.entryId&&(d._entryId=c.dataset.entryId),d}),e=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",t=Domma.elements.progression(this,{layout:e,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...o.length?{items:o}:{}});if((this.dataset.mode||"timeline")==="timeline"){const c={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((d,n)=>{const y=o[n];if(!y)return;const g=d.querySelector(".dm-progression-title");if(y.icon&&g){const b=document.createElement("span");b.dataset.icon=y.icon,b.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",b.setAttribute("aria-hidden","true"),g.prepend(b)}const l=c[y.status];if(l&&g){const b=document.createElement("span");b.className=`badge badge-${l.tone} cms-timeline-status`,b.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const w=document.createElement("span");w.dataset.icon=l.icon,w.dataset.iconSize="14",w.setAttribute("aria-hidden","true"),b.append(w,document.createTextNode(l.label)),g.after(b)}})}Domma.icons.scan(this),o.length&&(this._dmCtx={instance:t,items:o})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}const s=matchMedia("(prefers-reduced-motion: reduce)").matches||document.documentElement.classList.contains("dm-reduced-motion");document.querySelectorAll(".page-body .row[data-reveal]").forEach(o=>{const e=o.dataset.revealMode||"stagger",t=o.dataset.revealAnimation||"slide-up",c=parseInt(o.dataset.revealDuration,10)||(t.startsWith("swoosh")?700:400),d=parseInt(o.dataset.revealStagger,10)||(t.startsWith("swoosh")?90:60),n=parseInt(o.dataset.revealDelay,10)||0,y=o.dataset.revealDirection==="rtl",g=o.dataset.revealReplay==="true",l=Array.from(o.children);if(s&&t.startsWith("swoosh"))return;const b={"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=p=>t!=="swoosh-sides"?b[t]||"":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)",u=t.startsWith("swoosh"),f=u?"cubic-bezier(.2,.8,.2,1)":"ease",h=(p,k)=>{p.style.opacity="0",p.style.transform=w(k),u&&(p.style.filter="blur(10px)")};l.forEach((p,k)=>{h(p,k),p.style.transition=`opacity ${c}ms ${f}, transform ${c}ms ${f}`+(u?`, filter ${c}ms ${f}`:"");const q=y?l.length-1-k:k;p.style.transitionDelay=e==="stagger"?`${n+q*d}ms`:`${n}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const p=new IntersectionObserver(k=>{k.forEach(q=>{if(q.isIntersecting){if(q.target.offsetWidth,q.target.style.opacity="1",q.target.style.transform="",u&&(q.target.style.filter=""),!g){p.unobserve(q.target);const r=q.target;r.addEventListener("transitionend",function D(v){v.target!==r||v.propertyName!=="opacity"||(r.removeEventListener("transitionend",D),r.style.transition="",r.style.transitionDelay="")})}}else g&&q.boundingClientRect.top>0&&h(q.target,l.indexOf(q.target))})},{threshold:.1});l.forEach(k=>p.observe(k))}))}),ue(document.querySelector(".page-body")||document),a.find(".card[data-collapsible]").each(function(){const o=this.querySelector(".card-header");o&&o.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),a.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const o=this.dataset.soTarget,e=document.getElementById(o);if(!e)return;const t=E.slideover({title:e.dataset.soTitle||"",size:e.dataset.soSize||"md",position:e.dataset.soPosition||"right"});e.style.display="",t.setContent(e),t.open()})})}if(typeof $.setup=="function"){const s=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const e=V(o.dataset.config);Object.assign(s,e)}catch{}}),Object.keys(s).length>0){const o={};for(const[e,t]of Object.entries(s)){const c=t?.events?.click,{confirm:d,toast:n,alert:y,prompt:g,...l}=c||{};if(d||n||y||g){$(e).on("click",async function(W){if(W.preventDefault(),d&&!await E.confirm(d))return;let z=null;if(!(g&&(z=await E.prompt(g,{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),n&&E.toast(n,{type:l.toastType||"success"}),y&&E.alert(y)}});const{target:w,toggleClass:u,addClass:f,removeClass:h,setText:p,setVal:k,setAttr:q,href:r,toastType:D,promptPlaceholder:v,promptDefault:x,...N}=l,O={...t.events};Object.keys(N).length?O.click=w?{target:w,...N}:N:delete O.click;const j={...t,events:O};Object.keys(O).length||delete j.events,Object.keys(j).length&&(o[e]=j)}else o[e]=t}$.setup(o)}}a.length&&R(a.get(0))}),window.wireCTAButtons=R;function R(m){m.querySelectorAll(".dm-cta-trigger").forEach(i=>{i.__ctaWired||(i.__ctaWired=!0,i.addEventListener("click",async()=>{const M=i.dataset.action,L=i.dataset.entry,I=i.dataset.confirm;let C=S.get("auth_token");if(!C){E.toast("Please log in to perform this action.",{type:"warning"});return}if(I&&!await E.confirm(I))return;const A=Array.from(i.childNodes).map(s=>s.cloneNode(!0));i.disabled=!0,i.textContent="Running\u2026";const a=s=>fetch(`/api/actions/${M}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(i.dataset.scope?{entryId:L,scope:i.dataset.scope}:{entryId:L})});try{let s=await a(C);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const t=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(t.ok){const{token:c}=await t.json();S.set("auth_token",c),C=c,s=await a(C)}}}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="",A.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 M=S.get("auth_token");function L(C,A){const a=document.createElement("div");a.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(A){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=C;return a.appendChild(s),a}function I(C,A){const a=document.createElement("template");a.innerHTML=A||"",C.replaceWith(a.content)}i.forEach(async C=>{const A=C.dataset.collectionAttrs;if(!A)return;if(!M){C.replaceChildren(L("",!0));return}const a=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:A})});try{let s=M,o=await a(s);if(o.status===401){const t=S.get("auth_refresh_token");if(t){const c=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:t})});if(c.ok){const{token:d}=await c.json();S.set("auth_token",d),s=d,o=await a(s)}}}if(!o.ok){const{error:t}=await o.json().catch(()=>({}));C.replaceChildren(L(t||"Unable to load entries.",!1));return}const{html:e}=await o.json();I(C,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{C.replaceChildren(L("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(M=>{let L;try{L=V(M.dataset.payload)}catch{return}const{columns:I,rows:C,search:A,sortable:a,exportable:s,pageSize:o,empty:e,ctaConfig:t,slug:c,exportCfg:d}=L;if(!I?.length)return;if(t){const g=I.findIndex(l=>l.key==="_cta");g!==-1&&(I[g]={key:"_cta",title:"",render:(l,b)=>{const w=document.createElement("button");if(w.className=`btn btn-${t.style||"primary"} dm-cta-trigger`,w.dataset.action=t.action||"",t.scope&&(w.dataset.scope=t.scope),w.dataset.entry=b._entryId||"",t.confirm&&(w.dataset.confirm=t.confirm),t.icon){const u=document.createElement("span");u.dataset.icon=t.icon,w.appendChild(u),w.appendChild(document.createTextNode(" "))}return w.appendChild(document.createTextNode(t.label||"Run")),w}})}const n="col-table-"+Math.random().toString(36).slice(2,7),y=document.createElement("div");y.id=n,M.replaceChildren(y),T.create("#"+n,{data:C,columns:I,search:A,sortable:a,exportable:s,pageSize:o,emptyMessage:e}),t&&R(y),M._dmTable={columns:I,rows:C,slug:c,exportCfg:d,render(g){T.create("#"+n,{data:g,columns:I,search:A,sortable:a,exportable:s,pageSize:o,emptyMessage:e}),t&&R(y)}}})})(),(function(){const i=document.querySelectorAll("[data-form-inline]");if(!i.length||typeof F>"u")return;function M(a){const s=a&&a.logic;return!!(s&&(s.visibility||s.requirement))}function L(a,s){const o={};return(a||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const t=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":t?"chooser":e.type,d={...e.formConfig||{}};d.span==="full"&&s&&(d.span=s);const n={type:c,required:M(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},...d}};e.minLength!==void 0&&(n.minLength=e.minLength),e.maxLength!==void 0&&(n.maxLength=e.maxLength),e.min!==void 0&&(n.min=e.min),e.max!==void 0&&(n.max=e.max),t&&(e.variant&&(n.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(n.multiple=!0),e.density&&(n.density=e.density),e.columns&&(n.columns=e.columns),e.accent&&(n.accent=e.accent),e.accentStyle&&(n.accentStyle=e.accentStyle),e.glow&&(n.glow=!0),e.glowColour&&(n.glowColour=e.glowColour),e.shadow&&(n.shadow=e.shadow),e.shadowColour&&(n.shadowColour=e.shadowColour)),o[e.name]=e.type==="file"?we(e,n):n}),o}function I(a){const s={};return(a||[]).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 C(a,s){(s||[]).forEach(o=>{if(o.type!=="date"||!o.name)return;const e=a.querySelector(`[name="${o.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function A(a,s,o){let e=a.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",a.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(a=>{let s;try{s=V(a.dataset.formInline)}catch{return}a.querySelector(":scope > .cms-form-embed-summary")?.remove();const o=s.fields||[],e=s.settings||{},t=Number(e.columns)===3?3:2,c=e.layout||"grid",d=o.some(u=>u.type==="page-break"),n=new Map;Se(a,s.fileLimits,n),$e(a,o);const y=async u=>{if(a.dataset.triggerBlocked==="true"){const f=a._formLogicRuntime,h=f&&f.submitBlock?f.submitBlock().reasons:[];A(a,h.join(" ")||"This form cannot be submitted yet.",!0);const p=new Error("Blocked by a form trigger");throw p.formSubmitFailed=!0,p}try{const f=a.querySelector(".fb-form-honeypot"),h=f?.querySelector('input[type="text"]')||null,p=f?.querySelector('[name="_t"]')||null,k=Object.assign({},u);delete k._t,f?.dataset.hpName&&delete k[f.dataset.hpName],h!==null&&(k._hp=h.value),p!==null&&(k._t=p.value);const q=new Map(n);oe(a,q);const r=xe(k,o,q),D=Le(a,r.files);if(D){A(a,D,!0);const x=new Error(D);throw x.formSubmitFailed=!0,x}const v=await H.post(`/api/forms/submit/${s.slug}`,r.files.size?ke(r.values,r.files).body:r.values);if(v&&v.error){A(a,v.error,!0);const x=new Error(v.error);throw x.formSubmitFailed=!0,x}if(v?.redirect){window.location.href=v.redirect;return}for(be(a);a.firstChild;)a.removeChild(a.firstChild);A(a,v?.message||e.successMessage||"Thank you for your submission.",!1)}catch(f){throw f.formSubmitFailed||A(a,f.message||"Submission failed. Please try again.",!0),f}},g=Date.now();function l(u){const f=u.querySelector("form");if(!f||f.querySelector(".fb-form-honeypot"))return;const h=document.createElement("div");h.className="fb-form-honeypot",h.setAttribute("aria-hidden","true");const p=document.createElement("input");p.name=f.querySelector('[name="website"]')?"_website":"website",h.dataset.hpName=p.name,p.type="text",p.tabIndex=-1,p.autocomplete="url",p.placeholder="https://",h.appendChild(p);const k=document.createElement("input");k.name="_t",k.type="hidden",k.value=g,h.appendChild(k),f.appendChild(h)}function b(u){if(!window.FormLogicEngine||!o.some(h=>h.logic||Array.isArray(h.triggers)&&h.triggers.length))return null;const f=new window.FormLogicEngine.FormLogicRuntime(s,a,u?{baseValues:u}:void 0);if(f.init(),a._formLogicRuntime=f,a._formLogicObserved)return f;if(a._formLogicObserved=!0,a.parentNode&&typeof MutationObserver<"u"){const h=new MutationObserver(function(p){for(const k of p)for(const q of k.removedNodes)if(q===a||q.nodeType===1&&q.contains&&q.contains(a)){a._formLogicRuntime&&a._formLogicRuntime.destroy(),h.disconnect();return}});h.observe(a.parentNode,{childList:!0,subtree:!1}),a._formLogicObserver=h}return f}let w=!1;if(a.addEventListener("dm-form-end",u=>{const f=u.detail?.validation;if(f&&!f.valid){Z(a,f.missing),ee(f);return}w||(w=!0,y(u.detail?.values||{}).catch(()=>{}).finally(()=>{w=!1}))}),d&&F.wizard){const u=he(o,s.title),f=u.map(v=>({title:v.title,description:v.description,fields:L(v.fields,t)})),h=u.map(v=>v.fields);let p=0,k=null,q=null;const r=v=>{const x=window.FormLogicEngine;if(!x||!x.validateFields)return;const N=x.validateFields(a,h[p]||[]);N.valid||(v.preventDefault(),v.stopImmediatePropagation(),Z(a,N.missing),ee(N))},D=()=>{const v=h[p]||o;ve(a,e),te(a,e,v),C(a,v),Q(a,v),Ce(a,n),e.honeypot!==!1&&l(a),k&&k.destroy(),k=b(q?.getStepData?q.getStepData():void 0),a.querySelectorAll(".wizard-next, .wizard-submit").forEach(x=>x.addEventListener("click",r,!0))};q=F.wizard(a,{schema:{steps:f},layout:c,columns:t,finishText:e.submitText||"Finish",onSubmit:y,onStepChange:v=>{oe(a,n),p=v,setTimeout(D,0)}}),a.addEventListener("dm-form-trigger",v=>{if(v.detail?.type!=="goto-step"||!q?.goToStep)return;const x=Number(v.detail.action.step);!Number.isFinite(x)||x<0||x>=f.length||x!==p&&q.goToStep(x)}),Promise.resolve(q).then(D)}else if(F.render){const u=F.render(a,L(o,t),I(o),{submitText:e.submitText||"Submit",layout:c,columns:t,onSubmit:y});Promise.resolve(u).then(function(){ye(a,e,c),te(a,e,o),C(a,o),Q(a,o),e.honeypot!==!1&&l(a),b()})}})})();function Ee(m,i){const M=document.body,L=M.classList,I=new Set((M.dataset.railSite||"").split(" ").filter(Boolean)),C=()=>window.matchMedia("(min-width: 993px)").matches;m.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(t=>{const c=t.textContent.trim();c&&(t.setAttribute("title",c),t.querySelector("svg, [data-icon]")||(t.dataset.initial=c.charAt(0).toUpperCase()))});const A=document.createElement("button");A.type="button",A.className="dm-nav-collapse-toggle";const a=()=>{const t=L.contains("dm-vnav-collapsed");A.setAttribute("aria-pressed",String(t)),A.setAttribute("aria-label",t?"Expand menu":"Collapse menu"),A.title=A.getAttribute("aria-label");const c=i?"chevrons-left":"chevrons-right",d=i?"chevrons-right":"chevrons-left";A.innerHTML=`<span data-icon="${t?c:d}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(A)},s=t=>{L.toggle("dm-vnav-collapsed",t),S.set("nav_collapsed",t),a()};A.addEventListener("click",()=>s(!L.contains("dm-vnav-collapsed"))),a();const o=m.querySelector(".navbar-container")||m;o.insertBefore(A,o.firstChild?.nextSibling||null);const e=t=>{L.toggle("dm-vnav-autohide",t),L.toggle("dm-vnav-collapsible",!t&&I.has("collapsible")),t?L.remove("dm-vnav-collapsed"):I.has("collapsible")&&S.get("nav_collapsed")===!0&&L.add("dm-vnav-collapsed"),m.classList.remove("dm-nav-revealed"),a()};m.addEventListener("pointerdown",t=>{L.contains("dm-vnav-autohide")&&t.pointerType!=="mouse"&&!m.classList.contains("dm-nav-revealed")&&(m.classList.add("dm-nav-revealed"),t.preventDefault())}),document.addEventListener("pointerdown",t=>{m.contains(t.target)||m.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(m,{items:()=>{const t=L.contains("dm-vnav-autohide"),c=L.contains("dm-vnav-collapsed"),d=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:c?"Expand menu":"Collapse to icons",icon:c?"maximize":"minimize",disabled:!C()||t||!I.has("collapsible"),action:()=>s(!c)},{label:t?"Keep on screen":"Hide when not in use",icon:t?"eye":"eye-off",disabled:!C(),action:()=>{S.set("nav_autohide",!t),e(!t)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!d,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(I.has("autohide")),L.toggle("dm-vnav-collapsed",!I.has("autohide")&&I.has("start-collapsed")),a()}}]}})}function Ae(m){m.classList.add("dm-nav-scroll-hide");let i=window.scrollY,M=!1;window.addEventListener("scroll",()=>{M||(M=!0,requestAnimationFrame(()=>{M=!1;const L=window.scrollY,I=m.matches(":focus-within")||m.querySelector(".navbar-dropdown.open, .navbar-collapse.show");L>i&&L>m.offsetHeight&&!I?m.classList.add("dm-nav-scrolled-away"):L<i&&m.classList.remove("dm-nav-scrolled-away"),i=L}))},{passive:!0})}function Te(m){let i={};try{i=JSON.parse(m.dataset.contents||"{}")}catch{}const M=m.closest(".site-main"),L=document.querySelector(".site-content"),I=new Set(String(i.exclude||"").split(",").map(n=>n.trim().toLowerCase()).filter(Boolean)),C=n=>!n.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),A=n=>(L?[...L.querySelectorAll(n)]:[]).filter(C).filter(y=>!I.has(y.textContent.trim().toLowerCase())),a=i.headings==="h2"?A("h2"):i.headings==="h3"?A("h3"):i.headings==="h2h3"?A("h2, h3"):A("h2").length?A("h2"):A("h3");if(!a.length){M?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(n=>n.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 t=document.createElement("p");t.className="dm-contents-title",t.textContent=i.title||"",e.appendChild(t),o.appendChild(e);const c=document.createElement("ol");c.className="dm-contents-list"+(i.numbered?" is-numbered":"");const d=[];for(const n of a){if(!n.id){const l=(n.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let b=l;for(let w=2;s.has(b);w++)b=`${l}-${w}`;s.add(b),n.id=b}const y=document.createElement("li");y.className=`dm-contents-item dm-contents-${n.tagName.toLowerCase()}`;const g=document.createElement("a");g.href=`#${n.id}`,g.textContent=n.textContent.trim(),y.appendChild(g),c.appendChild(y),d.push([n,g])}if(o.appendChild(c),m.replaceChildren(o),i.scrollSpy!==!1&&"IntersectionObserver"in window){const n=new Map,y=()=>{const l=d.find(([b])=>n.get(b))||null;d.forEach(([,b])=>{const w=l&&b===l[1];b.classList.toggle("is-active",!!w),w?b.setAttribute("aria-current","location"):b.removeAttribute("aria-current")})},g=new IntersectionObserver(l=>{l.forEach(b=>n.set(b.target,b.isIntersecting)),y()},{rootMargin:"-80px 0px -60% 0px"});d.forEach(([l])=>g.observe(l))}if(M?.classList.contains("contents-collapsible")){const n=document.createElement("button");n.type="button",n.className="dm-contents-toggle";const y=()=>{const g=M.classList.contains("contents-collapsed");n.setAttribute("aria-expanded",String(!g)),n.setAttribute("aria-label",g?"Show the contents":"Hide the contents"),n.title=n.getAttribute("aria-label"),n.innerHTML=`<span data-icon="${g?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(n)};n.addEventListener("click",()=>{S.set("contents_collapsed",M.classList.toggle("contents-collapsed")),y()}),y(),e.insertBefore(n,e.firstChild)}if(M?.classList.contains("contents-floating")&&i.movable){o.classList.add("is-movable");const n=S.get("contents_position");n&&Number.isFinite(n.x)&&Object.assign(m.style,{left:`${n.x}px`,top:`${n.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",y=>{if(y.target.closest("button"))return;const g=m.getBoundingClientRect(),l=y.clientX-g.left,b=y.clientY-g.top,w=f=>{const h=Math.max(0,Math.min(window.innerWidth-g.width,f.clientX-l)),p=Math.max(0,Math.min(window.innerHeight-40,f.clientY-b));Object.assign(m.style,{left:`${h}px`,top:`${p}px`,right:"auto",bottom:"auto"})},u=()=>{document.removeEventListener("pointermove",w),S.set("contents_position",{x:parseInt(m.style.left,10),y:parseInt(m.style.top,10)})};document.addEventListener("pointermove",w),document.addEventListener("pointerup",u,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(m,{items:()=>{const n=M?.classList.contains("contents-collapsible"),y=M?.classList.contains("contents-collapsed"),g=d.find(([,l])=>l.classList.contains("is-active"));return[{header:"Contents"},...n?[{label:y?"Show the contents":"Collapse to a button",icon:y?"list":"minimize",action:()=>m.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!g,action:async()=>{const l=`${location.origin}${location.pathname}#${g[0].id}`;try{await _.copyToClipboard(l),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"})}]}}),M?.classList.contains("contents-autohide")&&(m.addEventListener("pointerdown",n=>{n.pointerType!=="mouse"&&!m.classList.contains("is-revealed")&&(m.classList.add("is-revealed"),n.preventDefault())}),document.addEventListener("pointerdown",n=>{m.contains(n.target)||m.classList.remove("is-revealed")}))}
@@ -29,6 +29,9 @@
29
29
  import {clip, plural} from '../../services/sidebarBadges.js';
30
30
  import fs from 'fs/promises';
31
31
  import path from 'path';
32
+ import {getConfig, saveConfig} from '../../config.js';
33
+ import {invalidateMeasurementId, measurementIdOf, normaliseMeasurementId} from '../../services/googleAnalytics.js';
34
+ import {invalidateTags} from '../../services/cache/index.js';
32
35
  import {authenticate as defaultAuthenticate, requireAdmin as defaultRequireAdmin} from '../../middleware/auth.js';
33
36
 
34
37
  /**
@@ -371,6 +374,22 @@ function shouldRecordJourney(dayEntries, cap = JOURNEY_DAILY_CAP) {
371
374
  export async function analyticsRoutes(fastify, opts = {}) {
372
375
  const authenticate = opts.authenticate || defaultAuthenticate;
373
376
  const requireAdmin = opts.requireAdmin || defaultRequireAdmin;
377
+ fastify.get('/settings', {preHandler: [authenticate, requireAdmin]}, async () => {
378
+ const site = getConfig('site');
379
+ return {measurementId: measurementIdOf(site), cookieBannerEnabled: site.cookieConsent?.enabled !== false};
380
+ });
381
+ fastify.put('/settings', {preHandler: [authenticate, requireAdmin]}, async (request, reply) => {
382
+ const raw = request.body?.measurementId;
383
+ if (typeof raw !== 'string' || (raw.trim() && !normaliseMeasurementId(raw))) {
384
+ return reply.code(400).send({error: 'A Google Analytics measurement ID looks like G-XXXXXXXXXX.'});
385
+ }
386
+ const site = getConfig('site');
387
+ const measurementId = normaliseMeasurementId(raw);
388
+ saveConfig('site', {...site, googleAnalytics: {...site.googleAnalytics, measurementId}});
389
+ invalidateMeasurementId();
390
+ await invalidateTags(['site']);
391
+ return {measurementId, cookieBannerEnabled: site.cookieConsent?.enabled !== false};
392
+ });
374
393
 
375
394
  await migrateLegacy();
376
395
 
@@ -0,0 +1,64 @@
1
+ /** Site-wide Google Business Profile connection, managed through SEO. */
2
+ import {createHash} from 'node:crypto';
3
+ import {createGoogleBusiness} from '../../services/googleBusiness.js';
4
+ import {originOf, trustedOrigin} from '../../services/passwordReset.js';
5
+ import {getUserById} from '../../services/users.js';
6
+ import {getPermissionsFor} from '../../services/roles.js';
7
+ import {getEffectiveRoles} from '../../services/userRoles.js';
8
+ import {resolveViewer} from '../../services/viewerSession.js';
9
+
10
+ const CALLBACK = '/api/seo/google-business/callback';
11
+ const cookieName = state => `dm_business_${createHash('sha256').update(state).digest('hex').slice(0, 16)}`;
12
+ const cookieValue = (header, name) => String(header || '').split(';').map(s => s.trim()).find(s => s.startsWith(`${name}=`))?.slice(name.length + 1) || '';
13
+ const esc = value => String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
14
+
15
+ export async function googleBusinessRoutes(fastify, opts) {
16
+ const service = opts.service || createGoogleBusiness();
17
+ const mayManage = {preHandler: [opts.authenticate, opts.requirePermission('seo', 'manage')]};
18
+ const redirectFor = opts.redirectFor || (async request => {
19
+ const origin = await trustedOrigin(originOf(request));
20
+ if (!origin) throw Object.assign(new Error('Set the Site URL in Site Settings before connecting Google.'), {statusCode: 400});
21
+ return `${origin}${CALLBACK}`;
22
+ });
23
+ const mayComplete = opts.mayComplete || (async id => {
24
+ const user = await getUserById(id);
25
+ return user?.isActive === true && getEffectiveRoles(user).some(r => getPermissionsFor('seo', 'manage').includes(r));
26
+ });
27
+ fastify.addHook('onSend', async (request, reply, payload) => { reply.header('Cache-Control', 'no-store'); return payload; });
28
+ // Google token failures are not CMS session failures: avoid the admin client's 401 login retry.
29
+ const safe = handler => async (request, reply) => {
30
+ try { return await handler(request, reply); }
31
+ catch (e) { return reply.code(e.statusCode === 401 ? 409 : e.statusCode || 500).send({error: e.statusCode ? e.message : 'The Google connection could not be saved. Try again.'}); }
32
+ };
33
+ fastify.get('/status', mayManage, safe(async request => ({...service.status(), redirectUri: await redirectFor(request)})));
34
+ fastify.post('/connect', {...mayManage, config: {rateLimit: {max: 10, timeWindow: '1 minute'}}}, safe(async (request, reply) => {
35
+ if (!request.user?.id) return reply.code(401).send({error: 'Sign in before connecting Google.'});
36
+ const redirectUri = await redirectFor(request);
37
+ const {url, state, binding} = service.begin({...request.body, redirectUri, userId: request.user.id});
38
+ reply.header('Set-Cookie', `${cookieName(state)}=${binding}; Path=${CALLBACK}; HttpOnly; SameSite=Lax; Max-Age=600${redirectUri.startsWith('https:') ? '; Secure' : ''}`);
39
+ return {url};
40
+ }));
41
+ fastify.get('/callback', {config: {rateLimit: {max: 30, timeWindow: '1 minute'}}}, async (request, reply) => {
42
+ const state = String(request.query?.state || '').slice(0, 200);
43
+ const name = cookieName(state);
44
+ reply.header('Set-Cookie', `${name}=; Path=${CALLBACK}; HttpOnly; SameSite=Lax; Max-Age=0`);
45
+ let ok = false;
46
+ let message;
47
+ try {
48
+ const ticket = service.take(state, cookieValue(request.headers.cookie, name));
49
+ const viewer = await resolveViewer(request).catch(() => null);
50
+ if (viewer?.id && viewer.id !== ticket.userId) throw new Error('Google sign-in was started by someone else. Press Connect yourself.');
51
+ if (!(await mayComplete(ticket.userId))) throw new Error('You no longer have permission to connect Google Business Profile.');
52
+ if (request.query?.error) throw new Error('Google sign-in was cancelled or access was declined. Press Connect to try again.');
53
+ await service.complete(ticket, request.query?.code);
54
+ ok = true;
55
+ message = 'Google Business Profile connected. Return to SEO to choose a location.';
56
+ } catch (e) { message = e.statusCode || e.message?.startsWith('Google sign-in') || e.message?.startsWith('You no longer') ? e.message : 'The Google connection could not be completed. Press Connect to try again.'; }
57
+ return reply.type('text/html; charset=utf-8').header('Referrer-Policy', 'no-referrer').header('X-Robots-Tag', 'noindex')
58
+ .send(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Google Business Profile</title><body><main style="font-family:system-ui;max-width:36rem;margin:15vh auto;padding:1rem"><h1>${ok ? 'Google connected' : 'Google not connected'}</h1><p id="business-result" data-ok="${ok}">${esc(message)}</p><p>You can close this window and return to SEO.</p><a href="/admin/#/seo">Return to SEO</a></main><script src="/admin/js/lib/google-business-callback.js" defer></script></body></html>`);
59
+ });
60
+ fastify.get('/accounts', mayManage, safe(request => service.accounts(request.query?.pageToken)));
61
+ fastify.get('/locations', mayManage, safe(request => service.locations(request.query?.account, request.query?.pageToken)));
62
+ fastify.put('/location', mayManage, safe(request => service.select(request.body?.name)));
63
+ fastify.delete('/connection', mayManage, safe(() => service.disconnect()));
64
+ }
@@ -9,12 +9,14 @@
9
9
  * Was /api/plugins/seo while SEO was a plugin (0.89.0).
10
10
  */
11
11
  import {authenticate as defaultAuthenticate, requirePermission as defaultRequirePermission} from '../../middleware/auth.js';
12
- import {getConfig} from '../../config.js';
12
+ import {getConfig, saveConfig} from '../../config.js';
13
13
  import {getPage, updatePage} from '../../services/content.js';
14
14
  import {getSitemapSources} from '../../services/hooks.js';
15
15
  import {seoEngine} from '../../services/seo/index.js';
16
16
  import {invalidateTags} from '../../services/cache/index.js';
17
17
  import {excluded} from '../../../admin/js/lib/seo-settings.js';
18
+ import {checkSeoDefaults, seoDefaults} from '../../../admin/js/lib/seo-defaults.js';
19
+ import {googleBusinessRoutes} from './google-business.js';
18
20
 
19
21
  /**
20
22
  * @param {import('fastify').FastifyInstance} fastify
@@ -194,11 +196,20 @@ export async function seoRoutes(fastify, opts = {}) {
194
196
  robots: buildRobotsTxt(origin)};
195
197
  });
196
198
 
197
- fastify.get('/settings', may('read'), async () => ({settings: seo().settings(), hooks: true}));
199
+ await fastify.register(googleBusinessRoutes, {prefix: '/google-business', authenticate, requirePermission, ...opts.googleBusiness});
200
+
201
+ fastify.get('/settings', may('read'), async () => ({settings: seo().settings(), defaults: seoDefaults(site()), hooks: true}));
198
202
  fastify.put('/settings', may('manage'), async (request, reply) => {
203
+ const defaults = request.body?.siteDefaults === undefined ? null : checkSeoDefaults(request.body.siteDefaults);
204
+ if (defaults?.error) return reply.code(400).send({error: defaults.error});
199
205
  const r = seo().saveSettings(request.body ?? {});
200
206
  if (r.errors) return reply.code(400).send({error: Object.values(r.errors).join(' '), errors: r.errors});
201
- return {settings: r.settings, hooks: true};
207
+ if (defaults) {
208
+ const current = site();
209
+ saveConfig('site', {...current, seo: {...current.seo, ...defaults.defaults}});
210
+ await invalidateTags(['site']);
211
+ }
212
+ return {settings: r.settings, defaults: seoDefaults(site()), hooks: true};
202
213
  });
203
214
 
204
215
  fastify.get('/extensions', may('read'), async () => ({