domma-cms 1.6.34 → 1.6.36

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 (106) hide show
  1. package/admin/js/api.js +1 -1
  2. package/admin/js/lib/effects-builder.js +1 -1
  3. package/admin/js/lib/pages-arrange.js +1 -1
  4. package/admin/js/lib/scribe-composer.js +1 -1
  5. package/admin/js/lib/scribe-model.js +1 -1
  6. package/admin/js/lib/shortcode-preview.js +1 -1
  7. package/admin/js/lib/shortcode-registry.js +2 -2
  8. package/admin/js/templates/docs/usage-site-settings.html +2 -1
  9. package/admin/js/templates/page-editor.html +2 -1
  10. package/admin/js/templates/pages.html +1 -1
  11. package/admin/js/templates/theme.html +1 -1
  12. package/admin/js/views/pages.js +3 -3
  13. package/admin/js/views/theme.js +5 -5
  14. package/package.json +1 -1
  15. package/plugins/_lib/call/ice.js +5 -2
  16. package/plugins/_lib/call/rooms.js +9 -3
  17. package/plugins/_lib/call/routes.js +7 -4
  18. package/plugins/_lib/mail/defaults.js +12 -0
  19. package/plugins/_lib/mail/hostGuard.js +86 -0
  20. package/plugins/_lib/mail/readRoutes.js +20 -1
  21. package/plugins/_lib/outreach/audience.js +76 -0
  22. package/plugins/_lib/outreach/collections/outreach-contacts.schema.json +12 -0
  23. package/plugins/_lib/outreach/collections/outreach-groups.schema.json +10 -0
  24. package/plugins/_lib/outreach/collections/outreach-unsubscribes.schema.json +11 -0
  25. package/plugins/_lib/outreach/expiry.js +46 -0
  26. package/plugins/_lib/outreach/index.js +15 -0
  27. package/plugins/_lib/outreach/ledger.js +127 -0
  28. package/plugins/_lib/outreach/sending.js +121 -0
  29. package/plugins/_lib/outreach/suppression.js +62 -0
  30. package/plugins/_lib/outreach/tokens.js +35 -0
  31. package/plugins/_lib/outreach/unsubscribe.js +46 -0
  32. package/plugins/_lib/public/call/call.js +3 -1
  33. package/plugins/blog/CLAUDE.md +11 -1
  34. package/plugins/blog/docs/guide.md +1 -1
  35. package/plugins/blog/lib/page.js +32 -0
  36. package/plugins/blog/plugin.js +58 -16
  37. package/plugins/blog/plugin.json +2 -2
  38. package/plugins/blog/plugin.public.js +5 -4
  39. package/plugins/blog/tests/public.test.js +30 -1
  40. package/plugins/booking/CLAUDE.md +4 -1
  41. package/plugins/booking/docs/guide.md +1 -1
  42. package/plugins/booking/plugin.js +16 -21
  43. package/plugins/booking/plugin.json +2 -2
  44. package/plugins/booking/tests/api.test.js +26 -1
  45. package/plugins/feedback/CLAUDE.md +7 -2
  46. package/plugins/feedback/admin/views/feedback.js +1 -1
  47. package/plugins/feedback/lib/receiver.js +1 -1
  48. package/plugins/feedback/plugin.json +3 -3
  49. package/plugins/free-tier.lock.json +63 -51
  50. package/plugins/mail-reader/CLAUDE.md +6 -0
  51. package/plugins/mail-reader/docs/guide.md +1 -0
  52. package/plugins/mail-reader/plugin.json +2 -2
  53. package/plugins/shopping-cart/CLAUDE.md +62 -1
  54. package/plugins/shopping-cart/admin/css/index.css +16 -0
  55. package/plugins/shopping-cart/admin/lib/chart.js +64 -0
  56. package/plugins/shopping-cart/admin/lib/kit.js +15 -0
  57. package/plugins/shopping-cart/admin/views/orders.js +1 -0
  58. package/plugins/shopping-cart/admin/views/overview.js +4 -53
  59. package/plugins/shopping-cart/admin/views/sales.js +133 -0
  60. package/plugins/shopping-cart/admin/views/settings.js +3 -1
  61. package/plugins/shopping-cart/admin/views/shop.js +2 -1
  62. package/plugins/shopping-cart/config.js +1 -0
  63. package/plugins/shopping-cart/docs/guide.md +26 -1
  64. package/plugins/shopping-cart/inject/body.html +2 -2
  65. package/plugins/shopping-cart/lib/accounts.js +185 -0
  66. package/plugins/shopping-cart/lib/commission.js +90 -0
  67. package/plugins/shopping-cart/lib/extensions.js +12 -4
  68. package/plugins/shopping-cart/lib/payments.js +7 -4
  69. package/plugins/shopping-cart/lib/render.js +13 -1
  70. package/plugins/shopping-cart/lib/sales.js +193 -0
  71. package/plugins/shopping-cart/lib/shop.js +21 -3
  72. package/plugins/shopping-cart/plugin.js +180 -35
  73. package/plugins/shopping-cart/plugin.json +3 -3
  74. package/plugins/shopping-cart/plugin.public.js +17 -1
  75. package/plugins/shopping-cart/public/account.js +277 -0
  76. package/plugins/shopping-cart/public/cart.js +16 -0
  77. package/plugins/shopping-cart/public/lib/session.js +82 -0
  78. package/plugins/shopping-cart/public/shop.css +27 -0
  79. package/plugins/shopping-cart/roles/shop-customer.json +8 -0
  80. package/plugins/shopping-cart/tests/accounts.test.js +136 -0
  81. package/plugins/shopping-cart/tests/api.test.js +34 -0
  82. package/plugins/shopping-cart/tests/commission.test.js +90 -0
  83. package/plugins/shopping-cart/tests/pure.test.js +16 -1
  84. package/plugins/shopping-cart/tests/sales.test.js +73 -0
  85. package/plugins/support/CLAUDE.md +6 -0
  86. package/plugins/support/admin/views/support.js +1 -1
  87. package/plugins/support/docs/guide.md +90 -0
  88. package/plugins/support/lib/receiver.js +1 -1
  89. package/plugins/support/plugin.json +3 -3
  90. package/public/css/site.css +1 -1
  91. package/public/js/effects.js +1 -1
  92. package/public/js/scribe-style.mjs +1 -1
  93. package/public/js/site.js +1 -1
  94. package/public/js/text-style.mjs +1 -1
  95. package/server/routes/api/analytics.js +32 -0
  96. package/server/routes/api/collections.js +148 -15
  97. package/server/routes/api/theme.js +12 -2
  98. package/server/routes/outreach-public.js +68 -0
  99. package/server/server.js +10 -1
  100. package/server/services/authChecks.js +8 -0
  101. package/server/services/collectionAccess.js +191 -0
  102. package/server/services/collections.js +8 -2
  103. package/server/services/markdown.js +7 -2
  104. package/server/services/outreachUnsubscribe.js +54 -0
  105. package/server/services/request-log.js +70 -0
  106. package/server/templates/page.html +3 -3
