@port60/template-kit 0.20.3 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@port60/template-kit",
3
- "version": "0.20.3",
3
+ "version": "0.21.0",
4
4
  "description": "Build Port60 site templates locally: scaffold, live-preview, validate against the platform contract, and package for studio upload. AI-agent ready: every scaffold ships AGENTS.md and validate emits machine-readable JSON.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -69,6 +69,9 @@ platform accepts it; if it fails here, the upload will fail identically.
69
69
  platform still owns the document head, consent and identity.
70
70
  - \`supports.pages\` owns section based bodies for \`home\` and \`about\` through the declared
71
71
  renderers in \`sections/\`.
72
+ - \`compositions\` owns each page's preferred order: the section types the design is built around,
73
+ each \`core\`, \`recommended\` or \`optional\`. An organisation may reorder, add or remove;
74
+ removing a core section warns them, it never stops them.
72
75
  - \`supports.pageTemplates: ["events"]\` owns the events listing only. Event details, RSVP and
73
76
  ticket purchase remain platform owned.
74
77
  - \`supports.pageTemplates: ["course"]\` owns a course detail presentation only. The course
@@ -124,6 +127,20 @@ excludes it, and conformance proofs always run on the canonical fixtures.
124
127
  - Navigation can contain two levels below a top item. Render every supplied child and branch on
125
128
  optional \`group\`, \`description\`, \`imageUrl\` and \`megaMenu\` promo metadata. Never hardcode
126
129
  menu groups that are not in \`nav\`.
130
+ - Navigation highlights are optional design support, not implied by the \`nav\` behaviour.
131
+ Declare \`supports.navigationHighlights: true\` only when your layout renders one supplied
132
+ \`site.nav.items[].megaMenu.promo\` card per expanded top-level menu. Otherwise declare false.
133
+ The platform resolves linked content into \`title\`, \`text\`, \`href\`, \`label\` and optional
134
+ \`imageUrl\`; no entity lookup belongs in a template. Preserve a text-only card when its image
135
+ is absent, omit an absent card and keep normal navigation links. Validation proves the explicit
136
+ declaration; missing declarations never enable the editor feature automatically.
137
+ - Field markers are optional and additive: mark the node that shows a field with
138
+ \`data-p60-field="title"\`, or \`data-p60-field="items.{{ forloop.index0 }}.label"\` for an
139
+ entry in a list, and declare \`supports.fieldMarkers: true\`. The charity then types into the
140
+ real heading on the page instead of into a side panel. The address is the field's name inside
141
+ that section's own content, and the marked node holds that field and nothing else, so wrap the
142
+ value in a span when punctuation or other copy sits beside it. Mark what you like; the editor
143
+ puts a caret only in plain text, and a marker naming nothing is a warning, never a failure.
127
144
  - Dynamic sections include appeals (\`causes\`), programmes (\`services\`), resources and
128
145
  locations. Derive or omit when a collection is empty and use the supplied URLs rather than
129
146
  constructing routes.
@@ -220,7 +220,7 @@
220
220
  "availableIn": [
221
221
  "layout"
222
222
  ],
223
- "description": "Optional mega-menu presentation metadata on a top-level item: columns (2 to 4) and an optional promo object with title, text, href, label and imageUrl. It is content only; the template owns hover, focus and responsive presentation in CSS.",
223
+ "description": "Optional presentation metadata on a top-level item. promo is at most one resolved navigation highlight with title, text, href, label and optional imageUrl, never an array. A linked course, event or campaign and a custom card produce the same fields; templates never fetch the linked record. Declare supports.navigationHighlights only when the layout renders this card and safely omits absent data. columns (2 to 4) is legacy metadata; responsive column layout belongs to the template.",
224
224
  "deprecated": true
225
225
  },
226
226
  {
@@ -591,7 +591,7 @@
591
591
  "availableIn": [
592
592
  "section:campaigns"
593
593
  ],
594
- "description": "What the tenant calls their campaigns section (\"Campaigns\" by default), the heading fallback.",
594
+ "description": "The platform word for the campaigns collection (\"Campaigns\"), the section's heading fallback. It does not vary: a tenant's own word for a link lives in their menu.",
595
595
  "deprecated": true
596
596
  },
597
597
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "The curated font catalogue behind `font` settings knobs. The tenant picks a FAMILY; the template's typographic system (weights, sizes, tracking) is author-owned, the host requests the template's declared weights for the chosen family, clamped to the family's real weights below (nearest wins). Curated, not the raw Google catalogue: every family here is open-licence (OFL/Apache), quality-checked, and tagged so non-technical admins choose by vibe. Served from Bunny Fonts (a privacy-first, EU-hosted drop-in mirror of the Google Fonts catalogue, same families, same css2 API). Additive-only within a contract major; never remove a family (published sites reference them).",
2
+ "description": "The curated font catalogue behind `font` settings knobs. A family marked `hosted` is served by the platform itself (charity-site public/fonts, declared in global.css) and never requested from the provider. The tenant picks a FAMILY; the template's typographic system (weights, sizes, tracking) is author-owned, the host requests the template's declared weights for the chosen family, clamped to the family's real weights below (nearest wins). Curated, not the raw Google catalogue: every family here is open-licence (OFL/Apache), quality-checked, and tagged so non-technical admins choose by vibe. Served from Bunny Fonts (a privacy-first, EU-hosted drop-in mirror of the Google Fonts catalogue, same families, same css2 API). Additive-only within a contract major; never remove a family (published sites reference them).",
3
3
  "provider": {
4
4
  "cssBase": "https://fonts.bunny.net/css2",
5
5
  "preconnect": "https://fonts.bunny.net"
@@ -62,6 +62,8 @@
62
62
  { "name": "Amiri", "category": "arabic", "tags": ["arabic", "serif", "classic"], "weights": [400, 700], "stack": "serif" },
63
63
  { "name": "Noto Kufi Arabic", "category": "arabic", "tags": ["arabic", "sans", "geometric"], "weights": [300, 400, 500, 600, 700], "stack": "sans-serif" },
64
64
  { "name": "Noto Naskh Arabic", "category": "arabic", "tags": ["arabic", "serif", "readable"], "weights": [400, 500, 600, 700], "stack": "serif" },
65
- { "name": "Scheherazade New", "category": "arabic", "tags": ["arabic", "serif", "traditional"], "weights": [400, 500, 600, 700], "stack": "serif" }
65
+ { "name": "Scheherazade New", "category": "arabic", "tags": ["arabic", "serif", "traditional"], "weights": [400, 500, 600, 700], "stack": "serif" },
66
+ { "name": "KFGQPC HAFS Uthmanic Script", "category": "arabic", "tags": ["arabic", "naskh", "quran", "mushaf"], "weights": [400], "stack": "serif", "hosted": true },
67
+ { "name": "KFGQPC Nastaleeq", "category": "arabic", "tags": ["arabic", "nastaliq", "quran", "mushaf"], "weights": [400], "stack": "serif", "hosted": true }
66
68
  ]
67
69
  }
@@ -149,6 +149,17 @@
149
149
  "default": false,
150
150
  "description": "True when the layout renders the `worship` context (the prayer/service-times rail). Declared honesty: the choosers badge worship-enabled tenants toward looks that show their times, and warn on looks that don't. Requires supports.layout; the layout must actually branch on `worship`."
151
151
  },
152
+ "navigationHighlights": {
153
+ "type": "boolean",
154
+ "default": false,
155
+ "description": "True when the layout displays the optional single site.nav.items[].megaMenu.promo card inside each expanded top-level menu. Requires supports.layout. The validator proves title, text, image and linked label render together; image-free and absent highlights must remain safe. The platform resolves linked courses, events and campaigns into the same promo fields as custom highlights. This flag does not enable a general mega menu or control column counts. Missing or false disables highlight editing for this exact template version."
156
+ },
157
+ "fieldMarkers": {
158
+ "type": "boolean",
159
+ "default": false,
160
+ "description": "True when the sections mark which node shows which field with data-p60-field, so the editor can put the caret on the page instead of in a side panel. The address is inside that section's own content: data-p60-field=\"title\", or data-p60-field=\"items.{{ forloop.index0 }}.label\" for an entry in a list. Purely additive: the attribute is markup, the contract is untouched, and a template that marks nothing keeps the section-level editor. The validator reads the marks from the source and warns on any that does not name a field of that section. Missing or false leaves this exact template version editing in the panel.",
161
+ "$comment": "Follows the navigationHighlights pattern: declared honesty, proven by the validator."
162
+ },
152
163
  "heroImagery": {
153
164
  "type": "boolean",
154
165
  "default": false,
@@ -265,6 +276,28 @@
265
276
  "default": [],
266
277
  "description": "One-click LOOKS: author-named bundles of knob values (scheme + fonts + width\u2026) applied together. Everything remains individually adjustable afterwards."
267
278
  },
279
+ "compositions": {
280
+ "type": "object",
281
+ "additionalProperties": false,
282
+ "description": "The template's preferred composition per supported page (site editor stage 4): the ordered section types it designs the page around, each with a role. `core` sections carry the design (removing one warns, never locks), `recommended` ones are on by default, `optional` ones are offered. A page a tenant never edited renders this composition; the Pages editor offers it as the order to reset to.",
283
+ "propertyNames": { "enum": ["home", "about"] },
284
+ "patternProperties": {
285
+ "^(home|about)$": {
286
+ "type": "array",
287
+ "minItems": 1,
288
+ "maxItems": 20,
289
+ "items": {
290
+ "type": "object",
291
+ "additionalProperties": false,
292
+ "required": ["type", "role"],
293
+ "properties": {
294
+ "type": { "type": "string", "minLength": 1, "maxLength": 50, "description": "A section type from supports.sections." },
295
+ "role": { "type": "string", "enum": ["core", "recommended", "optional"], "description": "core: the design depends on it; recommended: on by default; optional: offered." }
296
+ }
297
+ }
298
+ }
299
+ }
300
+ },
268
301
  "settings": {
269
302
  "type": "object",
270
303
  "additionalProperties": false,
@@ -81,18 +81,40 @@
81
81
  "sectionTypes": [
82
82
  "appealGrid",
83
83
  "articles",
84
+ "campaigns",
84
85
  "cta",
85
86
  "emergency",
86
87
  "events",
87
88
  "hero",
88
89
  "homeHero",
89
90
  "impact",
91
+ "impactMap",
92
+ "locations",
90
93
  "people",
94
+ "programmes",
95
+ "resources",
91
96
  "stats",
92
97
  "values",
93
98
  "whatsOn"
94
99
  ],
95
100
  "islands": {
101
+ "article_comments": [
102
+ ".article-comment",
103
+ ".article-comment-form",
104
+ ".article-comment-head",
105
+ ".article-comment-list",
106
+ ".article-comments",
107
+ ".article-comments-gate",
108
+ ".article-comments-note"
109
+ ],
110
+ "article_engagement": [
111
+ ".article-engage",
112
+ ".article-share-btn",
113
+ ".article-share-copied",
114
+ ".article-share-group",
115
+ ".article-share-word",
116
+ ".article-views"
117
+ ],
96
118
  "course_enrol": [
97
119
  ".course-enrol",
98
120
  ".course-pay-panel",
@@ -102,6 +124,7 @@
102
124
  ".course-variant-list"
103
125
  ],
104
126
  "donation_widget": [
127
+ ".action-card",
105
128
  ".amount-grid",
106
129
  ".amount-list",
107
130
  ".amount-option",
@@ -123,29 +146,101 @@
123
146
  ".carousel-slide",
124
147
  ".events-carousel"
125
148
  ],
149
+ "form": [
150
+ ".public-form",
151
+ ".public-form-check",
152
+ ".public-form-field",
153
+ ".public-form-head",
154
+ ".public-form-message",
155
+ ".public-form-privacy",
156
+ ".public-form-submit"
157
+ ],
126
158
  "hero_carousel": [
127
159
  ".hero-carousel",
128
160
  ".hero-dot",
129
161
  ".hero-dot--active",
130
162
  ".hero-dots",
131
163
  ".hero-slide",
164
+ ".hero-slide--active",
132
165
  ".hero-slide-img",
133
166
  ".hero-slide-scrim",
134
167
  ".hero-slides"
135
168
  ],
169
+ "language_switch": [
170
+ ".language-switch",
171
+ ".language-switch-label",
172
+ ".language-switch-select"
173
+ ],
136
174
  "latest_articles": [
137
175
  ".article-card",
138
176
  ".article-card-body",
139
177
  ".article-grid",
140
178
  ".article-strip-cta"
141
179
  ],
180
+ "map": [
181
+ ".impact-map",
182
+ ".impact-map-canvas",
183
+ ".impact-map-card",
184
+ ".impact-map-card-cta",
185
+ ".impact-map-card-kind",
186
+ ".impact-map-card-summary",
187
+ ".impact-map-card-title",
188
+ ".impact-map-empty",
189
+ ".impact-map-head",
190
+ ".impact-map-intro",
191
+ ".impact-map-pin",
192
+ ".impact-map-pin--active"
193
+ ],
142
194
  "member_menu": [
143
195
  ".nav-account",
196
+ ".nav-account-caret",
197
+ ".nav-account-icon",
144
198
  ".nav-account-menu",
199
+ ".nav-account-name",
145
200
  ".nav-account-toggle",
146
201
  ".nav-p60-signin",
202
+ ".nav-p60-signin-label",
147
203
  ".p60-mark"
148
204
  ],
205
+ "newsletter_signup": [
206
+ ".newsletter-consent",
207
+ ".newsletter-email",
208
+ ".newsletter-fields",
209
+ ".newsletter-label",
210
+ ".newsletter-message",
211
+ ".newsletter-signup",
212
+ ".newsletter-submit"
213
+ ],
214
+ "next_prayer": [
215
+ ".next-prayer",
216
+ ".next-prayer-countdown",
217
+ ".next-prayer-label",
218
+ ".next-prayer-name",
219
+ ".next-prayer-time"
220
+ ],
221
+ "primary_action_widget": [
222
+ ".action-card",
223
+ ".donate-card",
224
+ ".volunteer-card"
225
+ ],
226
+ "search": [
227
+ ".site-search",
228
+ ".site-search-context",
229
+ ".site-search-corrected",
230
+ ".site-search-empty",
231
+ ".site-search-fields",
232
+ ".site-search-form",
233
+ ".site-search-input",
234
+ ".site-search-kind",
235
+ ".site-search-label",
236
+ ".site-search-link",
237
+ ".site-search-more",
238
+ ".site-search-result",
239
+ ".site-search-results",
240
+ ".site-search-submit",
241
+ ".site-search-summary",
242
+ ".site-search-title"
243
+ ],
149
244
  "whats_on_strip": [
150
245
  ".info-event-chip",
151
246
  ".info-events-bar",
@@ -153,6 +248,12 @@
153
248
  ]
154
249
  },
155
250
  "contextVariables": {
251
+ "article": [
252
+ "page:article"
253
+ ],
254
+ "articles": [
255
+ "page:articles"
256
+ ],
156
257
  "brand.footerLogoUrl": [
157
258
  "layout"
158
259
  ],
@@ -170,6 +271,36 @@
170
271
  "layout",
171
272
  "section"
172
273
  ],
274
+ "campaigns": [
275
+ "section:campaigns"
276
+ ],
277
+ "campaigns[].href": [
278
+ "section:campaigns"
279
+ ],
280
+ "campaigns[].imageUrl": [
281
+ "section:campaigns"
282
+ ],
283
+ "campaigns[].summary": [
284
+ "section:campaigns"
285
+ ],
286
+ "campaigns[].title": [
287
+ "section:campaigns"
288
+ ],
289
+ "campaignsLabel": [
290
+ "section:campaigns"
291
+ ],
292
+ "categories": [
293
+ "page:article",
294
+ "page:articles"
295
+ ],
296
+ "causes": [
297
+ "section:appealGrid",
298
+ "section:emergency"
299
+ ],
300
+ "causes[]": [
301
+ "section:appealGrid",
302
+ "section:emergency"
303
+ ],
173
304
  "course": [
174
305
  "page:course"
175
306
  ],
@@ -288,6 +419,30 @@
288
419
  "latestArticles[].title": [
289
420
  "section:articles"
290
421
  ],
422
+ "locale": [
423
+ "layout"
424
+ ],
425
+ "locale.code": [
426
+ "layout"
427
+ ],
428
+ "locale.direction": [
429
+ "layout"
430
+ ],
431
+ "locale.languages": [
432
+ "layout"
433
+ ],
434
+ "locations": [
435
+ "section:locations"
436
+ ],
437
+ "locations[]": [
438
+ "section:locations"
439
+ ],
440
+ "nav.derived": [
441
+ "layout"
442
+ ],
443
+ "nav.footer": [
444
+ "layout"
445
+ ],
291
446
  "nav.items": [
292
447
  "layout"
293
448
  ],
@@ -297,15 +452,92 @@
297
452
  "nav.items[].cta": [
298
453
  "layout"
299
454
  ],
455
+ "nav.items[].description": [
456
+ "layout"
457
+ ],
458
+ "nav.items[].external": [
459
+ "layout"
460
+ ],
461
+ "nav.items[].group": [
462
+ "layout"
463
+ ],
300
464
  "nav.items[].href": [
301
465
  "layout"
302
466
  ],
467
+ "nav.items[].imageUrl": [
468
+ "layout"
469
+ ],
303
470
  "nav.items[].label": [
304
471
  "layout"
305
472
  ],
473
+ "nav.items[].megaMenu": [
474
+ "layout"
475
+ ],
476
+ "relatedArticles": [
477
+ "page:article"
478
+ ],
479
+ "resources": [
480
+ "section:resources"
481
+ ],
482
+ "resources[]": [
483
+ "section:resources"
484
+ ],
306
485
  "section": [
307
486
  "section"
308
487
  ],
488
+ "section.action": [
489
+ "section"
490
+ ],
491
+ "section.actionStyle": [
492
+ "section"
493
+ ],
494
+ "section.primary": [
495
+ "section"
496
+ ],
497
+ "section.secondary": [
498
+ "section"
499
+ ],
500
+ "selectedCategory": [
501
+ "page:articles"
502
+ ],
503
+ "services": [
504
+ "section:programmes"
505
+ ],
506
+ "services[]": [
507
+ "section:programmes"
508
+ ],
509
+ "site": [
510
+ "layout",
511
+ "section"
512
+ ],
513
+ "site.actions": [
514
+ "layout",
515
+ "section"
516
+ ],
517
+ "site.brand": [
518
+ "layout",
519
+ "section"
520
+ ],
521
+ "site.content": [
522
+ "layout",
523
+ "section"
524
+ ],
525
+ "site.focus": [
526
+ "layout",
527
+ "section"
528
+ ],
529
+ "site.locale": [
530
+ "layout",
531
+ "section"
532
+ ],
533
+ "site.nav": [
534
+ "layout",
535
+ "section"
536
+ ],
537
+ "site.socials": [
538
+ "layout",
539
+ "section"
540
+ ],
309
541
  "socials": [
310
542
  "layout"
311
543
  ],
@@ -327,12 +559,24 @@
327
559
  "worship.congregationLabel": [
328
560
  "layout"
329
561
  ],
562
+ "worship.hijri": [
563
+ "layout"
564
+ ],
330
565
  "worship.href": [
331
566
  "layout"
332
567
  ],
568
+ "worship.jumah": [
569
+ "layout"
570
+ ],
571
+ "worship.next": [
572
+ "layout"
573
+ ],
333
574
  "worship.note": [
334
575
  "layout"
335
576
  ],
577
+ "worship.observances": [
578
+ "layout"
579
+ ],
336
580
  "worship.times": [
337
581
  "layout"
338
582
  ],
@@ -356,6 +600,9 @@
356
600
  "--ink",
357
601
  "--line",
358
602
  "--line-strong",
603
+ "--map-mode",
604
+ "--map-projection",
605
+ "--marker",
359
606
  "--muted",
360
607
  "--primary",
361
608
  "--primary-soft",
@@ -393,6 +640,8 @@
393
640
  "Inter",
394
641
  "Josefin Sans",
395
642
  "Jost",
643
+ "KFGQPC HAFS Uthmanic Script",
644
+ "KFGQPC Nastaleeq",
396
645
  "Karla",
397
646
  "Lato",
398
647
  "Libre Baskerville",
@@ -431,6 +680,7 @@
431
680
  ],
432
681
  "properties": [
433
682
  "changelog",
683
+ "compositions",
434
684
  "description",
435
685
  "fonts",
436
686
  "format",
@@ -438,7 +688,9 @@
438
688
  "label",
439
689
  "looks",
440
690
  "name",
691
+ "requiresCapabilities",
441
692
  "settings",
693
+ "suitsProfiles",
442
694
  "supports",
443
695
  "version"
444
696
  ]
@@ -1,4 +1,4 @@
1
- // LiquidJS DoS budgets — ONE definition for the production engine, the validator and the studio
1
+ // LiquidJS DoS budgets. ONE definition for the production engine, the validator and the studio
2
2
  // preview, so "validated ⇒ renders in production" includes the resource ceiling. Third-party
3
3
  // templates run in the shared SSR process (developer program T1.3): without these, an untrusted
4
4
  // `{% for %}`/`{% capture %}` burns CPU/memory for every tenant on the box. The numbers are far
@@ -1,6 +1,6 @@
1
- // CONTENT FOOTPRINTS (content model v1 — docs/template-content-model.md): which site.* paths a
1
+ // CONTENT FOOTPRINTS (content model v1, docs/template-content-model.md): which site.* paths a
2
2
  // template touches, decidable at publish because the dialect is closed. Lives in the ENGINE so
3
- // the production loader computes the same footprint the validator stamps — the render path
3
+ // the production loader computes the same footprint the validator stamps, the render path
4
4
  // fetches precisely what a template reads, and the two can never disagree.
5
5
  import contentModel from '../contract/v1/content-model.json' with { type: 'json' };
6
6
 
@@ -10,7 +10,7 @@ export { contentModel };
10
10
  // The dialect is closed, so the site paths a template reads are decidable from its sources: every
11
11
  // reference is a literal `site.…` chain (dynamic indexing is refused below, and aliasing the tree
12
12
  // itself is refused so a chain can never hide behind a variable). The footprint is collection-
13
- // granular — `content.events` — because item fields ride the collection fetch.
13
+ // granular, `content.events`, because item fields ride the collection fetch.
14
14
 
15
15
  const CHAIN = /\bsite((?:\.[A-Za-z_][A-Za-z0-9_-]*|\[\s*'[^']*'\s*\]|\[\s*"[^"]*"\s*\])+)/g;
16
16
  const DYNAMIC_INDEX = /\bsite(?:\.[A-Za-z_][A-Za-z0-9_-]*|\[\s*(?:'[^']*'|"[^"]*")\s*\])*\[\s*(?!\s*['"])[^\]]/;
@@ -26,7 +26,7 @@ function segmentsOf(chain) {
26
26
 
27
27
  /**
28
28
  * Extract the content footprint from the artifact's liquid sources.
29
- * Returns { footprint: string[], minModelVersion: string|null, errors: string[] } — footprint
29
+ * Returns { footprint: string[], minModelVersion: string|null, errors: string[] }, footprint
30
30
  * entries are 'brand' | 'nav' | 'socials' | 'locale' | 'content.<collection>'.
31
31
  */
32
32
  export function extractContentFootprint(files) {
@@ -38,10 +38,10 @@ export function extractContentFootprint(files) {
38
38
  for (const [path, source] of Object.entries(files)) {
39
39
  if (!path.endsWith('.liquid')) continue;
40
40
  if (DYNAMIC_INDEX.test(source)) {
41
- errors.push(`${path}: dynamic indexing into site.* is refused — the content footprint must be decidable at publish. Read a named collection instead.`);
41
+ errors.push(`${path}: dynamic indexing into site.* is refused, the content footprint must be decidable at publish. Read a named collection instead.`);
42
42
  }
43
43
  if (BARE_SITE.test(source)) {
44
- errors.push(`${path}: aliasing or outputting the bare site tree is refused — reference a named path (site.brand, site.content.<collection>) so the footprint stays decidable.`);
44
+ errors.push(`${path}: aliasing or outputting the bare site tree is refused, reference a named path (site.brand, site.content.<collection>) so the footprint stays decidable.`);
45
45
  }
46
46
  let m;
47
47
  CHAIN.lastIndex = 0;
@@ -53,17 +53,17 @@ export function extractContentFootprint(files) {
53
53
  touched.add(head);
54
54
  } else if (head === 'content') {
55
55
  if (segs.length < 2) {
56
- errors.push(`${path}: references site.content without a collection — name the collection (the footprint must be decidable).`);
56
+ errors.push(`${path}: references site.content without a collection, name the collection (the footprint must be decidable).`);
57
57
  continue;
58
58
  }
59
59
  const collection = segs[1];
60
60
  if (!collections[collection]) {
61
- errors.push(`${path}: site.content.${collection} is not in content model ${contentModel.version} — see contract/v1/content-model.json for the collections that exist.`);
61
+ errors.push(`${path}: site.content.${collection} is not in content model ${contentModel.version}, see contract/v1/content-model.json for the collections that exist.`);
62
62
  continue;
63
63
  }
64
64
  touched.add(`content.${collection}`);
65
65
  } else {
66
- errors.push(`${path}: site.${head} is not part of the content model — site carries brand, nav, socials, locale and content.*.`);
66
+ errors.push(`${path}: site.${head} is not part of the content model, site carries brand, nav, socials, locale and content.*.`);
67
67
  }
68
68
  }
69
69
  }
@@ -1,6 +1,6 @@
1
1
  // Applies the Port60 dialect (contract/v1/dialect.json) to a LiquidJS instance. Plain JS on purpose:
2
2
  // this exact module is imported by BOTH the rendering engine (Vite-bundled into charity-site) and the
3
- // Node conformance validator (scripts/validate-template.mjs) — one enforcement implementation, so the
3
+ // Node conformance validator (scripts/validate-template.mjs), one enforcement implementation, so the
4
4
  // validator can never disagree with production rendering. The dialect object is passed IN (parsed
5
5
  // JSON); this module does no file loading.
6
6
 
@@ -8,7 +8,7 @@
8
8
  export const ISLAND_MARK_START = '\u0000P60_ISLAND:';
9
9
  export const ISLAND_MARK_END = '\u0000';
10
10
 
11
- /** The layout's {% content %} slot rides the island marker protocol under a reserved name — the
11
+ /** The layout's {% content %} slot rides the island marker protocol under a reserved name, the
12
12
  * slash makes collision with registry island names (snake_case words) impossible. */
13
13
  export const CONTENT_SLOT = 'P60/content';
14
14
 
@@ -45,12 +45,12 @@ export function configureDialect(liquid, dialect, islandNames) {
45
45
  async render(ctx) {
46
46
  const name = String(await this.liquid.evalValue(this.args, ctx));
47
47
  if (!islandNames.has(name)) {
48
- return ''; // unknown island — omitted at render; rejected at publish by the validator
48
+ return ''; // unknown island, omitted at render; rejected at publish by the validator
49
49
  }
50
50
  return `${ISLAND_MARK_START}${name}${ISLAND_MARK_END}`;
51
51
  }
52
52
  });
53
- // {% content %} — the layout's page slot (contract v1, roadmap 8.2). Registered everywhere; a
53
+ // {% content %}, the layout's page slot (contract v1, roadmap 8.2). Registered everywhere; a
54
54
  // section that emits it is rejected by the validator and stripped by the section renderer.
55
55
  liquid.registerTag('content', {
56
56
  parse() {},
@@ -1,2 +1,2 @@
1
- // GENERATED by scripts/build-behaviors-runtime.mjs — do not edit by hand.
1
+ // GENERATED by scripts/build-behaviors-runtime.mjs, do not edit by hand.
2
2
  "use strict";var p60Behaviors=(()=>{var N=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var F=(i,d)=>{for(var e in d)N(i,e,{get:d[e],enumerable:!0})},O=(i,d,e,n)=>{if(d&&typeof d=="object"||typeof d=="function")for(let t of B(d))!R.call(i,t)&&t!==e&&N(i,t,{get:()=>d[t],enumerable:!(n=C(d,t))||n.enumerable});return i};var _=i=>O(N({},"__esModule",{value:!0}),i);var se={};F(se,{initBehaviors:()=>ie});var M=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches;function v(i,d){let e=`data-p60-bound-${d}`;return i.hasAttribute(e)?!0:(i.setAttribute(e,""),!1)}var q=.15;function H(i,d){if(i.length===0)return;if(M()||!("IntersectionObserver"in window)){i.forEach(d);return}let e=new IntersectionObserver(n=>{for(let t of n){if(!t.isIntersecting)continue;(t.boundingClientRect.height>=window.innerHeight||t.intersectionRatio>=q-.001)&&(e.unobserve(t.target),d(t.target))}},{rootMargin:"0px 0px -10% 0px",threshold:[0,q]});i.forEach(n=>e.observe(n))}function D(i){let d=Array.from(i.querySelectorAll("[data-p60-reveal-group]")).filter(n=>!v(n,"reveal-group"));for(let n of d)Array.from(n.children).forEach((t,r)=>{t.style.setProperty("--p60-reveal-index",String(r)),t.classList.add("p60-reveal-child")});H(d,n=>n.classList.add("is-revealed"));let e=Array.from(i.querySelectorAll("[data-p60-reveal]")).filter(n=>!v(n,"reveal"));H(e,n=>n.classList.add("is-revealed"))}var $=/\d[\d,   ]*(?:\.\d+)?/;function V(i){let d=Array.from(i.querySelectorAll("[data-p60-count]")).filter(e=>!v(e,"count"));H(d,e=>{e.classList.add("is-revealed");let n=e.textContent??"",t=$.exec(n);if(!t)return;let r=t[0].replace(/[,   ]+$/,""),l=n.slice(0,t.index),o=n.slice(t.index+r.length),s=/[,   ]/.test(r),c=r.includes(".")?r.length-r.indexOf(".")-1:0,a=Number.parseFloat(r.replace(/[,   ]/g,"")),u=Number.parseFloat(e.getAttribute("data-p60-count")??""),m=Number.isFinite(u)?u:a;if(!Number.isFinite(m)||M())return;let f=Math.min(Number.parseInt(e.getAttribute("data-p60-count-duration")??"1200",10)||1200,4e3),p=new Intl.NumberFormat(document.documentElement.lang||void 0,{useGrouping:s,minimumFractionDigits:c,maximumFractionDigits:c}),b=performance.now(),E=y=>{let A=Math.min((y-b)/f,1),g=1-Math.pow(1-A,3);e.textContent=A<1?l+p.format(m*g)+o:n,A<1&&requestAnimationFrame(E)};requestAnimationFrame(E)})}function Y(i){let d=Array.from(i.querySelectorAll("[data-p60-progress]")).filter(e=>!v(e,"progress"));if(!M()){for(let e of d){let n=e.style.width||getComputedStyle(e).width;e.style.transition="none",e.style.width="0px",e.offsetWidth,e.style.transition="width 1.1s cubic-bezier(0.22, 1, 0.36, 1)",e.dataset.p60ProgressTarget=n}H(d,e=>{e.classList.add("is-revealed"),e.style.width=e.dataset.p60ProgressTarget??""})}}function z(i){let d=Array.from(i.querySelectorAll("[data-p60-countdown]")).filter(e=>!v(e,"countdown"));for(let e of d){let n=Date.parse(e.getAttribute("data-p60-until")??"");if(!Number.isFinite(n))continue;let t=e.getAttribute("data-p60-countdown")||"dhm",r=t==="dhms",l=()=>{let s=n-Date.now();if(s<=0){e.classList.add("is-elapsed");let b=e.getAttribute("data-p60-elapsed-text");b&&(e.textContent=b),window.clearInterval(o);return}let c=Math.floor(s/1e3),a=Math.floor(c/86400),u=Math.floor(c%86400/3600),m=Math.floor(c%3600/60),f=c%60,p=[];t.includes("d")&&a>0&&p.push(`${a}d`),p.push(`${u}h`,`${m}m`),r&&p.push(`${f}s`),e.textContent=p.join(" ")},o=window.setInterval(l,r?1e3:15e3);l()}}function K(i){let d=Array.from(i.querySelectorAll("[data-p60-accordion]")).filter(e=>!v(e,"accordion"));for(let e of d){let n=e.getAttribute("data-p60-accordion")!=="multi",t=Array.from(e.querySelectorAll("details"));for(let r of t){r.addEventListener("toggle",()=>{if(r.classList.toggle("is-open",r.open),r.open&&n)for(let s of t)s!==r&&s.open&&(s.open=!1)}),r.open&&r.classList.add("is-open");let o=r.querySelector("summary")?.nextElementSibling;o&&!M()&&r.addEventListener("toggle",()=>{if(!r.open)return;let s=o.scrollHeight;o.style.overflow="hidden",o.animate([{height:"0px",opacity:"0"},{height:`${s}px`,opacity:"1"}],{duration:260,easing:"ease-out"}).addEventListener("finish",()=>{o.style.overflow=""})})}}}function U(i){let d=Array.from(i.querySelectorAll("[data-p60-carousel]")).filter(e=>!v(e,"carousel"));for(let e of d){let n=Array.from(e.querySelectorAll("[data-p60-slide]"));if(n.length<2)continue;let t=Math.max(0,n.findIndex(g=>g.classList.contains("is-active"))),r=[],l=document.createElement("nav");l.className="p60-carousel-dots",l.setAttribute("aria-label","Slides"),n.forEach((g,L)=>{let h=document.createElement("button");h.type="button",h.className="p60-carousel-dot",h.setAttribute("aria-label",`Go to slide ${L+1}`),h.addEventListener("click",()=>o(L,!0)),r.push(h),l.appendChild(h)}),e.appendChild(l),e.setAttribute("aria-roledescription","carousel"),e.hasAttribute("tabindex")||e.setAttribute("tabindex","0");let o=(g,L=!1)=>{t=(g+n.length)%n.length,n.forEach((h,x)=>{h.classList.toggle("is-active",x===t),x===t?(h.removeAttribute("aria-hidden"),h.removeAttribute("inert")):(h.setAttribute("aria-hidden","true"),h.setAttribute("inert",""))}),r.forEach((h,x)=>{h.classList.toggle("p60-carousel-dot--active",x===t),h.setAttribute("aria-current",x===t?"true":"false")}),e.style.setProperty("--p60-carousel-index",String(t)),L&&E()};e.querySelectorAll("[data-p60-carousel-prev]").forEach(g=>g.addEventListener("click",()=>o(t-1,!0))),e.querySelectorAll("[data-p60-carousel-next]").forEach(g=>g.addEventListener("click",()=>o(t+1,!0))),e.addEventListener("keydown",g=>{g.key==="ArrowLeft"&&o(t-1,!0),g.key==="ArrowRight"&&o(t+1,!0)});let s=0,c=0;e.addEventListener("pointerdown",g=>{s=g.clientX,c=g.clientY}),e.addEventListener("pointerup",g=>{let L=g.clientX-s,h=g.clientY-c;Math.abs(L)>40&&Math.abs(L)>Math.abs(h)&&o(t+(L<0?1:-1),!0)});let a=!1,u=!1,m,f=e.getAttribute("data-p60-carousel")==="auto"&&!M(),p=Math.max(Number.parseInt(e.getAttribute("data-p60-interval")??"6000",10)||6e3,3e3),b=()=>{m!==void 0&&window.clearInterval(m),m=void 0},E=()=>{b(),!(!f||u||M())&&(m=window.setInterval(()=>{!a&&!document.hidden&&o(t+1)},p))},y=null,A=g=>{u=g,e.classList.toggle("is-paused",u),y&&(y.setAttribute("aria-label",u?"Start slide rotation":"Stop slide rotation"),y.classList.toggle("p60-carousel-pause--stopped",u)),E()};f&&(y=document.createElement("button"),y.type="button",y.className="p60-carousel-pause",y.setAttribute("aria-label","Stop slide rotation"),y.addEventListener("click",()=>A(!u)),l.insertBefore(y,l.firstChild)),e.addEventListener("mouseenter",()=>{a=!0}),e.addEventListener("mouseleave",()=>{a=!1}),e.addEventListener("focusin",g=>{g.target!==y&&m!==void 0&&A(!0)}),window.matchMedia("(prefers-reduced-motion: reduce)").addEventListener?.("change",g=>{g.matches&&A(!0)}),o(t),E()}}function X(i){let d=Array.from(i.querySelectorAll("[data-p60-sticky-header]")).filter(e=>!v(e,"sticky-header"));for(let e of d){let n=e.getAttribute("data-p60-sticky-header")==="auto-hide",t=window.scrollY,r=!1,l=()=>{let o=window.scrollY;e.classList.toggle("is-condensed",o>24),n&&(o>160&&o>t?e.classList.add("is-hidden"):e.classList.remove("is-hidden")),t=o,r=!1};window.addEventListener("scroll",()=>{r||(r=!0,requestAnimationFrame(l))},{passive:!0}),l()}}function W(i){let d=Array.from(i.querySelectorAll("[data-p60-sticky-cta]")).filter(e=>!v(e,"sticky-cta"));for(let e of d){let n=Number.parseInt(e.getAttribute("data-p60-sticky-cta")??"480",10)||480,t=!1,r=()=>{e.classList.toggle("is-stuck",window.scrollY>n),t=!1};window.addEventListener("scroll",()=>{t||(t=!0,requestAnimationFrame(r))},{passive:!0}),r()}}function j(i){let d=Array.from(i.querySelectorAll("[data-p60-lightbox]")).filter(e=>!v(e,"lightbox"));for(let e of d){let n=Array.from(e.querySelectorAll("a[data-p60-lightbox-item]"));if(n.length===0)continue;let t=null,r=0,l=null,o=()=>{t?.remove(),t=null,e.classList.remove("is-open"),document.removeEventListener("keydown",c),l?.focus()},s=u=>{r=(u+n.length)%n.length;let m=n[r];if(!t)return;let f=t.querySelector(".p60-lightbox-img"),p=t.querySelector(".p60-lightbox-caption");f&&(f.src=m.href);let b=m.getAttribute("data-p60-caption")??m.querySelector("img")?.alt??"";p&&(p.textContent=b),f&&(f.alt=b)},c=u=>{if(u.key==="Escape"&&o(),u.key==="ArrowLeft"&&s(r-1),u.key==="ArrowRight"&&s(r+1),u.key==="Tab"&&t){let m=Array.from(t.querySelectorAll("button")),f=m.indexOf(document.activeElement),p=u.shiftKey?f<=0?m.length-1:f-1:f>=m.length-1?0:f+1;m[p]?.focus(),u.preventDefault()}},a=(u,m)=>{l=m,t=document.createElement("div"),t.className="p60-lightbox",t.setAttribute("role","dialog"),t.setAttribute("aria-modal","true"),M()&&(t.style.transition="none"),t.innerHTML='<figure><img class="p60-lightbox-img" alt=""><figcaption class="p60-lightbox-caption"></figcaption></figure>';let f=(b,E,y)=>{let A=document.createElement("button");return A.type="button",A.className=b,A.setAttribute("aria-label",E),A.addEventListener("click",y),t.appendChild(A),A};n.length>1&&(f("p60-lightbox-prev","Previous image",()=>s(r-1)),f("p60-lightbox-next","Next image",()=>s(r+1)));let p=f("p60-lightbox-close","Close",o);t.addEventListener("click",b=>{b.target===t&&o()}),document.body.appendChild(t),e.classList.add("is-open"),document.addEventListener("keydown",c),s(u),p.focus()};n.forEach((u,m)=>u.addEventListener("click",f=>{f.preventDefault(),a(m,u)}))}}function G(i){let d=Array.from(i.querySelectorAll("[data-p60-tabs]")).filter(e=>!v(e,"tabs"));for(let e of d){let n=Array.from(e.querySelectorAll("[data-p60-tab]")),t=Array.from(e.querySelectorAll("[data-p60-panel]"));if(n.length===0||t.length===0)continue;let r=o=>t.find(s=>s.getAttribute("data-p60-panel")===o.getAttribute("data-p60-tab"));n[0].closest("[data-p60-tabs] > *")?.setAttribute("role","tablist");let l=(o,s=!1)=>{for(let c of n){let a=c===o;c.classList.toggle("is-active",a),c.setAttribute("aria-selected",a?"true":"false"),c.setAttribute("tabindex",a?"0":"-1")}for(let c of t){let a=c===r(o);c.classList.toggle("is-active",a),c.hidden=!a}s&&o.focus()};n.forEach((o,s)=>{o.setAttribute("role","tab"),o.addEventListener("click",c=>{c.preventDefault(),l(o)}),o.addEventListener("keydown",c=>{let a=c.key;a==="ArrowLeft"&&l(n[(s-1+n.length)%n.length],!0),a==="ArrowRight"&&l(n[(s+1)%n.length],!0),a==="Home"&&l(n[0],!0),a==="End"&&l(n[n.length-1],!0)})}),t.forEach(o=>o.setAttribute("role","tabpanel")),l(n.find(o=>o.classList.contains("is-active"))??n[0])}}var J=60,Q=280,k=()=>window.matchMedia("(hover: hover)").matches,Z='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',T=new Set,P=!1;function I(i){i.openTimer!==null&&(window.clearTimeout(i.openTimer),i.openTimer=null),i.closeTimer!==null&&(window.clearTimeout(i.closeTimer),i.closeTimer=null)}function w(i,d=!1){I(i);for(let e of Array.from(T))e!==i&&i.item.contains(e.item)&&w(e);i.item.classList.remove("is-open"),i.toggle.setAttribute("aria-expanded","false"),i.openedBy=null,T.delete(i),d&&i.toggle.focus()}function S(i,d){I(i);for(let e of Array.from(T))e!==i&&e.nav===i.nav&&!e.item.contains(i.item)&&w(e);i.item.classList.add("is-open"),i.toggle.setAttribute("aria-expanded","true"),i.openedBy=d,T.add(i)}function ee(i){for(let e of Array.from(i.querySelectorAll("[data-p60-nav]"))){let n=Array.from(e.querySelectorAll("[data-p60-nav-item]")).filter(t=>t.closest("[data-p60-nav]")===e&&!v(t,"nav-item"));for(let t of n){let r=a=>a.closest("[data-p60-nav-item]")===t,l=Array.from(t.querySelectorAll("[data-p60-nav-toggle]")).find(r)??null,o=Array.from(t.querySelectorAll("[data-p60-nav-menu]")).find(r)??null;if(!l||!o)continue;let s={nav:e,item:t,toggle:l,menu:o,openedBy:null,lastPointer:"",openTimer:null,closeTimer:null},c=()=>T.has(s);l.setAttribute("aria-expanded","false"),o.id&&!l.hasAttribute("aria-controls")&&l.setAttribute("aria-controls",o.id),t.addEventListener("mouseenter",()=>{k()&&(s.closeTimer!==null&&(window.clearTimeout(s.closeTimer),s.closeTimer=null),!c()&&s.openTimer===null&&(s.openTimer=window.setTimeout(()=>{s.openTimer=null,S(s,"hover")},J)))}),t.addEventListener("mouseleave",()=>{k()&&(s.openTimer!==null&&(window.clearTimeout(s.openTimer),s.openTimer=null),c()&&s.openedBy==="hover"&&s.closeTimer===null&&(s.closeTimer=window.setTimeout(()=>{s.closeTimer=null,w(s)},Q)))}),l.addEventListener("pointerdown",a=>{s.lastPointer=a.pointerType??""}),l.addEventListener("click",a=>{let u=a.detail===0||s.lastPointer==="touch"||s.lastPointer==="pen"||!k();if(!c()){a.preventDefault(),a.stopPropagation(),S(s,"toggle");return}!u&&s.openedBy==="hover"||(a.preventDefault(),a.stopPropagation(),w(s))}),l.addEventListener("keydown",a=>{a.key==="ArrowDown"&&(a.preventDefault(),c()||S(s,"toggle"),o.querySelector(Z)?.focus())}),t.addEventListener("keydown",a=>{a.key==="Escape"&&c()&&(a.stopPropagation(),w(s,!0))}),t.addEventListener("focusin",a=>{a.target instanceof Node&&o.contains(a.target)&&!c()&&S(s,"focus")}),t.addEventListener("focusout",a=>{let u=a.relatedTarget;u instanceof Node&&!t.contains(u)&&c()&&w(s)})}if(!v(e,"nav-fit")){let r=f=>{for(let p=1;p<=3;p++)e.classList.toggle(`is-fit-${p}`,p<=f)},l=f=>{let p=f.getBoundingClientRect();return p.width>0||p.height>0?[f]:Array.from(f.children).flatMap(l)},o=f=>{let p=-1/0;for(let b of f.flatMap(l)){let E=b.getBoundingClientRect();if(p!==-1/0&&E.top>=p-2)return!0;p=Math.max(p,E.bottom)}return!1},s=()=>{if(e.scrollWidth>e.clientWidth+1||o(Array.from(e.children)))return!1;let f=e.parentElement;return!(f&&getComputedStyle(f).flexWrap==="wrap"&&o(Array.from(f.children)))},c=0,a=()=>{if(c=performance.now(),e.offsetParent===null||!getComputedStyle(e).flexDirection.startsWith("row")){r(0);return}r(0);let f=0;for(;!s()&&f<3;)r(++f);c=performance.now()},u=!1,m=()=>{u||(u=!0,requestAnimationFrame(()=>{u=!1,a()}))};window.addEventListener("resize",m,{passive:!0}),document.fonts?.ready.then(m).catch(()=>{}),document.fonts?.addEventListener("loadingdone",m),"ResizeObserver"in window&&new ResizeObserver(()=>{performance.now()-c>120&&m()}).observe(e),a()}}let d=Array.from(i.querySelectorAll("[data-p60-nav-burger]")).filter(e=>!v(e,"nav-burger"));for(let e of d){let n=e.getAttribute("aria-controls")||e.getAttribute("data-p60-nav-burger")||"",t=n?document.getElementById(n):null;if(!t)continue;let r=()=>t.classList.contains("is-open"),l=o=>{if(t.classList.toggle("is-open",o),e.classList.toggle("is-open",o),e.setAttribute("aria-expanded",String(o)),!o)for(let s of Array.from(T))t.contains(s.item)&&w(s)};e.setAttribute("aria-expanded","false"),e.addEventListener("click",o=>{o.stopPropagation(),l(!r())}),t.addEventListener("click",o=>{o.target instanceof Element&&o.target.closest("a")&&l(!1)}),document.addEventListener("keydown",o=>{o.key==="Escape"&&r()&&(l(!1),e.focus())}),document.addEventListener("click",o=>{r()&&o.target instanceof Node&&!t.contains(o.target)&&!e.contains(o.target)&&l(!1)}),window.addEventListener("resize",()=>{r()&&e.offsetParent===null&&l(!1)})}P||(P=!0,document.addEventListener("click",e=>{for(let n of Array.from(T))(!(e.target instanceof Node)||!n.item.contains(e.target))&&w(n)}))}function te(i){let d=Array.from(i.querySelectorAll("[data-p60-reading-progress]")).filter(e=>!v(e,"reading-progress"));for(let e of d){let n=e.getAttribute("data-p60-reading-target"),t=n?document.getElementById(n):null;if(n&&!t)continue;e.setAttribute("role","progressbar"),e.setAttribute("aria-valuemin","0"),e.setAttribute("aria-valuemax","100"),!e.hasAttribute("aria-label")&&!e.hasAttribute("aria-labelledby")&&e.setAttribute("aria-label","Reading progress");let r=!1,l=()=>{let s=window.innerHeight,c=0,a=Math.max(document.documentElement.scrollHeight-s,0);if(t){let m=t.getBoundingClientRect();c=m.top+window.scrollY,a=Math.max(m.height-s,0)}let u=a===0?window.scrollY>=c?1:0:Math.min(Math.max((window.scrollY-c)/a,0),1);e.style.setProperty("--p60-reading-progress",u.toFixed(4)),e.setAttribute("aria-valuenow",String(Math.round(u*100))),e.classList.toggle("is-started",u>0),e.classList.toggle("is-complete",u>=1),r=!1},o=()=>{r||(r=!0,requestAnimationFrame(l))};window.addEventListener("scroll",o,{passive:!0}),window.addEventListener("resize",o,{passive:!0}),l()}}function ne(i){let d=Array.from(i.querySelectorAll("[data-p60-show-more]")).filter(e=>!v(e,"show-more"));for(let e of d){let n=Math.max(Number.parseInt(e.getAttribute("data-p60-show-more")??"6",10)||6,1),t=Array.from(e.querySelectorAll("[data-p60-show-more-item]")),r=e.querySelector("[data-p60-show-more-toggle]")??(e.id?document.querySelector(`[data-p60-show-more-toggle][aria-controls="${e.id}"]`):null);if(!r)continue;let l=t.slice(n);if(l.length===0){r.hidden=!0;continue}let o=r.textContent??"",s=r.getAttribute("data-p60-show-more-toggle")??"";e.id&&!r.hasAttribute("aria-controls")&&r.setAttribute("aria-controls",e.id);let c=a=>{for(let u of l)u.hidden=!a;e.classList.toggle("is-expanded",a),r.setAttribute("aria-expanded",a?"true":"false"),s&&(r.textContent=a?s:o)};r.addEventListener("click",()=>c(r.getAttribute("aria-expanded")!=="true")),c(e.classList.contains("is-expanded"))}}function oe(i){let d=Array.from(i.querySelectorAll("[data-p60-scrollspy]")).filter(e=>!v(e,"scrollspy"));for(let e of d){let n=Number.parseInt(e.getAttribute("data-p60-scrollspy")??"96",10)||96,t=[];for(let s of Array.from(e.querySelectorAll('a[href^="#"]'))){let c=decodeURIComponent((s.getAttribute("href")??"").slice(1)),a=c?document.getElementById(c):null;a&&t.push({link:s,target:a})}if(t.length===0)continue;let r=!1,l=()=>{let s=null,c=-1/0;for(let{link:a,target:u}of t){let m=u.getBoundingClientRect().top;m<=n&&m>c&&(c=m,s=a)}for(let{link:a}of t){let u=a===s;a.classList.toggle("is-current",u),u?a.setAttribute("aria-current","location"):a.removeAttribute("aria-current")}r=!1},o=()=>{r||(r=!0,requestAnimationFrame(l))};window.addEventListener("scroll",o,{passive:!0}),window.addEventListener("resize",o,{passive:!0}),window.addEventListener("hashchange",o),l()}}var re=[["reveal",D],["counter",V],["progress",Y],["countdown",z],["accordion",K],["carousel",U],["stickyHeader",X],["stickyCta",W],["lightbox",j],["tabs",G],["readingProgress",te],["showMore",ne],["scrollspy",oe],["nav",ee]];function ie(i=document){document.documentElement.classList.add("p60-js");for(let[d,e]of re)try{e(i)}catch(n){console.warn(`[p60] behaviour "${d}" failed to initialise; the others still run`,n)}}return _(se);})();