domma-cms 1.6.55 → 1.6.57

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 (113) hide show
  1. package/admin/css/calculator.css +1 -1
  2. package/admin/js/lib/analytics-shape.js +1 -1
  3. package/admin/js/lib/calculator-engine.js +1 -1
  4. package/admin/js/lib/calculator.js +22 -10
  5. package/admin/js/lib/form-editor-arrange.js +1 -1
  6. package/admin/js/lib/forms-arrange.js +1 -1
  7. package/admin/js/lib/notes-arrange.js +1 -1
  8. package/admin/js/lib/search-settings-arrange.js +1 -1
  9. package/admin/js/lib/seo-arrange.js +1 -0
  10. package/admin/js/lib/todo-arrange.js +1 -1
  11. package/admin/js/lib/tool-kit.js +1 -1
  12. package/admin/js/lib/tool-recovery.js +4 -4
  13. package/admin/js/templates/analytics.html +26 -14
  14. package/admin/js/templates/contacts.html +26 -5
  15. package/admin/js/templates/form-editor.html +2 -0
  16. package/admin/js/templates/notes.html +11 -8
  17. package/admin/js/templates/search.html +9 -3
  18. package/admin/js/templates/seo.html +23 -17
  19. package/admin/js/templates/todo.html +18 -14
  20. package/admin/js/views/analytics.js +10 -10
  21. package/admin/js/views/contacts.js +47 -47
  22. package/admin/js/views/form-editor.js +7 -7
  23. package/admin/js/views/form-submissions.js +2 -2
  24. package/admin/js/views/forms.js +5 -5
  25. package/admin/js/views/notes.js +6 -6
  26. package/admin/js/views/search.js +2 -2
  27. package/admin/js/views/seo.js +43 -43
  28. package/admin/js/views/todo.js +8 -8
  29. package/package.json +1 -1
  30. package/plugins/_lib/admin/mail/compose-window.js +4 -1
  31. package/plugins/_lib/admin/mail/mail.css +1 -1
  32. package/plugins/_lib/admin/mail/reader-kit.js +364 -0
  33. package/plugins/_lib/admin/mail/reader-view.js +1009 -161
  34. package/plugins/_lib/admin/mail/when-closed.js +33 -3
  35. package/plugins/_lib/mail/readRoutes.js +48 -10
  36. package/plugins/_lib/mail/unreadBadge.js +4 -1
  37. package/plugins/blog/CLAUDE.md +14 -0
  38. package/plugins/blog/admin/css/index.css +5 -0
  39. package/plugins/blog/admin/lib/drafts.js +95 -0
  40. package/plugins/blog/admin/templates/blog.html +1 -1
  41. package/plugins/blog/admin/templates/categories.html +1 -1
  42. package/plugins/blog/admin/templates/comments.html +1 -1
  43. package/plugins/blog/admin/templates/post-editor.html +5 -1
  44. package/plugins/blog/admin/views/blog.js +44 -19
  45. package/plugins/blog/admin/views/categories.js +138 -20
  46. package/plugins/blog/admin/views/comments.js +32 -23
  47. package/plugins/blog/admin/views/kit.js +232 -22
  48. package/plugins/blog/admin/views/post-editor.js +348 -178
  49. package/plugins/blog/admin/views/settings.js +17 -3
  50. package/plugins/blog/plugin.js +1 -1
  51. package/plugins/blog/plugin.json +7 -7
  52. package/plugins/blog/plugin.public.js +1 -1
  53. package/plugins/blog/tests/drafts.test.js +50 -0
  54. package/plugins/booking/CLAUDE.md +13 -0
  55. package/plugins/booking/admin/lib/drafts.js +139 -0
  56. package/plugins/booking/admin/lib/kit.js +246 -25
  57. package/plugins/booking/admin/views/booking.js +11 -2
  58. package/plugins/booking/admin/views/bookings.js +7 -2
  59. package/plugins/booking/admin/views/diary.js +13 -6
  60. package/plugins/booking/admin/views/panels.js +85 -12
  61. package/plugins/booking/admin/views/people.js +41 -2
  62. package/plugins/booking/admin/views/services.js +45 -3
  63. package/plugins/booking/admin/views/settings.js +22 -2
  64. package/plugins/booking/plugin.json +2 -2
  65. package/plugins/booking/tests/admin-drafts.test.js +67 -0
  66. package/plugins/feedback/CLAUDE.md +12 -0
  67. package/plugins/feedback/admin/css/index.css +5 -0
  68. package/plugins/feedback/admin/lib/drafts.js +145 -0
  69. package/plugins/feedback/admin/lib/kit.js +161 -5
  70. package/plugins/feedback/admin/lib/settings.js +46 -28
  71. package/plugins/feedback/admin/views/feedback.js +1 -1
  72. package/plugins/feedback/admin/views/inbox.js +110 -46
  73. package/plugins/feedback/admin/views/send.js +174 -83
  74. package/plugins/feedback/plugin.json +3 -3
  75. package/plugins/feedback/tests/drafts.test.js +93 -0
  76. package/plugins/free-tier.lock.json +75 -63
  77. package/plugins/security/CLAUDE.md +9 -0
  78. package/plugins/security/admin/lib/screen-state.js +120 -0
  79. package/plugins/security/admin/templates/security.html +15 -10
  80. package/plugins/security/admin/views/security.js +270 -132
  81. package/plugins/security/plugin.json +2 -2
  82. package/plugins/security/tests/screen-state.test.js +69 -0
  83. package/plugins/shopping-cart/CLAUDE.md +13 -0
  84. package/plugins/shopping-cart/admin/css/index.css +21 -5
  85. package/plugins/shopping-cart/admin/lib/drafts.js +174 -0
  86. package/plugins/shopping-cart/admin/lib/kit.js +136 -28
  87. package/plugins/shopping-cart/admin/views/orders.js +117 -36
  88. package/plugins/shopping-cart/admin/views/overview.js +38 -8
  89. package/plugins/shopping-cart/admin/views/products.js +317 -68
  90. package/plugins/shopping-cart/admin/views/sales.js +21 -3
  91. package/plugins/shopping-cart/admin/views/settings.js +62 -3
  92. package/plugins/shopping-cart/admin/views/shop.js +18 -3
  93. package/plugins/shopping-cart/plugin.json +3 -3
  94. package/plugins/shopping-cart/tests/accounts.test.js +11 -0
  95. package/plugins/shopping-cart/tests/drafts.test.js +118 -0
  96. package/plugins/support/CLAUDE.md +14 -0
  97. package/plugins/support/admin/css/index.css +10 -0
  98. package/plugins/support/admin/lib/drafts.js +162 -0
  99. package/plugins/support/admin/lib/kit.js +156 -16
  100. package/plugins/support/admin/lib/settings.js +52 -29
  101. package/plugins/support/admin/views/desk.js +114 -33
  102. package/plugins/support/admin/views/inbox.js +85 -26
  103. package/plugins/support/admin/views/support.js +1 -1
  104. package/plugins/support/plugin.json +3 -3
  105. package/plugins/support/tests/drafts.test.js +92 -0
  106. package/public/js/site.js +1 -1
  107. package/server/routes/api/analytics.js +5 -1
  108. package/server/routes/api/contacts.js +42 -0
  109. package/server/routes/api/forms.js +8 -0
  110. package/server/routes/api/seo.js +4 -1
  111. package/server/routes/api/todo.js +50 -10
  112. package/server/server.js +1 -1
  113. package/server/templates/page.html +1 -1
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Kept drafts (admin/lib/drafts.js): what a panel keeps of unsaved typing, the
3
+ * question it is offered back with - OK always throws away, so Cancel, Escape
4
+ * and a backdrop click (all `false` from E.confirm) keep - and what a reply box
5
+ * holds once its message has gone.
6
+ */
7
+ import {test} from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import {
10
+ DRAFT_KEYS, RESTORE_CONFIRM, changedFields, afterSend, differs, draftFate, draftQuestion, draftText, draftValues, isDraft, makeDraft, newDraftQuestion,
11
+ settingsPrint, shortWhen, stableJson, ticketPrint
12
+ } from '../admin/lib/drafts.js';
13
+
14
+ test('stableJson ignores key order; differs compares by it', () => {
15
+ assert.equal(stableJson({b: 1, a: [1, {d: 2, c: 3}]}), stableJson({a: [1, {c: 3, d: 2}], b: 1}));
16
+ assert.equal(differs({text: 'a'}, {text: 'a'}), false);
17
+ assert.equal(differs({text: 'a'}, {text: ''}), true);
18
+ });
19
+
20
+ test('keys are one per panel and request - no shared map', () => {
21
+ assert.equal(DRAFT_KEYS.reply('ref-1'), 'support.draft.reply.ref-1');
22
+ assert.equal(DRAFT_KEYS.answer('e1'), 'support.draft.answer.e1');
23
+ assert.notEqual(DRAFT_KEYS.reply('x'), DRAFT_KEYS.answer('x'));
24
+ assert.notEqual(DRAFT_KEYS.compose, DRAFT_KEYS.settings);
25
+ });
26
+
27
+ test('makeDraft / isDraft / draftValues', () => {
28
+ const d = makeDraft({subject: 'x', other: 'y', n: 5}, 'fp', '2026-10-15T10:00:00.000Z');
29
+ assert.deepEqual(d, {data: {subject: 'x', other: 'y', n: 5}, was: 'fp', at: '2026-10-15T10:00:00.000Z'});
30
+ assert.ok(isDraft(d));
31
+ for (const bad of [null, 'x', {}, {data: []}, {data: 'x'}]) assert.equal(isDraft(bad), false);
32
+ assert.deepEqual(draftValues(d, {subject: '', where: 'here', n: ''}), {subject: 'x', where: 'here', n: '5'});
33
+ });
34
+
35
+ test('ticketPrint: someone writing moves it; reading or archiving does not', () => {
36
+ const t = {messages: [{id: 'm1', from: 'customer'}], status: 'open', notes: 'n', archived: false, unread: true};
37
+ assert.equal(ticketPrint(t), ticketPrint({...t, archived: true, unread: false, status: 'answered'}));
38
+ assert.notEqual(ticketPrint(t), ticketPrint({...t, messages: [...t.messages, {id: 'm2', from: 'domma'}]}));
39
+ // The inbox also counts its notes; the site never sees them.
40
+ assert.equal(ticketPrint(t), ticketPrint({...t, notes: 'changed'}));
41
+ assert.notEqual(ticketPrint(t, {notes: true}), ticketPrint({...t, notes: 'changed'}, {notes: true}));
42
+ assert.equal(ticketPrint(null), '');
43
+ });
44
+
45
+ test('afterSend: what stays in the reply box once a message has gone', () => {
46
+ assert.equal(afterSend('Hello', 'Hello'), '', 'unchanged while sending: emptied');
47
+ assert.equal(afterSend('Hello\n\nand one more thing', 'Hello'), 'and one more thing', 'carried on at the end: only the new part stays');
48
+ assert.equal(afterSend('Hullo', 'Hello'), 'Hullo', 'the text itself changed: all of it stays, unsent');
49
+ assert.equal(afterSend('', 'Hello'), '');
50
+ assert.equal(afterSend(undefined, undefined), '');
51
+ });
52
+
53
+ test('draftFate and draftQuestion: OK throws away; the text says what happened since', () => {
54
+ const d = makeDraft({text: 'Hi'}, 'A', '2026-10-15T10:00:00.000Z');
55
+ assert.equal(draftFate(d, null), 'deleted');
56
+ assert.equal(draftFate(d, 'B'), 'changed');
57
+ assert.equal(draftFate(d, 'A'), '');
58
+ assert.equal(draftFate({was: ''}, 'B'), '');
59
+
60
+ const plain = draftQuestion(d, 'A', {what: 'your reply to “Down”'});
61
+ assert.match(plain.text, /^Throw away the changes you left unsaved on your reply to “Down” \(15 Oct, \d\d:00\)\? Keeping them puts them back in the form\.$/);
62
+ assert.deepEqual(plain.options, RESTORE_CONFIRM);
63
+ assert.equal(plain.options.confirmText, 'Throw away');
64
+ assert.equal(plain.options.cancelText, 'Keep them');
65
+
66
+ const changed = draftQuestion(d, 'B', {what: 'x', changed: 'Domma has written since', changedAt: '2026-10-15T11:30:00.000Z'});
67
+ assert.match(changed.text, /Domma has written since \(15 Oct, \d\d:30\) - keeping them puts your changes over that\.$/);
68
+ assert.match(draftQuestion(d, 'B', {what: 'x'}).text, /It has been changed since - keeping/);
69
+
70
+ const gone = draftQuestion(d, null, {what: 'a request'});
71
+ assert.equal(gone.fate, 'deleted');
72
+ assert.equal(gone.options.confirmText, 'Throw away');
73
+ assert.equal(gone.options.cancelText, 'Copy them');
74
+ });
75
+
76
+ test('newDraftQuestion, settingsPrint and draftText', () => {
77
+ const q = newDraftQuestion(makeDraft({subject: 't'}, '', '2026-10-15T09:05:00.000Z'));
78
+ assert.match(q.text, /^Throw away the request you left unsent \(15 Oct, \d\d:05\)\? Keeping it puts it back in the form\.$/);
79
+ assert.equal(q.options.confirmText, 'Throw away');
80
+ assert.equal(settingsPrint({b: '2', a: '1'}), settingsPrint({a: '1', b: '2'}));
81
+ assert.equal(draftText(makeDraft({reply: 'Thanks', notes: ''})), 'reply:\nThanks');
82
+ assert.equal(shortWhen('nope'), '');
83
+ });
84
+
85
+ test('changedFields keeps only what was edited, so a picked-up draft leaves the rest as the record has it now', () => {
86
+ const saved = {status: 'acknowledged', reply: '', notes: ''};
87
+ const kept = changedFields({status: 'acknowledged', reply: 'Thanks', notes: ''}, saved);
88
+ assert.deepEqual(kept, {reply: 'Thanks'});
89
+ // Later, someone else set the status and wrote notes: the draft puts back only the reply.
90
+ assert.deepEqual(draftValues(makeDraft(kept), {status: 'planned', reply: '', notes: 'theirs'}), {status: 'planned', reply: 'Thanks', notes: 'theirs'});
91
+ assert.deepEqual(changedFields({a: '1'}, {a: '1'}), {});
92
+ });
package/public/js/site.js CHANGED
@@ -1 +1 @@
1
- import{decodeBase64Json as P}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as J,applyColour as X,applyPill as oe,applyFloat as ne,applyHighlight as R,applyItemHighlight as ae,itemHighlightAttrs as se,isCurrentItem as U,menuAlign as ie,applyClasses as O,cssClasses as W,applyEntrance as V}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{initFloatingMenus as re}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as G,markInvalidFields as K,showValidationPopup as Q}from"/public/js/forms.js?v=20261009-form-classes";import{buildWizardSteps as ce}from"/public/js/form-steps.mjs";import{applyFormClasses as Z,applySubmitButtons as le,applyWizardButtons as de}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 m of[...document.body.classList])m.startsWith("dm-custom-")&&document.body.classList.remove(m);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,u=0){return Array.isArray(d)?d.filter(p=>!(p&&(p.type==="spacer"||p.type==="theme-switcher"))).filter(p=>!(p&&p.type==="separator"&&u===0)).map(p=>p&&p.type==="separator"?{divider:!0}:Array.isArray(p?.items)?{...p,items:s(p.items,u+1)}:p):d},t=function(d){return Array.isArray(d)?d.filter(u=>!(u&&(u.type==="separator"||u.type==="spacer"||u.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 m=x.get(0);m.classList.add(`dm-nav-align-${ie(f)||"centre"}`),a&&x.get(0).classList.add("dm-nav-vertical",f.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const h=f.dock||{};a&&!e&&me(m,f.side==="right"),!a&&h.hideOnScroll&&ue(m),R(m,f),V(m,f),O(m,f.class),e&&(ne(x.get(0),f),re(x.get(0).parentNode||document));const i=m.querySelector(".navbar-toggle"),c=m.querySelector(".navbar-collapse");if(i&&c){let d=null;const u=()=>{c.classList.contains("show")&&i.click()},p=()=>{d&&(d.remove(),d=null),document.body.classList.remove("dm-drawer-open")},v=()=>{d||(d=document.createElement("div"),d.className="dm-navbar-backdrop",d.addEventListener("click",u),document.body.appendChild(d),document.body.classList.add("dm-drawer-open"))};i.addEventListener("click",()=>{requestAnimationFrame(()=>{c.classList.contains("show")?v():p()})}),c.addEventListener("click",C=>{C.target.closest("a")&&requestAnimationFrame(p)}),document.addEventListener("keydown",C=>{C.key==="Escape"&&c.classList.contains("show")&&u()})}const g=m.querySelector(".navbar-nav");if(g){const d=Array.from(g.children);let u=0;for(const p of f.items||[])if(!(p&&p.type==="separator")){if(p&&p.type==="spacer"){const v=document.createElement("li"),C=["xs","sm","md","lg","xl","flex"].includes(p.size)?p.size:"md";v.className=`dm-menu-spacer dm-menu-spacer--${C}`,v.setAttribute("aria-hidden","true"),g.insertBefore(v,d[u]||null);continue}if(p&&p.type==="theme-switcher"){const v=document.createElement("li");v.className="dm-menu-theme-switcher";const C=document.createElement("span");C.dataset.themeSwitcher="menu",C.dataset.position="inline",v.appendChild(C),g.insertBefore(v,d[u]||null);continue}u++}window.CMSThemeSwitcher?.scan(g)}const b=t(f.items||[]);m.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(d=>{const u=b[parseInt(d.dataset.index,10)];if(!u)return;const p=d.dataset.subindex!=null,v=p&&Array.isArray(u.items)?u.items[parseInt(d.dataset.subindex,10)]:u;if(!v)return;v.colour&&X(d,v.colour),v.pill&&!p&&oe(d,v.pill),ae(d,v),O(d.closest("li"),v.class);const C=J(v.badge);C&&d.appendChild(C),U(v.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;")},m=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';if(r&&r.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md"}" aria-hidden="true"></li>`;if(r&&r.type==="theme-switcher")return'<li class="dm-menu-theme-switcher"><span data-theme-switcher="menu" data-position="inline"></span></li>';const A=a(r.url||"#"),L=e(r.text||""),D=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(m).join("")}</ul>`:"",N=se(r),ee=[N.hover&&` data-menu-item-hover="${a(N.hover)}"`,N.active&&` data-menu-item-active="${a(N.active)}"`].filter(Boolean).join(""),te=N.accent?` style="--dm-menu-highlight:${a(N.accent)}"`:"",Y=W(r.class).join(" ");return`<li${Y?` class="${a(Y)}"`:""}><a href="${A}"${ee}${te}>${L}</a>${D}</li>`},h=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:D,svg:N}=o[L];return`<a href="${n[L]}" target="_blank" rel="noopener noreferrer" aria-label="${D}" 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">${m(r).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const A=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(m).join("")}</ul>`:"",L=g(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>`,D=["cms-footer-col",...W(r.class)].join(" ");return`<li class="${a(D)}">${L}${A}</li>`},u=function(r,A,L=!1){return(r||[]).forEach(D=>{D&&(D.type==="separator"||D.type==="spacer"||D.type==="theme-switcher")||(L&&g(D)||A.push(D),Array.isArray(D.items)&&D.items.length&&u(D.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),g=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>':"",D=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>`:""}${D?`<p class="cms-footer-blurb">${e(D)}</p>`:""}${h()}</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(m).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(m).join("")}</ul></nav>`),t.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${t.legal.map(m).join("")}</ul></nav>`),s+=h();s+="</div>",y.html(s),window.CMSThemeSwitcher?.scan(y.get(0));const d=y.get(0).querySelector(".cms-footer-brand-icon");d&&f.brand?.icon&&d.setAttribute("data-icon",f.brand.icon),O(y.get(0),l.footer?.class);const p=[...u(t.primary,[],!0),...u(t.legal,[])];y.get(0).querySelectorAll(".footer-nav a").forEach((r,A)=>{const L=p[A];if(!L)return;L.colour&&X(r,L.colour);const D=J(L.badge);D&&r.appendChild(D),U(L.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}),V(y.get(0).querySelector(".footer-nav--primary"),{entrance:t.primaryEntrance}),V(y.get(0).querySelector(".footer-nav--legal"),{entrance:t.legalEntrance}),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"),q=l.footer?.motionDefault,z=M!=null?!!M:q==="on"?!0:q==="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 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&&pe(k),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const w=$(".page-body");if(w.length){w.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),w.find(".tabs").each(function(){Domma.elements.tabs(this)}),w.find(".carousel").each(function(){const n={primary:"--dm-primary",secondary:"--dm-secondary",accent:"--dm-primary",text:"--dm-text",surface:"--dm-surface",background:"--dm-background",success:"--dm-success",warning:"--dm-warning",danger:"--dm-danger",info:"--dm-info"},o=h=>{const i=String(h||"").trim();return i?n[i.toLowerCase()]?`var(${n[i.toLowerCase()]})`:CSS.supports("color",i)?i:"":""},s=o(this.dataset.background),t=o(this.dataset.arrowBackground),e=o(this.dataset.arrowColor);s&&(this.style.setProperty("--dm-carousel-background",s),this.classList.add("dm-carousel-custom-background")),t&&this.style.setProperty("--dm-carousel-arrow-background",t),e&&this.style.setProperty("--dm-carousel-arrow-color",e);const a=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${a}`);const m={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide",heightMode:this.dataset.heightMode==="average"?"average":"crop"};if(this.dataset.height){const h=Number(this.dataset.height);Number.isFinite(h)&&h>0&&(m.height=h)}if(this.dataset.animationDuration){const h=parseInt(this.dataset.animationDuration,10);Number.isFinite(h)&&(m.animationDuration=h)}this.dataset.animationEasing&&(m.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(h=>h.cloneNode(!0)),this._dmCarouselOpts=m),this._dmCarousel=Domma.elements.carousel(this,m)}),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 m=this,h=c=>String(c).padStart(2,"0"),i=c=>{const g=Math.max(0,Math.floor(c/1e3)),b=m.querySelector(".dm-timer-time");b&&(b.textContent=`${h(Math.floor(g/86400))}:${h(Math.floor(g%86400/3600))}:${h(Math.floor(g%3600/60))}:${h(g%60)}`)};a&&(n.onTick=i),Domma.elements.timer(this,n),a&&i(n.duration||0)}),w.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),w.find(".dm-progression").each(function(){const n=Array.from(this.querySelectorAll(".dm-progression-item")).map(t=>{const e={title:t.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:t.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return t.dataset.date&&(e.date=t.dataset.date),t.dataset.status&&(e.status=t.dataset.status),t.dataset.eventIcon&&(e.icon=t.dataset.eventIcon),t.dataset.entryId&&(e._entryId=t.dataset.entryId),e}),o=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",s=Domma.elements.progression(this,{layout:o,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...n.length?{items:n}:{}});if((this.dataset.mode||"timeline")==="timeline"){const t={completed:{label:"Completed",icon:"check-circle",tone:"success"},planned:{label:"Planned",icon:"circle",tone:"secondary"},"in-progress":{label:"In progress",icon:"clock",tone:"primary"},blocked:{label:"Blocked",icon:"alert-circle",tone:"danger"}};this.querySelectorAll(".dm-progression-item").forEach((e,a)=>{const m=n[a];if(!m)return;const h=e.querySelector(".dm-progression-title");if(m.icon&&h){const c=document.createElement("span");c.dataset.icon=m.icon,c.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",c.setAttribute("aria-hidden","true"),h.prepend(c)}const i=t[m.status];if(i&&h){const c=document.createElement("span");c.className=`badge badge-${i.tone} cms-timeline-status`,c.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const g=document.createElement("span");g.dataset.icon=i.icon,g.dataset.iconSize="14",g.setAttribute("aria-hidden","true"),c.append(g,document.createTextNode(i.label)),h.after(c)}})}Domma.icons.scan(this),n.length&&(this._dmCtx={instance:s,items:n})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(n=>{const o=n.dataset.revealMode||"stagger",s=n.dataset.revealAnimation||"slide-up",t=parseInt(n.dataset.revealDuration,10)||400,e=parseInt(n.dataset.revealStagger,10)||60,a=parseInt(n.dataset.revealDelay,10)||0,m=n.dataset.revealDirection==="rtl",h=Array.from(n.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};h.forEach((c,g)=>{c.style.opacity="0",c.style.transform=i[s]||"",c.style.transition=`opacity ${t}ms ease, transform ${t}ms ease`;const b=m?h.length-1-g:g;c.style.transitionDelay=o==="stagger"?`${a+b*e}ms`:`${a}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const c=new IntersectionObserver(g=>{g.forEach(b=>{b.isIntersecting&&(b.target.offsetWidth,b.target.style.opacity="1",b.target.style.transform="none",c.unobserve(b.target))})},{threshold:.1});h.forEach(g=>c.observe(g))}))}),w.find(".card[data-collapsible]").each(function(){const n=this.querySelector(".card-header");n&&n.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),w.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const n=this.dataset.soTarget,o=document.getElementById(n);if(!o)return;const s=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",s.setContent(o),s.open()})})}if(typeof $.setup=="function"){const n=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const s=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:m,alert:h,prompt:i,...c}=e||{};a||m||h||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 u=$(c.target);c.toggleClass&&u.toggleClass(c.toggleClass),c.addClass&&u.addClass(c.addClass),c.removeClass&&u.removeClass(c.removeClass),d!==null&&(c.setText&&u.text(d),c.setVal&&u.val(d),c.setAttr&&u.attr(c.setAttr,d))}c.href&&(window.location.href=c.href),m&&E.toast(m,{type:c.toastType||"success"}),h&&E.alert(h)}}),Object.keys(c).length&&(o[s]={...t,events:{...t.events,click:c}})):o[s]=t}$.setup(o)}}w.length&&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,y=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:y,scope:l.dataset.scope}:{entryId:y})});try{let s=await o(w);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const a=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(a.ok){const{token:m}=await a.json();S.set("auth_token",m),w=m,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 y(w,n){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(n){s.appendChild(document.createTextNode("Please "));const t=document.createElement("a");t.href="/login",t.textContent="sign in",s.appendChild(t),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=w;return o.appendChild(s),o}function k(w,n){const o=document.createElement("template");o.innerHTML=n||"",w.replaceWith(o.content)}l.forEach(async w=>{const n=w.dataset.collectionAttrs;if(!n)return;if(!x){w.replaceChildren(y("",!0));return}const o=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:n})});try{let s=x,t=await o(s);if(t.status===401){const a=S.get("auth_refresh_token");if(a){const m=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:a})});if(m.ok){const{token:h}=await m.json();S.set("auth_token",h),s=h,t=await o(s)}}}if(!t.ok){const{error:a}=await t.json().catch(()=>({}));w.replaceChildren(y(a||"Unable to load entries.",!1));return}const{html:e}=await t.json();k(w,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{w.replaceChildren(y("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),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:k,rows:w,search:n,sortable:o,exportable:s,pageSize:t,empty:e,ctaConfig:a,slug:m,exportCfg:h}=y;if(!k?.length)return;if(a){const g=k.findIndex(b=>b.key==="_cta");g!==-1&&(k[g]={key:"_cta",title:"",render:(b,d)=>{const u=document.createElement("button");if(u.className=`btn btn-${a.style||"primary"} dm-cta-trigger`,u.dataset.action=a.action||"",a.scope&&(u.dataset.scope=a.scope),u.dataset.entry=d._entryId||"",a.confirm&&(u.dataset.confirm=a.confirm),a.icon){const p=document.createElement("span");p.dataset.icon=a.icon,u.appendChild(p),u.appendChild(document.createTextNode(" "))}return u.appendChild(document.createTextNode(a.label||"Run")),u}})}const i="col-table-"+Math.random().toString(36).slice(2,7),c=document.createElement("div");c.id=i,x.replaceChildren(c),T.create("#"+i,{data:w,columns:k,search:n,sortable:o,exportable:s,pageSize:t,emptyMessage:e}),a&&j(c),x._dmTable={columns:k,rows:w,slug:m,exportCfg:h,render(g){T.create("#"+i,{data:g,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 y(o,s){const t={};return(o||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const a=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),m=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":a?"chooser":e.type,h={...e.formConfig||{}};h.span==="full"&&s&&(h.span=s);const i={type:m,required:x(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...h}};e.minLength!==void 0&&(i.minLength=e.minLength),e.maxLength!==void 0&&(i.maxLength=e.maxLength),e.min!==void 0&&(i.min=e.min),e.max!==void 0&&(i.max=e.max),a&&(e.variant&&(i.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(i.multiple=!0),e.density&&(i.density=e.density),e.columns&&(i.columns=e.columns),e.accent&&(i.accent=e.accent),e.accentStyle&&(i.accentStyle=e.accentStyle),e.glow&&(i.glow=!0),e.glowColour&&(i.glowColour=e.glowColour),e.shadow&&(i.shadow=e.shadow),e.shadowColour&&(i.shadowColour=e.shadowColour)),t[e.name]=i}),t}function k(o){const s={};return(o||[]).forEach(t=>{if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(s[t.name]=typeof e=="object"?e.value:e)}}),s}function w(o,s){(s||[]).forEach(t=>{if(t.type!=="date"||!t.name)return;const e=o.querySelector(`[name="${t.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function n(o,s,t){let e=o.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",o.appendChild(e)),e.textContent=s,e.style.cssText=t?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}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,m=e.layout||"grid",h=t.some(u=>u.type==="page-break"),i=async u=>{if(o.dataset.triggerBlocked==="true"){const p=o._formLogicRuntime,v=p&&p.submitBlock?p.submitBlock().reasons:[];n(o,v.join(" ")||"This form cannot be submitted yet.",!0);const C=new Error("Blocked by a form trigger");throw C.formSubmitFailed=!0,C}try{const p=o.querySelector(".fb-form-honeypot"),v=p?.querySelector('input[type="text"]')||null,C=p?.querySelector('[name="_t"]')||null,M=Object.assign({},u);delete M._t,p?.dataset.hpName&&delete M[p.dataset.hpName],v!==null&&(M._hp=v.value),C!==null&&(M._t=C.value);const q=await H.post(`/api/forms/submit/${s.slug}`,M);if(q&&q.error){n(o,q.error,!0);const z=new Error(q.error);throw z.formSubmitFailed=!0,z}if(q?.redirect){window.location.href=q.redirect;return}for(;o.firstChild;)o.removeChild(o.firstChild);n(o,q?.message||e.successMessage||"Thank you for your submission.",!1)}catch(p){throw p.formSubmitFailed||n(o,p.message||"Submission failed. Please try again.",!0),p}},c=Date.now();function g(u){const p=u.querySelector("form");if(!p||p.querySelector(".fb-form-honeypot"))return;const v=document.createElement("div");v.className="fb-form-honeypot",v.setAttribute("aria-hidden","true");const C=document.createElement("input");C.name=p.querySelector('[name="website"]')?"_website":"website",v.dataset.hpName=C.name,C.type="text",C.tabIndex=-1,C.autocomplete="url",C.placeholder="https://",v.appendChild(C);const M=document.createElement("input");M.name="_t",M.type="hidden",M.value=c,v.appendChild(M),p.appendChild(v)}function b(u){if(!window.FormLogicEngine||!t.some(v=>v.logic||Array.isArray(v.triggers)&&v.triggers.length))return null;const p=new window.FormLogicEngine.FormLogicRuntime(s,o,u?{baseValues:u}:void 0);if(p.init(),o._formLogicRuntime=p,o._formLogicObserved)return p;if(o._formLogicObserved=!0,o.parentNode&&typeof MutationObserver<"u"){const v=new MutationObserver(function(C){for(const M of C)for(const q of M.removedNodes)if(q===o||q.nodeType===1&&q.contains&&q.contains(o)){o._formLogicRuntime&&o._formLogicRuntime.destroy(),v.disconnect();return}});v.observe(o.parentNode,{childList:!0,subtree:!1}),o._formLogicObserver=v}return p}let d=!1;if(o.addEventListener("dm-form-end",u=>{const p=u.detail?.validation;if(p&&!p.valid){K(o,p.missing),Q(p);return}d||(d=!0,i(u.detail?.values||{}).catch(()=>{}).finally(()=>{d=!1}))}),h&&F.wizard){const u=ce(t,s.title),p=u.map(I=>({title:I.title,description:I.description,fields:y(I.fields,a)})),v=u.map(I=>I.fields);let C=0,M=null,q=null;const z=I=>{const r=window.FormLogicEngine;if(!r||!r.validateFields)return;const A=r.validateFields(o,v[C]||[]);A.valid||(I.preventDefault(),I.stopImmediatePropagation(),K(o,A.missing),Q(A))},B=()=>{const I=v[C]||t;de(o,e),Z(o,e,I),w(o,I),G(o,I),e.honeypot!==!1&&g(o),M&&M.destroy(),M=b(q?.getStepData?q.getStepData():void 0),o.querySelectorAll(".wizard-next, .wizard-submit").forEach(r=>r.addEventListener("click",z,!0))};q=F.wizard(o,{schema:{steps:p},layout:m,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"||!q?.goToStep)return;const r=Number(I.detail.action.step);!Number.isFinite(r)||r<0||r>=p.length||r!==C&&q.goToStep(r)}),Promise.resolve(q).then(B)}else if(F.render){const u=F.render(o,y(t,a),k(t),{submitText:e.submitText||"Submit",layout:m,columns:a,onSubmit:i});Promise.resolve(u).then(function(){le(o,e,m),Z(o,e,t),w(o,t),G(o,t),e.honeypot!==!1&&g(o),b()})}})})();function me(f,l){const x=document.body,y=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 m=a.textContent.trim();m&&(a.setAttribute("title",m),a.querySelector("svg, [data-icon]")||(a.dataset.initial=m.charAt(0).toUpperCase()))});const n=document.createElement("button");n.type="button",n.className="dm-nav-collapse-toggle";const o=()=>{const a=y.contains("dm-vnav-collapsed");n.setAttribute("aria-pressed",String(a)),n.setAttribute("aria-label",a?"Expand menu":"Collapse menu"),n.title=n.getAttribute("aria-label");const m=l?"chevrons-left":"chevrons-right",h=l?"chevrons-right":"chevrons-left";n.innerHTML=`<span data-icon="${a?m:h}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(n)},s=a=>{y.toggle("dm-vnav-collapsed",a),S.set("nav_collapsed",a),o()};n.addEventListener("click",()=>s(!y.contains("dm-vnav-collapsed"))),o();const t=f.querySelector(".navbar-container")||f;t.insertBefore(n,t.firstChild?.nextSibling||null);const e=a=>{y.toggle("dm-vnav-autohide",a),y.toggle("dm-vnav-collapsible",!a&&k.has("collapsible")),a?y.remove("dm-vnav-collapsed"):k.has("collapsible")&&S.get("nav_collapsed")===!0&&y.add("dm-vnav-collapsed"),f.classList.remove("dm-nav-revealed"),o()};f.addEventListener("pointerdown",a=>{y.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=y.contains("dm-vnav-autohide"),m=y.contains("dm-vnav-collapsed"),h=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:m?"Expand menu":"Collapse to icons",icon:m?"maximize":"minimize",disabled:!w()||a||!k.has("collapsible"),action:()=>s(!m)},{label:a?"Keep on screen":"Hide when not in use",icon:a?"eye":"eye-off",disabled:!w(),action:()=>{S.set("nav_autohide",!a),e(!a)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!h,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(k.has("autohide")),y.toggle("dm-vnav-collapsed",!k.has("autohide")&&k.has("start-collapsed")),o()}}]}})}function ue(f){f.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,k=f.matches(":focus-within")||f.querySelector(".navbar-dropdown.open, .navbar-collapse.show");y>l&&y>f.offsetHeight&&!k?f.classList.add("dm-nav-scrolled-away"):y<l&&f.classList.remove("dm-nav-scrolled-away"),l=y}))},{passive:!0})}function pe(f){let l={};try{l=JSON.parse(f.dataset.contents||"{}")}catch{}const x=f.closest(".site-main"),y=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=>(y?[...y.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 m=document.createElement("ol");m.className="dm-contents-list"+(l.numbered?" is-numbered":"");const h=[];for(const i of o){if(!i.id){const b=(i.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let d=b;for(let u=2;s.has(d);u++)d=`${b}-${u}`;s.add(d),i.id=d}const c=document.createElement("li");c.className=`dm-contents-item dm-contents-${i.tagName.toLowerCase()}`;const g=document.createElement("a");g.href=`#${i.id}`,g.textContent=i.textContent.trim(),c.appendChild(g),m.appendChild(c),h.push([i,g])}if(t.appendChild(m),f.replaceChildren(t),l.scrollSpy!==!1&&"IntersectionObserver"in window){const i=new Map,c=()=>{const b=h.find(([d])=>i.get(d))||null;h.forEach(([,d])=>{const u=b&&d===b[1];d.classList.toggle("is-active",!!u),u?d.setAttribute("aria-current","location"):d.removeAttribute("aria-current")})},g=new IntersectionObserver(b=>{b.forEach(d=>i.set(d.target,d.isIntersecting)),c()},{rootMargin:"-80px 0px -60% 0px"});h.forEach(([b])=>g.observe(b))}if(x?.classList.contains("contents-collapsible")){const i=document.createElement("button");i.type="button",i.className="dm-contents-toggle";const c=()=>{const g=x.classList.contains("contents-collapsed");i.setAttribute("aria-expanded",String(!g)),i.setAttribute("aria-label",g?"Show the contents":"Hide the contents"),i.title=i.getAttribute("aria-label"),i.innerHTML=`<span data-icon="${g?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(i)};i.addEventListener("click",()=>{S.set("contents_collapsed",x.classList.toggle("contents-collapsed")),c()}),c(),e.insertBefore(i,e.firstChild)}if(x?.classList.contains("contents-floating")&&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 g=f.getBoundingClientRect(),b=c.clientX-g.left,d=c.clientY-g.top,u=v=>{const C=Math.max(0,Math.min(window.innerWidth-g.width,v.clientX-b)),M=Math.max(0,Math.min(window.innerHeight-40,v.clientY-d));Object.assign(f.style,{left:`${C}px`,top:`${M}px`,right:"auto",bottom:"auto"})},p=()=>{document.removeEventListener("pointermove",u),S.set("contents_position",{x:parseInt(f.style.left,10),y:parseInt(f.style.top,10)})};document.addEventListener("pointermove",u),document.addEventListener("pointerup",p,{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"),g=h.find(([,b])=>b.classList.contains("is-active"));return[{header:"Contents"},...i?[{label:c?"Show the contents":"Collapse to a button",icon:c?"list":"minimize",action:()=>f.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!g,action:async()=>{const b=`${location.origin}${location.pathname}#${g[0].id}`;try{await _.copyToClipboard(b),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),x?.classList.contains("contents-autohide")&&(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
+ import{decodeBase64Json as P}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as J,applyColour as X,applyPill as oe,applyFloat as ne,applyHighlight as R,applyItemHighlight as ae,itemHighlightAttrs as se,isCurrentItem as U,menuAlign as ie,applyClasses as O,cssClasses as W,applyEntrance as V}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{initFloatingMenus as re}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as G,markInvalidFields as K,showValidationPopup as Q}from"/public/js/forms.js?v=20261009-form-classes";import{buildWizardSteps as ce}from"/public/js/form-steps.mjs";import{applyFormClasses as Z,applySubmitButtons as le,applyWizardButtons as de}from"/public/js/form-submit.mjs";document.addEventListener("click",h=>{const c=h.target?.closest?.(".dm-banner__dismiss");c&&(h.preventDefault(),c.closest(".dm-banner")?.remove())}),$(()=>{const h=window.__CMS_NAV__||{},c=window.__CMS_SITE__||{};if(c.autoTheme?.enabled){let o=function(e){const a=(e||"07:00").split(":");return+a[0]*60+(+a[1]||0)},s=function(){const e=new Date,a=e.getHours()*60+e.getMinutes();return a>=o(n.dayStart)&&a<o(n.nightStart)?n.dayTheme:n.nightTheme},t=function(e){const a=n.apply&&n.apply[e]||{theme:e,custom:""};Domma.theme.set(a.theme);for(const d of[...document.body.classList])d.startsWith("dm-custom-")&&document.body.classList.remove(d);a.custom&&document.body.classList.add(a.custom)};const n=c.autoTheme;t(s()),setInterval(()=>t(s()),6e4)}const x=$("#site-navbar");if(x.length&&h.brand){let s=function(u,p=0){return Array.isArray(u)?u.filter(m=>!(m&&(m.type==="spacer"||m.type==="theme-switcher"))).filter(m=>!(m&&m.type==="separator"&&p===0)).map(m=>m&&m.type==="separator"?{divider:!0}:Array.isArray(m?.items)?{...m,items:s(m.items,p+1)}:m):u};const n={...h.brand},o=n.size&&n.size!=="md"?` navbar-brand-${n.size}`:"";if(n.logo||n.icon||n.tagline){let u="";n.logo?u+=`<img src="${n.logo}" class="navbar-brand-logo" alt="${n.text||""}">`:n.icon&&(u+=`<span data-icon="${n.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),n.text&&(u+=`<span class="navbar-brand-text${o}">${n.text}</span>`),n.tagline&&(u+=`<small class="navbar-brand-tagline">${n.tagline}</small>`),n.html=u}else o&&n.text&&(n.html=`<span class="navbar-brand-text${o}">${n.text}</span>`);const t=s(h.items||[]),e=h.position==="floating",a=h.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:n,items:t,variant:h.variant||"dark",position:e?"static":h.position||"sticky",collapsible:!0,appearOnHover:!!h.appearOnHover}),Domma.icons.scan("#site-navbar");const d=x.get(0),f=()=>{if(!a)return;const p=d.querySelector('a[aria-current="page"]')?.closest(".navbar-dropdown");p&&!p.classList.contains("open")&&p.querySelector(":scope > .navbar-dropdown-toggle")?.click()};d.classList.add(`dm-nav-align-${ie(h)||"centre"}`),a&&x.get(0).classList.add("dm-nav-vertical",h.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const i=h.dock||{};a&&!e&&me(d,h.side==="right"),!a&&i.hideOnScroll&&ue(d),R(d,h),V(d,h),O(d,h.class),e&&(ne(x.get(0),h),re(x.get(0).parentNode||document));const l=d.querySelector(".navbar-toggle"),g=d.querySelector(".navbar-collapse");if(l&&g){let u=null;const p=()=>{g.classList.contains("show")&&l.click()},m=()=>{u&&(u.remove(),u=null),document.body.classList.remove("dm-drawer-open")},v=()=>{u||(u=document.createElement("div"),u.className="dm-navbar-backdrop",u.addEventListener("click",p),document.body.appendChild(u),document.body.classList.add("dm-drawer-open"))};l.addEventListener("click",()=>{requestAnimationFrame(()=>{g.classList.contains("show")?(f(),v()):m()})}),g.addEventListener("click",C=>{C.target.closest("a")&&requestAnimationFrame(m)}),document.addEventListener("keydown",C=>{C.key==="Escape"&&g.classList.contains("show")&&p()})}const b=d.querySelector(".navbar-nav");if(b){const u=Array.from(b.children);let p=0;for(const m of h.items||[])if(!(m&&m.type==="separator")){if(m&&m.type==="spacer"){const v=document.createElement("li"),C=["xs","sm","md","lg","xl","flex"].includes(m.size)?m.size:"md";v.className=`dm-menu-spacer dm-menu-spacer--${C}`,v.setAttribute("aria-hidden","true"),b.insertBefore(v,u[p]||null);continue}if(m&&m.type==="theme-switcher"){const v=document.createElement("li");v.className="dm-menu-theme-switcher";const C=document.createElement("span");C.dataset.themeSwitcher="menu",C.dataset.position="inline",v.appendChild(C),b.insertBefore(v,u[p]||null);continue}p++}window.CMSThemeSwitcher?.scan(b)}d.querySelectorAll("[data-path]").forEach(u=>{const p=u.dataset.path.split(".").map(Number),m=p.length>1,v=p.reduce((A,q)=>A?.items?.[q],{items:t});if(!v)return;v.colour&&X(u,v.colour),v.pill&&!m&&oe(u,v.pill),ae(u,v),O(u.closest("li"),v.class);const C=J(v.badge);C&&u.appendChild(C),U(v.url,location.pathname)&&(u.classList.add("active"),u.setAttribute("aria-current","page"))}),f()}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;")},d=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 M=a(r.url||"#"),L=e(r.text||""),D=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(d).join("")}</ul>`:"",N=se(r),ee=[N.hover&&` data-menu-item-hover="${a(N.hover)}"`,N.active&&` data-menu-item-active="${a(N.active)}"`].filter(Boolean).join(""),te=N.accent?` style="--dm-menu-highlight:${a(N.accent)}"`:"",Y=W(r.class).join(" ");return`<li${Y?` class="${a(Y)}"`:""}><a href="${M}"${ee}${te}>${L}</a>${D}</li>`},f=function(){if(!c.footer)return"";const r=Object.keys(o).filter(L=>n[L]);if(!r.length)return"";const M=W(c.footer.socialClass).join(" ");return`<div class="footer-social">${r.map(L=>{const{label:D,svg:N}=o[L];return`<a href="${n[L]}" target="_blank" rel="noopener noreferrer" aria-label="${D}" class="footer-social-link${M?` ${a(M)}`:""}">${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">${d(r).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const M=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(d).join("")}</ul>`:"",L=g(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>`,D=["cms-footer-col",...W(r.class)].join(" ");return`<li class="${a(D)}">${L}${M}</li>`},p=function(r,M,L=!1){return(r||[]).forEach(D=>{D&&(D.type==="separator"||D.type==="spacer"||D.type==="theme-switcher")||(L&&g(D)||M.push(D),Array.isArray(D.items)&&D.items.length&&p(D.items,M))}),M};const n=c.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let s='<div class="footer-inner container">';const t=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},i=["columns","row"].includes(c.footer?.layout)?c.footer.layout:"auto",l=i==="columns"||i==="auto"&&t.primary.some(r=>Array.isArray(r?.items)&&r.items.length),g=r=>l&&!r.url&&Array.isArray(r.items)&&r.items.length>0;if(l){const r=h.brand||{},M=r.text||c.title||"",L=r.logo?`<img src="${a(r.logo)}" class="cms-footer-brand-logo" alt="">`:r.icon?'<span class="cms-footer-brand-icon"></span>':"",D=String(c.footer?.blurb||"").trim();s='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',s+=`<div class="cms-footer-brand">${M||L?`<a class="cms-footer-brand-name" href="${a(r.url||"/")}">${L}<span>${e(M)}</span></a>`:""}${D?`<p class="cms-footer-blurb">${e(D)}</p>`:""}${f()}</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(d).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(d).join("")}</ul></nav>`),t.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${t.legal.map(d).join("")}</ul></nav>`),s+=f();s+="</div>",y.html(s),window.CMSThemeSwitcher?.scan(y.get(0));const u=y.get(0).querySelector(".cms-footer-brand-icon");u&&h.brand?.icon&&u.setAttribute("data-icon",h.brand.icon),O(y.get(0),c.footer?.class);const m=[...p(t.primary,[],!0),...p(t.legal,[])];y.get(0).querySelectorAll(".footer-nav a").forEach((r,M)=>{const L=m[M];if(!L)return;L.colour&&X(r,L.colour);const D=J(L.badge);D&&r.appendChild(D),U(L.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}),V(y.get(0).querySelector(".footer-nav--primary"),{entrance:t.primaryEntrance}),V(y.get(0).querySelector(".footer-nav--legal"),{entrance:t.legalEntrance}),O(y.get(0).querySelector(".footer-nav--primary"),t.primaryClass),O(y.get(0).querySelector(".footer-nav--legal"),t.legalClass);const C=c.footer?.showMotionToggle!==!1,A=S.get("reduced_motion"),q=c.footer?.motionDefault,z=A!=null?!!A:q==="on"?!0:q==="off"?!1:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),B=c.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 M=document.createElement("span");M.className=B?"footer-motion-label":"form-switch-label",M.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(M),I.appendChild(L)}}const k=document.getElementById("site-sidebar");k&&pe(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=f=>{const i=String(f||"").trim();return i?n[i.toLowerCase()]?`var(${n[i.toLowerCase()]})`:CSS.supports("color",i)?i:"":""},s=o(this.dataset.background),t=o(this.dataset.arrowBackground),e=o(this.dataset.arrowColor);s&&(this.style.setProperty("--dm-carousel-background",s),this.classList.add("dm-carousel-custom-background")),t&&this.style.setProperty("--dm-carousel-arrow-background",t),e&&this.style.setProperty("--dm-carousel-arrow-color",e);const a=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${a}`);const d={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide",heightMode:this.dataset.heightMode==="average"?"average":"crop"};if(this.dataset.height){const f=Number(this.dataset.height);Number.isFinite(f)&&f>0&&(d.height=f)}if(this.dataset.animationDuration){const f=parseInt(this.dataset.animationDuration,10);Number.isFinite(f)&&(d.animationDuration=f)}this.dataset.animationEasing&&(d.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(f=>f.cloneNode(!0)),this._dmCarouselOpts=d),this._dmCarousel=Domma.elements.carousel(this,d)}),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 d=this,f=l=>String(l).padStart(2,"0"),i=l=>{const g=Math.max(0,Math.floor(l/1e3)),b=d.querySelector(".dm-timer-time");b&&(b.textContent=`${f(Math.floor(g/86400))}:${f(Math.floor(g%86400/3600))}:${f(Math.floor(g%3600/60))}:${f(g%60)}`)};a&&(n.onTick=i),Domma.elements.timer(this,n),a&&i(n.duration||0)}),w.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),w.find(".dm-progression").each(function(){const n=Array.from(this.querySelectorAll(".dm-progression-item")).map(t=>{const e={title:t.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:t.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return t.dataset.date&&(e.date=t.dataset.date),t.dataset.status&&(e.status=t.dataset.status),t.dataset.eventIcon&&(e.icon=t.dataset.eventIcon),t.dataset.entryId&&(e._entryId=t.dataset.entryId),e}),o=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",s=Domma.elements.progression(this,{layout:o,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...n.length?{items:n}:{}});if((this.dataset.mode||"timeline")==="timeline"){const t={completed:{label:"Completed",icon:"check-circle",tone:"success"},planned:{label:"Planned",icon:"circle",tone:"secondary"},"in-progress":{label:"In progress",icon:"clock",tone:"primary"},blocked:{label:"Blocked",icon:"alert-circle",tone:"danger"}};this.querySelectorAll(".dm-progression-item").forEach((e,a)=>{const d=n[a];if(!d)return;const f=e.querySelector(".dm-progression-title");if(d.icon&&f){const l=document.createElement("span");l.dataset.icon=d.icon,l.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",l.setAttribute("aria-hidden","true"),f.prepend(l)}const i=t[d.status];if(i&&f){const l=document.createElement("span");l.className=`badge badge-${i.tone} cms-timeline-status`,l.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const g=document.createElement("span");g.dataset.icon=i.icon,g.dataset.iconSize="14",g.setAttribute("aria-hidden","true"),l.append(g,document.createTextNode(i.label)),f.after(l)}})}Domma.icons.scan(this),n.length&&(this._dmCtx={instance:s,items:n})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(n=>{const o=n.dataset.revealMode||"stagger",s=n.dataset.revealAnimation||"slide-up",t=parseInt(n.dataset.revealDuration,10)||400,e=parseInt(n.dataset.revealStagger,10)||60,a=parseInt(n.dataset.revealDelay,10)||0,d=n.dataset.revealDirection==="rtl",f=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)"};f.forEach((l,g)=>{l.style.opacity="0",l.style.transform=i[s]||"",l.style.transition=`opacity ${t}ms ease, transform ${t}ms ease`;const b=d?f.length-1-g:g;l.style.transitionDelay=o==="stagger"?`${a+b*e}ms`:`${a}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const l=new IntersectionObserver(g=>{g.forEach(b=>{b.isIntersecting&&(b.target.offsetWidth,b.target.style.opacity="1",b.target.style.transform="none",l.unobserve(b.target))})},{threshold:.1});f.forEach(g=>l.observe(g))}))}),w.find(".card[data-collapsible]").each(function(){const n=this.querySelector(".card-header");n&&n.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),w.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const n=this.dataset.soTarget,o=document.getElementById(n);if(!o)return;const s=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",s.setContent(o),s.open()})})}if(typeof $.setup=="function"){const n=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const s=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:d,alert:f,prompt:i,...l}=e||{};a||d||f||i?($(s).on("click",async function(b){if(b.preventDefault(),a&&!await E.confirm(a))return;let u=null;if(!(i&&(u=await E.prompt(i,{inputPlaceholder:l.promptPlaceholder||"",inputValue:l.promptDefault||""}),u===null))){if(l.target){const p=$(l.target);l.toggleClass&&p.toggleClass(l.toggleClass),l.addClass&&p.addClass(l.addClass),l.removeClass&&p.removeClass(l.removeClass),u!==null&&(l.setText&&p.text(u),l.setVal&&p.val(u),l.setAttr&&p.attr(l.setAttr,u))}l.href&&(window.location.href=l.href),d&&E.toast(d,{type:l.toastType||"success"}),f&&E.alert(f)}}),Object.keys(l).length&&(o[s]={...t,events:{...t.events,click:l}})):o[s]=t}$.setup(o)}}w.length&&j(w.get(0))}),window.wireCTAButtons=j;function j(h){h.querySelectorAll(".dm-cta-trigger").forEach(c=>{c.__ctaWired||(c.__ctaWired=!0,c.addEventListener("click",async()=>{const x=c.dataset.action,y=c.dataset.entry,k=c.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(c.childNodes).map(s=>s.cloneNode(!0));c.disabled=!0,c.textContent="Running\u2026";const o=s=>fetch(`/api/actions/${x}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(c.dataset.scope?{entryId:y,scope:c.dataset.scope}:{entryId:y})});try{let s=await o(w);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const a=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(a.ok){const{token:d}=await a.json();S.set("auth_token",d),w=d,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{c.disabled=!1,c.textContent="",n.forEach(s=>c.appendChild(s)),Domma.icons.scan(c)}}))})}(function(){const c=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!c.length)return;const x=S.get("auth_token");function y(w,n){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(n){s.appendChild(document.createTextNode("Please "));const t=document.createElement("a");t.href="/login",t.textContent="sign in",s.appendChild(t),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=w;return o.appendChild(s),o}function k(w,n){const o=document.createElement("template");o.innerHTML=n||"",w.replaceWith(o.content)}c.forEach(async w=>{const n=w.dataset.collectionAttrs;if(!n)return;if(!x){w.replaceChildren(y("",!0));return}const o=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:n})});try{let s=x,t=await o(s);if(t.status===401){const a=S.get("auth_refresh_token");if(a){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:a})});if(d.ok){const{token:f}=await d.json();S.set("auth_token",f),s=f,t=await o(s)}}}if(!t.ok){const{error:a}=await t.json().catch(()=>({}));w.replaceChildren(y(a||"Unable to load entries.",!1));return}const{html:e}=await t.json();k(w,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{w.replaceChildren(y("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),j(document.body)},100)})(),(function(){const c=document.querySelectorAll("[data-collection-table]");!c.length||typeof T>"u"||!T.create||c.forEach(x=>{let y;try{y=P(x.dataset.payload)}catch{return}const{columns:k,rows:w,search:n,sortable:o,exportable:s,pageSize:t,empty:e,ctaConfig:a,slug:d,exportCfg:f}=y;if(!k?.length)return;if(a){const g=k.findIndex(b=>b.key==="_cta");g!==-1&&(k[g]={key:"_cta",title:"",render:(b,u)=>{const p=document.createElement("button");if(p.className=`btn btn-${a.style||"primary"} dm-cta-trigger`,p.dataset.action=a.action||"",a.scope&&(p.dataset.scope=a.scope),p.dataset.entry=u._entryId||"",a.confirm&&(p.dataset.confirm=a.confirm),a.icon){const m=document.createElement("span");m.dataset.icon=a.icon,p.appendChild(m),p.appendChild(document.createTextNode(" "))}return p.appendChild(document.createTextNode(a.label||"Run")),p}})}const i="col-table-"+Math.random().toString(36).slice(2,7),l=document.createElement("div");l.id=i,x.replaceChildren(l),T.create("#"+i,{data:w,columns:k,search:n,sortable:o,exportable:s,pageSize:t,emptyMessage:e}),a&&j(l),x._dmTable={columns:k,rows:w,slug:d,exportCfg:f,render(g){T.create("#"+i,{data:g,columns:k,search:n,sortable:o,exportable:s,pageSize:t,emptyMessage:e}),a&&j(l)}}})})(),(function(){const c=document.querySelectorAll("[data-form-inline]");if(!c.length||typeof F>"u")return;function x(o){const s=o&&o.logic;return!!(s&&(s.visibility||s.requirement))}function y(o,s){const t={};return(o||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const a=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),d=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":a?"chooser":e.type,f={...e.formConfig||{}};f.span==="full"&&s&&(f.span=s);const i={type:d,required:x(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...f}};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;"}c.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,d=e.layout||"grid",f=t.some(p=>p.type==="page-break"),i=async p=>{if(o.dataset.triggerBlocked==="true"){const m=o._formLogicRuntime,v=m&&m.submitBlock?m.submitBlock().reasons:[];n(o,v.join(" ")||"This form cannot be submitted yet.",!0);const C=new Error("Blocked by a form trigger");throw C.formSubmitFailed=!0,C}try{const m=o.querySelector(".fb-form-honeypot"),v=m?.querySelector('input[type="text"]')||null,C=m?.querySelector('[name="_t"]')||null,A=Object.assign({},p);delete A._t,m?.dataset.hpName&&delete A[m.dataset.hpName],v!==null&&(A._hp=v.value),C!==null&&(A._t=C.value);const q=await H.post(`/api/forms/submit/${s.slug}`,A);if(q&&q.error){n(o,q.error,!0);const z=new Error(q.error);throw z.formSubmitFailed=!0,z}if(q?.redirect){window.location.href=q.redirect;return}for(;o.firstChild;)o.removeChild(o.firstChild);n(o,q?.message||e.successMessage||"Thank you for your submission.",!1)}catch(m){throw m.formSubmitFailed||n(o,m.message||"Submission failed. Please try again.",!0),m}},l=Date.now();function g(p){const m=p.querySelector("form");if(!m||m.querySelector(".fb-form-honeypot"))return;const v=document.createElement("div");v.className="fb-form-honeypot",v.setAttribute("aria-hidden","true");const C=document.createElement("input");C.name=m.querySelector('[name="website"]')?"_website":"website",v.dataset.hpName=C.name,C.type="text",C.tabIndex=-1,C.autocomplete="url",C.placeholder="https://",v.appendChild(C);const A=document.createElement("input");A.name="_t",A.type="hidden",A.value=l,v.appendChild(A),m.appendChild(v)}function b(p){if(!window.FormLogicEngine||!t.some(v=>v.logic||Array.isArray(v.triggers)&&v.triggers.length))return null;const m=new window.FormLogicEngine.FormLogicRuntime(s,o,p?{baseValues:p}:void 0);if(m.init(),o._formLogicRuntime=m,o._formLogicObserved)return m;if(o._formLogicObserved=!0,o.parentNode&&typeof MutationObserver<"u"){const v=new MutationObserver(function(C){for(const A of C)for(const q of A.removedNodes)if(q===o||q.nodeType===1&&q.contains&&q.contains(o)){o._formLogicRuntime&&o._formLogicRuntime.destroy(),v.disconnect();return}});v.observe(o.parentNode,{childList:!0,subtree:!1}),o._formLogicObserver=v}return m}let u=!1;if(o.addEventListener("dm-form-end",p=>{const m=p.detail?.validation;if(m&&!m.valid){K(o,m.missing),Q(m);return}u||(u=!0,i(p.detail?.values||{}).catch(()=>{}).finally(()=>{u=!1}))}),f&&F.wizard){const p=ce(t,s.title),m=p.map(I=>({title:I.title,description:I.description,fields:y(I.fields,a)})),v=p.map(I=>I.fields);let C=0,A=null,q=null;const z=I=>{const r=window.FormLogicEngine;if(!r||!r.validateFields)return;const M=r.validateFields(o,v[C]||[]);M.valid||(I.preventDefault(),I.stopImmediatePropagation(),K(o,M.missing),Q(M))},B=()=>{const I=v[C]||t;de(o,e),Z(o,e,I),w(o,I),G(o,I),e.honeypot!==!1&&g(o),A&&A.destroy(),A=b(q?.getStepData?q.getStepData():void 0),o.querySelectorAll(".wizard-next, .wizard-submit").forEach(r=>r.addEventListener("click",z,!0))};q=F.wizard(o,{schema:{steps:m},layout:d,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"||!q?.goToStep)return;const r=Number(I.detail.action.step);!Number.isFinite(r)||r<0||r>=m.length||r!==C&&q.goToStep(r)}),Promise.resolve(q).then(B)}else if(F.render){const p=F.render(o,y(t,a),k(t),{submitText:e.submitText||"Submit",layout:d,columns:a,onSubmit:i});Promise.resolve(p).then(function(){le(o,e,d),Z(o,e,t),w(o,t),G(o,t),e.honeypot!==!1&&g(o),b()})}})})();function me(h,c){const x=document.body,y=x.classList,k=new Set((x.dataset.railSite||"").split(" ").filter(Boolean)),w=()=>window.matchMedia("(min-width: 993px)").matches;h.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(a=>{const d=a.textContent.trim();d&&(a.setAttribute("title",d),a.querySelector("svg, [data-icon]")||(a.dataset.initial=d.charAt(0).toUpperCase()))});const n=document.createElement("button");n.type="button",n.className="dm-nav-collapse-toggle";const o=()=>{const a=y.contains("dm-vnav-collapsed");n.setAttribute("aria-pressed",String(a)),n.setAttribute("aria-label",a?"Expand menu":"Collapse menu"),n.title=n.getAttribute("aria-label");const d=c?"chevrons-left":"chevrons-right",f=c?"chevrons-right":"chevrons-left";n.innerHTML=`<span data-icon="${a?d:f}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(n)},s=a=>{y.toggle("dm-vnav-collapsed",a),S.set("nav_collapsed",a),o()};n.addEventListener("click",()=>s(!y.contains("dm-vnav-collapsed"))),o();const t=h.querySelector(".navbar-container")||h;t.insertBefore(n,t.firstChild?.nextSibling||null);const e=a=>{y.toggle("dm-vnav-autohide",a),y.toggle("dm-vnav-collapsible",!a&&k.has("collapsible")),a?y.remove("dm-vnav-collapsed"):k.has("collapsible")&&S.get("nav_collapsed")===!0&&y.add("dm-vnav-collapsed"),h.classList.remove("dm-nav-revealed"),o()};h.addEventListener("pointerdown",a=>{y.contains("dm-vnav-autohide")&&a.pointerType!=="mouse"&&!h.classList.contains("dm-nav-revealed")&&(h.classList.add("dm-nav-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{h.contains(a.target)||h.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(h,{items:()=>{const a=y.contains("dm-vnav-autohide"),d=y.contains("dm-vnav-collapsed"),f=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:d?"Expand menu":"Collapse to icons",icon:d?"maximize":"minimize",disabled:!w()||a||!k.has("collapsible"),action:()=>s(!d)},{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:!f,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(k.has("autohide")),y.toggle("dm-vnav-collapsed",!k.has("autohide")&&k.has("start-collapsed")),o()}}]}})}function ue(h){h.classList.add("dm-nav-scroll-hide");let c=window.scrollY,x=!1;window.addEventListener("scroll",()=>{x||(x=!0,requestAnimationFrame(()=>{x=!1;const y=window.scrollY,k=h.matches(":focus-within")||h.querySelector(".navbar-dropdown.open, .navbar-collapse.show");y>c&&y>h.offsetHeight&&!k?h.classList.add("dm-nav-scrolled-away"):y<c&&h.classList.remove("dm-nav-scrolled-away"),c=y}))},{passive:!0})}function pe(h){let c={};try{c=JSON.parse(h.dataset.contents||"{}")}catch{}const x=h.closest(".site-main"),y=document.querySelector(".site-content"),k=new Set(String(c.exclude||"").split(",").map(i=>i.trim().toLowerCase()).filter(Boolean)),w=i=>!i.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),n=i=>(y?[...y.querySelectorAll(i)]:[]).filter(w).filter(l=>!k.has(l.textContent.trim().toLowerCase())),o=c.headings==="h2"?n("h2"):c.headings==="h3"?n("h3"):c.headings==="h2h3"?n("h2, h3"):n("h2").length?n("h2"):n("h3");if(!o.length){x?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(i=>i.id)),t=document.createElement("nav");t.className="dm-contents",t.setAttribute("aria-label",c.title||"Page contents"),c.hover&&(t.dataset.menuHover=c.hover),c.selected&&(t.dataset.menuActive=c.selected),c.accent&&t.style.setProperty("--dm-menu-highlight",c.accent.startsWith("#")?c.accent:`var(--dm-${c.accent})`),c.font&&(t.style.fontFamily=c.font),c.fontSize&&(t.style.fontSize=c.fontSize),c.fontWeight&&(t.style.fontWeight=c.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const a=document.createElement("p");a.className="dm-contents-title",a.textContent=c.title||"",e.appendChild(a),t.appendChild(e);const d=document.createElement("ol");d.className="dm-contents-list"+(c.numbered?" is-numbered":"");const f=[];for(const i of o){if(!i.id){const b=(i.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let u=b;for(let p=2;s.has(u);p++)u=`${b}-${p}`;s.add(u),i.id=u}const l=document.createElement("li");l.className=`dm-contents-item dm-contents-${i.tagName.toLowerCase()}`;const g=document.createElement("a");g.href=`#${i.id}`,g.textContent=i.textContent.trim(),l.appendChild(g),d.appendChild(l),f.push([i,g])}if(t.appendChild(d),h.replaceChildren(t),c.scrollSpy!==!1&&"IntersectionObserver"in window){const i=new Map,l=()=>{const b=f.find(([u])=>i.get(u))||null;f.forEach(([,u])=>{const p=b&&u===b[1];u.classList.toggle("is-active",!!p),p?u.setAttribute("aria-current","location"):u.removeAttribute("aria-current")})},g=new IntersectionObserver(b=>{b.forEach(u=>i.set(u.target,u.isIntersecting)),l()},{rootMargin:"-80px 0px -60% 0px"});f.forEach(([b])=>g.observe(b))}if(x?.classList.contains("contents-collapsible")){const i=document.createElement("button");i.type="button",i.className="dm-contents-toggle";const l=()=>{const g=x.classList.contains("contents-collapsed");i.setAttribute("aria-expanded",String(!g)),i.setAttribute("aria-label",g?"Show the contents":"Hide the contents"),i.title=i.getAttribute("aria-label"),i.innerHTML=`<span data-icon="${g?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(i)};i.addEventListener("click",()=>{S.set("contents_collapsed",x.classList.toggle("contents-collapsed")),l()}),l(),e.insertBefore(i,e.firstChild)}if(x?.classList.contains("contents-floating")&&c.movable){t.classList.add("is-movable");const i=S.get("contents_position");i&&Number.isFinite(i.x)&&Object.assign(h.style,{left:`${i.x}px`,top:`${i.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",l=>{if(l.target.closest("button"))return;const g=h.getBoundingClientRect(),b=l.clientX-g.left,u=l.clientY-g.top,p=v=>{const C=Math.max(0,Math.min(window.innerWidth-g.width,v.clientX-b)),A=Math.max(0,Math.min(window.innerHeight-40,v.clientY-u));Object.assign(h.style,{left:`${C}px`,top:`${A}px`,right:"auto",bottom:"auto"})},m=()=>{document.removeEventListener("pointermove",p),S.set("contents_position",{x:parseInt(h.style.left,10),y:parseInt(h.style.top,10)})};document.addEventListener("pointermove",p),document.addEventListener("pointerup",m,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(h,{items:()=>{const i=x?.classList.contains("contents-collapsible"),l=x?.classList.contains("contents-collapsed"),g=f.find(([,b])=>b.classList.contains("is-active"));return[{header:"Contents"},...i?[{label:l?"Show the contents":"Collapse to a button",icon:l?"list":"minimize",action:()=>h.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!g,action:async()=>{const b=`${location.origin}${location.pathname}#${g[0].id}`;try{await _.copyToClipboard(b),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),x?.classList.contains("contents-autohide")&&(h.addEventListener("pointerdown",i=>{i.pointerType!=="mouse"&&!h.classList.contains("is-revealed")&&(h.classList.add("is-revealed"),i.preventDefault())}),document.addEventListener("pointerdown",i=>{h.contains(i.target)||h.classList.remove("is-revealed")}))}
@@ -66,11 +66,15 @@ const REALTIME_WINDOW_MS = 5 * 60 * 1000;
66
66
  * for ever). Prototype names are refused explicitly as well - the '/' rule
67
67
  * already excludes them, but `lifetime["constructor"] + 1` turning every total
68
68
  * into a string is exactly the bug a relaxed rule must not bring back.
69
+ * A path the browser would resolve off the site is refused too: '//host/x' is
70
+ * protocol-relative, and browsers read '\' as '/', so '/\host' is the same.
71
+ * The admin opens these as links.
69
72
  */
70
73
  const URL_MAX = 2048;
71
74
  const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
72
75
  function isBeaconPath(url) {
73
- return typeof url === 'string' && url.length <= URL_MAX && url.startsWith('/') && !/\s/.test(url);
76
+ return typeof url === 'string' && url.length <= URL_MAX && url.startsWith('/') && !url.startsWith('//')
77
+ && !url.includes('\\') && !/\s/.test(url);
74
78
  }
75
79
  function isSafeKey(key) {
76
80
  return !UNSAFE_KEYS.has(key) && !UNSAFE_KEYS.has(key.replace(/^\/+/, ''));
@@ -35,6 +35,7 @@ import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../.
35
35
  import {hooks} from '../../services/hooks.js';
36
36
  import {getPermissionsFor} from '../../services/roles.js';
37
37
  import {getEffectiveRoles} from '../../services/userRoles.js';
38
+ import {ago, clip, plural} from '../../services/sidebarBadges.js';
38
39
 
39
40
  /**
40
41
  * The permission that lets someone rename or delete a group they did not
@@ -238,6 +239,41 @@ function toGroupName(entry) {
238
239
  return entry.data.name ?? '';
239
240
  }
240
241
 
242
+ /**
243
+ * The sidebar badge: how many contacts this user has - an inventory, so it is
244
+ * left out of a closed folder's total. The popover gives the starred and the
245
+ * groups, and the four most recently changed. Plain text throughout.
246
+ *
247
+ * @param {object[]} contacts - the caller's own, as toContact() shapes them
248
+ * @param {number} groupCount - every group (groups are shared)
249
+ * @param {number} [now]
250
+ * @returns {object}
251
+ */
252
+ export function contactsBadge(contacts, groupCount = 0, now = Date.now()) {
253
+ const list = Array.isArray(contacts) ? contacts : [];
254
+ const stamp = c => String(c.updatedAt || c.createdAt || '');
255
+ const recent = [...list].sort((a, b) => stamp(b).localeCompare(stamp(a))).slice(0, 4);
256
+ return {
257
+ count: list.length,
258
+ label: plural(list.length, 'contact'),
259
+ tone: 'info',
260
+ inventory: true,
261
+ details: {
262
+ title: 'Contacts',
263
+ rows: [
264
+ {label: 'Contacts', value: String(list.length)},
265
+ {label: 'Starred', value: String(list.filter(c => c.favourite === true).length)},
266
+ {label: 'Groups', value: String(groupCount)}
267
+ ],
268
+ items: recent.map(c => ({
269
+ text: clip(c.name || 'Unnamed'),
270
+ meta: stamp(c) ? `changed ${ago(stamp(c), now)}` : ''
271
+ })),
272
+ empty: 'No contacts yet.'
273
+ }
274
+ };
275
+ }
276
+
241
277
  /**
242
278
  * Auth is injectable, the shape dashboard.js and plugins.js use.
243
279
  *
@@ -308,6 +344,12 @@ export async function contactsRoutes(fastify, opts = {}) {
308
344
  // Contacts CRUD
309
345
  // -------------------------------------------------------------------------
310
346
 
347
+ /** GET /api/contacts/count - the sidebar badge (contactsBadge). */
348
+ fastify.get('/count', {preHandler: [authenticate, requirePermission('contacts', 'read')]}, async (request) => {
349
+ const [contacts, groups] = await Promise.all([loadContacts(userId(request)), loadGroups()]);
350
+ return contactsBadge(contacts, groups.length);
351
+ });
352
+
311
353
  /** GET /api/contacts/contacts - supports ?q=, ?group=, ?favourites=true */
312
354
  fastify.get('/contacts', {preHandler: [authenticate, requirePermission('contacts', 'read')]}, async (request, reply) => {
313
355
  const uid = userId(request);
@@ -329,6 +329,8 @@ async function cachedStats(form, now) {
329
329
  statsCache.set(form.slug, {at: now, value});
330
330
  return value;
331
331
  }
332
+ /** A form's submissions changed (sent, deleted, cleared) or the form went: the next badge reads afresh. */
333
+ const forgetStats = (slug) => { statsCache.delete(slug); };
332
334
 
333
335
  export function formsBadge(forms = [], now = Date.now()) {
334
336
  const week = forms.reduce((n, f) => n + (f.stats?.week || 0), 0);
@@ -376,6 +378,8 @@ export async function formsRoutes(fastify) {
376
378
  const result = await Promise.all(forms.map(async form => {
377
379
  // Counted where the submissions screen reads them (submissionsCollectionFor).
378
380
  const stats = await submissionStats(form, now);
381
+ // The list has just counted them: the badge, asked straight after, agrees with it.
382
+ statsCache.set(form.slug, {at: now, value: stats});
379
383
  // listForms returns metadata only; readForm returns full record with meta
380
384
  let full = null;
381
385
  try { full = await readForm(form.slug); } catch { /* skip */ }
@@ -556,6 +560,7 @@ export async function formsRoutes(fastify) {
556
560
  } catch {
557
561
  return reply.status(404).send({ error: 'Form not found.' });
558
562
  }
563
+ forgetStats(slug);
559
564
  return { ok: true };
560
565
  });
561
566
 
@@ -666,6 +671,7 @@ export async function formsRoutes(fastify) {
666
671
  } catch {
667
672
  return reply.status(404).send({ error: 'Collection not found for this form.' });
668
673
  }
674
+ forgetStats(slug);
669
675
  return { ok: true };
670
676
  });
671
677
 
@@ -690,6 +696,7 @@ export async function formsRoutes(fastify) {
690
696
  } catch {
691
697
  return reply.status(404).send({ error: 'Submission not found.' });
692
698
  }
699
+ forgetStats(slug);
693
700
  return { ok: true };
694
701
  });
695
702
 
@@ -905,6 +912,7 @@ export async function formsRoutes(fastify) {
905
912
  excused,
906
913
  outcome: ended ? {type: 'ended', trigger: ended.key, field: ended.field} : null
907
914
  });
915
+ forgetStats(slug);
908
916
  // What a visitor sent is theirs, not the site's content: keep it out of the site repo.
909
917
  try { keepSubmissionsOut(ROOT, targetSlug); } catch { /* never fail a submission over this */ }
910
918
  } else {
@@ -41,7 +41,10 @@ export async function seoRoutes(fastify, opts = {}) {
41
41
 
42
42
  fastify.get('/count', may('read'), async () => {
43
43
  const s = seo().summary();
44
- if (!s.at) return {count: 0, label: 'Not checked yet'};
44
+ if (!s.at) {
45
+ return {count: 0, label: 'Not checked yet',
46
+ details: {title: 'SEO', rows: [], items: [], empty: 'Not checked yet - open SEO to check every page.'}};
47
+ }
45
48
  const bad = (seo().store.audit().results || []).filter(r => r.error || r.failing > 0);
46
49
  return {
47
50
  count: bad.length,
@@ -23,6 +23,41 @@ import {getPermissionsFor} from '../../services/roles.js';
23
23
 
24
24
  const SLUG = 'todo-items';
25
25
 
26
+ /** What a to-do may hold - the admin view's own lists (admin/js/lib/todo-arrange.js). */
27
+ export const TODO_STATUSES = ['not-started', 'in-progress', 'completed'];
28
+ export const TODO_PRIORITIES = ['highest', 'high', 'medium', 'low', 'lowest'];
29
+
30
+ /** Most urgent first; an unknown priority sits with medium. */
31
+ const priorityRank = (p) => { const i = TODO_PRIORITIES.indexOf(p); return i === -1 ? 2 : i; };
32
+ const isHigh = (p) => p === 'highest' || p === 'high';
33
+
34
+ /** A status or priority of null or '' was not given. */
35
+ const blank = (v) => v === null || v === undefined || v === '';
36
+
37
+ /**
38
+ * Check the fields a create or update carries. Only what is present is
39
+ * checked, and a null or '' status or priority counts as absent. A CHANGE to
40
+ * something outside the lists is refused rather than stored, since a stray
41
+ * status or priority falls out of every filter and sort - but a value equal to
42
+ * what the task already holds is let through: todo-items keeps these as free
43
+ * text, so a legacy or API-made task may hold 'urgent' or 'High', and the
44
+ * editor sends it back untouched with any edit.
45
+ *
46
+ * @param {object} body
47
+ * @param {object} [stored] - the task's current data, on an update
48
+ * @returns {string|null} the problem, or null
49
+ */
50
+ export function todoFieldError(body, stored = {}) {
51
+ if (!body || typeof body !== 'object') return null;
52
+ const same = (key) => key in body && stored && body[key] === stored[key];
53
+ if (!blank(body.status) && !same('status') && !TODO_STATUSES.includes(body.status)) return `status must be one of ${TODO_STATUSES.join(', ')}`;
54
+ if (!blank(body.priority) && !same('priority') && !TODO_PRIORITIES.includes(body.priority)) return `priority must be one of ${TODO_PRIORITIES.join(', ')}`;
55
+ if ('dueAt' in body && !blank(body.dueAt) && !same('dueAt')
56
+ && !(typeof body.dueAt === 'string' && /^\d{4}-\d{2}-\d{2}/.test(body.dueAt))) return 'dueAt must be a YYYY-MM-DD day or null';
57
+ if ('text' in body && !same('text') && (typeof body.text !== 'string' || !body.text.trim())) return 'text cannot be empty';
58
+ return null;
59
+ }
60
+
26
61
  /** 'YYYY-MM-DD' of a date, in the server's local time. */
27
62
  function localDay(d) {
28
63
  const pad = (n) => String(n).padStart(2, '0');
@@ -65,7 +100,7 @@ export async function todoCalendarItems({from, to, user}) {
65
100
  endAt: `${addDays(day, 1)}T00:00:00.000Z`,
66
101
  allDay: true,
67
102
  url: '#/todo',
68
- meta: t.priority === 'high' ? 'high priority' : ''
103
+ meta: isHigh(t.priority) ? `${t.priority} priority` : ''
69
104
  }));
70
105
  }
71
106
 
@@ -124,17 +159,16 @@ export async function todoRoutes(fastify, opts = {}) {
124
159
  const daysBetween = (a, b) => Math.round((Date.parse(a) - Date.parse(b)) / 86_400_000);
125
160
  const overdue = open.filter(t => dayOf(t) && dayOf(t) < today);
126
161
  const dueToday = open.filter(t => dayOf(t) === today);
127
- const PRIORITY = {high: 0, medium: 1, low: 2};
128
162
 
129
163
  // Most urgent first: overdue (oldest first), due today, then by due day, then priority.
130
164
  const urgent = [...open].sort((a, b) =>
131
165
  (dayOf(a) ?? '9999') .localeCompare(dayOf(b) ?? '9999')
132
- || (PRIORITY[a.priority] ?? 1) - (PRIORITY[b.priority] ?? 1));
166
+ || priorityRank(a.priority) - priorityRank(b.priority));
133
167
  const items = urgent.slice(0, 5).map(t => {
134
168
  const day = dayOf(t);
135
169
  const d = day ? daysBetween(day, today) : null;
136
170
  const when = d === null ? '' : d < 0 ? `${plural(-d, 'day')} overdue` : d === 0 ? 'due today' : d === 1 ? 'due tomorrow' : `due in ${d} days`;
137
- const meta = [when, t.priority === 'high' ? 'high priority' : ''].filter(Boolean).join(' · ');
171
+ const meta = [when, isHigh(t.priority) ? `${t.priority} priority` : ''].filter(Boolean).join(' · ');
138
172
  return {text: clip(t.text), meta, tone: d !== null && d < 0 ? 'danger' : undefined};
139
173
  });
140
174
 
@@ -178,12 +212,15 @@ export async function todoRoutes(fastify, opts = {}) {
178
212
  return reply.code(400).send({ error: 'text is required' });
179
213
  }
180
214
 
215
+ const problem = todoFieldError(request.body);
216
+ if (problem) return reply.code(400).send({error: problem});
217
+
181
218
  const {dueAt} = request.body ?? {};
182
219
  const entry = await createEntry(SLUG, {
183
220
  text: text.trim(),
184
- status: status ?? 'not-started',
185
- priority: priority ?? 'medium',
186
- dueAt: dueAt ?? null,
221
+ status: blank(status) ? 'not-started' : status,
222
+ priority: blank(priority) ? 'medium' : priority,
223
+ dueAt: dueAt || null,
187
224
  userId: uid ?? null
188
225
  }, {createdBy: uid, source: 'admin'});
189
226
 
@@ -194,15 +231,18 @@ export async function todoRoutes(fastify, opts = {}) {
194
231
  fastify.put('/items/:id', {preHandler: [authenticate, requirePermission('todo', 'update')]}, async (request, reply) => {
195
232
  const uid = userId(request);
196
233
  const { id } = request.params;
197
- const patch = request.body ?? {};
198
-
234
+ const patch = request.body && typeof request.body === 'object' ? request.body : {};
199
235
  const entry = await getEntry(SLUG, id);
200
236
  if (!entry) return reply.code(404).send({error: 'Todo not found'});
201
237
  if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Todo not found'});
202
238
 
239
+ const problem = todoFieldError(patch, entry.data);
240
+ if (problem) return reply.code(400).send({error: problem});
241
+
203
242
  const merged = {...entry.data};
204
243
  for (const key of ['text', 'status', 'priority', 'dueAt']) {
205
- if (key in patch) merged[key] = key === 'text' ? String(patch[key]).trim() : patch[key];
244
+ if (!(key in patch) || (blank(patch[key]) && key !== 'dueAt')) continue;
245
+ merged[key] = key === 'text' ? String(patch[key]).trim() : key === 'dueAt' ? (patch.dueAt || null) : patch[key];
206
246
  }
207
247
 
208
248
  const updated = await updateEntry(SLUG, id, merged);
package/server/server.js CHANGED
@@ -637,7 +637,7 @@ const coreToolItems = [
637
637
  {text: 'Todo', url: '#/todo', icon: 'check-square', permission: 'todo', countUrl: '/api/todo/count'},
638
638
  {text: 'Calculator', url: '#/calculator', icon: 'calculator', permission: 'calculator'},
639
639
  {text: 'Analytics', url: '#/analytics', icon: 'chart-bar', permission: 'analytics', countUrl: '/api/analytics/active-count'},
640
- {text: 'Contacts', url: '#/contacts', icon: 'users', permission: 'contacts'},
640
+ {text: 'Contacts', url: '#/contacts', icon: 'users', permission: 'contacts', countUrl: '/api/contacts/count', inventory: true},
641
641
  {text: 'SEO', url: '#/seo', icon: 'search', permission: 'seo', countUrl: '/api/seo/count'}
642
642
  ];
643
643
  for (const item of coreToolItems) {
@@ -173,7 +173,7 @@
173
173
  </script>
174
174
 
175
175
  <!-- Site initialisation -->
176
- <script src="/public/js/site.js?v=20261014-menu-entrance" type="module"></script>
176
+ <script src="/public/js/site.js?v=20261015-menu-current-branch" type="module"></script>
177
177
  <script src="/public/js/collection-context.js?v=20261010-utf8-payloads" type="module"></script>
178
178
  {{ctxMenusModule}}
179
179