@port60/template-kit 0.20.3 → 0.20.4

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.
@@ -1,12 +1,12 @@
1
- /* GENERATED by scripts/build-preview-base.mjs from src/styles/global.css — do not edit. */
1
+ /* GENERATED by scripts/build-preview-base.mjs from src/styles/global.css, do not edit. */
2
2
  /* ---- PLATFORM BASE (the base/skin split, 2026-07-14) ----
3
3
  This stylesheet is the platform's base layer: reset, CSS-variable DECLARATIONS (the theming
4
- contract — values here are the platform defaults a template overrides via :root:root), chrome
4
+ contract, values here are the platform defaults a template overrides via :root:root), chrome
5
5
  mechanics (header/nav/burger/footer/auth), and ALL functional-island styling (donation widget,
6
6
  donate page, events/tickets, services, articles, members area, consent, address lookup). A
7
- template's LOOK does not live here — Classic's is src/templates/default/theme.ts, and every
7
+ template's LOOK does not live here, Classic's is src/templates/default/theme.ts, and every
8
8
  template ships a themeCss injected at body-end. Fonts load from the layout <head> (preconnect +
9
- stylesheet link) instead of a CSS @import — an @import chains render-blocking requests. */
9
+ stylesheet link) instead of a CSS @import, an @import chains render-blocking requests. */
10
10
 
