@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.
@@ -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,15 +614,41 @@ 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;
597
624
  }
598
625
 
626
+ .campaign-form {
627
+ margin-block: 2rem;
628
+ }
629
+
630
+ /* An embedded form belongs to the campaign, not a second page inside it. */
631
+ .p60-form--embedded {
632
+ padding: clamp(1rem, 3vw, 2rem);
633
+ border: 1px solid var(--line);
634
+ border-radius: var(--radius-sm, 12px);
635
+ background: var(--surface);
636
+ color: var(--ink);
637
+ }
638
+
639
+ .p60-form--embedded .p60-form-control {
640
+ background: var(--bg);
641
+ border-color: color-mix(in srgb, var(--ink) 35%, var(--surface));
642
+ }
643
+
644
+ .p60-form--embedded .p60-form-control:focus {
645
+ border-color: var(--primary);
646
+ }
647
+
648
+ .p60-form--embedded > :last-child {
649
+ margin-block-end: 0;
650
+ }
651
+
599
652
  .p60-form-intro {
600
653
  color: var(--muted);
601
654
  margin: 0 0 1.5rem;
@@ -609,6 +662,18 @@ a.eyebrow:hover {
609
662
  margin-bottom: 1.15rem;
610
663
  }
611
664
 
665
+ .p60-form-respondent {
666
+ min-inline-size: 0;
667
+ border: 0;
668
+ padding: 0 0 0.5rem;
669
+ margin: 0 0 1.5rem;
670
+ border-block-end: 1px solid var(--line);
671
+ }
672
+
673
+ .p60-form-respondent > legend {
674
+ margin-block-end: 1rem;
675
+ }
676
+
612
677
  .p60-form-label {
613
678
  display: block;
614
679
  font-weight: 600;
@@ -676,7 +741,7 @@ textarea.p60-form-control {
676
741
  font-size: 0.9rem;
677
742
  }
678
743
 
679
- /* Checkbox rows — pin the box small and top-align it with the first line of the label text. */
744
+ /* Checkbox rows, pin the box small and top-align it with the first line of the label text. */
680
745
  .p60-form-check {
681
746
  display: flex;
682
747
  align-items: flex-start;
@@ -690,11 +755,18 @@ textarea.p60-form-control {
690
755
  width: 1.05rem;
691
756
  height: 1.05rem;
692
757
  flex: 0 0 auto;
693
- margin: 0.15rem 0 0;
694
- accent-color: var(--primary);
758
+ margin: 0.25rem 0 0;
759
+ padding: 0;
760
+ accent-color: var(--accent, var(--primary));
695
761
  cursor: pointer;
696
762
  }
697
763
 
764
+ .p60-form-check input:is([type='checkbox'], [type='radio']):focus-visible {
765
+ outline: 2px solid var(--ink);
766
+ outline-offset: 3px;
767
+ box-shadow: none;
768
+ }
769
+
698
770
  .p60-form-check.has-error span {
699
771
  color: var(--danger);
700
772
  }
@@ -704,7 +776,7 @@ textarea.p60-form-control {
704
776
  font-style: italic;
705
777
  }
706
778
 
707
- /* MULTISELECT checkbox list / CHOICE radio list — vertical lists reusing the aligned check-row. */
779
+ /* MULTISELECT checkbox list / CHOICE radio list, vertical lists reusing the aligned check-row. */
708
780
  .p60-form-checklist,
709
781
  .p60-form-radios {
710
782
  display: flex;
@@ -718,7 +790,7 @@ textarea.p60-form-control {
718
790
  margin-bottom: 0;
719
791
  }
720
792
 
721
- /* Searchable multi-select dropdown (MultiSelectDropdown) — the "pick many from a long list" widget. */
793
+ /* Searchable multi-select dropdown (MultiSelectDropdown), the "pick many from a long list" widget. */
722
794
  .p60-ms {
723
795
  position: relative;
724
796
  }
@@ -840,7 +912,7 @@ textarea.p60-form-control {
840
912
  font-size: 0.9rem;
841
913
  }
842
914
 
843
- /* ---- Custom desktop date picker (DateField) — native <input type=date> is used on touch ---- */
915
+ /* ---- Custom desktop date picker (DateField), native <input type=date> is used on touch ---- */
844
916
  .p60-date {
845
917
  position: relative;
846
918
  }
@@ -971,7 +1043,7 @@ textarea.p60-form-control {
971
1043
  background: transparent;
972
1044
  }
973
1045
 
974
- /* (hero / impact / band — the home page's Classic presentation — live in templates/default/theme.ts) */
1046
+ /* (hero / impact / band, the home page's Classic presentation, live in templates/default/theme.ts) */
975
1047
 
976
1048
  /* ---------- donation widget ---------- */
977
1049
 
@@ -985,7 +1057,7 @@ textarea.p60-form-control {
985
1057
  gap: 1rem;
986
1058
  }
987
1059
 
988
- /* MOBILE BREAKOUT (owner, 2026-08-17): on phones the nesting stack squashes the card badly —
1060
+ /* MOBILE BREAKOUT (owner, 2026-08-17): on phones the nesting stack squashes the card badly ,
989
1061
  measured on Sanctuary: 390px viewport → 346 container → 294 aside → 249px card → ~195px of
990
1062
  actual controls. A donate form is a working control surface; it gets the full screen width
991
1063
  (edge to edge, side borders/radius dropped, its own padding kept). The negative-margin trick
@@ -1202,7 +1274,7 @@ textarea.p60-form-control {
1202
1274
  color: var(--primary-strong);
1203
1275
  }
1204
1276
 
1205
- /* Named suggested amounts — a vertical list with the amount + its label (e.g. "Feeds a family"). */
1277
+ /* Named suggested amounts, a vertical list with the amount + its label (e.g. "Feeds a family"). */
1206
1278
  .amount-list {
1207
1279
  display: grid;
1208
1280
  gap: 0.6rem;
@@ -1275,6 +1347,12 @@ textarea.p60-form-control {
1275
1347
  padding-left: 1.8rem;
1276
1348
  }
1277
1349
 
1350
+ .cover-costs-row {
1351
+ display: flex;
1352
+ gap: 0.5rem;
1353
+ align-items: flex-start;
1354
+ }
1355
+
1278
1356
  .donor-fields {
1279
1357
  display: grid;
1280
1358
  grid-template-columns: 1fr 1fr;
@@ -1427,7 +1505,7 @@ textarea.p60-form-control {
1427
1505
  margin-top: 0.4rem;
1428
1506
  }
1429
1507
 
1430
- /* (about-page .prose/.value-grid — Classic's section presentation — live in templates/default/theme.ts) */
1508
+ /* (about-page .prose/.value-grid, Classic's section presentation, live in templates/default/theme.ts) */
1431
1509
 
1432
1510
  /* ---------- responsive ---------- */
1433
1511
 
@@ -1615,11 +1693,11 @@ textarea.p60-form-control {
1615
1693
  /* --- Articles (public) --- */
1616
1694
  /* The blog is long-form READING (unlike the full-width marketing Services pages): the article body
1617
1695
  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,
1696
+ typography, content only carries semantic structure (mark, pull quotes, captions, tables,
1619
1697
  embeds), styled here. */
1620
1698
  .article-article {
1621
1699
  /* 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 —
1700
+ the running text keeps its own readable measure via .article-body/.article-article-head ,
1623
1701
  so navigating article ↔ other pages no longer visibly narrows the page. */
1624
1702
  max-width: var(--container);
1625
1703
  margin: 0 auto;
@@ -1673,7 +1751,7 @@ textarea.p60-form-control {
1673
1751
  }
1674
1752
 
1675
1753
  .article-body {
1676
- /* Fills the container — same call as .service-body ("widened by preference"): the page reads as
1754
+ /* Fills the container, same call as .service-body ("widened by preference"): the page reads as
1677
1755
  one full-width surface; line-height carries the readability. */
1678
1756
  margin: 0;
1679
1757
  color: var(--ink);
@@ -1728,7 +1806,7 @@ textarea.p60-form-control {
1728
1806
  .article-body img.size-full {
1729
1807
  max-width: 100%;
1730
1808
  }
1731
- /* The semantic highlight — theme-tinted, never an author-picked colour. */
1809
+ /* The semantic highlight, theme-tinted, never an author-picked colour. */
1732
1810
  .article-body mark {
1733
1811
  background: color-mix(in srgb, var(--gold) 38%, transparent);
1734
1812
  color: inherit;
@@ -1741,7 +1819,7 @@ textarea.p60-form-control {
1741
1819
  padding: 0.25rem 1rem;
1742
1820
  color: var(--muted);
1743
1821
  }
1744
- /* The pull quote — a stand-out breath in the reading flow. */
1822
+ /* The pull quote, a stand-out breath in the reading flow. */
1745
1823
  .article-body blockquote.pull {
1746
1824
  border-inline-start: none;
1747
1825
  border-top: 2px solid var(--line-strong);
@@ -1834,7 +1912,7 @@ textarea.p60-form-control {
1834
1912
  margin: 1.5rem 0 0;
1835
1913
  }
1836
1914
 
1837
- /* Engagement bar under the article. Wraps — never scrolls sideways. */
1915
+ /* Engagement bar under the article. Wraps, never scrolls sideways. */
1838
1916
  .article-engage {
1839
1917
  margin: 1.5rem 0;
1840
1918
  display: flex;
@@ -1870,7 +1948,7 @@ textarea.p60-form-control {
1870
1948
  font-size: 0.95rem;
1871
1949
  }
1872
1950
 
1873
- /* The share row — plain share-intent buttons (no network SDKs). Wraps with the bar. */
1951
+ /* The share row, plain share-intent buttons (no network SDKs). Wraps with the bar. */
1874
1952
  .article-share-group {
1875
1953
  display: flex;
1876
1954
  align-items: center;
@@ -1900,7 +1978,7 @@ textarea.p60-form-control {
1900
1978
  border-color: var(--accent);
1901
1979
  color: var(--accent-strong);
1902
1980
  }
1903
- /* Brand tints on hover — recognisable without shouting at rest (and dark-theme safe: the
1981
+ /* Brand tints on hover, recognisable without shouting at rest (and dark-theme safe: the
1904
1982
  at-rest button is theme ink on theme surface). */
1905
1983
  .article-share-btn--facebook:hover { color: #1877f2; border-color: #1877f2; }
1906
1984
  .article-share-btn--x:hover { color: var(--ink); border-color: var(--ink); }
@@ -2014,7 +2092,7 @@ textarea.p60-form-control {
2014
2092
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
2015
2093
  gap: 1.5rem;
2016
2094
  }
2017
- /* MOBILE: the stacked grid becomes an edge-to-edge SWIPE ROW with a peek (owner, 2026-08-17) —
2095
+ /* MOBILE: the stacked grid becomes an edge-to-edge SWIPE ROW with a peek (owner, 2026-08-17) ,
2018
2096
  the scroller bleeds to the screen edges, each card keeps its padding, the first card aligns to
2019
2097
  the text gutter, and the cut-off next card IS the "swipe me" affordance (the reference maps'
2020
2098
  card idiom). Snap keeps cards settling cleanly; the scrollbar is hidden, the peek replaces it. */
@@ -2095,7 +2173,7 @@ textarea.p60-form-control {
2095
2173
  .article-card-counts {
2096
2174
  margin-inline-start: 0.6rem;
2097
2175
  }
2098
- /* The featured (newest) card: a magazine lead — image beside copy on wide screens. */
2176
+ /* The featured (newest) card: a magazine lead, image beside copy on wide screens. */
2099
2177
  .article-card--featured {
2100
2178
  display: grid;
2101
2179
  grid-template-columns: 1.2fr 1fr;
@@ -2132,7 +2210,7 @@ textarea.p60-form-control {
2132
2210
  color: var(--muted);
2133
2211
  }
2134
2212
  .article-more {
2135
- /* Spans the page frame like the hero + back-link (the reading column stays 70ch) — one frame
2213
+ /* Spans the page frame like the hero + back-link (the reading column stays 70ch), one frame
2136
2214
  edge + one reading edge, nothing floating at a third width. Grid auto-fills the row. */
2137
2215
  max-width: var(--container);
2138
2216
  margin: 2.5rem auto 0;
@@ -2178,7 +2256,7 @@ textarea.p60-form-control {
2178
2256
  .event-card-image-placeholder {
2179
2257
  display: grid;
2180
2258
  place-items: center;
2181
- /* Same fallback-tint convention as course cards — see .course-card-tint. */
2259
+ /* Same fallback-tint convention as course cards, see .course-card-tint. */
2182
2260
  background: var(--media-tint, hsl(210 34% 33%));
2183
2261
  }
2184
2262
  .event-card-image-placeholder span {
@@ -2212,7 +2290,7 @@ textarea.p60-form-control {
2212
2290
  align-self: start;
2213
2291
  }
2214
2292
 
2215
- /* .event-detail carries NO max-width — page bodies fill var(--container) (the standing
2293
+ /* .event-detail carries NO max-width, page bodies fill var(--container) (the standing
2216
2294
  full-width rule; the 760px reading measure was removed 2026-07-24, as on courses). */
2217
2295
  .event-detail-image {
2218
2296
  width: 100%;
@@ -2427,7 +2505,7 @@ textarea.p60-form-control {
2427
2505
  align-items: center;
2428
2506
  gap: 0.5rem;
2429
2507
  }
2430
- /* MOBILE: the rotator is MEDIA — it bleeds edge to edge like the hero and the map, with the
2508
+ /* MOBILE: the rotator is MEDIA, it bleeds edge to edge like the hero and the map, with the
2431
2509
  arrows overlaid on the image instead of eating width beside it. */
2432
2510
  @media (max-width: 640px) {
2433
2511
  .events-carousel {
@@ -2875,7 +2953,7 @@ textarea.p60-form-control {
2875
2953
 
2876
2954
  /* ---- Members area (shell + sections; see components/members/) ----
2877
2955
  Desktop: left rail + content. Small screens: the rail becomes a horizontal, scrollable pill
2878
- strip above the content — same registry, no separate mobile markup. */
2956
+ strip above the content, same registry, no separate mobile markup. */
2879
2957
  .members-head {
2880
2958
  margin-bottom: 1.6rem;
2881
2959
  }
@@ -2911,7 +2989,7 @@ textarea.p60-form-control {
2911
2989
  color: var(--ink);
2912
2990
  text-align: left;
2913
2991
  cursor: pointer;
2914
- /* Labels WRAP inside the pill (never overflow it) — long names and translations both fit. */
2992
+ /* Labels WRAP inside the pill (never overflow it), long names and translations both fit. */
2915
2993
  line-height: 1.25;
2916
2994
  }
2917
2995
  .members-rail-item .members-rail-icon { flex: 0 0 auto; }
@@ -3050,7 +3128,7 @@ textarea.p60-form-control {
3050
3128
  }
3051
3129
  }
3052
3130
 
3053
- /* Just above the burger point the full-size nav is tight — compact spacing spreads it cleanly
3131
+ /* Just above the burger point the full-size nav is tight, compact spacing spreads it cleanly
3054
3132
  instead of letting anything drop. */
3055
3133
  @media (min-width: 961px) and (max-width: 1120px) {
3056
3134
  .site-nav {
@@ -3172,7 +3250,7 @@ textarea.p60-form-control {
3172
3250
 
3173
3251
  /* ---- Members rail on mobile: a LEFT SLIDE-IN DRAWER behind a round user-icon button ----
3174
3252
  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
3253
+ off-canvas, sliding in over a scrim. The trigger is a slim bar, round user-icon button + the
3176
3254
  current section name. Desktop hides all of this and shows the rail inline. */
3177
3255
  .members-mobile-bar {
3178
3256
  display: none;
@@ -3280,7 +3358,7 @@ textarea.p60-form-control {
3280
3358
  }
3281
3359
 
3282
3360
  /* ---- 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. ---- */
3361
+ view-all). Newspaper-style, the sidebar stacks below the article on small screens. ---- */
3284
3362
  .article-front {
3285
3363
  display: grid;
3286
3364
  grid-template-columns: minmax(0, 1fr) 300px;
@@ -3383,7 +3461,7 @@ textarea.p60-form-control {
3383
3461
  }
3384
3462
  }
3385
3463
 
3386
- /* ---- Consent banner + preference centre (platform-level, always on — compliance is
3464
+ /* ---- Consent banner + preference centre (platform-level, always on, compliance is
3387
3465
  platform-owned). Fixed bottom layer; the preference centre grows out of the same layer. ---- */
3388
3466
  .consent-layer {
3389
3467
  position: fixed;
@@ -3539,7 +3617,7 @@ textarea.p60-form-control {
3539
3617
  color: inherit;
3540
3618
  }
3541
3619
 
3542
- /* ---- Marketing opt-in checkbox (the explicit reg-22 consent — deliberately quiet, never
3620
+ /* ---- Marketing opt-in checkbox (the explicit reg-22 consent, deliberately quiet, never
3543
3621
  pre-ticked) + the members contact toggle. ---- */
3544
3622
  .marketing-optin {
3545
3623
  display: flex;
@@ -3608,11 +3686,11 @@ textarea.p60-form-control {
3608
3686
  }
3609
3687
 
3610
3688
  /* ---- Cross-document view transitions (MPA soft-feel) ----
3611
- Every nav click is a full document navigation (Astro MPA — no client router). This asks the
3689
+ Every nav click is a full document navigation (Astro MPA, no client router). This asks the
3612
3690
  browser to HOLD the old frame and cross-fade to the new page once it's ready (Chrome 126+,
3613
3691
  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. */
3692
+ (masthead/nav) cross-fades between identical pixels, i.e. visually persists. True soft-nav
3693
+ with persistent island state would be Astro's ClientRouter, a later, bigger step. */
3616
3694
  @view-transition {
3617
3695
  navigation: auto;
3618
3696
  }
@@ -3624,7 +3702,7 @@ textarea.p60-form-control {
3624
3702
  }
3625
3703
  }
3626
3704
 
3627
- /* The article page's back-link needs air before the chips/title (detail page only — the front
3705
+ /* The article page's back-link needs air before the chips/title (detail page only, the front
3628
3706
  page's grid must keep its own alignment). */
3629
3707
  .services-back + .article-article {
3630
3708
  margin-top: 1.75rem;
@@ -3741,7 +3819,7 @@ textarea.p60-form-control {
3741
3819
  mixed grids keep one card anatomy. The tint arrives as a CSS VARIABLE, not an inline background,
3742
3820
  so a template whose palette collides with the green→indigo band can override this rule and
3743
3821
  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
3822
+ /* Admin-entered prose can contain unbroken tokens (URLs, pasted refs), never let one push a
3745
3823
  container wide. Applies to every surface that renders tenant summaries/bodies. */
3746
3824
  .event-detail-desc,
3747
3825
  .event-about,
@@ -4023,7 +4101,7 @@ textarea.p60-form-control {
4023
4101
  color: var(--muted);
4024
4102
  white-space: pre-line;
4025
4103
  }
4026
- /* The members-area PILL BUTTON (user, 2026-07-22) — the one action idiom across member surfaces
4104
+ /* The members-area PILL BUTTON (user, 2026-07-22), the one action idiom across member surfaces
4027
4105
  (directions, household invite/remove, …): bordered pill, hover fill, danger modifier. */
4028
4106
  .member-pill-btn {
4029
4107
  display: inline-flex;
@@ -4046,7 +4124,7 @@ textarea.p60-form-control {
4046
4124
  .member-pill-btn:disabled { opacity: 0.5; cursor: default; }
4047
4125
  .member-pill-btn-danger { color: #c4462e; }
4048
4126
 
4049
- /* Get directions = a proper pill button (user, 2026-07-22) — the resource-chip idiom, so it
4127
+ /* Get directions = a proper pill button (user, 2026-07-22), the resource-chip idiom, so it
4050
4128
  reads as an action rather than text among the venue lines. */
4051
4129
  .member-course-directions {
4052
4130
  display: inline-flex;
@@ -4067,7 +4145,7 @@ textarea.p60-form-control {
4067
4145
  background: color-mix(in srgb, currentColor 8%, transparent);
4068
4146
  text-decoration: none;
4069
4147
  }
4070
- /* The feedback-loop accordion (2026-07-21): latest note + attendance history — renders only when
4148
+ /* The feedback-loop accordion (2026-07-21): latest note + attendance history, renders only when
4071
4149
  a course runner actually recorded something. */
4072
4150
  .member-course-feedback {
4073
4151
  margin-top: 0.6rem;
@@ -4129,6 +4207,8 @@ textarea.p60-form-control {
4129
4207
  }
4130
4208
  .member-course-attendance li:last-child { border-bottom: none; }
4131
4209
  .member-course-attendance li.is-absent { opacity: 0.7; }
4210
+ /* Which class this mark was, for a learner who attends more than one. */
4211
+ .member-course-attendance-class { display: block; font-size: 0.8em; font-style: normal; opacity: 0.75; }
4132
4212
 
4133
4213
  /* C1.9 breaks (informational) + C1.8 course materials (presigned downloads). */
4134
4214
  .member-course-breaks {
@@ -4147,7 +4227,7 @@ textarea.p60-form-control {
4147
4227
  text-transform: uppercase;
4148
4228
  opacity: 0.7;
4149
4229
  }
4150
- /* Download CHIPS (user, 2026-07-22): the whole pill is the button — unmistakably tappable. */
4230
+ /* Download CHIPS (user, 2026-07-22): the whole pill is the button, unmistakably tappable. */
4151
4231
  .member-resource-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.45rem; }
4152
4232
  .member-resource-chip {
4153
4233
  display: inline-flex;
@@ -4167,7 +4247,7 @@ textarea.p60-form-control {
4167
4247
  }
4168
4248
  .member-resource-chip-size { font-weight: 400; opacity: 0.65; font-size: 0.85rem; }
4169
4249
 
4170
- /* "Billing & payment methods" (user, 2026-07-22): the billing half — one line per ongoing
4250
+ /* "Billing & payment methods" (user, 2026-07-22): the billing half, one line per ongoing
4171
4251
  commitment (a family subscription = ONE line: total + who it covers). */
4172
4252
  .members-subsection-title {
4173
4253
  font-size: 1.05rem;
@@ -4193,7 +4273,7 @@ textarea.p60-form-control {
4193
4273
  .member-billing-detail { font-size: 0.9rem; margin-top: 0.15rem; }
4194
4274
  .member-billing-pastdue { color: var(--accent, #d33); font-weight: 600; }
4195
4275
 
4196
- /* The course TITLE BAR (user, 2026-07-22): a distinct full-width band at the top of the card —
4276
+ /* The course TITLE BAR (user, 2026-07-22): a distinct full-width band at the top of the card ,
4197
4277
  bleeds to the card edges via negative margins, slightly larger type. */
4198
4278
  .member-course-title-bar {
4199
4279
  margin: -1rem -1.1rem 0.35rem;
@@ -4206,7 +4286,7 @@ textarea.p60-form-control {
4206
4286
  }
4207
4287
 
4208
4288
  /* 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,
4289
+ an ACCORDION row, a full-width, obviously-tappable bar (chevron + hover), default collapsed,
4210
4290
  one open at a time. */
4211
4291
  .member-course-people {
4212
4292
  margin-top: 0.85rem;
@@ -4310,7 +4390,7 @@ textarea.p60-form-control {
4310
4390
  flex-wrap: wrap;
4311
4391
  }
4312
4392
 
4313
- /* Courses picker refinement (2026-07-19): tap-to-select cards — the native checkbox/radio is
4393
+ /* Courses picker refinement (2026-07-19): tap-to-select cards, the native checkbox/radio is
4314
4394
  visually hidden (kept in the DOM for keyboard + screen-reader semantics); selection reads as
4315
4395
  the highlighted card. The footer is an itemised order summary instead of a one-line count. */
4316
4396
  .course-variant-card,
@@ -4430,7 +4510,7 @@ textarea.p60-form-control {
4430
4510
  font-weight: 600;
4431
4511
  }
4432
4512
 
4433
- /* ---- Prayer timetable page (worship WP5) — theme-neutral: borders/highlights derive from
4513
+ /* ---- Prayer timetable page (worship WP5), theme-neutral: borders/highlights derive from
4434
4514
  currentColor so every template's palette carries through. ---- */
4435
4515
  .pt-head {
4436
4516
  display: flex;
@@ -4505,7 +4585,7 @@ textarea.p60-form-control {
4505
4585
  opacity: 0.7;
4506
4586
  }
4507
4587
 
4508
- /* Small screens: the month table becomes stacked day cards — no horizontal scrolling. */
4588
+ /* Small screens: the month table becomes stacked day cards, no horizontal scrolling. */
4509
4589
  .pt-cards {
4510
4590
  display: none;
4511
4591
  list-style: none;
@@ -4583,7 +4663,7 @@ textarea.p60-form-control {
4583
4663
  font-size: 0.92rem;
4584
4664
  }
4585
4665
 
4586
- /* ── People / team cards — desktop grid, single column on phones ───────────────────────────── */
4666
+ /* ── People / team cards, desktop grid, single column on phones ───────────────────────────── */
4587
4667
  .people-grid {
4588
4668
  display: grid;
4589
4669
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
@@ -4614,29 +4694,29 @@ textarea.p60-form-control {
4614
4694
  .people-grid { grid-template-columns: 1fr; }
4615
4695
  }
4616
4696
 
4617
- /* ── Impact map island (WP4) — the OVERLAY borrows the theme: pins wear --marker (a brand accent by
4697
+ /* ── Impact map island (WP4), the OVERLAY borrows the theme: pins wear --marker (a brand accent by
4618
4698
  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
4699
+ theme + --map-mode (see lib/mapTheme.ts). Brand on the overlay, neutrals on the map, the recipe
4620
4700
  that stays legible on any theme. Templates restyle any of this via the tokens or these classes. */
4621
4701
  .impact-map { margin: 24px 0; }
4622
4702
  .impact-map-head h2 { margin: 0 0 6px; }
4623
4703
  .impact-map-intro { margin: 0 0 14px; opacity: 0.85; }
4624
4704
  .impact-map-stats { margin: 0 0 14px; color: var(--muted); font-weight: 600; letter-spacing: 0.01em; }
4625
4705
  /* 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
4706
+ longitude by 84°N-58°S (whole Greenland in, only the polar void and Antarctica trimmed), which in
4707
+ Web Mercator is a 1.497:1 box, so a frame with that exact ratio holds the full planet edge to
4628
4708
  edge, full width AND full height, at any screen size. A ratio wider than the band (a max-height
4629
4709
  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
4710
+ NARROWER than the frame, dead bars at the sides; a narrower ratio (min-height on phones) just
4631
4711
  adds benign vertical margin. Relative + clipped so MapLibre always has a real box and can never
4632
4712
  escape it. Keep this ratio in step with the island's planet bounds. */
4633
4713
  .impact-map-stage { position: relative; width: 100%; aspect-ratio: 1.497; min-height: 300px;
4634
4714
  border-radius: 14px; overflow: hidden; }
4635
4715
  /* 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
4716
+ width FROM the aspect-ratio (300 x 1.497 = 449px), on phones the stage overflowed the page
4637
4717
  sideways, shoving every other section into a lopsided "left with a right gutter" layout. Width
4638
4718
  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
4719
+ /* MOBILE BREAKOUT: a map is a canvas, every pixel is information, nothing needs a margin, so on
4640
4720
  phones it bleeds edge to edge (media bleeds, prose gutters: the hero already sets the rhythm).
4641
4721
  Cooperative gestures keep page-scrolling safe over the full-width canvas. */
4642
4722
  @media (max-width: 640px) {
@@ -4662,7 +4742,7 @@ textarea.p60-form-control {
4662
4742
  card on every screen size, as it always has. */
4663
4743
  .impact-map-card--small-screens { display: none; }
4664
4744
  @media (max-width: 640px) {
4665
- /* Full width, flush under the full-bleed map — the two read as one attached sheet (the same
4745
+ /* Full width, flush under the full-bleed map, the two read as one attached sheet (the same
4666
4746
  breakout as the donate card; the hairline top border is the seam between map and card). */
4667
4747
  .impact-map-card--small-screens {
4668
4748
  display: block;
@@ -4685,10 +4765,10 @@ textarea.p60-form-control {
4685
4765
  Sized with width/height (NOT inset) because MapLibre forces position:relative on this element,
4686
4766
  which would defeat absolute insets and collapse the box to zero height. */
4687
4767
  .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
4768
+ /* Live-map markers are a MapLibre circle layer (WebGL), they scale with zoom and take their colour
4769
+ from --marker (circle-color), so there is no DOM marker element to style here. The pin canvas
4690
4770
  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"
4771
+ /* The INLINE marker card (a MapLibre popup anchored at the clicked marker, where "See more"
4692
4772
  lives): the shell takes the theme's surface; the tip is recoloured per anchor side to match. */
4693
4773
  .impact-map-popup .maplibregl-popup-content { background: var(--surface); color: var(--ink);
4694
4774
  border: 1px solid var(--line); border-radius: var(--radius-sm); box-shadow: var(--shadow);