package/public/js/site.js CHANGED
@@ -1 +1 @@
1
- import{decodeBase64Json as P}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as Y,applyColour as J,applyPill as te,applyFloat as ne,applyHighlight as R,applyItemHighlight as oe,itemHighlightAttrs as ae,isCurrentItem as X,menuAlign as se,applyClasses as O,cssClasses as W}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{initFloatingMenus as ie}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as U,markInvalidFields as G,showValidationPopup as K}from"/public/js/forms.js?v=20261009-form-classes";import{buildWizardSteps as re}from"/public/js/form-steps.mjs";import{applyFormClasses as Q,applySubmitButtons as ce,applyWizardButtons as le}from"/public/js/form-submit.mjs";document.addEventListener("click",p=>{const l=p.target?.closest?.(".dm-banner__dismiss");l&&(p.preventDefault(),l.closest(".dm-banner")?.remove())}),$(()=>{const p=window.__CMS_NAV__||{},l=window.__CMS_SITE__||{};if(l.autoTheme?.enabled){let n=function(e){const a=(e||"07:00").split(":");return+a[0]*60+(+a[1]||0)},s=function(){const e=new Date,a=e.getHours()*60+e.getMinutes();return a>=n(o.dayStart)&&a<n(o.nightStart)?o.dayTheme:o.nightTheme},t=function(e){const a=o.apply&&o.apply[e]||{theme:e,custom:""};Domma.theme.set(a.theme);for(const f of[...document.body.classList])f.startsWith("dm-custom-")&&document.body.classList.remove(f);a.custom&&document.body.classList.add(a.custom)};const o=l.autoTheme;t(s()),setInterval(()=>t(s()),6e4)}const x=$("#site-navbar");if(x.length&&p.brand){let s=function(d,m=0){return Array.isArray(d)?d.filter(u=>!(u&&(u.type==="spacer"||u.type==="theme-switcher"))).filter(u=>!(u&&u.type==="separator"&&m===0)).map(u=>u&&u.type==="separator"?{divider:!0}:Array.isArray(u?.items)?{...u,items:s(u.items,m+1)}:u):d},t=function(d){return Array.isArray(d)?d.filter(m=>!(m&&(m.type==="separator"||m.type==="spacer"||m.type==="theme-switcher"))):[]};const o={...p.brand},n=o.size&&o.size!=="md"?` navbar-brand-${o.size}`:"";if(o.logo||o.icon||o.tagline){let d="";o.logo?d+=`<img src="${o.logo}" class="navbar-brand-logo" alt="${o.text||""}">`:o.icon&&(d+=`<span data-icon="${o.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),o.text&&(d+=`<span class="navbar-brand-text${n}">${o.text}</span>`),o.tagline&&(d+=`<small class="navbar-brand-tagline">${o.tagline}</small>`),o.html=d}else n&&o.text&&(o.html=`<span class="navbar-brand-text${n}">${o.text}</span>`);const e=p.position==="floating",a=p.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:o,items:s(p.items||[]),variant:p.variant||"dark",position:e?"static":p.position||"sticky",collapsible:!0,appearOnHover:!!p.appearOnHover}),Domma.icons.scan("#site-navbar");const f=x.get(0);f.classList.add(`dm-nav-align-${se(p)||"centre"}`),a&&x.get(0).classList.add("dm-nav-vertical",p.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const v=p.dock||{};a&&!e&&de(f,p.side==="right"),!a&&v.hideOnScroll&&me(f),R(f,p),O(f,p.class),e&&(ne(x.get(0),p),ie(x.get(0).parentNode||document));const i=f.querySelector(".navbar-toggle"),c=f.querySelector(".navbar-collapse");if(i&&c){let d=null;const m=()=>{c.classList.contains("show")&&i.click()},u=()=>{d&&(d.remove(),d=null),document.body.classList.remove("dm-drawer-open")},g=()=>{d||(d=document.createElement("div"),d.className="dm-navbar-backdrop",d.addEventListener("click",m),document.body.appendChild(d),document.body.classList.add("dm-drawer-open"))};i.addEventListener("click",()=>{requestAnimationFrame(()=>{c.classList.contains("show")?g():u()})}),c.addEventListener("click",C=>{C.target.closest("a")&&requestAnimationFrame(u)}),document.addEventListener("keydown",C=>{C.key==="Escape"&&c.classList.contains("show")&&m()})}const h=f.querySelector(".navbar-nav");if(h){const d=Array.from(h.children);let m=0;for(const u of p.items||[])if(!(u&&u.type==="separator")){if(u&&u.type==="spacer"){const g=document.createElement("li"),C=["xs","sm","md","lg","xl","flex"].includes(u.size)?u.size:"md";g.className=`dm-menu-spacer dm-menu-spacer--${C}`,g.setAttribute("aria-hidden","true"),h.insertBefore(g,d[m]||null);continue}if(u&&u.type==="theme-switcher"){const g=document.createElement("li");g.className="dm-menu-theme-switcher";const C=document.createElement("span");C.dataset.themeSwitcher="menu",C.dataset.position="inline",g.appendChild(C),h.insertBefore(g,d[m]||null);continue}m++}window.CMSThemeSwitcher?.scan(h)}const b=t(p.items||[]);f.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(d=>{const m=b[parseInt(d.dataset.index,10)];if(!m)return;const u=d.dataset.subindex!=null,g=u&&Array.isArray(m.items)?m.items[parseInt(d.dataset.subindex,10)]:m;if(!g)return;g.colour&&J(d,g.colour),g.pill&&!u&&te(d,g.pill),oe(d,g),O(d.closest("li"),g.class);const C=Y(g.badge);C&&d.appendChild(C),X(g.url,location.pathname)&&(d.classList.add("active"),d.setAttribute("aria-current","page"))})}const y=$("#site-footer");if(y.length){let e=function(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},a=function(r){return e(r).replace(/'/g,"&#39;")},f=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 A=a(r.url||"#"),k=e(r.text||""),q=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(f).join("")}</ul>`:"",N=ae(r),Z=[N.hover&&` data-menu-item-hover="${a(N.hover)}"`,N.active&&` data-menu-item-active="${a(N.active)}"`].filter(Boolean).join(""),ee=N.accent?` style="--dm-menu-highlight:${a(N.accent)}"`:"",V=W(r.class).join(" ");return`<li${V?` class="${a(V)}"`:""}><a href="${A}"${Z}${ee}>${k}</a>${q}</li>`},v=function(){if(!l.footer)return"";const r=Object.keys(n).filter(k=>o[k]);if(!r.length)return"";const A=W(l.footer.socialClass).join(" ");return`<div class="footer-social">${r.map(k=>{const{label:q,svg:N}=n[k];return`<a href="${o[k]}" target="_blank" rel="noopener noreferrer" aria-label="${q}" class="footer-social-link${A?` ${a(A)}`:""}">${N}</a>`}).join("")}</div>`},b=function(r){if(!r||r.type==="separator"||r.type==="spacer")return"";if(r.type==="theme-switcher")return`<li class="cms-footer-col">${f(r).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const A=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(f).join("")}</ul>`:"",k=h(r)?`<span class="cms-footer-col-title">${e(r.text||"")}</span>`:`<a class="cms-footer-col-title" href="${a(r.url||"#")}">${e(r.text||"")}</a>`,q=["cms-footer-col",...W(r.class)].join(" ");return`<li class="${a(q)}">${k}${A}</li>`},m=function(r,A,k=!1){return(r||[]).forEach(q=>{q&&(q.type==="separator"||q.type==="spacer"||q.type==="theme-switcher")||(k&&h(q)||A.push(q),Array.isArray(q.items)&&q.items.length&&m(q.items,A))}),A};const o=l.social||{},n={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let s='<div class="footer-inner container">';const t=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},i=["columns","row"].includes(l.footer?.layout)?l.footer.layout:"auto",c=i==="columns"||i==="auto"&&t.primary.some(r=>Array.isArray(r?.items)&&r.items.length),h=r=>c&&!r.url&&Array.isArray(r.items)&&r.items.length>0;if(c){const r=p.brand||{},A=r.text||l.title||"",k=r.logo?`<img src="${a(r.logo)}" class="cms-footer-brand-logo" alt="">`:r.icon?'<span class="cms-footer-brand-icon"></span>':"",q=String(l.footer?.blurb||"").trim();s='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',s+=`<div class="cms-footer-brand">${A||k?`<a class="cms-footer-brand-name" href="${a(r.url||"/")}">${k}<span>${e(A)}</span></a>`:""}${q?`<p class="cms-footer-blurb">${e(q)}</p>`:""}${v()}</div>`,t.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${t.primary.map(b).join("")}</ul></nav>`),s+='</div><div class="cms-footer-bottom">',t.copyright&&(s+=`<p class="cms-footer-copyright">${e(t.copyright)}</p>`),t.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${t.legal.map(f).join("")}</ul></nav>`),s+="</div>"}else t.copyright&&(s+=`<p>${e(t.copyright)}</p>`),t.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary"><ul>${t.primary.map(f).join("")}</ul></nav>`),t.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${t.legal.map(f).join("")}</ul></nav>`),s+=v();s+="</div>",y.html(s),window.CMSThemeSwitcher?.scan(y.get(0));const d=y.get(0).querySelector(".cms-footer-brand-icon");d&&p.brand?.icon&&d.setAttribute("data-icon",p.brand.icon),O(y.get(0),l.footer?.class);const u=[...m(t.primary,[],!0),...m(t.legal,[])];y.get(0).querySelectorAll(".footer-nav a").forEach((r,A)=>{const k=u[A];if(!k)return;k.colour&&J(r,k.colour);const q=Y(k.badge);q&&r.appendChild(q),X(k.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),R(y.get(0).querySelector(".footer-nav--primary"),{highlight:t.primaryHighlight}),R(y.get(0).querySelector(".footer-nav--legal"),{highlight:t.legalHighlight}),O(y.get(0).querySelector(".footer-nav--primary"),t.primaryClass),O(y.get(0).querySelector(".footer-nav--legal"),t.legalClass);const C=l.footer?.showMotionToggle!==!1,M=S.get("reduced_motion"),D=l.footer?.motionDefault,z=M!=null?!!M:D==="on"?!0:D==="off"?!1:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),B=l.footer?.motionControl==="checkbox",I=y.get(0).querySelector(".cms-footer-bottom")||y.get(0).querySelector(".footer-inner");if(C&&I){const r=document.createElement("input");r.type="checkbox",r.className=B?"footer-motion-checkbox":"form-switch-input",r.id="dm-motion-switch",r.checked=z,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const A=document.createElement("span");A.className=B?"footer-motion-label":"form-switch-label",A.textContent="Reduce motion";const k=document.createElement("label");k.className=B?"footer-motion-check":"form-switch footer-motion-switch",k.title="Reduce motion",k.appendChild(r),k.appendChild(A),I.appendChild(k)}}const L=document.getElementById("site-sidebar");L&&ue(L),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const w=$(".page-body");if(w.length){w.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),w.find(".tabs").each(function(){Domma.elements.tabs(this)}),w.find(".carousel").each(function(){const o={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const n=parseInt(this.dataset.animationDuration,10);Number.isFinite(n)&&(o.animationDuration=n)}this.dataset.animationEasing&&(o.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(n=>n.cloneNode(!0)),this._dmCarouselOpts=o),this._dmCarousel=Domma.elements.carousel(this,o)}),w.find(".dm-countdown").each(function(){const o={autoStart:!0},n=String(this.dataset.format||"").trim(),s=String(this.dataset.to||"").trim(),t=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s),e=t?new Date(+t[1],+t[2]-1,+t[3]).getTime():s?new Date(s).getTime():NaN;Number.isFinite(e)?o.duration=Math.max(0,e-Date.now()):this.dataset.duration&&(o.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const a=/^dd:/i.test(n);o.format=a?"hh:mm:ss":n||(o.duration>=36e5?"hh:mm:ss":"mm:ss");const f=this,v=c=>String(c).padStart(2,"0"),i=c=>{const h=Math.max(0,Math.floor(c/1e3)),b=f.querySelector(".dm-timer-time");b&&(b.textContent=`${v(Math.floor(h/86400))}:${v(Math.floor(h%86400/3600))}:${v(Math.floor(h%3600/60))}:${v(h%60)}`)};a&&(o.onTick=i),Domma.elements.timer(this,o),a&&i(o.duration||0)}),w.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),w.find(".dm-progression").each(function(){const o=Array.from(this.querySelectorAll(".dm-progression-item")).map(t=>{const e={title:t.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:t.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return t.dataset.date&&(e.date=t.dataset.date),t.dataset.status&&(e.status=t.dataset.status),t.dataset.eventIcon&&(e.icon=t.dataset.eventIcon),t.dataset.entryId&&(e._entryId=t.dataset.entryId),e}),n=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",s=Domma.elements.progression(this,{layout:n,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 t={completed:{label:"Completed",icon:"check-circle",tone:"success"},planned:{label:"Planned",icon:"circle",tone:"secondary"},"in-progress":{label:"In progress",icon:"clock",tone:"primary"},blocked:{label:"Blocked",icon:"alert-circle",tone:"danger"}};this.querySelectorAll(".dm-progression-item").forEach((e,a)=>{const f=o[a];if(!f)return;const v=e.querySelector(".dm-progression-title");if(f.icon&&v){const c=document.createElement("span");c.dataset.icon=f.icon,c.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",c.setAttribute("aria-hidden","true"),v.prepend(c)}const i=t[f.status];if(i&&v){const c=document.createElement("span");c.className=`badge badge-${i.tone} cms-timeline-status`,c.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const h=document.createElement("span");h.dataset.icon=i.icon,h.dataset.iconSize="14",h.setAttribute("aria-hidden","true"),c.append(h,document.createTextNode(i.label)),v.after(c)}})}Domma.icons.scan(this),o.length&&(this._dmCtx={instance:s,items:o})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(o=>{const n=o.dataset.revealMode||"stagger",s=o.dataset.revealAnimation||"slide-up",t=parseInt(o.dataset.revealDuration,10)||400,e=parseInt(o.dataset.revealStagger,10)||60,a=parseInt(o.dataset.revealDelay,10)||0,f=o.dataset.revealDirection==="rtl",v=Array.from(o.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};v.forEach((c,h)=>{c.style.opacity="0",c.style.transform=i[s]||"",c.style.transition=`opacity ${t}ms ease, transform ${t}ms ease`;const b=f?v.length-1-h:h;c.style.transitionDelay=n==="stagger"?`${a+b*e}ms`:`${a}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const c=new IntersectionObserver(h=>{h.forEach(b=>{b.isIntersecting&&(b.target.offsetWidth,b.target.style.opacity="1",b.target.style.transform="none",c.unobserve(b.target))})},{threshold:.1});v.forEach(h=>c.observe(h))}))}),w.find(".card[data-collapsible]").each(function(){const o=this.querySelector(".card-header");o&&o.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),w.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const o=this.dataset.soTarget,n=document.getElementById(o);if(!n)return;const s=E.slideover({title:n.dataset.soTitle||"",size:n.dataset.soSize||"md",position:n.dataset.soPosition||"right"});n.style.display="",s.setContent(n),s.open()})})}if(typeof $.setup=="function"){const o=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(n=>{try{const s=P(n.dataset.config);Object.assign(o,s)}catch{}}),Object.keys(o).length>0){const n={};for(const[s,t]of Object.entries(o)){const e=t?.events?.click,{confirm:a,toast:f,alert:v,prompt:i,...c}=e||{};a||f||v||i?($(s).on("click",async function(b){if(b.preventDefault(),a&&!await E.confirm(a))return;let d=null;if(!(i&&(d=await E.prompt(i,{inputPlaceholder:c.promptPlaceholder||"",inputValue:c.promptDefault||""}),d===null))){if(c.target){const m=$(c.target);c.toggleClass&&m.toggleClass(c.toggleClass),c.addClass&&m.addClass(c.addClass),c.removeClass&&m.removeClass(c.removeClass),d!==null&&(c.setText&&m.text(d),c.setVal&&m.val(d),c.setAttr&&m.attr(c.setAttr,d))}c.href&&(window.location.href=c.href),f&&E.toast(f,{type:c.toastType||"success"}),v&&E.alert(v)}}),Object.keys(c).length&&(n[s]={...t,events:{...t.events,click:c}})):n[s]=t}$.setup(n)}}w.length&&j(w.get(0))}),window.wireCTAButtons=j;function j(p){p.querySelectorAll(".dm-cta-trigger").forEach(l=>{l.__ctaWired||(l.__ctaWired=!0,l.addEventListener("click",async()=>{const x=l.dataset.action,y=l.dataset.entry,L=l.dataset.confirm;let w=S.get("auth_token");if(!w){E.toast("Please log in to perform this action.",{type:"warning"});return}if(L&&!await E.confirm(L))return;const o=Array.from(l.childNodes).map(s=>s.cloneNode(!0));l.disabled=!0,l.textContent="Running\u2026";const n=s=>fetch(`/api/actions/${x}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(l.dataset.scope?{entryId:y,scope:l.dataset.scope}:{entryId:y})});try{let s=await n(w);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const a=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(a.ok){const{token:f}=await a.json();S.set("auth_token",f),w=f,s=await n(w)}}}const t=await s.json().catch(()=>({}));if(!s.ok||t.success===!1)throw new Error(t.error||t.message||`Error ${s.status}`);E.toast(t.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{l.disabled=!1,l.textContent="",o.forEach(s=>l.appendChild(s)),Domma.icons.scan(l)}}))})}(function(){const l=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!l.length)return;const x=S.get("auth_token");function y(w,o){const n=document.createElement("div");n.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(o){s.appendChild(document.createTextNode("Please "));const t=document.createElement("a");t.href="/login",t.textContent="sign in",s.appendChild(t),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=w;return n.appendChild(s),n}function L(w,o){const n=document.createElement("template");n.innerHTML=o||"",w.replaceWith(n.content)}l.forEach(async w=>{const o=w.dataset.collectionAttrs;if(!o)return;if(!x){w.replaceChildren(y("",!0));return}const n=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:o})});try{let s=x,t=await n(s);if(t.status===401){const a=S.get("auth_refresh_token");if(a){const f=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:a})});if(f.ok){const{token:v}=await f.json();S.set("auth_token",v),s=v,t=await n(s)}}}if(!t.ok){const{error:a}=await t.json().catch(()=>({}));w.replaceChildren(y(a||"Unable to load entries.",!1));return}const{html:e}=await t.json();L(w,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{w.replaceChildren(y("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),j(document.body)},100)})(),(function(){const l=document.querySelectorAll("[data-collection-table]");!l.length||typeof T>"u"||!T.create||l.forEach(x=>{let y;try{y=P(x.dataset.payload)}catch{return}const{columns:L,rows:w,search:o,sortable:n,exportable:s,pageSize:t,empty:e,ctaConfig:a,slug:f,exportCfg:v}=y;if(!L?.length)return;if(a){const h=L.findIndex(b=>b.key==="_cta");h!==-1&&(L[h]={key:"_cta",title:"",render:(b,d)=>{const m=document.createElement("button");if(m.className=`btn btn-${a.style||"primary"} dm-cta-trigger`,m.dataset.action=a.action||"",a.scope&&(m.dataset.scope=a.scope),m.dataset.entry=d._entryId||"",a.confirm&&(m.dataset.confirm=a.confirm),a.icon){const u=document.createElement("span");u.dataset.icon=a.icon,m.appendChild(u),m.appendChild(document.createTextNode(" "))}return m.appendChild(document.createTextNode(a.label||"Run")),m}})}const i="col-table-"+Math.random().toString(36).slice(2,7),c=document.createElement("div");c.id=i,x.replaceChildren(c),T.create("#"+i,{data:w,columns:L,search:o,sortable:n,exportable:s,pageSize:t,emptyMessage:e}),a&&j(c),x._dmTable={columns:L,rows:w,slug:f,exportCfg:v,render(h){T.create("#"+i,{data:h,columns:L,search:o,sortable:n,exportable:s,pageSize:t,emptyMessage:e}),a&&j(c)}}})})(),(function(){const l=document.querySelectorAll("[data-form-inline]");if(!l.length||typeof F>"u")return;function x(n){const s=n&&n.logic;return!!(s&&(s.visibility||s.requirement))}function y(n,s){const t={};return(n||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const a=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),f=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":a?"chooser":e.type,v={...e.formConfig||{}};v.span==="full"&&s&&(v.span=s);const i={type:f,required:x(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...v}};e.minLength!==void 0&&(i.minLength=e.minLength),e.maxLength!==void 0&&(i.maxLength=e.maxLength),e.min!==void 0&&(i.min=e.min),e.max!==void 0&&(i.max=e.max),a&&(e.variant&&(i.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(i.multiple=!0),e.density&&(i.density=e.density),e.columns&&(i.columns=e.columns),e.accent&&(i.accent=e.accent),e.accentStyle&&(i.accentStyle=e.accentStyle),e.glow&&(i.glow=!0),e.glowColour&&(i.glowColour=e.glowColour),e.shadow&&(i.shadow=e.shadow),e.shadowColour&&(i.shadowColour=e.shadowColour)),t[e.name]=i}),t}function L(n){const s={};return(n||[]).forEach(t=>{if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(s[t.name]=typeof e=="object"?e.value:e)}}),s}function w(n,s){(s||[]).forEach(t=>{if(t.type!=="date"||!t.name)return;const e=n.querySelector(`[name="${t.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function o(n,s,t){let e=n.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",n.appendChild(e)),e.textContent=s,e.style.cssText=t?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}l.forEach(n=>{let s;try{s=P(n.dataset.formInline)}catch{return}const t=s.fields||[],e=s.settings||{},a=Number(e.columns)===3?3:2,f=e.layout||"grid",v=t.some(m=>m.type==="page-break"),i=async m=>{if(n.dataset.triggerBlocked==="true"){const u=n._formLogicRuntime,g=u&&u.submitBlock?u.submitBlock().reasons:[];o(n,g.join(" ")||"This form cannot be submitted yet.",!0);const C=new Error("Blocked by a form trigger");throw C.formSubmitFailed=!0,C}try{const u=n.querySelector(".fb-form-honeypot"),g=u?.querySelector('input[type="text"]')||null,C=u?.querySelector('[name="_t"]')||null,M=Object.assign({},m);delete M._t,u?.dataset.hpName&&delete M[u.dataset.hpName],g!==null&&(M._hp=g.value),C!==null&&(M._t=C.value);const D=await H.post(`/api/forms/submit/${s.slug}`,M);if(D&&D.error){o(n,D.error,!0);const z=new Error(D.error);throw z.formSubmitFailed=!0,z}if(D?.redirect){window.location.href=D.redirect;return}for(;n.firstChild;)n.removeChild(n.firstChild);o(n,D?.message||e.successMessage||"Thank you for your submission.",!1)}catch(u){throw u.formSubmitFailed||o(n,u.message||"Submission failed. Please try again.",!0),u}},c=Date.now();function h(m){const u=m.querySelector("form");if(!u||u.querySelector(".fb-form-honeypot"))return;const g=document.createElement("div");g.className="fb-form-honeypot",g.setAttribute("aria-hidden","true");const C=document.createElement("input");C.name=u.querySelector('[name="website"]')?"_website":"website",g.dataset.hpName=C.name,C.type="text",C.tabIndex=-1,C.autocomplete="url",C.placeholder="https://",g.appendChild(C);const M=document.createElement("input");M.name="_t",M.type="hidden",M.value=c,g.appendChild(M),u.appendChild(g)}function b(m){if(!window.FormLogicEngine||!t.some(g=>g.logic||Array.isArray(g.triggers)&&g.triggers.length))return null;const u=new window.FormLogicEngine.FormLogicRuntime(s,n,m?{baseValues:m}:void 0);if(u.init(),n._formLogicRuntime=u,n._formLogicObserved)return u;if(n._formLogicObserved=!0,n.parentNode&&typeof MutationObserver<"u"){const g=new MutationObserver(function(C){for(const M of C)for(const D of M.removedNodes)if(D===n||D.nodeType===1&&D.contains&&D.contains(n)){n._formLogicRuntime&&n._formLogicRuntime.destroy(),g.disconnect();return}});g.observe(n.parentNode,{childList:!0,subtree:!1}),n._formLogicObserver=g}return u}let d=!1;if(n.addEventListener("dm-form-end",m=>{const u=m.detail?.validation;if(u&&!u.valid){G(n,u.missing),K(u);return}d||(d=!0,i(m.detail?.values||{}).catch(()=>{}).finally(()=>{d=!1}))}),v&&F.wizard){const m=re(t,s.title),u=m.map(I=>({title:I.title,description:I.description,fields:y(I.fields,a)})),g=m.map(I=>I.fields);let C=0,M=null,D=null;const z=I=>{const r=window.FormLogicEngine;if(!r||!r.validateFields)return;const A=r.validateFields(n,g[C]||[]);A.valid||(I.preventDefault(),I.stopImmediatePropagation(),G(n,A.missing),K(A))},B=()=>{const I=g[C]||t;le(n,e),Q(n,e,I),w(n,I),U(n,I),e.honeypot!==!1&&h(n),M&&M.destroy(),M=b(D?.getStepData?D.getStepData():void 0),n.querySelectorAll(".wizard-next, .wizard-submit").forEach(r=>r.addEventListener("click",z,!0))};D=F.wizard(n,{schema:{steps:u},layout:f,columns:a,finishText:e.submitText||"Finish",onSubmit:i,onStepChange:I=>{C=I,setTimeout(B,0)}}),n.addEventListener("dm-form-trigger",I=>{if(I.detail?.type!=="goto-step"||!D?.goToStep)return;const r=Number(I.detail.action.step);!Number.isFinite(r)||r<0||r>=u.length||r!==C&&D.goToStep(r)}),Promise.resolve(D).then(B)}else if(F.render){const m=F.render(n,y(t,a),L(t),{submitText:e.submitText||"Submit",layout:f,columns:a,onSubmit:i});Promise.resolve(m).then(function(){ce(n,e,f),Q(n,e,t),w(n,t),U(n,t),e.honeypot!==!1&&h(n),b()})}})})();function de(p,l){const x=document.body,y=x.classList,L=new Set((x.dataset.railSite||"").split(" ").filter(Boolean)),w=()=>window.matchMedia("(min-width: 993px)").matches;p.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(a=>{const f=a.textContent.trim();f&&(a.setAttribute("title",f),a.querySelector("svg, [data-icon]")||(a.dataset.initial=f.charAt(0).toUpperCase()))});const o=document.createElement("button");o.type="button",o.className="dm-nav-collapse-toggle";const n=()=>{const a=y.contains("dm-vnav-collapsed");o.setAttribute("aria-pressed",String(a)),o.setAttribute("aria-label",a?"Expand menu":"Collapse menu"),o.title=o.getAttribute("aria-label");const f=l?"chevrons-left":"chevrons-right",v=l?"chevrons-right":"chevrons-left";o.innerHTML=`<span data-icon="${a?f:v}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(o)},s=a=>{y.toggle("dm-vnav-collapsed",a),S.set("nav_collapsed",a),n()};o.addEventListener("click",()=>s(!y.contains("dm-vnav-collapsed"))),n();const t=p.querySelector(".navbar-container")||p;t.insertBefore(o,t.firstChild?.nextSibling||null);const e=a=>{y.toggle("dm-vnav-autohide",a),y.toggle("dm-vnav-collapsible",!a&&L.has("collapsible")),a?y.remove("dm-vnav-collapsed"):L.has("collapsible")&&S.get("nav_collapsed")===!0&&y.add("dm-vnav-collapsed"),p.classList.remove("dm-nav-revealed"),n()};p.addEventListener("pointerdown",a=>{y.contains("dm-vnav-autohide")&&a.pointerType!=="mouse"&&!p.classList.contains("dm-nav-revealed")&&(p.classList.add("dm-nav-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{p.contains(a.target)||p.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(p,{items:()=>{const a=y.contains("dm-vnav-autohide"),f=y.contains("dm-vnav-collapsed"),v=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:f?"Expand menu":"Collapse to icons",icon:f?"maximize":"minimize",disabled:!w()||a||!L.has("collapsible"),action:()=>s(!f)},{label:a?"Keep on screen":"Hide when not in use",icon:a?"eye":"eye-off",disabled:!w(),action:()=>{S.set("nav_autohide",!a),e(!a)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!v,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(L.has("autohide")),y.toggle("dm-vnav-collapsed",!L.has("autohide")&&L.has("start-collapsed")),n()}}]}})}function me(p){p.classList.add("dm-nav-scroll-hide");let l=window.scrollY,x=!1;window.addEventListener("scroll",()=>{x||(x=!0,requestAnimationFrame(()=>{x=!1;const y=window.scrollY,L=p.matches(":focus-within")||p.querySelector(".navbar-dropdown.open, .navbar-collapse.show");y>l&&y>p.offsetHeight&&!L?p.classList.add("dm-nav-scrolled-away"):y<l&&p.classList.remove("dm-nav-scrolled-away"),l=y}))},{passive:!0})}function ue(p){let l={};try{l=JSON.parse(p.dataset.contents||"{}")}catch{}const x=p.closest(".site-main"),y=document.querySelector(".site-content"),L=new Set(String(l.exclude||"").split(",").map(i=>i.trim().toLowerCase()).filter(Boolean)),w=i=>!i.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),o=i=>(y?[...y.querySelectorAll(i)]:[]).filter(w).filter(c=>!L.has(c.textContent.trim().toLowerCase())),n=l.headings==="h2"?o("h2"):l.headings==="h3"?o("h3"):l.headings==="h2h3"?o("h2, h3"):o("h2").length?o("h2"):o("h3");if(!n.length){x?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(i=>i.id)),t=document.createElement("nav");t.className="dm-contents",t.setAttribute("aria-label",l.title||"Page contents"),l.hover&&(t.dataset.menuHover=l.hover),l.selected&&(t.dataset.menuActive=l.selected),l.accent&&t.style.setProperty("--dm-menu-highlight",l.accent.startsWith("#")?l.accent:`var(--dm-${l.accent})`),l.font&&(t.style.fontFamily=l.font),l.fontSize&&(t.style.fontSize=l.fontSize),l.fontWeight&&(t.style.fontWeight=l.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const a=document.createElement("p");a.className="dm-contents-title",a.textContent=l.title||"",e.appendChild(a),t.appendChild(e);const f=document.createElement("ol");f.className="dm-contents-list"+(l.numbered?" is-numbered":"");const v=[];for(const i of n){if(!i.id){const b=(i.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let d=b;for(let m=2;s.has(d);m++)d=`${b}-${m}`;s.add(d),i.id=d}const c=document.createElement("li");c.className=`dm-contents-item dm-contents-${i.tagName.toLowerCase()}`;const h=document.createElement("a");h.href=`#${i.id}`,h.textContent=i.textContent.trim(),c.appendChild(h),f.appendChild(c),v.push([i,h])}if(t.appendChild(f),p.replaceChildren(t),l.scrollSpy!==!1&&"IntersectionObserver"in window){const i=new Map,c=()=>{const b=v.find(([d])=>i.get(d))||null;v.forEach(([,d])=>{const m=b&&d===b[1];d.classList.toggle("is-active",!!m),m?d.setAttribute("aria-current","location"):d.removeAttribute("aria-current")})},h=new IntersectionObserver(b=>{b.forEach(d=>i.set(d.target,d.isIntersecting)),c()},{rootMargin:"-80px 0px -60% 0px"});v.forEach(([b])=>h.observe(b))}if(x?.classList.contains("contents-collapsible")){const i=document.createElement("button");i.type="button",i.className="dm-contents-toggle";const c=()=>{const h=x.classList.contains("contents-collapsed");i.setAttribute("aria-expanded",String(!h)),i.setAttribute("aria-label",h?"Show the contents":"Hide the contents"),i.title=i.getAttribute("aria-label"),i.innerHTML=`<span data-icon="${h?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(i)};i.addEventListener("click",()=>{S.set("contents_collapsed",x.classList.toggle("contents-collapsed")),c()}),c(),e.insertBefore(i,e.firstChild)}if(x?.classList.contains("contents-floating")&&l.movable){t.classList.add("is-movable");const i=S.get("contents_position");i&&Number.isFinite(i.x)&&Object.assign(p.style,{left:`${i.x}px`,top:`${i.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",c=>{if(c.target.closest("button"))return;const h=p.getBoundingClientRect(),b=c.clientX-h.left,d=c.clientY-h.top,m=g=>{const C=Math.max(0,Math.min(window.innerWidth-h.width,g.clientX-b)),M=Math.max(0,Math.min(window.innerHeight-40,g.clientY-d));Object.assign(p.style,{left:`${C}px`,top:`${M}px`,right:"auto",bottom:"auto"})},u=()=>{document.removeEventListener("pointermove",m),S.set("contents_position",{x:parseInt(p.style.left,10),y:parseInt(p.style.top,10)})};document.addEventListener("pointermove",m),document.addEventListener("pointerup",u,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(p,{items:()=>{const i=x?.classList.contains("contents-collapsible"),c=x?.classList.contains("contents-collapsed"),h=v.find(([,b])=>b.classList.contains("is-active"));return[{header:"Contents"},...i?[{label:c?"Show the contents":"Collapse to a button",icon:c?"list":"minimize",action:()=>p.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!h,action:async()=>{const b=`${location.origin}${location.pathname}#${h[0].id}`;try{await _.copyToClipboard(b),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),x?.classList.contains("contents-autohide")&&(p.addEventListener("pointerdown",i=>{i.pointerType!=="mouse"&&!p.classList.contains("is-revealed")&&(p.classList.add("is-revealed"),i.preventDefault())}),document.addEventListener("pointerdown",i=>{p.contains(i.target)||p.classList.remove("is-revealed")}))}
1
+ import{decodeBase64Json as P}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as Y,applyColour as J,applyPill as te,applyFloat as oe,applyHighlight as R,applyItemHighlight as ne,itemHighlightAttrs as ae,isCurrentItem as X,menuAlign as se,applyClasses as O,cssClasses as W}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{initFloatingMenus as ie}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as U,markInvalidFields as G,showValidationPopup as K}from"/public/js/forms.js?v=20261009-form-classes";import{buildWizardSteps as re}from"/public/js/form-steps.mjs";import{applyFormClasses as Q,applySubmitButtons as ce,applyWizardButtons as le}from"/public/js/form-submit.mjs";document.addEventListener("click",f=>{const l=f.target?.closest?.(".dm-banner__dismiss");l&&(f.preventDefault(),l.closest(".dm-banner")?.remove())}),$(()=>{const f=window.__CMS_NAV__||{},l=window.__CMS_SITE__||{};if(l.autoTheme?.enabled){let o=function(e){const a=(e||"07:00").split(":");return+a[0]*60+(+a[1]||0)},s=function(){const e=new Date,a=e.getHours()*60+e.getMinutes();return a>=o(n.dayStart)&&a<o(n.nightStart)?n.dayTheme:n.nightTheme},t=function(e){const a=n.apply&&n.apply[e]||{theme:e,custom:""};Domma.theme.set(a.theme);for(const p of[...document.body.classList])p.startsWith("dm-custom-")&&document.body.classList.remove(p);a.custom&&document.body.classList.add(a.custom)};const n=l.autoTheme;t(s()),setInterval(()=>t(s()),6e4)}const x=$("#site-navbar");if(x.length&&f.brand){let s=function(d,m=0){return Array.isArray(d)?d.filter(u=>!(u&&(u.type==="spacer"||u.type==="theme-switcher"))).filter(u=>!(u&&u.type==="separator"&&m===0)).map(u=>u&&u.type==="separator"?{divider:!0}:Array.isArray(u?.items)?{...u,items:s(u.items,m+1)}:u):d},t=function(d){return Array.isArray(d)?d.filter(m=>!(m&&(m.type==="separator"||m.type==="spacer"||m.type==="theme-switcher"))):[]};const n={...f.brand},o=n.size&&n.size!=="md"?` navbar-brand-${n.size}`:"";if(n.logo||n.icon||n.tagline){let d="";n.logo?d+=`<img src="${n.logo}" class="navbar-brand-logo" alt="${n.text||""}">`:n.icon&&(d+=`<span data-icon="${n.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),n.text&&(d+=`<span class="navbar-brand-text${o}">${n.text}</span>`),n.tagline&&(d+=`<small class="navbar-brand-tagline">${n.tagline}</small>`),n.html=d}else o&&n.text&&(n.html=`<span class="navbar-brand-text${o}">${n.text}</span>`);const e=f.position==="floating",a=f.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:n,items:s(f.items||[]),variant:f.variant||"dark",position:e?"static":f.position||"sticky",collapsible:!0,appearOnHover:!!f.appearOnHover}),Domma.icons.scan("#site-navbar");const p=x.get(0);p.classList.add(`dm-nav-align-${se(f)||"centre"}`),a&&x.get(0).classList.add("dm-nav-vertical",f.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const g=f.dock||{};a&&!e&&de(p,f.side==="right"),!a&&g.hideOnScroll&&me(p),R(p,f),O(p,f.class),e&&(oe(x.get(0),f),ie(x.get(0).parentNode||document));const i=p.querySelector(".navbar-toggle"),c=p.querySelector(".navbar-collapse");if(i&&c){let d=null;const m=()=>{c.classList.contains("show")&&i.click()},u=()=>{d&&(d.remove(),d=null),document.body.classList.remove("dm-drawer-open")},y=()=>{d||(d=document.createElement("div"),d.className="dm-navbar-backdrop",d.addEventListener("click",m),document.body.appendChild(d),document.body.classList.add("dm-drawer-open"))};i.addEventListener("click",()=>{requestAnimationFrame(()=>{c.classList.contains("show")?y():u()})}),c.addEventListener("click",C=>{C.target.closest("a")&&requestAnimationFrame(u)}),document.addEventListener("keydown",C=>{C.key==="Escape"&&c.classList.contains("show")&&m()})}const h=p.querySelector(".navbar-nav");if(h){const d=Array.from(h.children);let m=0;for(const u of f.items||[])if(!(u&&u.type==="separator")){if(u&&u.type==="spacer"){const y=document.createElement("li"),C=["xs","sm","md","lg","xl","flex"].includes(u.size)?u.size:"md";y.className=`dm-menu-spacer dm-menu-spacer--${C}`,y.setAttribute("aria-hidden","true"),h.insertBefore(y,d[m]||null);continue}if(u&&u.type==="theme-switcher"){const y=document.createElement("li");y.className="dm-menu-theme-switcher";const C=document.createElement("span");C.dataset.themeSwitcher="menu",C.dataset.position="inline",y.appendChild(C),h.insertBefore(y,d[m]||null);continue}m++}window.CMSThemeSwitcher?.scan(h)}const b=t(f.items||[]);p.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(d=>{const m=b[parseInt(d.dataset.index,10)];if(!m)return;const u=d.dataset.subindex!=null,y=u&&Array.isArray(m.items)?m.items[parseInt(d.dataset.subindex,10)]:m;if(!y)return;y.colour&&J(d,y.colour),y.pill&&!u&&te(d,y.pill),ne(d,y),O(d.closest("li"),y.class);const C=Y(y.badge);C&&d.appendChild(C),X(y.url,location.pathname)&&(d.classList.add("active"),d.setAttribute("aria-current","page"))})}const v=$("#site-footer");if(v.length){let e=function(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},a=function(r){return e(r).replace(/'/g,"&#39;")},p=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 A=a(r.url||"#"),L=e(r.text||""),q=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(p).join("")}</ul>`:"",N=ae(r),Z=[N.hover&&` data-menu-item-hover="${a(N.hover)}"`,N.active&&` data-menu-item-active="${a(N.active)}"`].filter(Boolean).join(""),ee=N.accent?` style="--dm-menu-highlight:${a(N.accent)}"`:"",V=W(r.class).join(" ");return`<li${V?` class="${a(V)}"`:""}><a href="${A}"${Z}${ee}>${L}</a>${q}</li>`},g=function(){if(!l.footer)return"";const r=Object.keys(o).filter(L=>n[L]);if(!r.length)return"";const A=W(l.footer.socialClass).join(" ");return`<div class="footer-social">${r.map(L=>{const{label:q,svg:N}=o[L];return`<a href="${n[L]}" target="_blank" rel="noopener noreferrer" aria-label="${q}" class="footer-social-link${A?` ${a(A)}`:""}">${N}</a>`}).join("")}</div>`},b=function(r){if(!r||r.type==="separator"||r.type==="spacer")return"";if(r.type==="theme-switcher")return`<li class="cms-footer-col">${p(r).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const A=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(p).join("")}</ul>`:"",L=h(r)?`<span class="cms-footer-col-title">${e(r.text||"")}</span>`:`<a class="cms-footer-col-title" href="${a(r.url||"#")}">${e(r.text||"")}</a>`,q=["cms-footer-col",...W(r.class)].join(" ");return`<li class="${a(q)}">${L}${A}</li>`},m=function(r,A,L=!1){return(r||[]).forEach(q=>{q&&(q.type==="separator"||q.type==="spacer"||q.type==="theme-switcher")||(L&&h(q)||A.push(q),Array.isArray(q.items)&&q.items.length&&m(q.items,A))}),A};const n=l.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let s='<div class="footer-inner container">';const t=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},i=["columns","row"].includes(l.footer?.layout)?l.footer.layout:"auto",c=i==="columns"||i==="auto"&&t.primary.some(r=>Array.isArray(r?.items)&&r.items.length),h=r=>c&&!r.url&&Array.isArray(r.items)&&r.items.length>0;if(c){const r=f.brand||{},A=r.text||l.title||"",L=r.logo?`<img src="${a(r.logo)}" class="cms-footer-brand-logo" alt="">`:r.icon?'<span class="cms-footer-brand-icon"></span>':"",q=String(l.footer?.blurb||"").trim();s='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',s+=`<div class="cms-footer-brand">${A||L?`<a class="cms-footer-brand-name" href="${a(r.url||"/")}">${L}<span>${e(A)}</span></a>`:""}${q?`<p class="cms-footer-blurb">${e(q)}</p>`:""}${g()}</div>`,t.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${t.primary.map(b).join("")}</ul></nav>`),s+='</div><div class="cms-footer-bottom">',t.copyright&&(s+=`<p class="cms-footer-copyright">${e(t.copyright)}</p>`),t.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${t.legal.map(p).join("")}</ul></nav>`),s+="</div>"}else t.copyright&&(s+=`<p>${e(t.copyright)}</p>`),t.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary"><ul>${t.primary.map(p).join("")}</ul></nav>`),t.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${t.legal.map(p).join("")}</ul></nav>`),s+=g();s+="</div>",v.html(s),window.CMSThemeSwitcher?.scan(v.get(0));const d=v.get(0).querySelector(".cms-footer-brand-icon");d&&f.brand?.icon&&d.setAttribute("data-icon",f.brand.icon),O(v.get(0),l.footer?.class);const u=[...m(t.primary,[],!0),...m(t.legal,[])];v.get(0).querySelectorAll(".footer-nav a").forEach((r,A)=>{const L=u[A];if(!L)return;L.colour&&J(r,L.colour);const q=Y(L.badge);q&&r.appendChild(q),X(L.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),R(v.get(0).querySelector(".footer-nav--primary"),{highlight:t.primaryHighlight}),R(v.get(0).querySelector(".footer-nav--legal"),{highlight:t.legalHighlight}),O(v.get(0).querySelector(".footer-nav--primary"),t.primaryClass),O(v.get(0).querySelector(".footer-nav--legal"),t.legalClass);const C=l.footer?.showMotionToggle!==!1,M=S.get("reduced_motion"),D=l.footer?.motionDefault,z=M!=null?!!M:D==="on"?!0:D==="off"?!1:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),B=l.footer?.motionControl==="checkbox",I=v.get(0).querySelector(".cms-footer-bottom")||v.get(0).querySelector(".footer-inner");if(C&&I){const r=document.createElement("input");r.type="checkbox",r.className=B?"footer-motion-checkbox":"form-switch-input",r.id="dm-motion-switch",r.checked=z,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const A=document.createElement("span");A.className=B?"footer-motion-label":"form-switch-label",A.textContent="Reduce motion";const L=document.createElement("label");L.className=B?"footer-motion-check":"form-switch footer-motion-switch",L.title="Reduce motion",L.appendChild(r),L.appendChild(A),I.appendChild(L)}}const k=document.getElementById("site-sidebar");k&&ue(k),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const w=$(".page-body");if(w.length){w.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),w.find(".tabs").each(function(){Domma.elements.tabs(this)}),w.find(".carousel").each(function(){const n={primary:"--dm-primary",secondary:"--dm-secondary",accent:"--dm-primary",text:"--dm-text",surface:"--dm-surface",background:"--dm-background",success:"--dm-success",warning:"--dm-warning",danger:"--dm-danger",info:"--dm-info"},o=g=>{const i=String(g||"").trim();return i?n[i.toLowerCase()]?`var(${n[i.toLowerCase()]})`:CSS.supports("color",i)?i:"":""},s=o(this.dataset.background),t=o(this.dataset.arrowBackground),e=o(this.dataset.arrowColor);s&&(this.style.setProperty("--dm-carousel-background",s),this.classList.add("dm-carousel-custom-background")),t&&this.style.setProperty("--dm-carousel-arrow-background",t),e&&this.style.setProperty("--dm-carousel-arrow-color",e);const a=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${a}`);const p={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const g=parseInt(this.dataset.animationDuration,10);Number.isFinite(g)&&(p.animationDuration=g)}this.dataset.animationEasing&&(p.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(g=>g.cloneNode(!0)),this._dmCarouselOpts=p),this._dmCarousel=Domma.elements.carousel(this,p)}),w.find(".dm-countdown").each(function(){const n={autoStart:!0},o=String(this.dataset.format||"").trim(),s=String(this.dataset.to||"").trim(),t=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s),e=t?new Date(+t[1],+t[2]-1,+t[3]).getTime():s?new Date(s).getTime():NaN;Number.isFinite(e)?n.duration=Math.max(0,e-Date.now()):this.dataset.duration&&(n.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const a=/^dd:/i.test(o);n.format=a?"hh:mm:ss":o||(n.duration>=36e5?"hh:mm:ss":"mm:ss");const p=this,g=c=>String(c).padStart(2,"0"),i=c=>{const h=Math.max(0,Math.floor(c/1e3)),b=p.querySelector(".dm-timer-time");b&&(b.textContent=`${g(Math.floor(h/86400))}:${g(Math.floor(h%86400/3600))}:${g(Math.floor(h%3600/60))}:${g(h%60)}`)};a&&(n.onTick=i),Domma.elements.timer(this,n),a&&i(n.duration||0)}),w.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),w.find(".dm-progression").each(function(){const n=Array.from(this.querySelectorAll(".dm-progression-item")).map(t=>{const e={title:t.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:t.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return t.dataset.date&&(e.date=t.dataset.date),t.dataset.status&&(e.status=t.dataset.status),t.dataset.eventIcon&&(e.icon=t.dataset.eventIcon),t.dataset.entryId&&(e._entryId=t.dataset.entryId),e}),o=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",s=Domma.elements.progression(this,{layout:o,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...n.length?{items:n}:{}});if((this.dataset.mode||"timeline")==="timeline"){const t={completed:{label:"Completed",icon:"check-circle",tone:"success"},planned:{label:"Planned",icon:"circle",tone:"secondary"},"in-progress":{label:"In progress",icon:"clock",tone:"primary"},blocked:{label:"Blocked",icon:"alert-circle",tone:"danger"}};this.querySelectorAll(".dm-progression-item").forEach((e,a)=>{const p=n[a];if(!p)return;const g=e.querySelector(".dm-progression-title");if(p.icon&&g){const c=document.createElement("span");c.dataset.icon=p.icon,c.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",c.setAttribute("aria-hidden","true"),g.prepend(c)}const i=t[p.status];if(i&&g){const c=document.createElement("span");c.className=`badge badge-${i.tone} cms-timeline-status`,c.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const h=document.createElement("span");h.dataset.icon=i.icon,h.dataset.iconSize="14",h.setAttribute("aria-hidden","true"),c.append(h,document.createTextNode(i.label)),g.after(c)}})}Domma.icons.scan(this),n.length&&(this._dmCtx={instance:s,items:n})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(n=>{const o=n.dataset.revealMode||"stagger",s=n.dataset.revealAnimation||"slide-up",t=parseInt(n.dataset.revealDuration,10)||400,e=parseInt(n.dataset.revealStagger,10)||60,a=parseInt(n.dataset.revealDelay,10)||0,p=n.dataset.revealDirection==="rtl",g=Array.from(n.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};g.forEach((c,h)=>{c.style.opacity="0",c.style.transform=i[s]||"",c.style.transition=`opacity ${t}ms ease, transform ${t}ms ease`;const b=p?g.length-1-h:h;c.style.transitionDelay=o==="stagger"?`${a+b*e}ms`:`${a}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const c=new IntersectionObserver(h=>{h.forEach(b=>{b.isIntersecting&&(b.target.offsetWidth,b.target.style.opacity="1",b.target.style.transform="none",c.unobserve(b.target))})},{threshold:.1});g.forEach(h=>c.observe(h))}))}),w.find(".card[data-collapsible]").each(function(){const n=this.querySelector(".card-header");n&&n.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),w.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const n=this.dataset.soTarget,o=document.getElementById(n);if(!o)return;const s=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",s.setContent(o),s.open()})})}if(typeof $.setup=="function"){const n=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const s=P(o.dataset.config);Object.assign(n,s)}catch{}}),Object.keys(n).length>0){const o={};for(const[s,t]of Object.entries(n)){const e=t?.events?.click,{confirm:a,toast:p,alert:g,prompt:i,...c}=e||{};a||p||g||i?($(s).on("click",async function(b){if(b.preventDefault(),a&&!await E.confirm(a))return;let d=null;if(!(i&&(d=await E.prompt(i,{inputPlaceholder:c.promptPlaceholder||"",inputValue:c.promptDefault||""}),d===null))){if(c.target){const m=$(c.target);c.toggleClass&&m.toggleClass(c.toggleClass),c.addClass&&m.addClass(c.addClass),c.removeClass&&m.removeClass(c.removeClass),d!==null&&(c.setText&&m.text(d),c.setVal&&m.val(d),c.setAttr&&m.attr(c.setAttr,d))}c.href&&(window.location.href=c.href),p&&E.toast(p,{type:c.toastType||"success"}),g&&E.alert(g)}}),Object.keys(c).length&&(o[s]={...t,events:{...t.events,click:c}})):o[s]=t}$.setup(o)}}w.length&&j(w.get(0))}),window.wireCTAButtons=j;function j(f){f.querySelectorAll(".dm-cta-trigger").forEach(l=>{l.__ctaWired||(l.__ctaWired=!0,l.addEventListener("click",async()=>{const x=l.dataset.action,v=l.dataset.entry,k=l.dataset.confirm;let w=S.get("auth_token");if(!w){E.toast("Please log in to perform this action.",{type:"warning"});return}if(k&&!await E.confirm(k))return;const n=Array.from(l.childNodes).map(s=>s.cloneNode(!0));l.disabled=!0,l.textContent="Running\u2026";const o=s=>fetch(`/api/actions/${x}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(l.dataset.scope?{entryId:v,scope:l.dataset.scope}:{entryId:v})});try{let s=await o(w);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const a=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(a.ok){const{token:p}=await a.json();S.set("auth_token",p),w=p,s=await o(w)}}}const t=await s.json().catch(()=>({}));if(!s.ok||t.success===!1)throw new Error(t.error||t.message||`Error ${s.status}`);E.toast(t.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{l.disabled=!1,l.textContent="",n.forEach(s=>l.appendChild(s)),Domma.icons.scan(l)}}))})}(function(){const l=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!l.length)return;const x=S.get("auth_token");function v(w,n){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(n){s.appendChild(document.createTextNode("Please "));const t=document.createElement("a");t.href="/login",t.textContent="sign in",s.appendChild(t),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=w;return o.appendChild(s),o}function k(w,n){const o=document.createElement("template");o.innerHTML=n||"",w.replaceWith(o.content)}l.forEach(async w=>{const n=w.dataset.collectionAttrs;if(!n)return;if(!x){w.replaceChildren(v("",!0));return}const o=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:n})});try{let s=x,t=await o(s);if(t.status===401){const a=S.get("auth_refresh_token");if(a){const p=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:a})});if(p.ok){const{token:g}=await p.json();S.set("auth_token",g),s=g,t=await o(s)}}}if(!t.ok){const{error:a}=await t.json().catch(()=>({}));w.replaceChildren(v(a||"Unable to load entries.",!1));return}const{html:e}=await t.json();k(w,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{w.replaceChildren(v("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),j(document.body)},100)})(),(function(){const l=document.querySelectorAll("[data-collection-table]");!l.length||typeof T>"u"||!T.create||l.forEach(x=>{let v;try{v=P(x.dataset.payload)}catch{return}const{columns:k,rows:w,search:n,sortable:o,exportable:s,pageSize:t,empty:e,ctaConfig:a,slug:p,exportCfg:g}=v;if(!k?.length)return;if(a){const h=k.findIndex(b=>b.key==="_cta");h!==-1&&(k[h]={key:"_cta",title:"",render:(b,d)=>{const m=document.createElement("button");if(m.className=`btn btn-${a.style||"primary"} dm-cta-trigger`,m.dataset.action=a.action||"",a.scope&&(m.dataset.scope=a.scope),m.dataset.entry=d._entryId||"",a.confirm&&(m.dataset.confirm=a.confirm),a.icon){const u=document.createElement("span");u.dataset.icon=a.icon,m.appendChild(u),m.appendChild(document.createTextNode(" "))}return m.appendChild(document.createTextNode(a.label||"Run")),m}})}const i="col-table-"+Math.random().toString(36).slice(2,7),c=document.createElement("div");c.id=i,x.replaceChildren(c),T.create("#"+i,{data:w,columns:k,search:n,sortable:o,exportable:s,pageSize:t,emptyMessage:e}),a&&j(c),x._dmTable={columns:k,rows:w,slug:p,exportCfg:g,render(h){T.create("#"+i,{data:h,columns:k,search:n,sortable:o,exportable:s,pageSize:t,emptyMessage:e}),a&&j(c)}}})})(),(function(){const l=document.querySelectorAll("[data-form-inline]");if(!l.length||typeof F>"u")return;function x(o){const s=o&&o.logic;return!!(s&&(s.visibility||s.requirement))}function v(o,s){const t={};return(o||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const a=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),p=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":a?"chooser":e.type,g={...e.formConfig||{}};g.span==="full"&&s&&(g.span=s);const i={type:p,required:x(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...g}};e.minLength!==void 0&&(i.minLength=e.minLength),e.maxLength!==void 0&&(i.maxLength=e.maxLength),e.min!==void 0&&(i.min=e.min),e.max!==void 0&&(i.max=e.max),a&&(e.variant&&(i.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(i.multiple=!0),e.density&&(i.density=e.density),e.columns&&(i.columns=e.columns),e.accent&&(i.accent=e.accent),e.accentStyle&&(i.accentStyle=e.accentStyle),e.glow&&(i.glow=!0),e.glowColour&&(i.glowColour=e.glowColour),e.shadow&&(i.shadow=e.shadow),e.shadowColour&&(i.shadowColour=e.shadowColour)),t[e.name]=i}),t}function k(o){const s={};return(o||[]).forEach(t=>{if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(s[t.name]=typeof e=="object"?e.value:e)}}),s}function w(o,s){(s||[]).forEach(t=>{if(t.type!=="date"||!t.name)return;const e=o.querySelector(`[name="${t.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function n(o,s,t){let e=o.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",o.appendChild(e)),e.textContent=s,e.style.cssText=t?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}l.forEach(o=>{let s;try{s=P(o.dataset.formInline)}catch{return}const t=s.fields||[],e=s.settings||{},a=Number(e.columns)===3?3:2,p=e.layout||"grid",g=t.some(m=>m.type==="page-break"),i=async m=>{if(o.dataset.triggerBlocked==="true"){const u=o._formLogicRuntime,y=u&&u.submitBlock?u.submitBlock().reasons:[];n(o,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 u=o.querySelector(".fb-form-honeypot"),y=u?.querySelector('input[type="text"]')||null,C=u?.querySelector('[name="_t"]')||null,M=Object.assign({},m);delete M._t,u?.dataset.hpName&&delete M[u.dataset.hpName],y!==null&&(M._hp=y.value),C!==null&&(M._t=C.value);const D=await H.post(`/api/forms/submit/${s.slug}`,M);if(D&&D.error){n(o,D.error,!0);const z=new Error(D.error);throw z.formSubmitFailed=!0,z}if(D?.redirect){window.location.href=D.redirect;return}for(;o.firstChild;)o.removeChild(o.firstChild);n(o,D?.message||e.successMessage||"Thank you for your submission.",!1)}catch(u){throw u.formSubmitFailed||n(o,u.message||"Submission failed. Please try again.",!0),u}},c=Date.now();function h(m){const u=m.querySelector("form");if(!u||u.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=u.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 M=document.createElement("input");M.name="_t",M.type="hidden",M.value=c,y.appendChild(M),u.appendChild(y)}function b(m){if(!window.FormLogicEngine||!t.some(y=>y.logic||Array.isArray(y.triggers)&&y.triggers.length))return null;const u=new window.FormLogicEngine.FormLogicRuntime(s,o,m?{baseValues:m}:void 0);if(u.init(),o._formLogicRuntime=u,o._formLogicObserved)return u;if(o._formLogicObserved=!0,o.parentNode&&typeof MutationObserver<"u"){const y=new MutationObserver(function(C){for(const M of C)for(const D of M.removedNodes)if(D===o||D.nodeType===1&&D.contains&&D.contains(o)){o._formLogicRuntime&&o._formLogicRuntime.destroy(),y.disconnect();return}});y.observe(o.parentNode,{childList:!0,subtree:!1}),o._formLogicObserver=y}return u}let d=!1;if(o.addEventListener("dm-form-end",m=>{const u=m.detail?.validation;if(u&&!u.valid){G(o,u.missing),K(u);return}d||(d=!0,i(m.detail?.values||{}).catch(()=>{}).finally(()=>{d=!1}))}),g&&F.wizard){const m=re(t,s.title),u=m.map(I=>({title:I.title,description:I.description,fields:v(I.fields,a)})),y=m.map(I=>I.fields);let C=0,M=null,D=null;const z=I=>{const r=window.FormLogicEngine;if(!r||!r.validateFields)return;const A=r.validateFields(o,y[C]||[]);A.valid||(I.preventDefault(),I.stopImmediatePropagation(),G(o,A.missing),K(A))},B=()=>{const I=y[C]||t;le(o,e),Q(o,e,I),w(o,I),U(o,I),e.honeypot!==!1&&h(o),M&&M.destroy(),M=b(D?.getStepData?D.getStepData():void 0),o.querySelectorAll(".wizard-next, .wizard-submit").forEach(r=>r.addEventListener("click",z,!0))};D=F.wizard(o,{schema:{steps:u},layout:p,columns:a,finishText:e.submitText||"Finish",onSubmit:i,onStepChange:I=>{C=I,setTimeout(B,0)}}),o.addEventListener("dm-form-trigger",I=>{if(I.detail?.type!=="goto-step"||!D?.goToStep)return;const r=Number(I.detail.action.step);!Number.isFinite(r)||r<0||r>=u.length||r!==C&&D.goToStep(r)}),Promise.resolve(D).then(B)}else if(F.render){const m=F.render(o,v(t,a),k(t),{submitText:e.submitText||"Submit",layout:p,columns:a,onSubmit:i});Promise.resolve(m).then(function(){ce(o,e,p),Q(o,e,t),w(o,t),U(o,t),e.honeypot!==!1&&h(o),b()})}})})();function de(f,l){const x=document.body,v=x.classList,k=new Set((x.dataset.railSite||"").split(" ").filter(Boolean)),w=()=>window.matchMedia("(min-width: 993px)").matches;f.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(a=>{const p=a.textContent.trim();p&&(a.setAttribute("title",p),a.querySelector("svg, [data-icon]")||(a.dataset.initial=p.charAt(0).toUpperCase()))});const n=document.createElement("button");n.type="button",n.className="dm-nav-collapse-toggle";const o=()=>{const a=v.contains("dm-vnav-collapsed");n.setAttribute("aria-pressed",String(a)),n.setAttribute("aria-label",a?"Expand menu":"Collapse menu"),n.title=n.getAttribute("aria-label");const p=l?"chevrons-left":"chevrons-right",g=l?"chevrons-right":"chevrons-left";n.innerHTML=`<span data-icon="${a?p:g}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(n)},s=a=>{v.toggle("dm-vnav-collapsed",a),S.set("nav_collapsed",a),o()};n.addEventListener("click",()=>s(!v.contains("dm-vnav-collapsed"))),o();const t=f.querySelector(".navbar-container")||f;t.insertBefore(n,t.firstChild?.nextSibling||null);const e=a=>{v.toggle("dm-vnav-autohide",a),v.toggle("dm-vnav-collapsible",!a&&k.has("collapsible")),a?v.remove("dm-vnav-collapsed"):k.has("collapsible")&&S.get("nav_collapsed")===!0&&v.add("dm-vnav-collapsed"),f.classList.remove("dm-nav-revealed"),o()};f.addEventListener("pointerdown",a=>{v.contains("dm-vnav-autohide")&&a.pointerType!=="mouse"&&!f.classList.contains("dm-nav-revealed")&&(f.classList.add("dm-nav-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{f.contains(a.target)||f.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(f,{items:()=>{const a=v.contains("dm-vnav-autohide"),p=v.contains("dm-vnav-collapsed"),g=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:p?"Expand menu":"Collapse to icons",icon:p?"maximize":"minimize",disabled:!w()||a||!k.has("collapsible"),action:()=>s(!p)},{label:a?"Keep on screen":"Hide when not in use",icon:a?"eye":"eye-off",disabled:!w(),action:()=>{S.set("nav_autohide",!a),e(!a)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!g,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(k.has("autohide")),v.toggle("dm-vnav-collapsed",!k.has("autohide")&&k.has("start-collapsed")),o()}}]}})}function me(f){f.classList.add("dm-nav-scroll-hide");let l=window.scrollY,x=!1;window.addEventListener("scroll",()=>{x||(x=!0,requestAnimationFrame(()=>{x=!1;const v=window.scrollY,k=f.matches(":focus-within")||f.querySelector(".navbar-dropdown.open, .navbar-collapse.show");v>l&&v>f.offsetHeight&&!k?f.classList.add("dm-nav-scrolled-away"):v<l&&f.classList.remove("dm-nav-scrolled-away"),l=v}))},{passive:!0})}function ue(f){let l={};try{l=JSON.parse(f.dataset.contents||"{}")}catch{}const x=f.closest(".site-main"),v=document.querySelector(".site-content"),k=new Set(String(l.exclude||"").split(",").map(i=>i.trim().toLowerCase()).filter(Boolean)),w=i=>!i.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),n=i=>(v?[...v.querySelectorAll(i)]:[]).filter(w).filter(c=>!k.has(c.textContent.trim().toLowerCase())),o=l.headings==="h2"?n("h2"):l.headings==="h3"?n("h3"):l.headings==="h2h3"?n("h2, h3"):n("h2").length?n("h2"):n("h3");if(!o.length){x?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(i=>i.id)),t=document.createElement("nav");t.className="dm-contents",t.setAttribute("aria-label",l.title||"Page contents"),l.hover&&(t.dataset.menuHover=l.hover),l.selected&&(t.dataset.menuActive=l.selected),l.accent&&t.style.setProperty("--dm-menu-highlight",l.accent.startsWith("#")?l.accent:`var(--dm-${l.accent})`),l.font&&(t.style.fontFamily=l.font),l.fontSize&&(t.style.fontSize=l.fontSize),l.fontWeight&&(t.style.fontWeight=l.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const a=document.createElement("p");a.className="dm-contents-title",a.textContent=l.title||"",e.appendChild(a),t.appendChild(e);const p=document.createElement("ol");p.className="dm-contents-list"+(l.numbered?" is-numbered":"");const g=[];for(const i of o){if(!i.id){const b=(i.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let d=b;for(let m=2;s.has(d);m++)d=`${b}-${m}`;s.add(d),i.id=d}const c=document.createElement("li");c.className=`dm-contents-item dm-contents-${i.tagName.toLowerCase()}`;const h=document.createElement("a");h.href=`#${i.id}`,h.textContent=i.textContent.trim(),c.appendChild(h),p.appendChild(c),g.push([i,h])}if(t.appendChild(p),f.replaceChildren(t),l.scrollSpy!==!1&&"IntersectionObserver"in window){const i=new Map,c=()=>{const b=g.find(([d])=>i.get(d))||null;g.forEach(([,d])=>{const m=b&&d===b[1];d.classList.toggle("is-active",!!m),m?d.setAttribute("aria-current","location"):d.removeAttribute("aria-current")})},h=new IntersectionObserver(b=>{b.forEach(d=>i.set(d.target,d.isIntersecting)),c()},{rootMargin:"-80px 0px -60% 0px"});g.forEach(([b])=>h.observe(b))}if(x?.classList.contains("contents-collapsible")){const i=document.createElement("button");i.type="button",i.className="dm-contents-toggle";const c=()=>{const h=x.classList.contains("contents-collapsed");i.setAttribute("aria-expanded",String(!h)),i.setAttribute("aria-label",h?"Show the contents":"Hide the contents"),i.title=i.getAttribute("aria-label"),i.innerHTML=`<span data-icon="${h?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(i)};i.addEventListener("click",()=>{S.set("contents_collapsed",x.classList.toggle("contents-collapsed")),c()}),c(),e.insertBefore(i,e.firstChild)}if(x?.classList.contains("contents-floating")&&l.movable){t.classList.add("is-movable");const i=S.get("contents_position");i&&Number.isFinite(i.x)&&Object.assign(f.style,{left:`${i.x}px`,top:`${i.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",c=>{if(c.target.closest("button"))return;const h=f.getBoundingClientRect(),b=c.clientX-h.left,d=c.clientY-h.top,m=y=>{const C=Math.max(0,Math.min(window.innerWidth-h.width,y.clientX-b)),M=Math.max(0,Math.min(window.innerHeight-40,y.clientY-d));Object.assign(f.style,{left:`${C}px`,top:`${M}px`,right:"auto",bottom:"auto"})},u=()=>{document.removeEventListener("pointermove",m),S.set("contents_position",{x:parseInt(f.style.left,10),y:parseInt(f.style.top,10)})};document.addEventListener("pointermove",m),document.addEventListener("pointerup",u,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(f,{items:()=>{const i=x?.classList.contains("contents-collapsible"),c=x?.classList.contains("contents-collapsed"),h=g.find(([,b])=>b.classList.contains("is-active"));return[{header:"Contents"},...i?[{label:c?"Show the contents":"Collapse to a button",icon:c?"list":"minimize",action:()=>f.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!h,action:async()=>{const b=`${location.origin}${location.pathname}#${h[0].id}`;try{await _.copyToClipboard(b),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),x?.classList.contains("contents-autohide")&&(f.addEventListener("pointerdown",i=>{i.pointerType!=="mouse"&&!f.classList.contains("is-revealed")&&(f.classList.add("is-revealed"),i.preventDefault())}),document.addEventListener("pointerdown",i=>{f.contains(i.target)||f.classList.remove("is-revealed")}))}
@@ -1 +1 @@
1
- export const SIZE_MAP={xs:".75rem",sm:".875rem",base:"1rem",lg:"1.125rem",xl:"1.25rem","2xl":"1.5rem","3xl":"1.875rem","4xl":"2.25rem"},WEIGHT_MAP={thin:"100",light:"300",normal:"400",medium:"500",semibold:"600",bold:"700",extrabold:"800",black:"900"},SPACING_MAP={tight:"-0.05em",normal:"0em",wide:"0.05em",wider:"0.1em"},COLOUR_TOKENS={primary:"var(--dm-color-primary)",secondary:"var(--dm-color-secondary)",muted:"var(--dm-text-muted)",danger:"var(--dm-color-danger)",success:"var(--dm-color-success)",warning:"var(--dm-color-warning)",info:"var(--dm-color-info)"},FONT_MAP={Georgia:"Georgia,serif",Arial:"Arial,sans-serif",Verdana:"Verdana,sans-serif","Courier New":"'Courier New',monospace","Times New Roman":"'Times New Roman',serif","Trebuchet MS":"'Trebuchet MS',sans-serif"},TRANSFORM_MAP={upper:"uppercase",lower:"lowercase",capitalize:"capitalize",none:"none"},DECORATION_MAP={underline:"underline","line-through":"line-through",none:"none"};function l(e){return e!=null&&e!==!1&&e!=="false"&&e!=="0"}function i(e){return String(e).replace(/["<>]/g,"").trim()}export function buildTextStyle(e={}){const o=[];if(e.size&&o.push(`font-size:${SIZE_MAP[e.size]||i(e.size)}`),e["point-size"]){const r=parseFloat(e["point-size"]);!isNaN(r)&&r>0&&o.push(`font-size:${r}pt`)}l(e.bold)?o.push("font-weight:700"):e.weight&&WEIGHT_MAP[e.weight]&&o.push(`font-weight:${WEIGHT_MAP[e.weight]}`),l(e.italic)&&o.push("font-style:italic");const n=e.colour||e.color;return n&&o.push(`color:${COLOUR_TOKENS[n]||i(n)}`),e.font&&FONT_MAP[e.font]&&o.push(`font-family:${FONT_MAP[e.font]}`),e.transform&&TRANSFORM_MAP[e.transform]&&o.push(`text-transform:${TRANSFORM_MAP[e.transform]}`),e.decoration&&DECORATION_MAP[e.decoration]&&o.push(`text-decoration:${DECORATION_MAP[e.decoration]}`),e.spacing&&o.push(`letter-spacing:${SPACING_MAP[e.spacing]||i(e.spacing)}`),e.align&&(o.push("display:block"),o.push(`text-align:${i(e.align)}`)),e.style&&o.push(i(e.style).replace(/;+$/,"")),o.join(";")}
1
+ export const SIZE_MAP={xs:".75rem",sm:".875rem",base:"1rem",lg:"1.125rem",xl:"1.25rem","2xl":"1.5rem","3xl":"1.875rem","4xl":"2.25rem"},WEIGHT_MAP={thin:"100",light:"300",normal:"400",medium:"500",semibold:"600",bold:"700",extrabold:"800",black:"900"},SPACING_MAP={tight:"-0.05em",normal:"0em",wide:"0.05em",wider:"0.1em"},COLOUR_TOKENS={primary:"var(--dm-color-primary)",secondary:"var(--dm-color-secondary)",muted:"var(--dm-text-muted)",danger:"var(--dm-color-danger)",success:"var(--dm-color-success)",warning:"var(--dm-color-warning)",info:"var(--dm-color-info)"},FONT_MAP={Georgia:"Georgia,serif",Arial:"Arial,sans-serif",Verdana:"Verdana,sans-serif","Courier New":"'Courier New',monospace","Times New Roman":"'Times New Roman',serif","Trebuchet MS":"'Trebuchet MS',sans-serif",Roboto:"'Roboto',sans-serif",Inter:"'Inter',sans-serif","Open Sans":"'Open Sans',sans-serif",Lato:"'Lato',sans-serif",Nunito:"'Nunito',sans-serif",Poppins:"'Poppins',sans-serif","Source Sans 3":"'Source Sans 3',sans-serif",Montserrat:"'Montserrat',sans-serif",Raleway:"'Raleway',sans-serif","DM Sans":"'DM Sans',sans-serif","Work Sans":"'Work Sans',sans-serif",Merriweather:"'Merriweather',serif","Playfair Display":"'Playfair Display',serif",Lora:"'Lora',serif","Roboto Slab":"'Roboto Slab',serif",Lexend:"'Lexend',sans-serif","Atkinson Hyperlegible":"'Atkinson Hyperlegible',sans-serif",Andika:"'Andika',sans-serif"},CMS_WEB_FONTS=Object.freeze(["Roboto","Inter","Open Sans","Lato","Nunito","Poppins","Source Sans 3","Montserrat","Raleway","DM Sans","Work Sans","Merriweather","Playfair Display","Lora","Roboto Slab","Lexend","Atkinson Hyperlegible","Andika"]),TRANSFORM_MAP={upper:"uppercase",lower:"lowercase",capitalize:"capitalize",none:"none"},DECORATION_MAP={underline:"underline","line-through":"line-through",none:"none"};function r(e){return e!=null&&e!==!1&&e!=="false"&&e!=="0"}function o(e){return String(e).replace(/["<>]/g,"").trim()}export function buildTextStyle(e={}){const n=[];if(e.size&&n.push(`font-size:${SIZE_MAP[e.size]||o(e.size)}`),e["point-size"]){const s=parseFloat(e["point-size"]);!isNaN(s)&&s>0&&n.push(`font-size:${s}pt`)}r(e.bold)?n.push("font-weight:700"):e.weight&&WEIGHT_MAP[e.weight]&&n.push(`font-weight:${WEIGHT_MAP[e.weight]}`),r(e.italic)&&n.push("font-style:italic");const i=e.colour||e.color;return i&&n.push(`color:${COLOUR_TOKENS[i]||o(i)}`),e.font&&FONT_MAP[e.font]&&n.push(`font-family:${FONT_MAP[e.font]}`),e.transform&&TRANSFORM_MAP[e.transform]&&n.push(`text-transform:${TRANSFORM_MAP[e.transform]}`),e.decoration&&DECORATION_MAP[e.decoration]&&n.push(`text-decoration:${DECORATION_MAP[e.decoration]}`),e.spacing&&n.push(`letter-spacing:${SPACING_MAP[e.spacing]||o(e.spacing)}`),e.align&&(n.push("display:block"),n.push(`text-align:${o(e.align)}`)),e.style&&n.push(o(e.style).replace(/;+$/,"")),n.join(";")}
@@ -56,8 +56,33 @@ const JOURNEYS_FILE = path.join(DATA_DIR, 'journeys.json');
56
56
  const EXCLUDE_ADMIN = true;
57
57
  const EXCLUDE_PATHS = ['/api'];
58
58
  const JOURNEY_DAILY_CAP = 5000;
59
+ const JOURNEY_RETENTION_DAYS = 90; // the longest range the screens offer; older days are never read
59
60
  const REALTIME_WINDOW_MS = 5 * 60 * 1000;
60
61
 
62
+ /**
63
+ * The beacon's `url` becomes an object key in lifetime.json and daily.json, so
64
+ * it must be a site path: starting with '/', no whitespace, at most 2 KB
65
+ * (anything a browser posts is far shorter; a 1 MB key would otherwise persist
66
+ * for ever). Prototype names are refused explicitly as well - the '/' rule
67
+ * already excludes them, but `lifetime["constructor"] + 1` turning every total
68
+ * into a string is exactly the bug a relaxed rule must not bring back.
69
+ */
70
+ const URL_MAX = 2048;
71
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
72
+ function isBeaconPath(url) {
73
+ return typeof url === 'string' && url.length <= URL_MAX && url.startsWith('/') && !/\s/.test(url);
74
+ }
75
+ function isSafeKey(key) {
76
+ return !UNSAFE_KEYS.has(key) && !UNSAFE_KEYS.has(key.replace(/^\/+/, ''));
77
+ }
78
+
79
+ /** Drop journey days older than the retention - nothing reads them, and the file is rewritten on every hit. */
80
+ function pruneJourneys(journeys, today, days = JOURNEY_RETENTION_DAYS) {
81
+ const cutoff = todayKey(new Date(Date.parse(today) - days * 86400000));
82
+ for (const day of Object.keys(journeys)) if (day < cutoff) delete journeys[day];
83
+ return journeys;
84
+ }
85
+
61
86
  async function readJson(file, fallback) {
62
87
  try {
63
88
  return JSON.parse(await fs.readFile(file, 'utf8'));
@@ -401,11 +426,17 @@ export async function analyticsRoutes(fastify, opts = {}) {
401
426
  if (!url || typeof url !== 'string') {
402
427
  return reply.status(400).send({ error: 'url is required' });
403
428
  }
429
+ if (!isBeaconPath(url)) {
430
+ return reply.status(400).send({ error: 'url must be a site path of 2 KB or less' });
431
+ }
404
432
  if (dnt === true || dnt === '1') {
405
433
  return { ok: true, ignored: 'dnt' };
406
434
  }
407
435
 
408
436
  const normalised = url.split('?')[0].split('#')[0].replace(/\/+$/, '') || '/';
437
+ if (!isSafeKey(normalised)) {
438
+ return reply.status(400).send({ error: 'url must be a site path of 2 KB or less' });
439
+ }
409
440
  if (EXCLUDE_ADMIN && normalised.startsWith('/admin')) {
410
441
  return { ok: true, ignored: 'admin' };
411
442
  }
@@ -432,6 +463,7 @@ export async function analyticsRoutes(fastify, opts = {}) {
432
463
  // Journey event - only if sid is a string of reasonable shape, and under cap
433
464
  const safeSid = typeof sid === 'string' && sid.length > 0 && sid.length <= 64 ? sid : null;
434
465
  const safeRef = typeof ref === 'string' && ref.length <= 512 ? ref : '';
466
+ pruneJourneys(journeys, today);
435
467
  if (safeSid) {
436
468
  if (!journeys[today]) journeys[today] = [];
437
469
  if (shouldRecordJourney(journeys[today])) {
@@ -64,9 +64,73 @@ import {hooks} from '../../services/hooks.js';
64
64
  import {authenticateApiToken, scopeAllows} from '../../services/apiTokens.js';
65
65
  import {isProjectEnabled, resolveArtefactProject} from '../../services/projects.js';
66
66
  import {maskConnections, restoreConnections} from '../../services/connectionSecrets.js';
67
+ import {
68
+ callerId,
69
+ isRoot,
70
+ isUserDataPreset,
71
+ mayReadRow,
72
+ mayWriteRow,
73
+ ownRowsFilter,
74
+ pinOwner,
75
+ pluginDenial
76
+ } from '../../services/collectionAccess.js';
67
77
 
68
78
  const ALL_PRESET_SLUGS = new Set(['roles', 'user-profiles', ...PRESET_COLLECTION_SLUGS]);
69
79
 
80
+ /**
81
+ * The generic routes are open to anyone holding `collections`, which every
82
+ * level-1 admin does. Two kinds of collection need more than that (see
83
+ * services/collectionAccess.js): a plugin's collection needs that plugin's own
84
+ * permission, and the Tools' user-data presets (notes, tasks, contacts) are
85
+ * kept to the caller's own rows. Answers the schema, or sends 404/403 and
86
+ * answers null - the caller returns at once.
87
+ *
88
+ * @param {object} request
89
+ * @param {object} reply
90
+ * @returns {Promise<object|null>}
91
+ */
92
+ async function admitted(request, reply) {
93
+ const schema = await getCollection(request.params.slug);
94
+ if (!schema) {
95
+ reply.status(404).send({ error: 'Collection not found' });
96
+ return null;
97
+ }
98
+ const denial = pluginDenial(schema, request.user);
99
+ if (denial) {
100
+ reply.status(403).send({ error: denial });
101
+ return null;
102
+ }
103
+ return schema;
104
+ }
105
+
106
+ /**
107
+ * One row of a user-data preset the caller may read (or, with `write`,
108
+ * change): the row, or null after a 404 (no such row) or 403 (not theirs).
109
+ * Any other collection's rows pass as they are.
110
+ *
111
+ * @param {object} request
112
+ * @param {object} reply
113
+ * @param {{write?: boolean}} [opts]
114
+ * @returns {Promise<object|null>}
115
+ */
116
+ async function ownRow(request, reply, {write = false} = {}) {
117
+ const {slug, id} = request.params;
118
+ const entry = await getEntry(slug, id);
119
+ if (!entry) {
120
+ reply.status(404).send({ error: 'Entry not found' });
121
+ return null;
122
+ }
123
+ const allowed = write ? mayWriteRow(slug, entry, request.user) : mayReadRow(slug, entry, request.user);
124
+ if (!allowed) {
125
+ reply.status(403).send({ error: NOT_YOUR_ROW });
126
+ return null;
127
+ }
128
+ return entry;
129
+ }
130
+
131
+ const NOT_YOUR_ROW = 'This entry belongs to another user';
132
+ const NOT_YOUR_ROWS = 'Only the site owner can do this to everyone\'s entries - the Tool works on your own';
133
+
70
134
  /**
71
135
  * The sidebar badge on Collections (GET /collections/_count): how many
72
136
  * collections this user sees and how many entries they hold, with where they
@@ -364,6 +428,9 @@ export async function collectionsRoutes(fastify) {
364
428
  });
365
429
 
366
430
  fastify.put('/collections/:slug', canUpdate, async (request, reply) => {
431
+ // A plugin's schema is the plugin's: without this, opening `api.read` to
432
+ // the public is one PUT away from every row the entry routes refuse.
433
+ if (!(await admitted(request, reply))) return;
367
434
  try {
368
435
  return await updateCollection(request.params.slug, request.body || {});
369
436
  } catch (err) {
@@ -375,6 +442,7 @@ export async function collectionsRoutes(fastify) {
375
442
  if (ALL_PRESET_SLUGS.has(request.params.slug)) {
376
443
  return reply.status(403).send({ error: 'Cannot delete a preset collection' });
377
444
  }
445
+ if (!(await admitted(request, reply))) return;
378
446
  try {
379
447
  await deleteCollection(request.params.slug);
380
448
  return { success: true };
@@ -404,10 +472,11 @@ export async function collectionsRoutes(fastify) {
404
472
  * adding the `qs` dependency for one route.
405
473
  */
406
474
  fastify.get('/collections/:slug/entries', canRead, async (request, reply) => {
407
- const schema = await getCollection(request.params.slug);
408
- if (!schema) return reply.status(404).send({ error: 'Collection not found' });
475
+ if (!(await admitted(request, reply))) return;
409
476
  const { page, limit, sort, order, search } = request.query;
410
- const filter = extractBracketed(request.query, 'filter');
477
+ // The caller's own rows of a user-data preset - after their filter, so
478
+ // `filter[userId]=someone-else` cannot widen it.
479
+ const filter = {...extractBracketed(request.query, 'filter'), ...ownRowsFilter(request.params.slug, request.user)};
411
480
  const result = await listEntries(request.params.slug, {
412
481
  page: parseInt(page, 10) || 1,
413
482
  limit: parseInt(limit, 10) || 50,
@@ -423,15 +492,18 @@ export async function collectionsRoutes(fastify) {
423
492
  });
424
493
 
425
494
  fastify.get('/collections/:slug/entries/:id', canRead, async (request, reply) => {
426
- const entry = await getEntry(request.params.slug, request.params.id);
427
- if (!entry) return reply.status(404).send({ error: 'Entry not found' });
495
+ if (!(await admitted(request, reply))) return;
496
+ const entry = await ownRow(request, reply);
497
+ if (!entry) return;
428
498
  return request.params.slug === 'api-tokens' ? redactTokenHash(entry) : entry;
429
499
  });
430
500
 
431
501
  fastify.post('/collections/:slug/entries', canCreate, async (request, reply) => {
502
+ if (!(await admitted(request, reply))) return;
432
503
  const user = request.user;
433
504
  try {
434
- let data = request.body?.data || {};
505
+ // A note, task or contact made here is the caller's, whatever userId was sent.
506
+ let data = pinOwner(request.params.slug, request.body?.data || {}, user);
435
507
  // A role created in the admin belongs to no plugin; see keepRoleSystemFields.
436
508
  if (request.params.slug === 'roles') {
437
509
  data = {...data};
@@ -468,9 +540,10 @@ export async function collectionsRoutes(fastify) {
468
540
  ? request.body.field.trim()
469
541
  : 'spam';
470
542
  const value = request.body?.spam === false ? false : true;
543
+ if (!(await admitted(request, reply))) return;
471
544
  try {
472
- const existing = await getEntry(slug, id);
473
- if (!existing) return reply.code(404).send({error: 'Entry not found'});
545
+ const existing = await ownRow(request, reply, {write: true});
546
+ if (!existing) return;
474
547
 
475
548
  const entry = await updateEntry(slug, id, {...(existing.data || {}), [field]: value});
476
549
  hooks.emit('collection:entryUpdated', {
@@ -484,8 +557,15 @@ export async function collectionsRoutes(fastify) {
484
557
  });
485
558
 
486
559
  fastify.put('/collections/:slug/entries/:id', canUpdate, async (request, reply) => {
560
+ if (!(await admitted(request, reply))) return;
561
+ let existing = null;
562
+ if (isUserDataPreset(request.params.slug)) {
563
+ existing = await ownRow(request, reply, {write: true});
564
+ if (!existing) return;
565
+ }
487
566
  try {
488
- let data = request.body?.data || {};
567
+ // A row of a user-data preset keeps its owner - a save cannot hand it to someone else.
568
+ let data = pinOwner(request.params.slug, request.body?.data || {}, request.user, existing);
489
569
  if (request.params.slug === 'roles') data = await keepRoleSystemFields(request.params.id, data);
490
570
  const entry = await updateEntry(request.params.slug, request.params.id, data);
491
571
  if (request.params.slug === 'roles') await invalidateRoles();
@@ -498,6 +578,8 @@ export async function collectionsRoutes(fastify) {
498
578
  });
499
579
 
500
580
  fastify.delete('/collections/:slug/entries/:id', canDelete, async (request, reply) => {
581
+ if (!(await admitted(request, reply))) return;
582
+ if (isUserDataPreset(request.params.slug) && !(await ownRow(request, reply, {write: true}))) return;
501
583
  if (request.params.slug === 'roles') {
502
584
  const entry = await getEntry('roles', request.params.id);
503
585
  if (entry?.data?.level === 0) {
@@ -525,6 +607,11 @@ export async function collectionsRoutes(fastify) {
525
607
  if (request.params.slug === 'roles') {
526
608
  return reply.status(403).send({ error: 'Roles cannot be cleared - delete them one at a time' });
527
609
  }
610
+ if (!(await admitted(request, reply))) return;
611
+ // Clearing wipes everyone's rows, so for a user-data preset only root may.
612
+ if (isUserDataPreset(request.params.slug) && !isRoot(request.user)) {
613
+ return reply.status(403).send({ error: NOT_YOUR_ROWS });
614
+ }
528
615
  try {
529
616
  await clearEntries(request.params.slug);
530
617
  return { success: true };
@@ -539,6 +626,7 @@ export async function collectionsRoutes(fastify) {
539
626
 
540
627
  // Entries keep their ids and meta dates; see migrateStorage() in services/collections.js.
541
628
  fastify.post('/collections/:slug/migrate-storage', canUpdate, async (request, reply) => {
629
+ if (!(await admitted(request, reply))) return;
542
630
  try {
543
631
  return await migrateStorage(request.params.slug, request.body?.storage);
544
632
  } catch (err) {
@@ -553,9 +641,11 @@ export async function collectionsRoutes(fastify) {
553
641
  // -------------------------------------------------------------------------
554
642
 
555
643
  fastify.get('/collections/:slug/export', canRead, async (request, reply) => {
644
+ if (!(await admitted(request, reply))) return;
556
645
  const format = request.query.format === 'csv' ? 'csv' : 'json';
557
646
  try {
558
- const output = await exportEntries(request.params.slug, format);
647
+ // A user-data preset exports the caller's own rows, as the Tool would.
648
+ const output = await exportEntries(request.params.slug, format, {filter: ownRowsFilter(request.params.slug, request.user)});
559
649
  if (format === 'csv') {
560
650
  reply.header('Content-Type', 'text/csv');
561
651
  reply.header('Content-Disposition', `attachment; filename="${request.params.slug}-entries.csv"`);
@@ -573,8 +663,13 @@ export async function collectionsRoutes(fastify) {
573
663
  const user = request.user;
574
664
  const entries = request.body?.entries;
575
665
  if (!Array.isArray(entries)) return reply.status(400).send({ error: 'entries must be an array' });
666
+ if (!(await admitted(request, reply))) return;
576
667
  try {
577
- const result = await importEntries(request.params.slug, entries, { createdBy: user?.id || null });
668
+ // Imported notes, tasks or contacts are the caller's own, whatever userId the file carries.
669
+ const own = isUserDataPreset(request.params.slug)
670
+ ? entries.map(item => ({data: pinOwner(request.params.slug, (item?.data && typeof item.data === 'object') ? item.data : (item || {}), user)}))
671
+ : entries;
672
+ const result = await importEntries(request.params.slug, own, { createdBy: user?.id || null });
578
673
  return reply.status(201).send(result);
579
674
  } catch (err) {
580
675
  return reply.status(400).send({ error: err.message });
@@ -624,6 +719,7 @@ export async function collectionsRoutes(fastify) {
624
719
  const denied = await checkPublicAccess(schema, 'read', request, reply);
625
720
  if (denied !== undefined) return;
626
721
  }
722
+ if (nobodysRows(request, reply, filter)) return;
627
723
 
628
724
  const result = await listEntries(request.params.slug, {
629
725
  page: parseInt(page, 10) || 1,
@@ -637,15 +733,38 @@ export async function collectionsRoutes(fastify) {
637
733
  return applyReadFieldAllowlist(schema, result);
638
734
  }
639
735
 
736
+ /**
737
+ * The public surface of a user-data preset (its `api` block says admins may
738
+ * read) still shows people only their own rows: the filter joins the
739
+ * caller's, and a caller with no identity - a visitor, or a project token -
740
+ * has no rows here. Sends 403 and answers true when that is the case.
741
+ *
742
+ * @param {object} request
743
+ * @param {object} reply
744
+ * @param {object} [filter] - the list filter to narrow, in place
745
+ * @returns {boolean}
746
+ */
747
+ function nobodysRows(request, reply, filter) {
748
+ const slug = request.params.slug;
749
+ if (!isUserDataPreset(slug) || isRoot(request.user)) return false;
750
+ if (!callerId(request.user)) {
751
+ reply.status(403).send({ error: NOT_YOUR_ROW });
752
+ return true;
753
+ }
754
+ if (filter) Object.assign(filter, ownRowsFilter(slug, request.user));
755
+ return false;
756
+ }
757
+
640
758
  async function publicGetEntry(request, reply) {
641
759
  const schema = await getCollection(request.params.slug);
642
760
  if (!schema) return reply.status(404).send({ error: 'Collection not found' });
643
761
 
644
762
  const denied = await checkPublicAccess(schema, 'read', request, reply);
645
763
  if (denied !== undefined) return;
764
+ if (nobodysRows(request, reply)) return;
646
765
 
647
- const entry = await getEntry(request.params.slug, request.params.id);
648
- if (!entry) return reply.status(404).send({ error: 'Entry not found' });
766
+ const entry = await ownRow(request, reply);
767
+ if (!entry) return;
649
768
  return applyReadFieldAllowlist(schema, entry);
650
769
  }
651
770
 
@@ -714,6 +833,10 @@ export async function collectionsRoutes(fastify) {
714
833
 
715
834
  const denied = await checkPublicAccess(schema, 'read', request, reply);
716
835
  if (denied !== undefined) return;
836
+ // A fragment renders every row it is given; a user-data preset has no "everyone's rows" for anyone but root.
837
+ if (isUserDataPreset(attrs.slug) && !isRoot(request.user)) {
838
+ return reply.status(403).send({ error: NOT_YOUR_ROWS });
839
+ }
717
840
 
718
841
  const { renderCollectionFragment } = await import('../../services/markdown.js');
719
842
  const html = await renderCollectionFragment(attrs);
@@ -763,9 +886,10 @@ export async function collectionsRoutes(fastify) {
763
886
 
764
887
  const denied = await checkPublicAccess(schema, 'create', request, reply);
765
888
  if (denied !== undefined) return;
889
+ if (nobodysRows(request, reply)) return;
766
890
 
767
891
  try {
768
- let data = request.body?.data || {};
892
+ let data = pinOwner(request.params.slug, request.body?.data || {}, request.user);
769
893
  // A role created in the admin belongs to no plugin; see keepRoleSystemFields.
770
894
  if (request.params.slug === 'roles') {
771
895
  data = {...data};
@@ -787,9 +911,16 @@ export async function collectionsRoutes(fastify) {
787
911
 
788
912
  const denied = await checkPublicAccess(schema, 'update', request, reply);
789
913
  if (denied !== undefined) return;
914
+ if (nobodysRows(request, reply)) return;
915
+ let existing = null;
916
+ if (isUserDataPreset(request.params.slug)) {
917
+ existing = await ownRow(request, reply, {write: true});
918
+ if (!existing) return;
919
+ }
790
920
 
791
921
  try {
792
- return await updateEntry(request.params.slug, request.params.id, request.body?.data || {});
922
+ const data = pinOwner(request.params.slug, request.body?.data || {}, request.user, existing);
923
+ return await updateEntry(request.params.slug, request.params.id, data);
793
924
  } catch (err) {
794
925
  const status = err.message === 'Entry not found' ? 404 : 400;
795
926
  return reply.status(status).send({ error: err.message });
@@ -802,6 +933,8 @@ export async function collectionsRoutes(fastify) {
802
933
 
803
934
  const denied = await checkPublicAccess(schema, 'delete', request, reply);
804
935
  if (denied !== undefined) return;
936
+ if (nobodysRows(request, reply)) return;
937
+ if (isUserDataPreset(request.params.slug) && !(await ownRow(request, reply, {write: true}))) return;
805
938
 
806
939
  try {
807
940
  await deleteEntry(request.params.slug, request.params.id);
@@ -108,9 +108,19 @@ export async function themeRoutes(fastify, opts = {}) {
108
108
 
109
109
  // The admin applies its theme before anything else loads. A custom admin
110
110
  // theme is its base, its class, and the CSS behind that class.
111
- fastify.get('/theme/admin', {preHandler: [authenticate]}, async () => {
111
+ // `?id=` asks how a CANDIDATE theme would apply - Content > Theme previews
112
+ // a pick live with it before anything is saved. A candidate other than the
113
+ // saved theme needs theme.read: an unused Theme Roller draft's tokens are
114
+ // deliberately not readable by every signed-in user (see /theme/catalog).
115
+ fastify.get('/theme/admin', {preHandler: [authenticate]}, async (request, reply) => {
112
116
  const cfg = await loadThemeConfig();
113
- const id = cfg.adminTheme || 'charcoal-dark';
117
+ const saved = cfg.adminTheme || 'charcoal-dark';
118
+ const asked = String(request.query?.id || '');
119
+ if (asked && asked !== saved) {
120
+ await requirePermission('theme', 'read')(request, reply);
121
+ if (reply.sent) return reply;
122
+ }
123
+ const id = asked || saved;
114
124
  const {dommaTheme, customClass} = resolveThemeClasses(id, cfg);
115
125
  return {theme: id, dommaTheme, customClass, css: customClass ? compileCustomThemeCss([id]) : ''};
116
126
  });