11
11
  :root {
12
12
  --bg: #fbf6ef;
@@ -23,16 +23,16 @@
23
23
  /* Impact map. --marker = the map pin colour (a deliberate brand accent by default; the live-map
24
24
  markers are a zoom-scaling MapLibre circle layer that takes this colour). --map-mode = the
25
25
  basemap colour mode read by the map island: `auto` derives the map's paper/ink from the theme
26
- neutrals below, `light`/`dark`/`muted` force a base. Template-owned — set in theme.css or exposed
26
+ neutrals below, `light`/`dark`/`muted` force a base. Template-owned, set in theme.css or exposed
27
27
  as Appearance knobs; the tenant never edits map colour directly. */
28
28
  --marker: var(--accent);
29
29
  --map-mode: auto;
30
- /* flat (default) or globe — MapLibre's own globe projection, a rotatable sphere. */
30
+ /* flat (default) or globe, MapLibre's own globe projection, a rotatable sphere. */
31
31
  --map-projection: flat;
32
32
  --line: rgba(31, 42, 51, 0.1);
33
33
  --line-strong: rgba(18, 135, 122, 0.28);
34
34
  --danger: #c0392b;
35
- /* Donate/primary CTA hover — a clear GREEN "go" cue, shared by every template (a template may
35
+ /* Donate/primary CTA hover, a clear GREEN "go" cue, shared by every template (a template may
36
36
  override the token, but by default hovering a donate button turns green, not just lighter). */
37
37
  --cta-hover: #16a34a;
38
38
  --cta-hover-strong: #128a3f;
@@ -51,10 +51,37 @@ html {
51
51
  scroll-behavior: smooth;
52
52
  }
53
53
 
54
- /* Arabic-script text (Arabic, Persian, Urdu…) rendered by a proper webfont, sized to match Latin.
55
- unicode-range scopes it to Arabic codepoints only — Latin keeps Inter/Fraunces, and the font is
56
- downloaded only when an Arabic glyph appears. size-adjust enlarges it so mixed text reads evenly.
57
- To use a different (or licensed, e.g. King Saud) font, swap src + keep everything else. */
54
+ /* Arabic-script text (Arabic, Persian, Urdu…) rendered by the platform's own webfonts, sized to
55
+ match Latin. unicode-range scopes them to Arabic codepoints only, Latin keeps the template's
56
+ faces, and a file is downloaded only when an Arabic glyph appears.
57
+ The platform Arabic face is KFGQPC HAFS Uthmanic Script (King Fahd Glorious Quran Printing
58
+ Complex, self-hosted under public/fonts/, licence beside it, never modified). It ships one weight,
59
+ so IBM Plex Sans Arabic stays declared behind it for bold and for any glyph HAFS lacks.
60
+ Template font slots name both faces first too (engine/fonts.ts ARABIC_FACES), so a template's own
61
+ body font never drops them; a tenant who picks an Arabic family for a slot keeps their choice.
62
+ To swap the platform face: replace the file and src here, and the name in fonts.ts. */
63
+ @font-face {
64
+ font-family: 'KFGQPC HAFS Uthmanic Script';
65
+ font-style: normal;
66
+ font-weight: 400;
67
+ font-display: swap;
68
+ size-adjust: 130%;
69
+ src: url('/fonts/kfgqpc-hafs-uthmanic-script/UthmanicHafs_V22.woff2') format('woff2');
70
+ unicode-range: U+0600-06FF, U+0750-077F, U+0870-08FF, U+FB50-FDFF, U+FE70-FEFF, U+200C-200D;
71
+ }
72
+ /* A platform-hosted CATALOGUE family: not on the default Arabic stack, loaded only when a tenant
73
+ picks it for a font slot. A Quranic Nastaliq face (the Indo-Pak mushaf style): Arabic and Quran
74
+ text render, but Urdu prose letters come out as dotted circles in Chrome, so it is not an Urdu
75
+ body font. size-adjust 130%: 120% sat level with the Latin x-height, the owner reads Arabic more easily a step larger. */
76
+ @font-face {
77
+ font-family: 'KFGQPC Nastaleeq';
78
+ font-style: normal;
79
+ font-weight: 400;
80
+ font-display: swap;
81
+ size-adjust: 130%;
82
+ src: url('/fonts/kfgqpc-nastaleeq/KFGQPCNastaleeq-Regular.woff2') format('woff2');
83
+ unicode-range: U+0600-06FF, U+0750-077F, U+0870-08FF, U+FB50-FDFF, U+FE70-FEFF, U+200C-200D;
84
+ }
58
85
  @font-face {
59
86
  font-family: 'IBM Plex Sans Arabic';
60
87
  font-style: normal;
@@ -77,7 +104,7 @@ html {
77
104
  body {
78
105
  margin: 0;
79
106
  min-width: 320px;
80
- font-family: 'IBM Plex Sans Arabic', 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
107
+ font-family: 'KFGQPC HAFS Uthmanic Script', 'IBM Plex Sans Arabic', 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
81
108
  color: var(--ink);
82
109
  background: var(--bg); /* decoration (Classic's radial washes) is the template theme's job */
83
110
  }
@@ -92,8 +119,8 @@ img {
92
119
  display: block;
93
120
  }
94
121
 
95
- /* Typography (heading scale/family, paragraph rhythm) is TEMPLATE territory — see
96
- templates/default/theme.ts for Classic's. The base deliberately styles no bare h1–h4/p. */
122
+ /* Typography (heading scale/family, paragraph rhythm) is TEMPLATE territory, see
123
+ templates/default/theme.ts for Classic's. The base deliberately styles no bare h1-h4/p. */
97
124
 
98
125
  input, button, textarea, select {
99
126
  font: inherit;
@@ -123,7 +150,7 @@ input:focus {
123
150
 
124
151
  /* THE CONTENT SEAM. `.container` (contained) and `.full` (edge-to-edge) are the layout intents every
125
152
  platform page renders through, and the contract a template fills to place content its way: a template
126
- STYLES these — and overrides `--container` for width — to match its own sections. A template must
153
+ STYLES these, and overrides `--container` for width, to match its own sections. A template must
127
154
  NEVER build a parallel content container (a class sized off `--container`); platform pages stay on
128
155
  `.container`, so a second container makes them drift from the template's sections. The publish-time
129
156
  validator enforces this. A template that leaves the seam alone inherits these platform defaults. */
@@ -132,7 +159,7 @@ input:focus {
132
159
  margin: 0 auto;
133
160
  }
134
161
 
135
- /* Full-bleed intent — edge-to-edge regardless of the container it sits in (a hero image, a colour
162
+ /* Full-bleed intent, edge-to-edge regardless of the container it sits in (a hero image, a colour
136
163
  band). Templates restyle it freely; the default breaks out to the viewport width. */
137
164
  .full {
138
165
  width: 100vw;
@@ -212,7 +239,7 @@ input:focus {
212
239
  display: flex;
213
240
  align-items: center;
214
241
  gap: 0.35rem;
215
- flex-wrap: nowrap; /* never let items drop to a second line — compact (below) then burger */
242
+ flex-wrap: nowrap; /* never let items drop to a second line, compact (below) then burger */
216
243
  }
217
244
 
218
245
  .site-nav a {
@@ -233,7 +260,7 @@ input:focus {
233
260
  box-shadow: 0 10px 22px rgba(239, 96, 68, 0.26);
234
261
  }
235
262
 
236
- /* Donate, the Port60 ID sign-in, and the account pill share one exact size — they sit side by
263
+ /* Donate, the Port60 ID sign-in, and the account pill share one exact size, they sit side by
237
264
  side, so any height/padding drift between them reads as misalignment. The sign-in/account pair
238
265
  is UNSCOPED: they are the member_menu ISLAND's chrome, which must look right inside ANY
239
266
  template's nav (artifact layouts don't use .site-nav). */
@@ -256,7 +283,7 @@ input:focus {
256
283
  box-shadow: 0 10px 22px rgba(22, 163, 74, 0.3);
257
284
  }
258
285
 
259
- /* Services dropdown — populated client-side from the published services. */
286
+ /* Services dropdown, populated client-side from the published services. */
260
287
  .site-nav .nav-dropdown {
261
288
  position: relative;
262
289
  display: inline-flex;
@@ -297,9 +324,9 @@ input:focus {
297
324
  background: var(--bg-tint);
298
325
  }
299
326
 
300
- /* The port60 ID sign-in button — the PLATFORM identity brand, deliberately constant across every
327
+ /* The port60 ID sign-in button, the PLATFORM identity brand, deliberately constant across every
301
328
  tenant site and template (the "Sign in with Google" pattern): port60 navy + cyan, independent
302
- of the site palette. The hosted widget it opens offers both sign-in and register. Unscoped —
329
+ of the site palette. The hosted widget it opens offers both sign-in and register. Unscoped ,
303
330
  island chrome, identical in any template's nav. */
304
331
  .nav-p60-signin.nav-p60-signin {
305
332
  display: inline-flex;
@@ -355,9 +382,9 @@ input:focus {
355
382
  height: 1.25rem;
356
383
  }
357
384
 
358
- /* Members account pill (signed in) — a clear "this is your account" affordance, weighted between
385
+ /* Members account pill (signed in), a clear "this is your account" affordance, weighted between
359
386
  plain nav links and the Donate CTA. The menu anchors right (it's the last nav item).
360
- `.open` mirrors hover/focus for touch. Unscoped — island chrome, identical in any template's
387
+ `.open` mirrors hover/focus for touch. Unscoped, island chrome, identical in any template's
361
388
  nav: the wrapper carries its own positioning anchor and menu-link styling so the dropdown works
362
389
  without .site-nav around it. */
363
390
  .nav-account {
@@ -405,7 +432,7 @@ input:focus {
405
432
  display: flex;
406
433
  }
407
434
  /* Artifact-template mobile panels (the layout behaviour API: #site-nav gains .open): the account
408
- menu flows INLINE like any expanded nav group — never a floating card half off the screen.
435
+ menu flows INLINE like any expanded nav group, never a floating card half off the screen.
409
436
  Keyed on the .open state the burger sets, so desktop hover/dropdowns are untouched. */
410
437
  #site-nav.open .nav-account {
411
438
  display: block;
@@ -463,7 +490,7 @@ input:focus {
463
490
  background: var(--bg-tint);
464
491
  }
465
492
 
466
- /* Optional tenant footer mark (branding.footerLogoUrl) — height-constrained like the header logo. */
493
+ /* Optional tenant footer mark (branding.footerLogoUrl), height-constrained like the header logo. */
467
494
  .footer-logo { display: block; height: 46px; width: auto; max-width: 220px; object-fit: contain; margin-bottom: 0.9rem; }
468
495
 
469
496
  .site-footer {
@@ -501,7 +528,7 @@ input:focus {
501
528
  color: rgba(93, 107, 120, 0.8);
502
529
  }
503
530
 
504
- /* Footer socials — tenant's social links, rendered by the template (ours: here). Variable-driven so
531
+ /* Footer socials, tenant's social links, rendered by the template (ours: here). Variable-driven so
505
532
  each theme tints them. */
506
533
  .footer-socials {
507
534
  display: flex;
@@ -529,10 +556,10 @@ input:focus {
529
556
  height: 1.15rem;
530
557
  }
531
558
 
532
- /* ---------- shared bits (.eyebrow/.card/.section-heading are template-owned — Classic's live in
559
+ /* ---------- shared bits (.eyebrow/.card/.section-heading are template-owned, Classic's live in
533
560
  templates/default/theme.ts; .button stays here because the islands depend on it) ---------- */
534
561
 
535
- /* A kicker may BE a link (the archive's section kicker doubles as the way back) — it keeps the
562
+ /* A kicker may BE a link (the archive's section kicker doubles as the way back), it keeps the
536
563
  kicker type treatment whatever the template; hover reveals the affordance. */
537
564
  a.eyebrow {
538
565
  text-decoration: none;
@@ -587,10 +614,10 @@ a.eyebrow:hover {
587
614
  border-color: var(--line);
588
615
  }
589
616
 
590
- /* ---------- Forms (schema-driven form renderer — components/FormRenderer + DateField) ----------
617
+ /* ---------- Forms (schema-driven form renderer, components/FormRenderer + DateField) ----------
591
618
  Themed entirely from the site tokens so a form looks native on any template. The one thing the base
592
619
  input{} rule gets wrong for us is the checkbox (it forces width:100%, stretching the box across the
593
- row) — pinned below, which is what fixes the check-box alignment. */
620
+ row), pinned below, which is what fixes the check-box alignment. */
594
621
 
595
622
  .p60-form {
596
623
  max-width: 640px;
@@ -676,7 +703,7 @@ textarea.p60-form-control {
676
703
  font-size: 0.9rem;
677
704
  }
678
705
 
679
- /* Checkbox rows — pin the box small and top-align it with the first line of the label text. */
706
+ /* Checkbox rows, pin the box small and top-align it with the first line of the label text. */
680
707
  .p60-form-check {
681
708
  display: flex;
682
709
  align-items: flex-start;
@@ -704,7 +731,7 @@ textarea.p60-form-control {
704
731
  font-style: italic;
705
732
  }
706
733
 
707
- /* MULTISELECT checkbox list / CHOICE radio list — vertical lists reusing the aligned check-row. */
734
+ /* MULTISELECT checkbox list / CHOICE radio list, vertical lists reusing the aligned check-row. */
708
735
  .p60-form-checklist,
709
736
  .p60-form-radios {
710
737
  display: flex;
@@ -718,7 +745,7 @@ textarea.p60-form-control {
718
745
  margin-bottom: 0;
719
746
  }
720
747
 
721
- /* Searchable multi-select dropdown (MultiSelectDropdown) — the "pick many from a long list" widget. */
748
+ /* Searchable multi-select dropdown (MultiSelectDropdown), the "pick many from a long list" widget. */
722
749
  .p60-ms {
723
750
  position: relative;
724
751
  }
@@ -840,7 +867,7 @@ textarea.p60-form-control {
840
867
  font-size: 0.9rem;
841
868
  }
842
869
 
843
- /* ---- Custom desktop date picker (DateField) — native <input type=date> is used on touch ---- */
870
+ /* ---- Custom desktop date picker (DateField), native <input type=date> is used on touch ---- */
844
871
  .p60-date {
845
872
  position: relative;
846
873
  }
@@ -971,7 +998,7 @@ textarea.p60-form-control {
971
998
  background: transparent;
972
999
  }
973
1000
 
974
- /* (hero / impact / band — the home page's Classic presentation — live in templates/default/theme.ts) */
1001
+ /* (hero / impact / band, the home page's Classic presentation, live in templates/default/theme.ts) */
975
1002
 
976
1003
  /* ---------- donation widget ---------- */
977
1004
 
@@ -985,7 +1012,7 @@ textarea.p60-form-control {
985
1012
  gap: 1rem;
986
1013
  }
987
1014
 
988
- /* MOBILE BREAKOUT (owner, 2026-08-17): on phones the nesting stack squashes the card badly —
1015
+ /* MOBILE BREAKOUT (owner, 2026-08-17): on phones the nesting stack squashes the card badly ,
989
1016
  measured on Sanctuary: 390px viewport → 346 container → 294 aside → 249px card → ~195px of
990
1017
  actual controls. A donate form is a working control surface; it gets the full screen width
991
1018
  (edge to edge, side borders/radius dropped, its own padding kept). The negative-margin trick
@@ -1202,7 +1229,7 @@ textarea.p60-form-control {
1202
1229
  color: var(--primary-strong);
1203
1230
  }
1204
1231
 
1205
- /* Named suggested amounts — a vertical list with the amount + its label (e.g. "Feeds a family"). */
1232
+ /* Named suggested amounts, a vertical list with the amount + its label (e.g. "Feeds a family"). */
1206
1233
  .amount-list {
1207
1234
  display: grid;
1208
1235
  gap: 0.6rem;
@@ -1275,6 +1302,12 @@ textarea.p60-form-control {
1275
1302
  padding-left: 1.8rem;
1276
1303
  }
1277
1304
 
1305
+ .cover-costs-row {
1306
+ display: flex;
1307
+ gap: 0.5rem;
1308
+ align-items: flex-start;
1309
+ }
1310
+
1278
1311
  .donor-fields {
1279
1312
  display: grid;
1280
1313
  grid-template-columns: 1fr 1fr;
@@ -1427,7 +1460,7 @@ textarea.p60-form-control {
1427
1460
  margin-top: 0.4rem;
1428
1461
  }
1429
1462
 
1430
- /* (about-page .prose/.value-grid — Classic's section presentation — live in templates/default/theme.ts) */
1463
+ /* (about-page .prose/.value-grid, Classic's section presentation, live in templates/default/theme.ts) */
1431
1464
 
1432
1465
  /* ---------- responsive ---------- */
1433
1466
 
@@ -1615,11 +1648,11 @@ textarea.p60-form-control {
1615
1648
  /* --- Articles (public) --- */
1616
1649
  /* The blog is long-form READING (unlike the full-width marketing Services pages): the article body
1617
1650
  sits in a ~70ch measure with article chrome (byline, reading time), and the theme owns all
1618
- typography — content only carries semantic structure (mark, pull quotes, captions, tables,
1651
+ typography, content only carries semantic structure (mark, pull quotes, captions, tables,
1619
1652
  embeds), styled here. */
1620
1653
  .article-article {
1621
1654
  /* The page FRAME matches every other page's container (hero image, engagement bar span it);
1622
- the running text keeps its own readable measure via .article-body/.article-article-head —
1655
+ the running text keeps its own readable measure via .article-body/.article-article-head ,
1623
1656
  so navigating article ↔ other pages no longer visibly narrows the page. */
1624
1657
  max-width: var(--container);
1625
1658
  margin: 0 auto;
@@ -1673,7 +1706,7 @@ textarea.p60-form-control {
1673
1706
  }
1674
1707
 
1675
1708
  .article-body {
1676
- /* Fills the container — same call as .service-body ("widened by preference"): the page reads as
1709
+ /* Fills the container, same call as .service-body ("widened by preference"): the page reads as
1677
1710
  one full-width surface; line-height carries the readability. */
1678
1711
  margin: 0;
1679
1712
  color: var(--ink);
@@ -1728,7 +1761,7 @@ textarea.p60-form-control {
1728
1761
  .article-body img.size-full {
1729
1762
  max-width: 100%;
1730
1763
  }
1731
- /* The semantic highlight — theme-tinted, never an author-picked colour. */
1764
+ /* The semantic highlight, theme-tinted, never an author-picked colour. */
1732
1765
  .article-body mark {
1733
1766
  background: color-mix(in srgb, var(--gold) 38%, transparent);
1734
1767
  color: inherit;
@@ -1741,7 +1774,7 @@ textarea.p60-form-control {
1741
1774
  padding: 0.25rem 1rem;
1742
1775
  color: var(--muted);
1743
1776
  }
1744
- /* The pull quote — a stand-out breath in the reading flow. */
1777
+ /* The pull quote, a stand-out breath in the reading flow. */
1745
1778
  .article-body blockquote.pull {
1746
1779
  border-inline-start: none;
1747
1780
  border-top: 2px solid var(--line-strong);
@@ -1834,7 +1867,7 @@ textarea.p60-form-control {
1834
1867
  margin: 1.5rem 0 0;
1835
1868
  }
1836
1869
 
1837
- /* Engagement bar under the article. Wraps — never scrolls sideways. */
1870
+ /* Engagement bar under the article. Wraps, never scrolls sideways. */
1838
1871
  .article-engage {
1839
1872
  margin: 1.5rem 0;
1840
1873
  display: flex;
@@ -1870,7 +1903,7 @@ textarea.p60-form-control {
1870
1903
  font-size: 0.95rem;
1871
1904
  }
1872
1905
 
1873
- /* The share row — plain share-intent buttons (no network SDKs). Wraps with the bar. */
1906
+ /* The share row, plain share-intent buttons (no network SDKs). Wraps with the bar. */
1874
1907
  .article-share-group {
1875
1908
  display: flex;
1876
1909
  align-items: center;
@@ -1900,7 +1933,7 @@ textarea.p60-form-control {
1900
1933
  border-color: var(--accent);
1901
1934
  color: var(--accent-strong);
1902
1935
  }
1903
- /* Brand tints on hover — recognisable without shouting at rest (and dark-theme safe: the
1936
+ /* Brand tints on hover, recognisable without shouting at rest (and dark-theme safe: the
1904
1937
  at-rest button is theme ink on theme surface). */
1905
1938
  .article-share-btn--facebook:hover { color: #1877f2; border-color: #1877f2; }
1906
1939
  .article-share-btn--x:hover { color: var(--ink); border-color: var(--ink); }
@@ -2014,7 +2047,7 @@ textarea.p60-form-control {
2014
2047
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
2015
2048
  gap: 1.5rem;
2016
2049
  }
2017
- /* MOBILE: the stacked grid becomes an edge-to-edge SWIPE ROW with a peek (owner, 2026-08-17) —
2050
+ /* MOBILE: the stacked grid becomes an edge-to-edge SWIPE ROW with a peek (owner, 2026-08-17) ,
2018
2051
  the scroller bleeds to the screen edges, each card keeps its padding, the first card aligns to
2019
2052
  the text gutter, and the cut-off next card IS the "swipe me" affordance (the reference maps'
2020
2053
  card idiom). Snap keeps cards settling cleanly; the scrollbar is hidden, the peek replaces it. */
@@ -2095,7 +2128,7 @@ textarea.p60-form-control {
2095
2128
  .article-card-counts {
2096
2129
  margin-inline-start: 0.6rem;
2097
2130
  }
2098
- /* The featured (newest) card: a magazine lead — image beside copy on wide screens. */
2131
+ /* The featured (newest) card: a magazine lead, image beside copy on wide screens. */
2099
2132
  .article-card--featured {
2100
2133
  display: grid;
2101
2134
  grid-template-columns: 1.2fr 1fr;
@@ -2132,7 +2165,7 @@ textarea.p60-form-control {
2132
2165
  color: var(--muted);
2133
2166
  }
2134
2167
  .article-more {
2135
- /* Spans the page frame like the hero + back-link (the reading column stays 70ch) — one frame
2168
+ /* Spans the page frame like the hero + back-link (the reading column stays 70ch), one frame
2136
2169
  edge + one reading edge, nothing floating at a third width. Grid auto-fills the row. */
2137
2170
  max-width: var(--container);
2138
2171
  margin: 2.5rem auto 0;
@@ -2178,7 +2211,7 @@ textarea.p60-form-control {
2178
2211
  .event-card-image-placeholder {
2179
2212
  display: grid;
2180
2213
  place-items: center;
2181
- /* Same fallback-tint convention as course cards — see .course-card-tint. */
2214
+ /* Same fallback-tint convention as course cards, see .course-card-tint. */
2182
2215
  background: var(--media-tint, hsl(210 34% 33%));
2183
2216
  }
2184
2217
  .event-card-image-placeholder span {
@@ -2212,7 +2245,7 @@ textarea.p60-form-control {
2212
2245
  align-self: start;
2213
2246
  }
2214
2247
 
2215
- /* .event-detail carries NO max-width — page bodies fill var(--container) (the standing
2248
+ /* .event-detail carries NO max-width, page bodies fill var(--container) (the standing
2216
2249
  full-width rule; the 760px reading measure was removed 2026-07-24, as on courses). */
2217
2250
  .event-detail-image {
2218
2251
  width: 100%;
@@ -2427,7 +2460,7 @@ textarea.p60-form-control {
2427
2460
  align-items: center;
2428
2461
  gap: 0.5rem;
2429
2462
  }
2430
- /* MOBILE: the rotator is MEDIA — it bleeds edge to edge like the hero and the map, with the
2463
+ /* MOBILE: the rotator is MEDIA, it bleeds edge to edge like the hero and the map, with the
2431
2464
  arrows overlaid on the image instead of eating width beside it. */
2432
2465
  @media (max-width: 640px) {
2433
2466
  .events-carousel {
@@ -2875,7 +2908,7 @@ textarea.p60-form-control {
2875
2908
 
2876
2909
  /* ---- Members area (shell + sections; see components/members/) ----
2877
2910
  Desktop: left rail + content. Small screens: the rail becomes a horizontal, scrollable pill
2878
- strip above the content — same registry, no separate mobile markup. */
2911
+ strip above the content, same registry, no separate mobile markup. */
2879
2912
  .members-head {
2880
2913
  margin-bottom: 1.6rem;
2881
2914
  }
@@ -2911,7 +2944,7 @@ textarea.p60-form-control {
2911
2944
  color: var(--ink);
2912
2945
  text-align: left;
2913
2946
  cursor: pointer;
2914
- /* Labels WRAP inside the pill (never overflow it) — long names and translations both fit. */
2947
+ /* Labels WRAP inside the pill (never overflow it), long names and translations both fit. */
2915
2948
  line-height: 1.25;
2916
2949
  }
2917
2950
  .members-rail-item .members-rail-icon { flex: 0 0 auto; }
@@ -3050,7 +3083,7 @@ textarea.p60-form-control {
3050
3083
  }
3051
3084
  }
3052
3085
 
3053
- /* Just above the burger point the full-size nav is tight — compact spacing spreads it cleanly
3086
+ /* Just above the burger point the full-size nav is tight, compact spacing spreads it cleanly
3054
3087
  instead of letting anything drop. */
3055
3088
  @media (min-width: 961px) and (max-width: 1120px) {
3056
3089
  .site-nav {
@@ -3172,7 +3205,7 @@ textarea.p60-form-control {
3172
3205
 
3173
3206
  /* ---- Members rail on mobile: a LEFT SLIDE-IN DRAWER behind a round user-icon button ----
3174
3207
  The rail itself becomes the drawer (same items, same markup): fixed full-height panel translated
3175
- off-canvas, sliding in over a scrim. The trigger is a slim bar — round user-icon button + the
3208
+ off-canvas, sliding in over a scrim. The trigger is a slim bar, round user-icon button + the
3176
3209
  current section name. Desktop hides all of this and shows the rail inline. */
3177
3210
  .members-mobile-bar {
3178
3211
  display: none;
@@ -3280,7 +3313,7 @@ textarea.p60-form-control {
3280
3313
  }
3281
3314
 
3282
3315
  /* ---- Articles front page: the newest article in full + a right sidebar (recent, topics,
3283
- view-all). Newspaper-style — the sidebar stacks below the article on small screens. ---- */
3316
+ view-all). Newspaper-style, the sidebar stacks below the article on small screens. ---- */
3284
3317
  .article-front {
3285
3318
  display: grid;
3286
3319
  grid-template-columns: minmax(0, 1fr) 300px;
@@ -3383,7 +3416,7 @@ textarea.p60-form-control {
3383
3416
  }
3384
3417
  }
3385
3418
 
3386
- /* ---- Consent banner + preference centre (platform-level, always on — compliance is
3419
+ /* ---- Consent banner + preference centre (platform-level, always on, compliance is
3387
3420
  platform-owned). Fixed bottom layer; the preference centre grows out of the same layer. ---- */
3388
3421
  .consent-layer {
3389
3422
  position: fixed;
@@ -3539,7 +3572,7 @@ textarea.p60-form-control {
3539
3572
  color: inherit;
3540
3573
  }
3541
3574
 
3542
- /* ---- Marketing opt-in checkbox (the explicit reg-22 consent — deliberately quiet, never
3575
+ /* ---- Marketing opt-in checkbox (the explicit reg-22 consent, deliberately quiet, never
3543
3576
  pre-ticked) + the members contact toggle. ---- */
3544
3577
  .marketing-optin {
3545
3578
  display: flex;
@@ -3608,11 +3641,11 @@ textarea.p60-form-control {
3608
3641
  }
3609
3642
 
3610
3643
  /* ---- Cross-document view transitions (MPA soft-feel) ----
3611
- Every nav click is a full document navigation (Astro MPA — no client router). This asks the
3644
+ Every nav click is a full document navigation (Astro MPA, no client router). This asks the
3612
3645
  browser to HOLD the old frame and cross-fade to the new page once it's ready (Chrome 126+,
3613
3646
  Safari 18+; others keep the instant swap): no blank frame between pages, and unchanged chrome
3614
- (masthead/nav) cross-fades between identical pixels — i.e. visually persists. True soft-nav
3615
- with persistent island state would be Astro's ClientRouter — a later, bigger step. */
3647
+ (masthead/nav) cross-fades between identical pixels, i.e. visually persists. True soft-nav
3648
+ with persistent island state would be Astro's ClientRouter, a later, bigger step. */
3616
3649
  @view-transition {
3617
3650
  navigation: auto;
3618
3651
  }
@@ -3624,7 +3657,7 @@ textarea.p60-form-control {
3624
3657
  }
3625
3658
  }
3626
3659
 
3627
- /* The article page's back-link needs air before the chips/title (detail page only — the front
3660
+ /* The article page's back-link needs air before the chips/title (detail page only, the front
3628
3661
  page's grid must keep its own alignment). */
3629
3662
  .services-back + .article-article {
3630
3663
  margin-top: 1.75rem;
@@ -3741,7 +3774,7 @@ textarea.p60-form-control {
3741
3774
  mixed grids keep one card anatomy. The tint arrives as a CSS VARIABLE, not an inline background,
3742
3775
  so a template whose palette collides with the green→indigo band can override this rule and
3743
3776
  restyle the fallback its own way (the templates-conform contract). */
3744
- /* Admin-entered prose can contain unbroken tokens (URLs, pasted refs) — never let one push a
3777
+ /* Admin-entered prose can contain unbroken tokens (URLs, pasted refs), never let one push a
3745
3778
  container wide. Applies to every surface that renders tenant summaries/bodies. */
3746
3779
  .event-detail-desc,
3747
3780
  .event-about,
@@ -4023,7 +4056,7 @@ textarea.p60-form-control {
4023
4056
  color: var(--muted);
4024
4057
  white-space: pre-line;
4025
4058
  }
4026
- /* The members-area PILL BUTTON (user, 2026-07-22) — the one action idiom across member surfaces
4059
+ /* The members-area PILL BUTTON (user, 2026-07-22), the one action idiom across member surfaces
4027
4060
  (directions, household invite/remove, …): bordered pill, hover fill, danger modifier. */
4028
4061
  .member-pill-btn {
4029
4062
  display: inline-flex;
@@ -4046,7 +4079,7 @@ textarea.p60-form-control {
4046
4079
  .member-pill-btn:disabled { opacity: 0.5; cursor: default; }
4047
4080
  .member-pill-btn-danger { color: #c4462e; }
4048
4081
 
4049
- /* Get directions = a proper pill button (user, 2026-07-22) — the resource-chip idiom, so it
4082
+ /* Get directions = a proper pill button (user, 2026-07-22), the resource-chip idiom, so it
4050
4083
  reads as an action rather than text among the venue lines. */
4051
4084
  .member-course-directions {
4052
4085
  display: inline-flex;
@@ -4067,7 +4100,7 @@ textarea.p60-form-control {
4067
4100
  background: color-mix(in srgb, currentColor 8%, transparent);
4068
4101
  text-decoration: none;
4069
4102
  }
4070
- /* The feedback-loop accordion (2026-07-21): latest note + attendance history — renders only when
4103
+ /* The feedback-loop accordion (2026-07-21): latest note + attendance history, renders only when
4071
4104
  a course runner actually recorded something. */
4072
4105
  .member-course-feedback {
4073
4106
  margin-top: 0.6rem;
@@ -4147,7 +4180,7 @@ textarea.p60-form-control {
4147
4180
  text-transform: uppercase;
4148
4181
  opacity: 0.7;
4149
4182
  }
4150
- /* Download CHIPS (user, 2026-07-22): the whole pill is the button — unmistakably tappable. */
4183
+ /* Download CHIPS (user, 2026-07-22): the whole pill is the button, unmistakably tappable. */
4151
4184
  .member-resource-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.45rem; }
4152
4185
  .member-resource-chip {
4153
4186
  display: inline-flex;
@@ -4167,7 +4200,7 @@ textarea.p60-form-control {
4167
4200
  }
4168
4201
  .member-resource-chip-size { font-weight: 400; opacity: 0.65; font-size: 0.85rem; }
4169
4202
 
4170
- /* "Billing & payment methods" (user, 2026-07-22): the billing half — one line per ongoing
4203
+ /* "Billing & payment methods" (user, 2026-07-22): the billing half, one line per ongoing
4171
4204
  commitment (a family subscription = ONE line: total + who it covers). */
4172
4205
  .members-subsection-title {
4173
4206
  font-size: 1.05rem;
@@ -4193,7 +4226,7 @@ textarea.p60-form-control {
4193
4226
  .member-billing-detail { font-size: 0.9rem; margin-top: 0.15rem; }
4194
4227
  .member-billing-pastdue { color: var(--accent, #d33); font-weight: 600; }
4195
4228
 
4196
- /* The course TITLE BAR (user, 2026-07-22): a distinct full-width band at the top of the card —
4229
+ /* The course TITLE BAR (user, 2026-07-22): a distinct full-width band at the top of the card ,
4197
4230
  bleeds to the card edges via negative margins, slightly larger type. */
4198
4231
  .member-course-title-bar {
4199
4232
  margin: -1rem -1.1rem 0.35rem;
@@ -4206,7 +4239,7 @@ textarea.p60-form-control {
4206
4239
  }
4207
4240
 
4208
4241
  /* Grouped course card (2026-07-22): the course owns venue/breaks/materials once; each PERSON is
4209
- an ACCORDION row — a full-width, obviously-tappable bar (chevron + hover), default collapsed,
4242
+ an ACCORDION row, a full-width, obviously-tappable bar (chevron + hover), default collapsed,
4210
4243
  one open at a time. */
4211
4244
  .member-course-people {
4212
4245
  margin-top: 0.85rem;
@@ -4310,7 +4343,7 @@ textarea.p60-form-control {
4310
4343
  flex-wrap: wrap;
4311
4344
  }
4312
4345
 
4313
- /* Courses picker refinement (2026-07-19): tap-to-select cards — the native checkbox/radio is
4346
+ /* Courses picker refinement (2026-07-19): tap-to-select cards, the native checkbox/radio is
4314
4347
  visually hidden (kept in the DOM for keyboard + screen-reader semantics); selection reads as
4315
4348
  the highlighted card. The footer is an itemised order summary instead of a one-line count. */
4316
4349
  .course-variant-card,
@@ -4430,7 +4463,7 @@ textarea.p60-form-control {
4430
4463
  font-weight: 600;
4431
4464
  }
4432
4465
 
4433
- /* ---- Prayer timetable page (worship WP5) — theme-neutral: borders/highlights derive from
4466
+ /* ---- Prayer timetable page (worship WP5), theme-neutral: borders/highlights derive from
4434
4467
  currentColor so every template's palette carries through. ---- */
4435
4468
  .pt-head {
4436
4469
  display: flex;
@@ -4505,7 +4538,7 @@ textarea.p60-form-control {
4505
4538
  opacity: 0.7;
4506
4539
  }
4507
4540
 
4508
- /* Small screens: the month table becomes stacked day cards — no horizontal scrolling. */
4541
+ /* Small screens: the month table becomes stacked day cards, no horizontal scrolling. */
4509
4542
  .pt-cards {
4510
4543
  display: none;
4511
4544
  list-style: none;
@@ -4583,7 +4616,7 @@ textarea.p60-form-control {
4583
4616
  font-size: 0.92rem;
4584
4617
  }
4585
4618
 
4586
- /* ── People / team cards — desktop grid, single column on phones ───────────────────────────── */
4619
+ /* ── People / team cards, desktop grid, single column on phones ───────────────────────────── */
4587
4620
  .people-grid {
4588
4621
  display: grid;
4589
4622
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
@@ -4614,29 +4647,29 @@ textarea.p60-form-control {
4614
4647
  .people-grid { grid-template-columns: 1fr; }
4615
4648
  }
4616
4649
 
4617
- /* ── Impact map island (WP4) — the OVERLAY borrows the theme: pins wear --marker (a brand accent by
4650
+ /* ── Impact map island (WP4), the OVERLAY borrows the theme: pins wear --marker (a brand accent by
4618
4651
  default), the card is a themed --surface. The BASEMAP colour is derived by the map island from the
4619
- theme + --map-mode (see lib/mapTheme.ts). Brand on the overlay, neutrals on the map — the recipe
4652
+ theme + --map-mode (see lib/mapTheme.ts). Brand on the overlay, neutrals on the map, the recipe
4620
4653
  that stays legible on any theme. Templates restyle any of this via the tokens or these classes. */
4621
4654
  .impact-map { margin: 24px 0; }
4622
4655
  .impact-map-head h2 { margin: 0 0 6px; }
4623
4656
  .impact-map-intro { margin: 0 0 14px; opacity: 0.85; }
4624
4657
  .impact-map-stats { margin: 0 0 14px; color: var(--muted); font-weight: 600; letter-spacing: 0.01em; }
4625
4658
  /* The stable stage, shaped like THE WORLD BAND ITSELF: the island's planet view is all 360° of
4626
- longitude by 84°N–58°S (whole Greenland in, only the polar void and Antarctica trimmed), which in
4627
- Web Mercator is a 1.497:1 box — so a frame with that exact ratio holds the full planet edge to
4659
+ longitude by 84°N-58°S (whole Greenland in, only the polar void and Antarctica trimmed), which in
4660
+ Web Mercator is a 1.497:1 box, so a frame with that exact ratio holds the full planet edge to
4628
4661
  edge, full width AND full height, at any screen size. A ratio wider than the band (a max-height
4629
4662
  clamp once made it so on large screens) turns the fit height-constrained and the world renders
4630
- NARROWER than the frame — dead bars at the sides; a narrower ratio (min-height on phones) just
4663
+ NARROWER than the frame, dead bars at the sides; a narrower ratio (min-height on phones) just
4631
4664
  adds benign vertical margin. Relative + clipped so MapLibre always has a real box and can never
4632
4665
  escape it. Keep this ratio in step with the island's planet bounds. */
4633
4666
  .impact-map-stage { position: relative; width: 100%; aspect-ratio: 1.497; min-height: 300px;
4634
4667
  border-radius: 14px; overflow: hidden; }
4635
4668
  /* width:100% is LOAD-BEARING: with height pinned by min-height and width left auto, CSS derives
4636
- width FROM the aspect-ratio (300 x 1.497 = 449px) — on phones the stage overflowed the page
4669
+ width FROM the aspect-ratio (300 x 1.497 = 449px), on phones the stage overflowed the page
4637
4670
  sideways, shoving every other section into a lopsided "left with a right gutter" layout. Width
4638
4671
  pinned, the ratio yields height and min-height only ever makes the stage TALLER. */
4639
- /* MOBILE BREAKOUT: a map is a canvas — every pixel is information, nothing needs a margin — so on
4672
+ /* MOBILE BREAKOUT: a map is a canvas, every pixel is information, nothing needs a margin, so on
4640
4673
  phones it bleeds edge to edge (media bleeds, prose gutters: the hero already sets the rhythm).
4641
4674
  Cooperative gestures keep page-scrolling safe over the full-width canvas. */
4642
4675
  @media (max-width: 640px) {
@@ -4662,7 +4695,7 @@ textarea.p60-form-control {
4662
4695
  card on every screen size, as it always has. */
4663
4696
  .impact-map-card--small-screens { display: none; }
4664
4697
  @media (max-width: 640px) {
4665
- /* Full width, flush under the full-bleed map — the two read as one attached sheet (the same
4698
+ /* Full width, flush under the full-bleed map, the two read as one attached sheet (the same
4666
4699
  breakout as the donate card; the hairline top border is the seam between map and card). */
4667
4700
  .impact-map-card--small-screens {
4668
4701
  display: block;
@@ -4685,10 +4718,10 @@ textarea.p60-form-control {
4685
4718
  Sized with width/height (NOT inset) because MapLibre forces position:relative on this element,
4686
4719
  which would defeat absolute insets and collapse the box to zero height. */
4687
4720
  .impact-map-gl { width: 100%; height: 100%; }
4688
- /* Live-map markers are a MapLibre circle layer (WebGL) — they scale with zoom and take their colour
4689
- from --marker (circle-color) — so there is no DOM marker element to style here. The pin canvas
4721
+ /* Live-map markers are a MapLibre circle layer (WebGL), they scale with zoom and take their colour
4722
+ from --marker (circle-color), so there is no DOM marker element to style here. The pin canvas
4690
4723
  fallback above keeps its own .impact-map-pin dots for the no-tiles case. */
4691
- /* The INLINE marker card (a MapLibre popup anchored at the clicked marker — where "See more"
4724
+ /* The INLINE marker card (a MapLibre popup anchored at the clicked marker, where "See more"
4692
4725
  lives): the shell takes the theme's surface; the tip is recoloured per anchor side to match. */
4693
4726
  .impact-map-popup .maplibregl-popup-content { background: var(--surface); color: var(--ink);
4694
4727
  border: 1px solid var(--line); border-radius: var(--radius-sm); box-shadow: var(--shadow);