@usableapp/cardds 0.2.2 → 0.4.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.
Files changed (103) hide show
  1. package/README.md +35 -107
  2. package/cardds.js +6 -449
  3. package/css/actions.css +41 -35
  4. package/css/base.css +9 -9
  5. package/css/card.css +26 -25
  6. package/css/choice.css +50 -50
  7. package/css/forms.css +18 -18
  8. package/css/journey.css +16 -16
  9. package/css/layover.css +45 -45
  10. package/css/lists.css +43 -43
  11. package/css/media.css +33 -33
  12. package/css/numbers.css +25 -25
  13. package/css/people.css +11 -11
  14. package/css/sheet.css +29 -29
  15. package/css/step.css +6 -6
  16. package/css/theme.template.css +6 -3
  17. package/css/tokens.css +2 -0
  18. package/dist/actions/Btn.d.ts +109 -108
  19. package/dist/actions/BtnRow.d.ts +2 -2
  20. package/dist/actions/Chip.d.ts +102 -101
  21. package/dist/actions/Dot.d.ts +2 -2
  22. package/dist/actions/Dropdown.d.ts +36 -19
  23. package/dist/actions/Dropdown.js +41 -11
  24. package/dist/actions/Fab.d.ts +2 -2
  25. package/dist/actions/IconBtn.d.ts +298 -10
  26. package/dist/actions/IconBtn.js +6 -8
  27. package/dist/actions/Link.d.ts +105 -104
  28. package/dist/actions/Segment.d.ts +3 -3
  29. package/dist/cardds.css +354 -345
  30. package/dist/cards/Card.d.ts +99 -98
  31. package/dist/cards/CardFoot.d.ts +2 -2
  32. package/dist/cards/CardHead.d.ts +2 -2
  33. package/dist/cards/CardParts.d.ts +7 -7
  34. package/dist/cards/Placeholder.d.ts +2 -2
  35. package/dist/cards/Stat.d.ts +7 -5
  36. package/dist/cards/Stat.js +2 -2
  37. package/dist/choice/Calendar.d.ts +17 -6
  38. package/dist/choice/Calendar.js +9 -3
  39. package/dist/choice/Check.d.ts +3 -3
  40. package/dist/choice/DayStrip.d.ts +3 -3
  41. package/dist/choice/Mood.d.ts +2 -2
  42. package/dist/choice/Slider.d.ts +3 -3
  43. package/dist/choice/Slider.js +12 -8
  44. package/dist/forms/AddRow.d.ts +2 -2
  45. package/dist/forms/Composer.d.ts +2 -2
  46. package/dist/forms/Field.d.ts +2 -2
  47. package/dist/forms/FileBtn.d.ts +2 -2
  48. package/dist/forms/Pin.d.ts +10 -7
  49. package/dist/forms/Pin.js +47 -18
  50. package/dist/journey/NoteRow.d.ts +2 -2
  51. package/dist/journey/Route.d.ts +2 -2
  52. package/dist/journey/TileBadge.d.ts +2 -2
  53. package/dist/layover/Banner.d.ts +2 -2
  54. package/dist/layover/Callout.d.ts +2 -2
  55. package/dist/layover/Deck.d.ts +3 -3
  56. package/dist/lists/Bubble.d.ts +3 -3
  57. package/dist/lists/Kv.d.ts +5 -5
  58. package/dist/lists/Legend.d.ts +3 -3
  59. package/dist/lists/Row.d.ts +5 -4
  60. package/dist/lists/Timeline.d.ts +3 -3
  61. package/dist/media/MapArea.d.ts +2 -2
  62. package/dist/media/Mosaic.d.ts +3 -3
  63. package/dist/media/Postcard.d.ts +9 -5
  64. package/dist/media/Postcard.js +8 -3
  65. package/dist/media/Quote.d.ts +2 -2
  66. package/dist/media/Tile.d.ts +5 -4
  67. package/dist/numbers/Badge.d.ts +2 -2
  68. package/dist/numbers/Band.d.ts +3 -3
  69. package/dist/numbers/Bars.d.ts +3 -3
  70. package/dist/numbers/DotGrid.d.ts +2 -2
  71. package/dist/numbers/Picker.d.ts +20 -8
  72. package/dist/numbers/Picker.js +51 -13
  73. package/dist/numbers/Ring.d.ts +2 -2
  74. package/dist/numbers/Track.d.ts +3 -3
  75. package/dist/people/Avatar.d.ts +100 -99
  76. package/dist/people/AvatarPick.d.ts +3 -3
  77. package/dist/scaffold/ActionBar.d.ts +3 -3
  78. package/dist/scaffold/AppBar.d.ts +2 -2
  79. package/dist/scaffold/BaseContent.d.ts +7 -7
  80. package/dist/scaffold/BaseContent.js +4 -4
  81. package/dist/scaffold/Centre.d.ts +3 -3
  82. package/dist/scaffold/EmptyState.d.ts +2 -2
  83. package/dist/scaffold/FilterRow.d.ts +2 -2
  84. package/dist/scaffold/FloatBar.d.ts +105 -104
  85. package/dist/scaffold/PagerAt.d.ts +2 -2
  86. package/dist/scaffold/Screen.d.ts +100 -99
  87. package/dist/scaffold/TopBar.d.ts +2 -2
  88. package/dist/sheets/CardBack.d.ts +2 -2
  89. package/dist/sheets/Drawer.d.ts +19 -8
  90. package/dist/sheets/Drawer.js +29 -5
  91. package/dist/sheets/Sheet.d.ts +29 -16
  92. package/dist/sheets/Sheet.js +157 -11
  93. package/dist/sheets/SheetBody.d.ts +2 -2
  94. package/dist/sheets/SheetHead.d.ts +2 -2
  95. package/dist/sheets/SheetStack.d.ts +15 -3
  96. package/dist/sheets/SheetStack.js +74 -2
  97. package/dist/sheets/SheetStage.d.ts +2 -2
  98. package/dist/sheets/Step.d.ts +2 -2
  99. package/dist/type/Icon.d.ts +22 -7
  100. package/dist/type/Icon.js +27 -4
  101. package/dist/type/Text.d.ts +7 -1
  102. package/dist/type/Text.js +2 -2
  103. package/package.json +3 -2
package/dist/cardds.css CHANGED
@@ -90,6 +90,8 @@
90
90
  --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
91
91
  --bar-h: calc(var(--tap-base) * 4 / 3); /* 64 float bar, day cell, deck actions */
92
92
  --header-h: calc(var(--tap-base) * 5 / 4); /* 60 top bar — .topbar and .appbar alike */
93
+ --card-pad-rest: var(--sp-5); /* 20 a card's padding before any variant — .card publishes it as --card-pad; the sheet's peek strip is built from it where no card is in scope */
94
+ --card-gap-rest: var(--sp-4); /* 16 …and its gap */
93
95
  --screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
94
96
  --map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
95
97
  --fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
@@ -415,10 +417,10 @@ img, svg { display: block; max-width: 100%; }
415
417
  button { font: inherit; cursor: pointer; }
416
418
 
417
419
  /* icon from icons.svg sprite: <svg class="icon"><use href="icons.svg#bell"/></svg> */
418
- .icon { width: var(--icon); height: var(--icon); flex: none; }
419
- .icon--sm { width: var(--icon-sm); height: var(--icon-sm); }
420
- .icon--xs { width: var(--icon-xs); height: var(--icon-xs); } /* inside captions, chips, links */
421
- .icon--lg { width: var(--icon-lg); height: var(--icon-lg); } /* empty states */
420
+ .icon { width: var(--icon-size, var(--icon)); height: var(--icon-size, var(--icon)); flex: none; }
421
+ .icon--sm { width: var(--icon-sm-size, var(--icon-sm)); height: var(--icon-sm-size, var(--icon-sm)); }
422
+ .icon--xs { width: var(--icon-xs-size, var(--icon-xs)); height: var(--icon-xs-size, var(--icon-xs)); } /* inside captions, chips, links */
423
+ .icon--lg { width: var(--icon-lg-size, var(--icon-lg)); height: var(--icon-lg-size, var(--icon-lg)); } /* empty states */
422
424
 
423
425
  /* mobile screen column — the standard horizontal inset, and nothing vertical.
424
426
  Top and bottom are zero on purpose: vertical room is only ever a RESERVE for
@@ -485,9 +487,9 @@ button { font: inherit; cursor: pointer; }
485
487
  In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
486
488
  .base-content {
487
489
  display: grid;
488
- gap: var(--sp-3);
490
+ gap: var(--base-content-gap, var(--sp-3));
489
491
  align-content: start; /* a tall column never stretches the rows — the words hug the top */
490
- padding-block: var(--sp-4);
492
+ padding-block: var(--base-content-pad-y, var(--sp-4));
491
493
  }
492
494
  .base-content > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
493
495
  .base-content .cta-pack {
@@ -503,9 +505,9 @@ button { font: inherit; cursor: pointer; }
503
505
  display: grid;
504
506
  justify-items: center;
505
507
  text-align: center;
506
- gap: var(--sp-2);
507
- padding: var(--sp-12) var(--sp-6);
508
- color: var(--on-surface); /* full ink, icon and words alike: muted read as "disabled" at the middle of an empty screen (Lh 2026-09-16) — muted is for meta, not a sentence */
508
+ gap: var(--empty-state-gap, var(--sp-2));
509
+ padding: var(--empty-state-pad-y, var(--sp-12)) var(--empty-state-pad-x, var(--sp-6));
510
+ color: var(--empty-state-ink, var(--on-surface)); /* full ink, icon and words alike: muted read as "disabled" at the middle of an empty screen (Lh 2026-09-16) — muted is for meta, not a sentence */
509
511
  }
510
512
  .empty-state__action { margin-top: var(--sp-2); }
511
513
  /* inside a card (a section's "nothing yet"): the card's ink */
@@ -521,12 +523,12 @@ button { font: inherit; cursor: pointer; }
521
523
  .card {
522
524
  --card-bg: var(--card-1-bg); /* the slot, readable by children (avatar rings, fold) */
523
525
  --card-ink: var(--card-1-ink);
524
- --card-pad: var(--sp-5); /* published so .bleed can cancel it exactly */
525
- --card-gap: var(--sp-4); /* published so a sheet's peek strip (sheet.css) is exact */
526
+ --card-pad: var(--card-pad-rest); /* published so .bleed can cancel it exactly */
527
+ --card-gap: var(--card-gap-rest); /* published so a sheet's peek strip (sheet.css) is exact */
526
528
  background: var(--card-bg);
527
529
  color: var(--card-ink);
528
530
  border: var(--border-w) solid var(--card-border);
529
- border-radius: var(--r-card);
531
+ border-radius: var(--card-radius, var(--r-card));
530
532
  padding: var(--card-pad);
531
533
  display: grid;
532
534
  gap: var(--card-gap);
@@ -557,8 +559,8 @@ button { font: inherit; cursor: pointer; }
557
559
  flex-wrap: wrap; /* the caption / overline take a line of their own — no wrapper div */
558
560
  align-items: center;
559
561
  justify-content: space-between;
560
- column-gap: var(--sp-3);
561
- row-gap: var(--sp-0);
562
+ column-gap: var(--card-head-col-gap, var(--sp-3));
563
+ row-gap: var(--card-head-row-gap, var(--sp-0));
562
564
  }
563
565
  /* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
564
566
  <div class="card__head"><span class="t-overline t-muted">…</span><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
@@ -569,8 +571,8 @@ button { font: inherit; cursor: pointer; }
569
571
  .card__head > .t-overline { order: -1; }
570
572
  .card__head--top { align-items: flex-start; }
571
573
 
572
- .card__media { border-radius: var(--r-media); overflow: hidden; position: relative; } /* relative: corner badges */
573
- .card__end { text-align: right; display: grid; gap: var(--sp-0); justify-items: end; } /* trailing value + caption */
574
+ .card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
575
+ .card__end { text-align: right; display: grid; gap: var(--card-end-gap, var(--sp-0)); justify-items: end; } /* trailing value + caption */
574
576
  .card__media > img { width: 100%; height: auto; object-fit: cover; }
575
577
 
576
578
  /* ---- the card skeleton: head · content · foot ----
@@ -634,7 +636,7 @@ button { font: inherit; cursor: pointer; }
634
636
  /* foot: the card's action buttons on one line. One button takes the width,
635
637
  up to four share it in even parts — so the gaps read even too — and from
636
638
  three on they drop their side padding so the labels keep the room. */
637
- .card__foot { display: flex; gap: var(--sp-2); align-items: center; }
639
+ .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; }
638
640
  .card__foot > .btn { flex: 1 1 0; min-width: 0; }
639
641
  .card__foot:has(> :nth-child(3)) > .btn { padding-inline: var(--sp-2); }
640
642
 
@@ -768,51 +770,52 @@ button { font: inherit; cursor: pointer; }
768
770
  .screen__centre > .pager__at { justify-self: center; }
769
771
 
770
772
  /* compact card inside a card (nested note, promo) */
771
- .card--sm { padding: var(--sp-4); --card-gap: var(--sp-2); border-radius: var(--r-card); }
773
+ .card--sm { padding: var(--card-sm-pad, var(--sp-4)); --card-gap: var(--sp-2); border-radius: var(--card-sm-radius, var(--r-card)); }
772
774
 
773
775
  /* the sheet stack (.sheet-stack) — sheets stacked, heads peeking — lives in
774
776
  stack.css; a stacked sheet is a plain .card, the material */
775
777
 
776
778
  /* trailing icon cluster in a card header */
777
- .icon-row { display: flex; align-items: center; gap: var(--sp-4); opacity: 0.85; }
779
+ .icon-row { display: flex; align-items: center; gap: var(--icon-row-gap, var(--sp-4)); opacity: 0.85; }
778
780
 
779
781
  /* ---- card list: rows, each row its own boundary ---- */
780
- .card-list { display: grid; gap: var(--sp-3); }
782
+ .card-list { display: grid; gap: var(--card-list-gap, var(--sp-3)); }
781
783
 
782
784
  .card--row {
783
- border-radius: var(--r-card);
784
- padding: var(--sp-4);
785
+ border-radius: var(--card-row-radius, var(--r-card));
786
+ padding: var(--card-row-pad, var(--sp-4));
785
787
  display: flex;
786
788
  align-items: center;
787
- gap: var(--sp-3);
789
+ gap: var(--card-row-gap, var(--sp-3));
788
790
  }
789
791
  .card--row .card__body { flex: 1; display: grid; gap: var(--sp-0); }
790
792
 
791
793
  /* leading thumb/icon in a row */
792
794
  .card__thumb {
793
- width: var(--tap-sm); height: var(--tap-sm);
795
+ width: var(--card-thumb-size, var(--tap-sm)); height: var(--card-thumb-size, var(--tap-sm));
794
796
  flex: none;
795
- border-radius: var(--r-control);
797
+ border-radius: var(--card-thumb-radius, var(--r-control));
796
798
  border: var(--border-w) solid currentColor;
797
799
  display: grid;
798
800
  place-items: center;
799
- font: var(--type-label);
801
+ font: var(--card-thumb-font, var(--type-label));
800
802
  opacity: 0.85;
801
803
  }
802
804
 
803
805
  /* ---- inline stat block (revenue-style) ---- */
804
- .stat { display: grid; gap: var(--sp-1); }
805
- .stat__row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
806
+ .stat { display: grid; gap: var(--stat-gap, var(--sp-1)); }
807
+ .stat--centre { text-align: center; justify-items: center; } /* the one number of a sheet or a stage, under a centred avatar */
808
+ .stat__row { display: flex; align-items: center; justify-content: space-between; gap: var(--stat-row-gap, var(--sp-3)); }
806
809
 
807
810
  .delta {
808
- font: var(--type-caption);
811
+ font: var(--delta-font, var(--type-caption));
809
812
  font-weight: 700;
810
- padding: var(--sp-1) var(--sp-2);
811
- border-radius: var(--r-chip);
812
- background: var(--card-3-bg);
813
- color: var(--card-3-ink);
813
+ padding: var(--delta-pad-y, var(--sp-1)) var(--delta-pad-x, var(--sp-2));
814
+ border-radius: var(--delta-radius, var(--r-chip));
815
+ background: var(--delta-bg, var(--card-3-bg));
816
+ color: var(--delta-ink, var(--card-3-ink));
814
817
  }
815
- .delta--neg { background: var(--neg); color: var(--card-3-ink); } /* light ink on the signal color in every slot */
818
+ .delta--neg { background: var(--delta-neg-bg, var(--neg)); color: var(--delta-neg-ink, var(--card-3-ink)); } /* light ink on the signal color in every slot */
816
819
 
817
820
  /* ---- css/stack.css — sheet stack: .sheet-stack — the menu, sheets stacked, heads peek, tap opens ---- */
818
821
  /* ============================================================
@@ -1105,7 +1108,7 @@ button { font: inherit; cursor: pointer; }
1105
1108
  page. Many sheets stacked as the app's menu are the other system,
1106
1109
  .sheet-stack (stack.css): the same material, its own class and file.
1107
1110
 
1108
- <article class="card sheet sheet--half" data-sheet-states="peek half 3q full">
1111
+ <article class="card sheet sheet--half">
1109
1112
  A tap on the handle TOGGLES the sheet between its own state and peek
1110
1113
  (Lh 2026-09-16) — never a step-by-step climb; a drag snaps to any of
1111
1114
  the listed states, and the state it lands on becomes the one a tap
@@ -1123,14 +1126,12 @@ button { font: inherit; cursor: pointer; }
1123
1126
  ============================================================ */
1124
1127
 
1125
1128
  .sheet {
1126
- --_handle: var(--sp-1);
1127
1129
  /* the strip a peeked sheet shows: the card's own top padding, the handle,
1128
1130
  the gap, one title row (--tap — the head rule below makes every head at
1129
1131
  least that tall) and the gap under it, so the cut lands exactly where
1130
1132
  the first control starts and nothing of it leaks through. Published
1131
1133
  (and registered in tokens.css) so a sheet whose head is taller can
1132
1134
  retune it: .my-sheet { --sheet-peek: … } */
1133
- --sheet-peek: calc(var(--card-pad) + var(--_handle) + 2 * var(--card-gap) + var(--tap));
1134
1135
  --sheet-half: 0.5; /* a share of the sheet's own height */
1135
1136
  --sheet-3q: 0.25; /* three quarters showing: slid by a quarter */
1136
1137
  /* the sheet's own height, seen from inside it: the box it slides in is the
@@ -1159,20 +1160,22 @@ button { font: inherit; cursor: pointer; }
1159
1160
  overflow: clip;
1160
1161
  border-radius: var(--r-card) var(--r-card) 0 0;
1161
1162
  border-bottom: 0;
1162
- box-shadow: var(--shadow-sheet);
1163
+ box-shadow: var(--sheet-shadow, var(--shadow-sheet));
1163
1164
  }
1164
1165
  .sheet--full { --_y: 0%; --_cover: 0rem; }
1165
1166
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
1166
1167
  .sheet--3q { --_y: calc(var(--sheet-3q) * 100%); --_cover: calc(var(--sheet-3q) * var(--_box)); }
1167
1168
  .sheet--peek { --_y: calc(100% - var(--sheet-peek)); --_cover: calc(var(--_box) - var(--sheet-peek)); }
1169
+ /* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
1170
+ have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
1171
+ inherits it. A sheet whose head is taller retunes it on the box. */
1172
+ :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--tap)); }
1173
+
1168
1174
  /* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
1169
1175
  .sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
1170
1176
  changes), and hidden once it has left so its shadow does not lie on the step's bottom edge; write it
1171
- with `inert`. A button names its sheet — <button aria-controls="when"> … <article id="when" class="card
1172
- sheet sheet--away" inert data-sheet-states="peek full"> — and cardds.js does the rest: a tap slides that
1173
- sheet up to full and sends any other one away; the handle takes it down to peek, never further, so the
1174
- words and the other buttons are back in reach; the same button again raises it from peek to full.
1175
- Nothing waits at peek before the first call. No bar on such a screen (actions.css).
1177
+ with `inert` (the React Sheet does). WHO brings it onto the screen is the app's business — a button, a route:
1178
+ cardds ties no component to another. Nothing waits at peek before the first call.
1176
1179
  WHAT IT IS FOR: the base offers WAYS IN — sign in · new phone · join — and each button calls a sheet
1177
1180
  that holds that way's WHOLE form, its submit on the sheet's foot, leading to the next page. The base
1178
1181
  carries no control. It is NOT for a form taken apart — one sheet per field, a "send" left on the base:
@@ -1255,7 +1258,7 @@ button { font: inherit; cursor: pointer; }
1255
1258
  opacity: 0.3;
1256
1259
  width: var(--tap-sm);
1257
1260
  height: var(--_handle);
1258
- border-radius: var(--r-chip);
1261
+ border-radius: var(--sheet-handle-radius, var(--r-chip));
1259
1262
  margin: 0 auto;
1260
1263
  padding: 0;
1261
1264
  touch-action: none;
@@ -1287,8 +1290,8 @@ button { font: inherit; cursor: pointer; }
1287
1290
  min-height: var(--tap);
1288
1291
  text-align: center;
1289
1292
  }
1290
- .sheet__title { font: var(--type-h2); letter-spacing: var(--track-h2); margin: 0; }
1291
- .sheet__sub { font: var(--type-caption); color: var(--card-muted); }
1293
+ .sheet__title { font: var(--sheet-title-font, var(--type-h2)); letter-spacing: var(--track-h2); margin: 0; }
1294
+ .sheet__sub { font: var(--sheet-sub-font, var(--type-caption)); color: var(--sheet-sub-ink, var(--card-muted)); }
1292
1295
 
1293
1296
  /* ============================================================
1294
1297
  .sheet-stage — the overlay stage: a page-height box the sheet
@@ -1310,7 +1313,7 @@ button { font: inherit; cursor: pointer; }
1310
1313
  margin-inline: auto;
1311
1314
  overflow: clip; /* not hidden: a clipped box is not a scroll container, so revealing a focused field never shifts the stage */
1312
1315
  container-type: size; /* the sheet's body reads this box's height as 100cqh (--_box above) */
1313
- background: var(--surface);
1316
+ background: var(--sheet-stage-bg, var(--surface));
1314
1317
  }
1315
1318
  /* on an app screen the stage is the page: it takes the screen's inset back (edge to
1316
1319
  edge, like a step) and, after a fixed .appbar, the room the screen reserved for
@@ -1345,7 +1348,7 @@ button { font: inherit; cursor: pointer; }
1345
1348
  position: absolute;
1346
1349
  inset: 0;
1347
1350
  isolation: isolate; /* the ring and the hint stack inside the map only — a sheet over the map covers them */
1348
- color: var(--on-surface);
1351
+ color: var(--map-ink, var(--on-surface));
1349
1352
  background:
1350
1353
  repeating-linear-gradient(75deg, transparent 0 5.5rem, var(--hairline) 5.5rem 6.25rem),
1351
1354
  repeating-linear-gradient(-18deg, transparent 0 8.75rem, var(--ink-tint) 8.75rem 9.25rem),
@@ -1363,9 +1366,9 @@ button { font: inherit; cursor: pointer; }
1363
1366
  height: var(--map-ring);
1364
1367
  translate: -50% -50%;
1365
1368
  border-radius: 50%;
1366
- border: var(--border-w) solid var(--accent);
1367
- background: color-mix(in srgb, var(--accent) 22%, transparent);
1368
- box-shadow: var(--shadow-float);
1369
+ border: var(--border-w) solid var(--map-ring-border, var(--accent));
1370
+ background: var(--map-ring-bg, color-mix(in srgb, var(--accent) 22%, transparent));
1371
+ box-shadow: var(--map-ring-shadow, var(--shadow-float));
1369
1372
  pointer-events: none;
1370
1373
  z-index: 1; /* above the engine's panes (Leaflet's tiles sit at 200, but inside their own stacking context) */
1371
1374
  }
@@ -1374,10 +1377,10 @@ button { font: inherit; cursor: pointer; }
1374
1377
  right: var(--sp-2);
1375
1378
  bottom: var(--sp-2);
1376
1379
  padding: var(--sp-0) var(--sp-2);
1377
- border-radius: var(--r-chip);
1378
- background: color-mix(in srgb, var(--surface) 80%, transparent);
1379
- color: var(--on-surface-muted);
1380
- font: var(--type-caption);
1380
+ border-radius: var(--map-credit-radius, var(--r-chip));
1381
+ background: var(--map-credit-bg, color-mix(in srgb, var(--surface) 80%, transparent));
1382
+ color: var(--map-credit-ink, var(--on-surface-muted));
1383
+ font: var(--map-credit-font, var(--type-caption));
1381
1384
  pointer-events: none;
1382
1385
  z-index: 1;
1383
1386
  }
@@ -1388,12 +1391,12 @@ button { font: inherit; cursor: pointer; }
1388
1391
  translate: -50% 0;
1389
1392
  max-width: calc(100% - 2 * var(--sp-4));
1390
1393
  padding: var(--sp-2) var(--sp-4);
1391
- border-radius: var(--r-chip);
1392
- background: var(--card-3-bg);
1393
- color: var(--card-3-ink);
1394
- font: var(--type-label);
1394
+ border-radius: var(--map-hint-radius, var(--r-chip));
1395
+ background: var(--map-hint-bg, var(--card-3-bg));
1396
+ color: var(--map-hint-ink, var(--card-3-ink));
1397
+ font: var(--map-hint-font, var(--type-label));
1395
1398
  text-align: center;
1396
- box-shadow: var(--shadow-float);
1399
+ box-shadow: var(--map-hint-shadow, var(--shadow-float));
1397
1400
  pointer-events: none;
1398
1401
  z-index: 1;
1399
1402
  }
@@ -1407,7 +1410,7 @@ button { font: inherit; cursor: pointer; }
1407
1410
  the sheet gives it up itself, as --_lift — the same token a raised sheet floats on. */
1408
1411
  .sheet-stage > .sheet { --_lift: var(--kb, 0rem); bottom: var(--_lift); }
1409
1412
  .sheet-stage > .sheet--raised { --_lift: calc(var(--tap-lg) + var(--kb, 0rem)); }
1410
- .sheet--raised { --_lift: var(--tap-lg); bottom: var(--_lift); border-radius: var(--r-card); border-bottom: var(--border-w) solid var(--card-border); }
1413
+ .sheet--raised { --_lift: var(--tap-lg); bottom: var(--_lift); border-radius: var(--sheet-raised-radius, var(--r-card)); border-bottom: var(--border-w) solid var(--card-border); }
1411
1414
  .sheet-stage__foot {
1412
1415
  position: absolute;
1413
1416
  inset-inline: 0;
@@ -1416,8 +1419,8 @@ button { font: inherit; cursor: pointer; }
1416
1419
  margin: 0;
1417
1420
  display: grid;
1418
1421
  place-items: center;
1419
- font: var(--type-caption);
1420
- color: var(--card-3-ink);
1422
+ font: var(--sheet-stage-foot-font, var(--type-caption));
1423
+ color: var(--sheet-stage-foot-ink, var(--card-3-ink));
1421
1424
  z-index: 2;
1422
1425
  }
1423
1426
 
@@ -1497,8 +1500,9 @@ button { font: inherit; cursor: pointer; }
1497
1500
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
1498
1501
  /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
1499
1502
  move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
1500
- (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. Should a bar be written
1501
- beside a sheet anyway it stands down (actions.css + cardds.js) — a net for a mistake, not a layout. */
1503
+ (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
1504
+ own screens are composed (the guide); the framework does not police it — a component never changes
1505
+ because another one is there. */
1502
1506
 
1503
1507
  /* the reading block: a .base-content (base.css) — the same words-on-the-base
1504
1508
  block as anywhere else (it was .step__card, and a .card before 2026-09-16;
@@ -1517,11 +1521,10 @@ button { font: inherit; cursor: pointer; }
1517
1521
  color: var(--on-surface);
1518
1522
  }
1519
1523
  /* the reading block is the .base-content — or the .pages that holds several of them, a walk of words
1520
- (card.css). --sheet-room: the strip a CALLED sheet keeps at peek (sheet.css, "called sheets"), published
1521
- on the step by cardds.js while one is on the screen, so the buttons under it can scroll clear of it;
1522
- nothing without one. */
1524
+ (card.css). Behind a PEEKED sheet the words keep the strip's room under them, so whatever is down there
1525
+ (the buttons that call the other sheets) scrolls clear of it — CSS alone: the box declares --sheet-peek. */
1526
+ .step:has(> .sheet--peek) > :is(.base-content, .pages) { padding-bottom: var(--sheet-peek); }
1523
1527
  .step > :is(.base-content, .pages) {
1524
- padding-bottom: var(--sheet-room, 0rem);
1525
1528
  flex: 0 1 auto;
1526
1529
  }
1527
1530
  .step > :is(.base-content, .pages) {
@@ -1541,13 +1544,13 @@ button { font: inherit; cursor: pointer; }
1541
1544
  .field {
1542
1545
  display: flex;
1543
1546
  align-items: center;
1544
- gap: var(--sp-2);
1545
- border: var(--border-w) solid var(--action-border);
1546
- border-radius: var(--r-control);
1547
- background: var(--action-bg);
1548
- color: var(--action-ink);
1549
- padding-inline: var(--sp-4);
1550
- min-height: var(--tap);
1547
+ gap: var(--field-gap, var(--sp-2));
1548
+ border: var(--border-w) solid var(--field-border, var(--action-border));
1549
+ border-radius: var(--field-radius, var(--r-control));
1550
+ background: var(--field-bg, var(--action-bg));
1551
+ color: var(--field-ink, var(--action-ink));
1552
+ padding-inline: var(--field-pad-x, var(--sp-4));
1553
+ min-height: var(--field-min-h, var(--tap));
1551
1554
  min-width: 0;
1552
1555
  }
1553
1556
 
@@ -1564,7 +1567,7 @@ button { font: inherit; cursor: pointer; }
1564
1567
  .field input::placeholder { color: inherit; opacity: 0.55; }
1565
1568
 
1566
1569
  /* field with its action inside: date pick, send, go — the button sits flush right */
1567
- .field--action { padding-right: var(--sp-1); min-height: var(--tap-lg); }
1570
+ .field--action { padding-right: var(--sp-1); min-height: var(--field-action-min-h, var(--tap-lg)); }
1568
1571
  .field--action > .icon-btn { margin-left: auto; }
1569
1572
 
1570
1573
  /* colored palettes drop the visible outline elsewhere — fields keep one */
@@ -1574,25 +1577,25 @@ button { font: inherit; cursor: pointer; }
1574
1577
  .add-row {
1575
1578
  display: inline-flex;
1576
1579
  align-items: center;
1577
- gap: var(--sp-2);
1580
+ gap: var(--add-row-gap, var(--sp-2));
1578
1581
  border: 0;
1579
1582
  background: none;
1580
1583
  color: inherit;
1581
- font: var(--type-label);
1582
- font-size: var(--fs-body);
1583
- padding: var(--sp-2) 0;
1584
- min-height: var(--tap-sm);
1584
+ font: var(--add-row-font, var(--type-label));
1585
+ font-size: var(--add-row-font-size, var(--fs-body));
1586
+ padding: var(--add-row-pad-y, var(--sp-2)) var(--add-row-pad-x, 0);
1587
+ min-height: var(--add-row-min-h, var(--tap-sm));
1585
1588
  }
1586
1589
  .add-row__plus {
1587
- width: var(--icon);
1588
- height: var(--icon);
1590
+ width: var(--add-row-plus-size, var(--icon));
1591
+ height: var(--add-row-plus-size, var(--icon));
1589
1592
  flex: none;
1590
1593
  border-radius: 50%;
1591
- background: var(--accent);
1592
- color: var(--on-accent);
1594
+ background: var(--add-row-plus-bg, var(--accent));
1595
+ color: var(--add-row-plus-ink, var(--on-accent));
1593
1596
  display: grid;
1594
1597
  place-items: center;
1595
- font-size: var(--fs-md);
1598
+ font-size: var(--add-row-plus-font-size, var(--fs-md));
1596
1599
  line-height: 1;
1597
1600
  }
1598
1601
  .add-row__plus .icon { width: var(--icon-xs); height: var(--icon-xs); }
@@ -1605,40 +1608,40 @@ button { font: inherit; cursor: pointer; }
1605
1608
  ============================================================ */
1606
1609
 
1607
1610
  /* ---- route: endpoint · dotted connector (icon in middle) · endpoint ---- */
1608
- .route { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; column-gap: var(--sp-1); } /* line → icon: --sp-1, as the via always had; code → line is the code row's own --sp-3 */
1611
+ .route { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; column-gap: var(--route-col-gap, var(--sp-1)); } /* line → icon: --sp-1, as the via always had; code → line is the code row's own --sp-3 */
1609
1612
  /* the two endpoints get EQUAL columns whatever their words measure, so the via
1610
1613
  (the icon alone) sits at the card's centre; each dotted run lives on ITS
1611
1614
  endpoint's code row and fills that side's leftover — with short names the
1612
1615
  runs are long and equal, a long place name wraps in its half instead of
1613
1616
  pushing the plane (Lh 2026-09-16) */
1614
1617
 
1615
- .route__point { display: grid; gap: var(--sp-0); min-width: 0; }
1618
+ .route__point { display: grid; gap: var(--route-point-gap, var(--sp-0)); min-width: 0; }
1616
1619
  .route__point--end { text-align: right; }
1617
1620
 
1618
- .route__code { font: var(--type-h2); letter-spacing: var(--track-h2); display: flex; align-items: center; gap: var(--sp-3); }
1621
+ .route__code { font: var(--route-code-font, var(--type-h2)); letter-spacing: var(--track-h2); display: flex; align-items: center; gap: var(--route-code-gap, var(--sp-3)); }
1619
1622
  .route__code::after, .route__point--end .route__code::before { content: ""; flex: 1; border-top: var(--rule-w) dotted currentColor; opacity: 0.65; }
1620
1623
  .route__point--end .route__code::after { content: none; }
1621
- .route__time { font: var(--type-body); font-variant-numeric: tabular-nums; }
1622
- .route__place { font: var(--type-caption); color: var(--card-muted); }
1624
+ .route__time { font: var(--route-time-font, var(--type-body)); font-variant-numeric: tabular-nums; }
1625
+ .route__place { font: var(--route-place-font, var(--type-caption)); color: var(--route-place-ink, var(--card-muted)); }
1623
1626
  .card--3 .route__place { color: inherit; opacity: 0.7; }
1624
1627
 
1625
- .route__via { display: flex; align-items: center; opacity: 0.65; font-size: var(--fs-body); }
1628
+ .route__via { display: flex; align-items: center; opacity: 0.65; font-size: var(--route-via-font-size, var(--fs-body)); }
1626
1629
 
1627
1630
  /* ---- tile badge: tiny label over a big value (gate, seat, zone) ---- */
1628
1631
  .tile-badge {
1629
1632
  display: inline-grid;
1630
1633
  justify-items: center;
1631
- gap: var(--sp-0);
1632
- padding: var(--sp-1) var(--sp-2);
1633
- border-radius: var(--r-badge);
1634
- background: var(--card-3-bg);
1635
- color: var(--card-3-ink);
1634
+ gap: var(--tile-badge-gap, var(--sp-0));
1635
+ padding: var(--tile-badge-pad-y, var(--sp-1)) var(--tile-badge-pad-x, var(--sp-2));
1636
+ border-radius: var(--tile-badge-radius, var(--r-badge));
1637
+ background: var(--tile-badge-bg, var(--card-3-bg));
1638
+ color: var(--tile-badge-ink, var(--card-3-ink));
1636
1639
  }
1637
- .tile-badge__label { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; opacity: 0.7; }
1640
+ .tile-badge__label { font: var(--tile-badge-label-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; opacity: 0.7; }
1638
1641
  .tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
1639
1642
 
1640
1643
  /* plain: no fill — a date box beside a title (Jun / 18) */
1641
- .tile-badge--plain { background: transparent; color: inherit; padding: 0; min-width: var(--tap-sm); }
1644
+ .tile-badge--plain { background: transparent; color: inherit; padding: 0; min-width: var(--tile-badge-plain-min-w, var(--tap-sm)); }
1642
1645
  .tile-badge--plain .tile-badge__value { font: 800 var(--fs-h1)/1 var(--font-display); }
1643
1646
 
1644
1647
  /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */
@@ -1648,9 +1651,9 @@ button { font: inherit; cursor: pointer; }
1648
1651
  .note-row {
1649
1652
  display: flex;
1650
1653
  align-items: center;
1651
- gap: var(--sp-2);
1652
- font: var(--type-caption);
1653
- color: var(--card-muted);
1654
+ gap: var(--note-row-gap, var(--sp-2));
1655
+ font: var(--note-row-font, var(--type-caption));
1656
+ color: var(--note-row-ink, var(--card-muted));
1654
1657
  }
1655
1658
  .card--3 .note-row { color: inherit; opacity: 0.7; }
1656
1659
 
@@ -1660,6 +1663,14 @@ button { font: inherit; cursor: pointer; }
1660
1663
  topbar, chips, buttons, selects, bottom CTA pack
1661
1664
  ============================================================ */
1662
1665
 
1666
+ /* KNOBS (Lh 2026-09-21) — cardds is a framework: a project adjusts ONE component from outside, without touching this file.
1667
+ A knob is a custom property a rule reads WITH ITS DEFAULT AS THE FALLBACK — `padding-inline: var(--btn-pad-x, var(--sp-6))` —
1668
+ so it is declared nowhere: unset, nothing moves (tests/style-snapshot proves it); set in the project's theme.css it retunes
1669
+ that component everywhere; set on any element it retunes the components inside that element only. Names: --<component>-<what>
1670
+ (--btn-pad-x, --chip-bg, --icon-btn-size, --appbar-pad-x); a variant says so (--btn-primary-bg, --chip-on-ink, --icon-btn-sm-size).
1671
+ The sizes that already were root tokens (--btn-h, --chip-h, --bar-h, --header-h, --fab…) are knobs too — set them the same way.
1672
+ Prefer a BASE when the whole system should move (tokens.css); reach for a knob when ONE component should. `npm run knobs` lists them all. */
1673
+
1663
1674
  /* ---- the bars across the top of a screen: one height, whatever they carry ----
1664
1675
  .topbar (screen title) and .appbar (app context) are the same bar with
1665
1676
  different contents, so the height is declared here once rather than left to
@@ -1670,8 +1681,8 @@ button { font: inherit; cursor: pointer; }
1670
1681
  .topbar, .appbar {
1671
1682
  display: flex;
1672
1683
  align-items: center;
1673
- gap: var(--sp-3);
1674
- padding-block: var(--sp-2);
1684
+ gap: var(--bar-gap, var(--sp-3));
1685
+ padding-block: var(--bar-pad-y, var(--sp-2));
1675
1686
  min-height: var(--header-h);
1676
1687
  }
1677
1688
 
@@ -1700,9 +1711,9 @@ button { font: inherit; cursor: pointer; }
1700
1711
  top: 0;
1701
1712
  max-width: var(--screen-w);
1702
1713
  margin-inline: auto;
1703
- padding-inline: var(--screen-pad); /* fixed + full-bleed: the screen's own inset, so the bar's contents line up with the title bar under it (Lh 2026-09-15) */
1714
+ padding-inline: var(--appbar-pad-x, var(--screen-pad)); /* fixed + full-bleed: the screen's own inset, so the bar's contents line up with the title bar under it (Lh 2026-09-15) */
1704
1715
  z-index: 40; /* topmost — card animations never cover it */
1705
- background: var(--surface); /* solid: sliding cards never show through */
1716
+ background: var(--appbar-bg, var(--surface)); /* solid: sliding cards never show through */
1706
1717
  }
1707
1718
  /* fixed, so it covers the top of the screen it sits in — reserve its height
1708
1719
  there, from the same token, the way the bottom bars reserve theirs. The
@@ -1719,19 +1730,19 @@ button { font: inherit; cursor: pointer; }
1719
1730
 
1720
1731
  /* ---- icon button (round) ---- */
1721
1732
  .icon-btn {
1722
- width: var(--tap); height: var(--tap);
1733
+ width: var(--icon-btn-size, var(--tap)); height: var(--icon-btn-size, var(--tap));
1723
1734
  flex: none;
1724
- border-radius: var(--r-chip);
1725
- border: var(--border-w) solid var(--outline); /* see .btn — same reason */
1726
- background: var(--action-bg);
1727
- color: var(--action-ink);
1735
+ border-radius: var(--icon-btn-radius, var(--r-chip));
1736
+ border: var(--border-w) solid var(--icon-btn-border, var(--outline)); /* see .btn — same reason */
1737
+ background: var(--icon-btn-bg, var(--action-bg));
1738
+ color: var(--icon-btn-ink, var(--action-ink));
1728
1739
  display: grid;
1729
1740
  place-items: center;
1730
1741
  font-size: var(--fs-lg);
1731
1742
  line-height: 1;
1732
1743
  }
1733
- .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; }
1734
- .icon-btn--sm { width: var(--tap-sm); height: var(--tap-sm); font-size: var(--fs-title); }
1744
+ .icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
1745
+ .icon-btn--sm { width: var(--icon-btn-sm-size, var(--tap-sm)); height: var(--icon-btn-sm-size, var(--tap-sm)); font-size: var(--fs-title); }
1735
1746
  /* fab: the square floating action, bottom-right of a stage or screen */
1736
1747
  .fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3)); box-shadow: var(--shadow-float); z-index: 31; }
1737
1748
 
@@ -1740,21 +1751,21 @@ button { font: inherit; cursor: pointer; }
1740
1751
  /* --outline, not --action-border: same reason as .chip — on a palette where
1741
1752
  --card-1-bg equals --action-bg a transparent border makes a plain button
1742
1753
  invisible on a white card. --primary and --quiet set their own edge. */
1743
- border: var(--border-w) solid var(--outline);
1744
- background: var(--action-bg);
1745
- color: var(--action-ink);
1746
- border-radius: var(--r-chip);
1747
- padding: var(--sp-4) var(--sp-6);
1754
+ border: var(--border-w) solid var(--btn-border, var(--outline));
1755
+ background: var(--btn-bg, var(--action-bg));
1756
+ color: var(--btn-ink, var(--action-ink));
1757
+ border-radius: var(--btn-radius, var(--r-chip));
1758
+ padding: var(--btn-pad-y, var(--sp-4)) var(--btn-pad-x, var(--sp-6));
1748
1759
  font: var(--type-label);
1749
- font-size: var(--fs-md);
1760
+ font-size: var(--btn-font-size, var(--fs-md));
1750
1761
  display: inline-flex;
1751
1762
  align-items: center;
1752
1763
  justify-content: center;
1753
- gap: var(--sp-2);
1764
+ gap: var(--btn-gap, var(--sp-2));
1754
1765
  min-height: var(--btn-h);
1755
1766
  text-decoration: none; /* .btn works as <a> too */
1756
1767
  }
1757
- .btn--primary { background: var(--accent); color: var(--on-accent); border-color: transparent; }
1768
+ .btn--primary { background: var(--btn-primary-bg, var(--accent)); color: var(--btn-primary-ink, var(--on-accent)); border-color: transparent; }
1758
1769
  /* quiet keeps a faint outline so it still reads as a button on colored palettes;
1759
1770
  it is ink on whatever it sits on — inherit, or on the dark slot (card--3) it
1760
1771
  would take the light-card action ink and vanish */
@@ -1774,25 +1785,25 @@ button { font: inherit; cursor: pointer; }
1774
1785
  .chip {
1775
1786
  display: inline-flex;
1776
1787
  align-items: center;
1777
- gap: var(--sp-2);
1778
- border-radius: var(--r-chip);
1788
+ gap: var(--chip-gap, var(--sp-2));
1789
+ border-radius: var(--chip-radius, var(--r-chip));
1779
1790
  /* --outline, not --action-border: a chip is a control, and on a palette
1780
1791
  where --card-1-bg == --action-bg (field, roast, hydro) a transparent
1781
1792
  border makes it invisible on a white card. Same edge fields, checks,
1782
1793
  toggles, tiles and the dropdown already use. */
1783
- border: var(--border-w) solid var(--outline);
1784
- background: var(--action-bg);
1785
- color: var(--action-ink);
1786
- padding: var(--sp-2) var(--sp-3);
1794
+ border: var(--border-w) solid var(--chip-border, var(--outline));
1795
+ background: var(--chip-bg, var(--action-bg));
1796
+ color: var(--chip-ink, var(--action-ink));
1797
+ padding: var(--chip-pad-y, var(--sp-2)) var(--chip-pad-x, var(--sp-3));
1787
1798
  font: var(--type-label);
1788
- font-size: var(--fs-sm);
1799
+ font-size: var(--chip-font-size, var(--fs-sm));
1789
1800
  min-height: var(--chip-h);
1790
1801
  text-decoration: none;
1791
1802
  justify-self: start; /* as a grid child it hugs its text, never stretches */
1792
1803
  }
1793
1804
  .dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); } /* signal dot: chips, legends */
1794
1805
  .dot--b { background: var(--dot-b); }
1795
- .chip--on { background: var(--accent); color: var(--on-accent); border-color: transparent; }
1806
+ .chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
1796
1807
  /* dark-slot inversion lives in card.css (.card--3 :is(...)) */
1797
1808
 
1798
1809
  .chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
@@ -1896,14 +1907,9 @@ button { font: inherit; cursor: pointer; }
1896
1907
  <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
1897
1908
  </div> — or the same two in an .action-bar__tier--pager under a control */
1898
1909
  .action-bar--pager { align-items: center; }
1899
- /* The bar belongs to the BASE (Lh, 2026-09-20): it is the move of a page of words — a pager turning
1900
- them, or one wide button. It is never in front of a sheet: a sheet that is on the screen covers it
1901
- (step.css, "layers"), a peeking one included, and the button that finishes a sheet is the sheet's own
1902
- foot (sheet.css). So a pager never works beside a sheet — that screen has work that cannot be walked
1903
- past — and neither does any other bar: while a sheet is on (only .sheet--away, a sheet not called yet,
1904
- is not), the bar takes no pointer here and cardds.js makes it inert, so the keyboard cannot reach what
1905
- the eye cannot see. */
1906
- :has(.step > .sheet:not(.sheet--away)) > .action-bar { pointer-events: none; }
1910
+ /* The bar belongs to the BASE: the move of a page of words — a pager turning them, or one wide button. A screen
1911
+ with a sheet finishes on the sheet's own foot instead (sheet.css). Guidance for composing, not a rule the
1912
+ CSS enforces: a component never changes because another one is there. */
1907
1913
  /* the page dots: under the card in a .screen__centre group; tight gaps */
1908
1914
  .pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
1909
1915
  .pager__at > * {
@@ -1933,6 +1939,9 @@ button { font: inherit; cursor: pointer; }
1933
1939
 
1934
1940
  /* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
1935
1941
  :is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
1942
+ /* …and whatever goes BETWEEN the two (a "skip" link) stands in the middle: the bar places it, never the link itself */
1943
+ :is(.action-bar--pager, .action-bar__tier--pager) > :not(.icon-btn):not(:first-child):not(:last-child) { margin-inline: auto; }
1944
+ :is(.action-bar--pager, .action-bar__tier--pager):has(> :nth-child(3)) > .icon-btn:first-child { margin-right: 0; }
1936
1945
 
1937
1946
  /* .screen's default bottom padding is a constant; derive the reserve from the
1938
1947
  bar instead, so a taller button (a longer label, a script with more leading)
@@ -1976,8 +1985,8 @@ button { font: inherit; cursor: pointer; }
1976
1985
  mask: radial-gradient(farthest-side, transparent calc(100% - var(--_w) - 1px), currentColor calc(100% - var(--_w)));
1977
1986
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--_w) - 1px), currentColor calc(100% - var(--_w)));
1978
1987
  }
1979
- .ring__value { font: var(--type-stat); letter-spacing: var(--track-display); }
1980
- .ring__label { font: var(--type-caption); color: var(--card-muted); }
1988
+ .ring__value { font: var(--ring-value-font, var(--type-stat)); letter-spacing: var(--track-display); }
1989
+ .ring__label { font: var(--ring-label-font, var(--type-caption)); color: var(--ring-label-ink, var(--card-muted)); }
1981
1990
  .card--3 .ring__label { color: inherit; opacity: 0.7; }
1982
1991
  .ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
1983
1992
  .ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
@@ -1997,10 +2006,10 @@ button { font: inherit; cursor: pointer; }
1997
2006
  --on-accent: var(--_bg);
1998
2007
  list-style: none;
1999
2008
  margin: 0;
2000
- padding: var(--sp-2);
2009
+ padding: var(--track-pad, var(--sp-2));
2001
2010
  display: flex;
2002
2011
  align-items: center;
2003
- border-radius: var(--r-chip);
2012
+ border-radius: var(--track-radius, var(--r-chip));
2004
2013
  background: var(--_bg);
2005
2014
  color: var(--_ink);
2006
2015
  }
@@ -2041,9 +2050,9 @@ button { font: inherit; cursor: pointer; }
2041
2050
  grid-template-areas: "label num" "foot num";
2042
2051
  align-items: end;
2043
2052
  gap: var(--sp-2) var(--sp-4);
2044
- min-height: var(--tile-h);
2053
+ min-height: var(--card-band-min-h, var(--tile-h));
2045
2054
  }
2046
- .band__label { grid-area: label; align-self: start; font: var(--type-label); font-size: var(--fs-body); line-height: 1.2; }
2055
+ .band__label { grid-area: label; align-self: start; font: var(--band-label-font, var(--type-label)); font-size: var(--band-label-font-size, var(--fs-body)); line-height: 1.2; }
2047
2056
  .band__num {
2048
2057
  grid-area: num;
2049
2058
  font: 800 var(--fs-giant) / 0.9 var(--font-display);
@@ -2051,14 +2060,14 @@ button { font: inherit; cursor: pointer; }
2051
2060
  font-variant-numeric: tabular-nums;
2052
2061
  }
2053
2062
  .band__num small { font-size: 0.5em; font-weight: 700; margin-right: 0.1em; } /* unit or % */
2054
- .band__foot { grid-area: foot; font: var(--type-caption); color: var(--card-muted); }
2063
+ .band__foot { grid-area: foot; font: var(--band-foot-font, var(--type-caption)); color: var(--band-foot-ink, var(--card-muted)); }
2055
2064
  .card--3 .band__foot { color: inherit; opacity: 0.7; }
2056
2065
  /* band stack: ONE card boundary; bands are frameless inside it, a hairline
2057
2066
  between each. Bands may still carry slot tints (card--2 / card--3). */
2058
2067
  .band-stack {
2059
2068
  display: grid;
2060
- border: var(--border-w) solid var(--card-border);
2061
- border-radius: var(--r-card);
2069
+ border: var(--border-w) solid var(--band-stack-border, var(--card-border));
2070
+ border-radius: var(--band-stack-radius, var(--r-card));
2062
2071
  overflow: hidden;
2063
2072
  }
2064
2073
  .band-stack > .card--band { border: 0; border-radius: 0; box-shadow: none; }
@@ -2109,7 +2118,7 @@ button { font: inherit; cursor: pointer; }
2109
2118
 
2110
2119
  /* ---- picker: horizontal snap picker, centre value big ----
2111
2120
  <div class="picker"><div class="picker__rail"><button class="picker__item">100</button>…</div><span class="picker__unit">ml</span></div> */
2112
- .picker { display: grid; gap: var(--sp-1); justify-items: center; position: relative; }
2121
+ .picker { display: grid; gap: var(--picker-gap, var(--sp-1)); justify-items: center; position: relative; }
2113
2122
  .picker__rail {
2114
2123
  width: 100%;
2115
2124
  display: grid;
@@ -2117,7 +2126,7 @@ button { font: inherit; cursor: pointer; }
2117
2126
  /* side padding = one third of the rail, so the first and last value can sit
2118
2127
  in the centre; the content box is then one third wide, and each column
2119
2128
  fills it (percent columns resolve against the content box) */
2120
- padding-inline: calc(100% / 3);
2129
+ padding-inline: var(--picker-rail-pad-x, calc(100% / 3));
2121
2130
  grid-auto-columns: 100%;
2122
2131
  overflow-x: auto;
2123
2132
  scroll-snap-type: x mandatory;
@@ -2130,8 +2139,8 @@ button { font: inherit; cursor: pointer; }
2130
2139
  border: 0;
2131
2140
  background: transparent;
2132
2141
  color: inherit;
2133
- padding: var(--sp-2) 0;
2134
- font: var(--type-stat);
2142
+ padding: var(--picker-item-pad-y, var(--sp-2)) var(--picker-item-pad-x, 0);
2143
+ font: var(--picker-item-font, var(--type-stat));
2135
2144
  letter-spacing: var(--track-display);
2136
2145
  font-variant-numeric: tabular-nums;
2137
2146
  opacity: 0.3;
@@ -2150,7 +2159,7 @@ button { font: inherit; cursor: pointer; }
2150
2159
  left: calc(100% / 3);
2151
2160
  }
2152
2161
  .picker::after { left: auto; right: calc(100% / 3); }
2153
- .picker__unit { font: var(--type-caption); color: var(--card-muted); }
2162
+ .picker__unit { font: var(--picker-unit-font, var(--type-caption)); color: var(--picker-unit-ink, var(--card-muted)); }
2154
2163
  .card--3 .picker__unit { color: inherit; opacity: 0.7; }
2155
2164
  /* the scrolled-to item lights up by position — no JS */
2156
2165
  @supports (animation-timeline: view(x)) {
@@ -2166,30 +2175,30 @@ button { font: inherit; cursor: pointer; }
2166
2175
  .badge {
2167
2176
  display: inline-grid;
2168
2177
  place-items: center;
2169
- min-width: var(--badge);
2170
- height: var(--badge);
2171
- padding: 0 var(--sp-2);
2172
- border-radius: var(--r-chip);
2173
- background: var(--accent);
2174
- color: var(--on-accent);
2175
- font: var(--type-label);
2178
+ min-width: var(--badge-min-w, var(--badge));
2179
+ height: var(--badge-h, var(--badge));
2180
+ padding: var(--badge-pad-y, 0) var(--badge-pad-x, var(--sp-2));
2181
+ border-radius: var(--badge-radius, var(--r-chip));
2182
+ background: var(--badge-bg, var(--accent));
2183
+ color: var(--badge-ink, var(--on-accent));
2184
+ font: var(--badge-font, var(--type-label));
2176
2185
  font-variant-numeric: tabular-nums;
2177
2186
  vertical-align: middle;
2178
2187
  }
2179
- .badge--soft { background: var(--ink-tint); color: inherit; }
2188
+ .badge--soft { background: var(--badge-soft-bg, var(--ink-tint)); color: inherit; }
2180
2189
 
2181
2190
  /* ---- bars: three or four bars with a count on top — the glance chart ----
2182
2191
  <div class="bars"><div class="bar" style="--v: 16"><span>+16</span></div>… --max on the parent */
2183
- .bars { --max: 20; --_h: 5rem; display: flex; align-items: end; gap: var(--sp-2); height: calc(var(--_h) + var(--sp-8)); }
2192
+ .bars { --max: 20; --_h: 5rem; display: flex; align-items: end; gap: var(--bars-gap, var(--sp-2)); height: calc(var(--_h) + var(--sp-8)); }
2184
2193
  .bar {
2185
2194
  flex: 1;
2186
2195
  min-width: 0;
2187
2196
  display: grid;
2188
2197
  align-content: end;
2189
2198
  justify-items: center;
2190
- gap: var(--sp-1);
2199
+ gap: var(--bar-gap, var(--sp-1));
2191
2200
  height: 100%;
2192
- font: var(--type-caption);
2201
+ font: var(--bar-font, var(--type-caption));
2193
2202
  font-weight: 700;
2194
2203
  }
2195
2204
  .bar::after {
@@ -2220,9 +2229,9 @@ button { font: inherit; cursor: pointer; }
2220
2229
  border-radius: 50%;
2221
2230
  display: grid;
2222
2231
  place-items: center;
2223
- background: var(--card-2-bg);
2224
- color: var(--card-2-ink);
2225
- font: var(--type-label);
2232
+ background: var(--avatar-bg, var(--card-2-bg));
2233
+ color: var(--avatar-ink, var(--card-2-ink));
2234
+ font: var(--avatar-font, var(--type-label));
2226
2235
  font-size: calc(var(--_size) * 0.3);
2227
2236
  text-transform: uppercase;
2228
2237
  letter-spacing: 0.02em;
@@ -2235,10 +2244,10 @@ button { font: inherit; cursor: pointer; }
2235
2244
  .avatar--lg { --_size: var(--avatar-lg); }
2236
2245
  .avatar--xl { --_size: var(--avatar-xl); } /* sheet head icon ring */
2237
2246
  /* outline avatar: no fill, an edge — icons in a ring, "add" slots */
2238
- .avatar--outline { background: transparent; color: inherit; border: var(--border-w) solid var(--outline); }
2239
- .avatar--add { background: transparent; color: inherit; border: var(--border-w) dashed var(--outline); cursor: pointer; }
2247
+ .avatar--outline { background: transparent; color: inherit; border: var(--border-w) solid var(--avatar-outline-border, var(--outline)); }
2248
+ .avatar--add { background: transparent; color: inherit; border: var(--border-w) dashed var(--avatar-add-border, var(--outline)); cursor: pointer; }
2240
2249
  /* accent avatar: the "+7" tail, the selected person */
2241
- .avatar--on { background: var(--accent); color: var(--on-accent); }
2250
+ .avatar--on { background: var(--avatar-on-bg, var(--accent)); color: var(--avatar-on-ink, var(--on-accent)); }
2242
2251
  /* halo: faint concentric rings behind a hero avatar (decorative) */
2243
2252
  .avatar--halo::before {
2244
2253
  content: "";
@@ -2260,7 +2269,7 @@ button { font: inherit; cursor: pointer; }
2260
2269
  <div class="avatar-pick"><button class="avatar-pick__item"><span class="avatar avatar--lg avatar--add">+</span><span class="avatar-pick__name">Add</span><span class="avatar-pick__role">member</span></button>…</div> */
2261
2270
  .avatar-pick {
2262
2271
  display: flex;
2263
- gap: var(--sp-4);
2272
+ gap: var(--avatar-pick-gap, var(--sp-4));
2264
2273
  overflow-x: auto;
2265
2274
  scroll-snap-type: x proximity;
2266
2275
  scrollbar-width: none; /* starts at the card's padding edge, like every row */
@@ -2269,18 +2278,18 @@ button { font: inherit; cursor: pointer; }
2269
2278
  .avatar-pick__item {
2270
2279
  scroll-snap-align: start;
2271
2280
  flex: none;
2272
- width: var(--avatar-lg);
2281
+ width: var(--avatar-pick-item-w, var(--avatar-lg));
2273
2282
  display: grid;
2274
2283
  justify-items: center;
2275
- gap: var(--sp-2);
2284
+ gap: var(--avatar-pick-item-gap, var(--sp-2));
2276
2285
  border: 0;
2277
2286
  background: transparent;
2278
2287
  color: inherit;
2279
2288
  padding: 0;
2280
2289
  text-align: center;
2281
2290
  }
2282
- .avatar-pick__name { font: var(--type-label); font-size: var(--fs-sm); }
2283
- .avatar-pick__role { font: var(--type-caption); color: var(--card-muted); margin-top: calc(-1 * var(--sp-1)); }
2291
+ .avatar-pick__name { font: var(--avatar-pick-name-font, var(--type-label)); font-size: var(--avatar-pick-name-font-size, var(--fs-sm)); }
2292
+ .avatar-pick__role { font: var(--avatar-pick-role-font, var(--type-caption)); color: var(--avatar-pick-role-ink, var(--card-muted)); margin-top: calc(-1 * var(--sp-1)); }
2284
2293
  .card--3 .avatar-pick__role { color: inherit; opacity: 0.7; }
2285
2294
 
2286
2295
  /* ---- css/choice.css — check, toggle, toggle chip, chip grid, days, calendar, mood, pin, composer ---- */
@@ -2298,13 +2307,13 @@ button { font: inherit; cursor: pointer; }
2298
2307
  <input type="checkbox" class="check" aria-label="Done"> */
2299
2308
  .check {
2300
2309
  appearance: none;
2301
- width: var(--tap);
2302
- height: var(--tap);
2310
+ width: var(--check-size, var(--tap));
2311
+ height: var(--check-size, var(--tap));
2303
2312
  flex: none;
2304
2313
  margin: 0;
2305
2314
  border-radius: 50%;
2306
- border: var(--border-w) solid var(--outline);
2307
- background: var(--action-bg);
2315
+ border: var(--border-w) solid var(--check-border, var(--outline));
2316
+ background: var(--check-bg, var(--action-bg));
2308
2317
  display: grid;
2309
2318
  place-items: center;
2310
2319
  cursor: pointer;
@@ -2321,7 +2330,7 @@ button { font: inherit; cursor: pointer; }
2321
2330
  .check:checked { background: var(--accent); border-color: transparent; }
2322
2331
  .check:checked::after { opacity: 1; }
2323
2332
  .check:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
2324
- .check--sm { width: var(--tap-sm); height: var(--tap-sm); }
2333
+ .check--sm { width: var(--check-sm-size, var(--tap-sm)); height: var(--check-sm-size, var(--tap-sm)); }
2325
2334
 
2326
2335
  /* ---- toggle: big switch ----
2327
2336
  <input type="checkbox" role="switch" class="toggle" aria-label="Private"> */
@@ -2334,9 +2343,9 @@ button { font: inherit; cursor: pointer; }
2334
2343
  height: var(--_h);
2335
2344
  flex: none;
2336
2345
  margin: 0;
2337
- border-radius: var(--r-chip);
2338
- border: var(--border-w) solid var(--outline);
2339
- background: var(--action-bg);
2346
+ border-radius: var(--toggle-radius, var(--r-chip));
2347
+ border: var(--border-w) solid var(--toggle-border, var(--outline));
2348
+ background: var(--toggle-bg, var(--action-bg));
2340
2349
  position: relative;
2341
2350
  cursor: pointer;
2342
2351
  transition: background 0.2s ease;
@@ -2365,7 +2374,7 @@ button { font: inherit; cursor: pointer; }
2365
2374
  .chip-grid {
2366
2375
  display: grid;
2367
2376
  grid-template-columns: repeat(auto-fill, minmax(min(5.5rem, 100%), 1fr));
2368
- gap: var(--sp-2);
2377
+ gap: var(--chip-grid-gap, var(--sp-2));
2369
2378
  }
2370
2379
  /* .chip hugs its text as a loose grid child (justify-self: start); inside a
2371
2380
  chip grid the whole point is equal, full-width pick targets, so stretch.
@@ -2392,7 +2401,7 @@ button { font: inherit; cursor: pointer; }
2392
2401
 
2393
2402
  /* ---- day strip: a week, one day selected ----
2394
2403
  <div class="day-strip" role="tablist"><button class="day" role="tab" aria-selected="true"><span class="day__name">Thu</span><span class="day__num">16</span></button>…</div> */
2395
- .day-strip { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--sp-1); }
2404
+ .day-strip { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--day-strip-gap, var(--sp-1)); }
2396
2405
  .day {
2397
2406
  display: grid;
2398
2407
  /* three rows, always: name · number · dot. The dot's row is reserved even on
@@ -2402,33 +2411,33 @@ button { font: inherit; cursor: pointer; }
2402
2411
  grid-template-rows: auto auto var(--dot-xs);
2403
2412
  align-content: center; /* the group sits centred in the cell, not spread out */
2404
2413
  justify-items: center;
2405
- gap: var(--sp-0);
2414
+ gap: var(--day-gap, var(--sp-0));
2406
2415
  min-width: 0;
2407
- min-height: var(--bar-h);
2408
- padding-block: var(--sp-3); /* equal air top and bottom, so the selected pill
2416
+ min-height: var(--day-min-h, var(--bar-h));
2417
+ padding-block: var(--day-pad-y, var(--sp-3)); /* equal air top and bottom, so the selected pill
2409
2418
  is the same shape on every day */
2410
2419
  padding-inline: 0;
2411
2420
  border: 0;
2412
- border-radius: var(--r-chip);
2421
+ border-radius: var(--day-radius, var(--r-chip));
2413
2422
  background: transparent;
2414
2423
  color: inherit;
2415
2424
  opacity: 0.6;
2416
2425
  }
2417
- .day__name { font: var(--type-caption); }
2426
+ .day__name { font: var(--day-name-font, var(--type-caption)); }
2418
2427
  .day__num { font: 700 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
2419
- .day[aria-selected="true"] { background: var(--accent); color: var(--on-accent); opacity: 1; }
2420
- .day__dot { grid-row: 3; align-self: center; width: var(--dot-xs); height: var(--dot-xs); border-radius: 50%; background: currentColor; }
2428
+ .day[aria-selected="true"] { background: var(--day-selected-bg, var(--accent)); color: var(--day-selected-ink, var(--on-accent)); opacity: 1; }
2429
+ .day__dot { grid-row: 3; align-self: center; width: var(--day-dot-size, var(--dot-xs)); height: var(--day-dot-size, var(--dot-xs)); border-radius: 50%; background: currentColor; }
2421
2430
 
2422
2431
  /* ---- calendar: month grid, 44px cells, one accent day, optional marks ----
2423
2432
  <div class="calendar"><div class="calendar__head">…</div><div class="calendar__grid"><span class="calendar__wd">Mo</span>×7 <button class="calendar__day" style="--start: 3">1</button>…</div></div> */
2424
- .calendar { display: grid; gap: var(--sp-3); }
2425
- .calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
2426
- .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--sp-1); justify-items: center; }
2427
- .calendar__wd { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; color: var(--card-muted); padding-block: var(--sp-2); }
2433
+ .calendar { display: grid; gap: var(--calendar-gap, var(--sp-3)); }
2434
+ .calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--calendar-head-gap, var(--sp-2)); }
2435
+ .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; }
2436
+ .calendar__wd { font: var(--calendar-wd-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; color: var(--calendar-wd-ink, var(--card-muted)); padding-block: var(--calendar-wd-pad-y, var(--sp-2)); }
2428
2437
  .card--3 .calendar__wd { color: inherit; opacity: 0.7; }
2429
2438
  .calendar__day {
2430
2439
  width: 100%;
2431
- max-width: var(--tap);
2440
+ max-width: var(--calendar-day-max-w, var(--tap));
2432
2441
  aspect-ratio: 1;
2433
2442
  display: grid;
2434
2443
  place-items: center;
@@ -2440,9 +2449,9 @@ button { font: inherit; cursor: pointer; }
2440
2449
  font-variant-numeric: tabular-nums;
2441
2450
  grid-column-start: var(--start, auto); /* --start on the 1st: weekday column 1–7 */
2442
2451
  }
2443
- .calendar__day--mark { background: var(--accent-soft); }
2444
- .calendar__day--mark2 { background: var(--accent-mid); color: var(--on-accent); }
2445
- .calendar__day[aria-selected="true"] { background: var(--accent); color: var(--on-accent); font-weight: 700; }
2452
+ .calendar__day--mark { background: var(--calendar-day-mark-bg, var(--accent-soft)); }
2453
+ .calendar__day--mark2 { background: var(--calendar-day-mark2-bg, var(--accent-mid)); color: var(--calendar-day-mark2-ink, var(--on-accent)); }
2454
+ .calendar__day[aria-selected="true"] { background: var(--calendar-day-selected-bg, var(--accent)); color: var(--calendar-day-selected-ink, var(--on-accent)); font-weight: 700; }
2446
2455
  .calendar__day:disabled { opacity: 0.3; }
2447
2456
 
2448
2457
  /* ---- mood: five dots in a pill, the picked one grows ----
@@ -2450,19 +2459,19 @@ button { font: inherit; cursor: pointer; }
2450
2459
  .mood {
2451
2460
  margin: 0;
2452
2461
  min-inline-size: 0; /* fieldset UA default is min-content — it must shrink with the card */
2453
- padding: var(--sp-1);
2454
- border: var(--border-w) solid var(--outline);
2455
- border-radius: var(--r-chip);
2456
- background: var(--action-bg);
2457
- color: var(--action-ink);
2462
+ padding: var(--mood-pad, var(--sp-1));
2463
+ border: var(--border-w) solid var(--mood-border, var(--outline));
2464
+ border-radius: var(--mood-radius, var(--r-chip));
2465
+ background: var(--mood-bg, var(--action-bg));
2466
+ color: var(--mood-ink, var(--action-ink));
2458
2467
  display: flex;
2459
2468
  justify-content: space-between;
2460
2469
  }
2461
- .mood__opt { flex: 1 1 0; max-width: var(--tap); min-width: 0; height: var(--tap); display: grid; place-items: center; position: relative; cursor: pointer; }
2470
+ .mood__opt { flex: 1 1 0; max-width: var(--mood-opt-max-w, var(--tap)); min-width: 0; height: var(--mood-opt-h, var(--tap)); display: grid; place-items: center; position: relative; cursor: pointer; }
2462
2471
  .mood__opt > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
2463
2472
  .mood__dot {
2464
- width: var(--icon-xs);
2465
- height: var(--icon-xs);
2473
+ width: var(--mood-dot-size, var(--icon-xs));
2474
+ height: var(--mood-dot-size, var(--icon-xs));
2466
2475
  border-radius: 50%;
2467
2476
  background: var(--_mood, currentColor);
2468
2477
  transition: scale 0.2s ease, box-shadow 0.2s ease;
@@ -2477,18 +2486,18 @@ button { font: inherit; cursor: pointer; }
2477
2486
 
2478
2487
  /* ---- pin: big code entry, one digit per pill ----
2479
2488
  <div class="pin"><input class="pin__cell" inputmode="numeric" maxlength="1" placeholder=" ">×4</div> */
2480
- .pin { display: flex; gap: var(--sp-3); }
2489
+ .pin { display: flex; gap: var(--pin-gap, var(--sp-3)); }
2481
2490
  .pin__cell {
2482
2491
  flex: 1;
2483
2492
  min-width: 0;
2484
- max-width: var(--fab);
2493
+ max-width: var(--pin-cell-max-w, var(--fab));
2485
2494
  aspect-ratio: 2 / 3;
2486
- border: var(--border-w) solid var(--outline);
2487
- border-radius: var(--r-chip);
2488
- background: var(--action-bg);
2489
- color: var(--action-ink);
2495
+ border: var(--border-w) solid var(--pin-cell-border, var(--outline));
2496
+ border-radius: var(--pin-cell-radius, var(--r-chip));
2497
+ background: var(--pin-cell-bg, var(--action-bg));
2498
+ color: var(--pin-cell-ink, var(--action-ink));
2490
2499
  text-align: center;
2491
- font: var(--type-stat);
2500
+ font: var(--pin-cell-font, var(--type-stat));
2492
2501
  font-variant-numeric: tabular-nums;
2493
2502
  caret-color: currentColor;
2494
2503
  }
@@ -2498,7 +2507,7 @@ button { font: inherit; cursor: pointer; }
2498
2507
  /* ---- composer: big text area with a bottom bar (icon · hint · send) ----
2499
2508
  <div class="composer"><textarea class="composer__input" placeholder="What do you need?"></textarea>
2500
2509
  <div class="composer__bar"><button class="icon-btn">…</button><span class="composer__hint">Your idea is born</span><button class="icon-btn icon-btn--invert">↑</button></div></div> */
2501
- .composer { display: grid; gap: var(--sp-3); }
2510
+ .composer { display: grid; gap: var(--composer-gap, var(--sp-3)); }
2502
2511
  .composer__input {
2503
2512
  width: 100%;
2504
2513
  min-height: 11rem;
@@ -2512,8 +2521,8 @@ button { font: inherit; cursor: pointer; }
2512
2521
  outline: none;
2513
2522
  }
2514
2523
  .composer__input::placeholder { color: inherit; opacity: 0.4; }
2515
- .composer__bar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
2516
- .composer__hint { font: var(--type-caption); color: var(--card-muted); }
2524
+ .composer__bar { display: flex; align-items: center; justify-content: space-between; gap: var(--composer-bar-gap, var(--sp-3)); }
2525
+ .composer__hint { font: var(--composer-hint-font, var(--type-caption)); color: var(--composer-hint-ink, var(--card-muted)); }
2517
2526
  .card--3 .composer__hint { color: inherit; opacity: 0.7; }
2518
2527
 
2519
2528
  /* ---- slider: one value along a track, a thumb-size knob ----
@@ -2522,7 +2531,7 @@ button { font: inherit; cursor: pointer; }
2522
2531
  slider does, the input's aria-label is enough. The track is a hairline rail;
2523
2532
  the knob is a full --tap, and the whole range is --tap tall, so the thumb has
2524
2533
  a real target anywhere along it. The value is the input's own. */
2525
- .slider { display: grid; gap: var(--sp-1); flex: 1; min-width: 0; position: relative; } /* relative: the glyph rides the knob */
2534
+ .slider { display: grid; gap: var(--slider-gap, var(--sp-1)); flex: 1; min-width: 0; position: relative; } /* relative: the glyph rides the knob */
2526
2535
  /* ---- the sample glyph: one letter IN the knob, on-accent, at the type size
2527
2536
  the slider sets — so a text-size slider shows the size before anything is
2528
2537
  read (Lh, 2026-09-12). cardds.js publishes the knob's place on the .slider
@@ -2533,8 +2542,8 @@ button { font: inherit; cursor: pointer; }
2533
2542
  position: absolute;
2534
2543
  bottom: 0;
2535
2544
  left: calc(var(--tap) / 2 + (100% - var(--tap)) * var(--slider-at, 0));
2536
- width: var(--tap);
2537
- height: var(--tap);
2545
+ width: var(--slider-glyph-size, var(--tap));
2546
+ height: var(--slider-glyph-size, var(--tap));
2538
2547
  /* the eye centres the INK, not the line box: a letter that stands on the
2539
2548
  baseline (ก) has no descender, while the face's box keeps 0.30em under it
2540
2549
  (fonts.css) — so the ink lands low when the box is centred; lifted 0.15em
@@ -2542,15 +2551,15 @@ button { font: inherit; cursor: pointer; }
2542
2551
  translate: -50% -0.15em;
2543
2552
  display: grid;
2544
2553
  place-items: center;
2545
- font: var(--type-h2);
2546
- color: var(--on-accent);
2554
+ font: var(--slider-glyph-font, var(--type-h2));
2555
+ color: var(--slider-glyph-ink, var(--on-accent));
2547
2556
  pointer-events: none;
2548
2557
  }
2549
- .slider__label { font: var(--type-label); }
2558
+ .slider__label { font: var(--slider-label-font, var(--type-label)); }
2550
2559
  .slider__range {
2551
2560
  appearance: none;
2552
2561
  width: 100%;
2553
- height: var(--tap);
2562
+ height: var(--slider-range-h, var(--tap));
2554
2563
  margin: 0;
2555
2564
  background: transparent;
2556
2565
  cursor: pointer;
@@ -2591,8 +2600,8 @@ button { font: inherit; cursor: pointer; }
2591
2600
  .row {
2592
2601
  display: flex;
2593
2602
  align-items: center;
2594
- gap: var(--sp-3);
2595
- min-height: var(--tap); /* the tap target's room — the row's only vertical size */
2603
+ gap: var(--row-gap, var(--sp-3));
2604
+ min-height: var(--row-min-h, var(--tap)); /* the tap target's room — the row's only vertical size */
2596
2605
  min-width: 0;
2597
2606
  color: inherit;
2598
2607
  text-decoration: none; /* .row works as <a> too — it is a tap target */
@@ -2601,20 +2610,20 @@ button { font: inherit; cursor: pointer; }
2601
2610
  Space BETWEEN rows is .rows' gap; a lone row as card content takes the card's
2602
2611
  gap. (.row--flush, which zeroed the old padding, is gone — every row is flush.) */
2603
2612
  .row__lead {
2604
- width: var(--tap);
2605
- height: var(--tap);
2613
+ width: var(--row-lead-size, var(--tap));
2614
+ height: var(--row-lead-size, var(--tap));
2606
2615
  flex: none;
2607
- border-radius: var(--r-control);
2608
- border: var(--border-w) solid var(--outline);
2616
+ border-radius: var(--row-lead-radius, var(--r-control));
2617
+ border: var(--border-w) solid var(--row-lead-border, var(--outline));
2609
2618
  display: grid;
2610
2619
  place-items: center;
2611
2620
  }
2612
- .row__body { flex: 1; min-width: 0; display: grid; gap: var(--sp-0); }
2613
- .row__title { font: var(--type-title); overflow-wrap: anywhere; } /* wraps freely — no clip, so no padding trick for Thai marks (Lh 2026-09-16, same call as the card heading) */
2614
- .row__meta { font: var(--type-caption); color: var(--card-muted); display: flex; align-items: center; gap: var(--sp-1); }
2621
+ .row__body { flex: 1; min-width: 0; display: grid; gap: var(--row-body-gap, var(--sp-0)); }
2622
+ .row__title { font: var(--row-title-font, var(--type-title)); overflow-wrap: anywhere; } /* wraps freely — no clip, so no padding trick for Thai marks (Lh 2026-09-16, same call as the card heading) */
2623
+ .row__meta { font: var(--row-meta-font, var(--type-caption)); color: var(--row-meta-ink, var(--card-muted)); display: flex; align-items: center; gap: var(--row-meta-gap, var(--sp-1)); }
2615
2624
  .card--3 .row__meta { color: inherit; opacity: 0.7; }
2616
- .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--sp-0); justify-items: end; flex: none; }
2617
- .row--lg { min-height: var(--fab); } /* menu rows: a taller target, round arrow at the end; .rows gives them more air */
2625
+ .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
2626
+ .row--lg { min-height: var(--row-lg-min-h, var(--fab)); } /* menu rows: a taller target, round arrow at the end; .rows gives them more air */
2618
2627
  .row--lg .row__title { font: 700 var(--fs-lg)/1.2 var(--font-display); }
2619
2628
  /* a finished item in a checklist (a to-do list, an onboarding "complete your introduction" list):
2620
2629
  the words go muted, a filled tick sits at the end, and the row is no longer a tap target —
@@ -2624,13 +2633,13 @@ button { font: inherit; cursor: pointer; }
2624
2633
  .row--done .row__title { color: var(--card-muted); }
2625
2634
  .card--3 .row--done .row__title { color: inherit; opacity: 0.7; }
2626
2635
  .row__done {
2627
- width: var(--tap-sm);
2628
- height: var(--tap-sm);
2636
+ width: var(--row-done-size, var(--tap-sm));
2637
+ height: var(--row-done-size, var(--tap-sm));
2629
2638
  flex: none;
2630
2639
  margin-left: auto;
2631
2640
  border-radius: 50%;
2632
- background: var(--accent);
2633
- color: var(--on-accent);
2641
+ background: var(--row-done-bg, var(--accent));
2642
+ color: var(--row-done-ink, var(--on-accent));
2634
2643
  display: grid;
2635
2644
  place-items: center;
2636
2645
  }
@@ -2651,20 +2660,20 @@ button { font: inherit; cursor: pointer; }
2651
2660
  /* ---- kv: label left, value right ----
2652
2661
  <dl class="kv"><div class="kv__row"><dt class="kv__key">Pay with</dt><dd class="kv__val">GHO</dd></div>…</dl> */
2653
2662
  .kv { margin: 0; display: grid; }
2654
- .kv__row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); min-height: var(--tap); }
2663
+ .kv__row { display: flex; align-items: center; justify-content: space-between; gap: var(--kv-row-gap, var(--sp-3)); min-height: var(--kv-row-min-h, var(--tap)); }
2655
2664
  .kv--lined > .kv__row + .kv__row { border-top: var(--border-w) solid var(--hairline); }
2656
- .kv__key { font: var(--type-body); color: var(--card-muted); }
2665
+ .kv__key { font: var(--kv-key-font, var(--type-body)); color: var(--kv-key-ink, var(--card-muted)); }
2657
2666
  .card--3 .kv__key { color: inherit; opacity: 0.7; }
2658
- .kv__val { margin: 0; font: var(--type-title); display: inline-flex; align-items: center; gap: var(--sp-2); text-align: right; min-width: 0; }
2667
+ .kv__val { margin: 0; font: var(--kv-val-font, var(--type-title)); display: inline-flex; align-items: center; gap: var(--kv-val-gap, var(--sp-2)); text-align: right; min-width: 0; }
2659
2668
 
2660
2669
  /* ---- kv grid: value over label, cells split by a rule (Sex | Weight) ----
2661
2670
  <div class="kv-grid"><div class="kv-cell"><svg class="icon"/><span class="kv-cell__val">15.6 kg</span><span class="kv-cell__key">Weight</span></div>…</div> */
2662
- .kv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(var(--tile-min), 100%), 1fr)); gap: var(--sp-4); }
2671
+ .kv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(var(--tile-min), 100%), 1fr)); gap: var(--kv-grid-gap, var(--sp-4)); }
2663
2672
  .kv-grid > .kv-cell + .kv-cell { border-left: var(--border-w) solid var(--hairline); padding-left: var(--sp-4); }
2664
- .kv-cell { display: grid; gap: var(--sp-0); align-content: start; min-width: 0; }
2673
+ .kv-cell { display: grid; gap: var(--kv-cell-gap, var(--sp-0)); align-content: start; min-width: 0; }
2665
2674
  .kv-cell > .icon { margin-bottom: var(--sp-1); }
2666
- .kv-cell__val { font: var(--type-h2); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
2667
- .kv-cell__key { font: var(--type-caption); color: var(--card-muted); }
2675
+ .kv-cell__val { font: var(--kv-cell-val-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
2676
+ .kv-cell__key { font: var(--kv-cell-key-font, var(--type-caption)); color: var(--kv-cell-key-ink, var(--card-muted)); }
2668
2677
  .card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
2669
2678
 
2670
2679
  /* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
@@ -2676,7 +2685,7 @@ button { font: inherit; cursor: pointer; }
2676
2685
  <div class="bubble"><p class="bubble__body">…</p><span class="bubble__actions">…icon-btns…</span><span class="bubble__meta">Dec 4 · 8:15</span></div>
2677
2686
  <div class="bubble bubble--right">…</div>
2678
2687
  </div> */
2679
- .thread { display: grid; gap: var(--sp-4); }
2688
+ .thread { display: grid; gap: var(--thread-gap, var(--sp-4)); }
2680
2689
  /* two columns: the message (max-content, but it gives way — minmax(0) — so a long one shrinks
2681
2690
  and wraps while the actions stay beside it) and the actions; the meta on a second row under the message */
2682
2691
  .bubble {
@@ -2696,12 +2705,12 @@ button { font: inherit; cursor: pointer; }
2696
2705
  .bubble__body {
2697
2706
  margin: 0;
2698
2707
  min-width: 0;
2699
- padding: var(--sp-3) var(--sp-4);
2700
- border-radius: var(--r-card);
2708
+ padding: var(--bubble-body-pad-y, var(--sp-3)) var(--bubble-body-pad-x, var(--sp-4));
2709
+ border-radius: var(--bubble-body-radius, var(--r-card));
2701
2710
  border-bottom-left-radius: var(--r-control);
2702
- background: var(--card-2-bg);
2703
- color: var(--card-2-ink);
2704
- font: var(--type-body);
2711
+ background: var(--bubble-body-bg, var(--card-2-bg));
2712
+ color: var(--bubble-body-ink, var(--card-2-ink));
2713
+ font: var(--bubble-body-font, var(--type-body));
2705
2714
  overflow-wrap: anywhere;
2706
2715
  }
2707
2716
  .bubble--right > .bubble__body {
@@ -2711,8 +2720,8 @@ button { font: inherit; cursor: pointer; }
2711
2720
  color: var(--on-accent);
2712
2721
  text-align: right;
2713
2722
  }
2714
- .bubble__actions { display: flex; gap: var(--sp-1); flex: none; opacity: 0.7; }
2715
- .bubble__meta { display: flex; align-items: center; gap: var(--sp-1); font: var(--type-caption); color: var(--on-surface-muted); }
2723
+ .bubble__actions { display: flex; gap: var(--bubble-actions-gap, var(--sp-1)); flex: none; opacity: 0.7; }
2724
+ .bubble__meta { display: flex; align-items: center; gap: var(--bubble-meta-gap, var(--sp-1)); font: var(--bubble-meta-font, var(--type-caption)); color: var(--bubble-meta-ink, var(--on-surface-muted)); }
2716
2725
  .bubble--right > .bubble__meta { justify-self: end; }
2717
2726
  .bubble__meta > .icon { color: var(--accent); }
2718
2727
  .card--3 .bubble__meta { color: inherit; opacity: 0.7; }
@@ -2724,7 +2733,7 @@ button { font: inherit; cursor: pointer; }
2724
2733
  margin: 0;
2725
2734
  padding: 0 0 0 var(--sp-6);
2726
2735
  display: grid;
2727
- gap: var(--sp-5);
2736
+ gap: var(--timeline-gap, var(--sp-5));
2728
2737
  position: relative;
2729
2738
  }
2730
2739
  .timeline::before {
@@ -2735,7 +2744,7 @@ button { font: inherit; cursor: pointer; }
2735
2744
  bottom: var(--sp-3);
2736
2745
  border-left: var(--border-w) solid var(--hairline);
2737
2746
  }
2738
- .timeline__item { display: grid; gap: var(--sp-2); position: relative; min-width: 0; }
2747
+ .timeline__item { display: grid; gap: var(--timeline-item-gap, var(--sp-2)); position: relative; min-width: 0; }
2739
2748
  .timeline__item::before {
2740
2749
  content: "";
2741
2750
  position: absolute;
@@ -2748,37 +2757,37 @@ button { font: inherit; cursor: pointer; }
2748
2757
  }
2749
2758
  .timeline__mark {
2750
2759
  justify-self: start;
2751
- font: var(--type-overline);
2760
+ font: var(--timeline-mark-font, var(--type-overline));
2752
2761
  letter-spacing: var(--track-overline);
2753
2762
  text-transform: uppercase;
2754
- padding: var(--sp-1) var(--sp-2);
2755
- border-radius: var(--r-chip);
2756
- background: var(--ink-tint);
2763
+ padding: var(--timeline-mark-pad-y, var(--sp-1)) var(--timeline-mark-pad-x, var(--sp-2));
2764
+ border-radius: var(--timeline-mark-radius, var(--r-chip));
2765
+ background: var(--timeline-mark-bg, var(--ink-tint));
2757
2766
  }
2758
2767
 
2759
2768
  /* ---- legend: signal dots with captions ----
2760
2769
  <div class="legend"><span class="legend__item"><span class="dot"></span> very high</span>…</div> */
2761
- .legend { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); font: var(--type-caption); color: var(--card-muted); }
2770
+ .legend { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); font: var(--legend-font, var(--type-caption)); color: var(--legend-ink, var(--card-muted)); }
2762
2771
  .card--3 .legend { color: inherit; opacity: 0.8; }
2763
- .legend__item { display: inline-flex; align-items: center; gap: var(--sp-2); min-height: var(--icon); }
2772
+ .legend__item { display: inline-flex; align-items: center; gap: var(--legend-item-gap, var(--sp-2)); min-height: var(--legend-item-min-h, var(--icon)); }
2764
2773
 
2765
2774
  /* ---- link: "See all ↗" in a card head, big enough to tap ----
2766
2775
  <a class="link" href="#">See all <svg class="icon icon--xs"/></a> */
2767
2776
  .link {
2768
2777
  display: inline-flex;
2769
2778
  align-items: center;
2770
- gap: var(--sp-1);
2771
- min-height: var(--tap-sm);
2779
+ gap: var(--link-gap, var(--sp-1));
2780
+ min-height: var(--link-min-h, var(--tap-sm));
2772
2781
  color: inherit;
2773
2782
  text-decoration: none;
2774
- font: var(--type-label);
2775
- font-size: var(--fs-body);
2783
+ font: var(--link-font, var(--type-label));
2784
+ font-size: var(--link-font-size, var(--fs-body));
2776
2785
  border: 0;
2777
2786
  background: transparent;
2778
2787
  padding: 0;
2779
2788
  cursor: pointer;
2780
2789
  }
2781
- .link--muted { color: var(--card-muted); }
2790
+ .link--muted { color: var(--link-muted-ink, var(--card-muted)); }
2782
2791
  .card--3 .link--muted { color: inherit; opacity: 0.7; }
2783
2792
 
2784
2793
  /* ---- css/media.css — cover card, quote, tile grid, fold, mosaic, wave ---- */
@@ -2827,16 +2836,16 @@ button { font: inherit; cursor: pointer; }
2827
2836
 
2828
2837
  /* ---- quote: giant mark, big text, attribution ----
2829
2838
  <blockquote class="quote"><p class="quote__text">It's overcast…</p><footer class="quote__by">Wednesday 15</footer></blockquote> */
2830
- .quote { margin: 0; display: grid; gap: var(--sp-3); }
2839
+ .quote { margin: 0; display: grid; gap: var(--quote-gap, var(--sp-3)); }
2831
2840
  .quote::before {
2832
2841
  content: "\201C";
2833
2842
  font: 900 calc(var(--fs-h1) * 3)/0.55 var(--font-display);
2834
2843
  height: var(--sp-10);
2835
2844
  opacity: 0.9;
2836
2845
  }
2837
- .quote__text { margin: 0; font: var(--type-h1); letter-spacing: var(--track-h1); text-wrap: balance; }
2846
+ .quote__text { margin: 0; font: var(--quote-text-font, var(--type-h1)); letter-spacing: var(--track-h1); text-wrap: balance; }
2838
2847
  .quote--sm .quote__text { font: var(--type-h2); letter-spacing: var(--track-h2); }
2839
- .quote__by { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; color: var(--card-muted); }
2848
+ .quote__by { font: var(--quote-by-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; color: var(--quote-by-ink, var(--card-muted)); }
2840
2849
  .card--3 .quote__by { color: inherit; opacity: 0.7; }
2841
2850
 
2842
2851
  /* ---- tile grid: big square tiles — icon top, label bottom ----
@@ -2845,36 +2854,36 @@ button { font: inherit; cursor: pointer; }
2845
2854
  .tile-grid {
2846
2855
  display: grid;
2847
2856
  grid-template-columns: repeat(auto-fill, minmax(min(var(--tile-min), 100%), 1fr));
2848
- gap: var(--sp-3);
2857
+ gap: var(--tile-grid-gap, var(--sp-3));
2849
2858
  }
2850
2859
  .tile-grid--3 { --tile-min: 6rem; } /* three across on a phone (attachments, upload kinds) */
2851
2860
  .tile {
2852
2861
  display: grid;
2853
2862
  align-content: space-between;
2854
2863
  justify-items: start;
2855
- gap: var(--sp-5);
2856
- min-height: var(--tile-h);
2864
+ gap: var(--tile-gap, var(--sp-5));
2865
+ min-height: var(--tile-min-h, var(--tile-h));
2857
2866
  min-width: 0;
2858
- padding: var(--sp-4);
2859
- border: var(--border-w) solid var(--outline);
2860
- border-radius: var(--r-card);
2861
- background: var(--action-bg);
2862
- color: var(--action-ink);
2867
+ padding: var(--tile-pad, var(--sp-4));
2868
+ border: var(--border-w) solid var(--tile-border, var(--outline));
2869
+ border-radius: var(--tile-radius, var(--r-card));
2870
+ background: var(--tile-bg, var(--action-bg));
2871
+ color: var(--tile-ink, var(--action-ink));
2863
2872
  font: inherit;
2864
2873
  text-align: left;
2865
2874
  position: relative;
2866
2875
  cursor: pointer;
2867
2876
  }
2868
- .tile__label { font: var(--type-title); }
2869
- .tile__value { font: var(--type-h2); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
2870
- .tile__sub { font: var(--type-caption); opacity: 0.7; }
2871
- .tile__text { display: grid; gap: var(--sp-0); }
2872
- .tile--on { background: var(--accent); color: var(--on-accent); border-color: transparent; }
2877
+ .tile__label { font: var(--tile-label-font, var(--type-title)); }
2878
+ .tile__value { font: var(--tile-value-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
2879
+ .tile__sub { font: var(--tile-sub-font, var(--type-caption)); opacity: 0.7; }
2880
+ .tile__text { display: grid; gap: var(--tile-text-gap, var(--sp-0)); }
2881
+ .tile--on { background: var(--tile-on-bg, var(--accent)); color: var(--tile-on-ink, var(--on-accent)); border-color: transparent; }
2873
2882
  .tile--pick > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
2874
2883
  .tile--pick:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
2875
2884
  .tile--pick:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
2876
2885
  .tile--wide { grid-column: 1 / -1; }
2877
- .tile--stat { min-height: var(--avatar-xl); gap: var(--sp-3); grid-template-columns: auto 1fr; align-items: center; }
2886
+ .tile--stat { min-height: var(--tile-stat-min-h, var(--avatar-xl)); gap: var(--tile-stat-gap, var(--sp-3)); grid-template-columns: auto 1fr; align-items: center; }
2878
2887
  /* upload: dashed, centred plus + label */
2879
2888
  .tile--upload {
2880
2889
  border-style: dashed;
@@ -2882,12 +2891,12 @@ button { font: inherit; cursor: pointer; }
2882
2891
  color: inherit;
2883
2892
  align-content: center;
2884
2893
  justify-items: center;
2885
- gap: var(--sp-2);
2894
+ gap: var(--tile-upload-gap, var(--sp-2));
2886
2895
  text-align: center;
2887
2896
  }
2888
2897
  /* tinted tiles: slot colors inside a card (schedule, notes) */
2889
- .tile--2 { background: var(--card-2-bg); color: var(--card-2-ink); border-color: transparent; }
2890
- .tile--3 { background: var(--card-3-bg); color: var(--card-3-ink); border-color: transparent; }
2898
+ .tile--2 { background: var(--tile-2-bg, var(--card-2-bg)); color: var(--tile-2-ink, var(--card-2-ink)); border-color: transparent; }
2899
+ .tile--3 { background: var(--tile-3-bg, var(--card-3-bg)); color: var(--tile-3-ink, var(--card-3-ink)); border-color: transparent; }
2891
2900
 
2892
2901
  /* ---- fold: a sticky-note corner on any card (decorative) ----
2893
2902
  No border. The card's outline follows its border box, so it ran straight
@@ -2911,7 +2920,7 @@ button { font: inherit; cursor: pointer; }
2911
2920
  <span class="wave"></span> — sits in a .row__body, fills the width */
2912
2921
  .wave {
2913
2922
  display: block;
2914
- height: var(--icon);
2923
+ height: var(--wave-h, var(--icon));
2915
2924
  width: 100%;
2916
2925
  background: repeating-linear-gradient(90deg, currentColor 0 var(--sp-1), transparent var(--sp-1) var(--sp-2));
2917
2926
  /* masks only read alpha: currentColor = "opaque" without naming a color */
@@ -2926,9 +2935,9 @@ button { font: inherit; cursor: pointer; }
2926
2935
  display: grid;
2927
2936
  grid-template-columns: 2fr 1fr;
2928
2937
  grid-auto-rows: 1fr;
2929
- gap: var(--sp-1);
2938
+ gap: var(--mosaic-gap, var(--sp-1));
2930
2939
  aspect-ratio: 4 / 3;
2931
- border-radius: var(--r-media);
2940
+ border-radius: var(--mosaic-radius, var(--r-media));
2932
2941
  overflow: hidden;
2933
2942
  }
2934
2943
  .mosaic > :first-child { grid-row: span 2; }
@@ -2958,16 +2967,16 @@ button { font: inherit; cursor: pointer; }
2958
2967
  position: relative;
2959
2968
  aspect-ratio: 3 / 2;
2960
2969
  perspective: 60rem;
2961
- border-radius: var(--r-media);
2970
+ border-radius: var(--postcard-radius, var(--r-media));
2962
2971
  }
2963
2972
  .postcard__face {
2964
2973
  position: absolute;
2965
2974
  inset: 0;
2966
2975
  display: grid;
2967
2976
  align-content: end;
2968
- padding: var(--sp-4);
2969
- gap: var(--sp-2);
2970
- border: var(--border-w) solid var(--card-border);
2977
+ padding: var(--postcard-face-pad, var(--sp-4));
2978
+ gap: var(--postcard-face-gap, var(--sp-2));
2979
+ border: var(--border-w) solid var(--postcard-face-border, var(--card-border));
2971
2980
  border-radius: inherit;
2972
2981
  overflow: hidden;
2973
2982
  isolation: isolate;
@@ -2976,11 +2985,11 @@ button { font: inherit; cursor: pointer; }
2976
2985
  backface-visibility: hidden;
2977
2986
  -webkit-backface-visibility: hidden;
2978
2987
  transition: transform var(--motion-flip) ease-in-out;
2979
- box-shadow: var(--shadow-float);
2988
+ box-shadow: var(--postcard-face-shadow, var(--shadow-float));
2980
2989
  }
2981
2990
  .postcard__face:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
2982
2991
  /* the front: the photo (or the wireframe box) fills it, the name reads on a scrim */
2983
- .postcard__front { background: var(--card-3-bg); color: var(--card-3-ink); }
2992
+ .postcard__front { background: var(--postcard-front-bg, var(--card-3-bg)); color: var(--postcard-front-ink, var(--card-3-ink)); }
2984
2993
  .postcard__front > img,
2985
2994
  .postcard__front > .ph {
2986
2995
  position: absolute;
@@ -3002,13 +3011,13 @@ button { font: inherit; cursor: pointer; }
3002
3011
  }
3003
3012
  /* the back: paper — the words, big enough to read at arm's length */
3004
3013
  .postcard__back {
3005
- background: var(--card-2-bg);
3006
- color: var(--card-2-ink);
3014
+ background: var(--postcard-back-bg, var(--card-2-bg));
3015
+ color: var(--postcard-back-ink, var(--card-2-ink));
3007
3016
  align-content: space-between;
3008
3017
  transform: rotateY(180deg);
3009
3018
  }
3010
- .postcard__caption { margin: 0; font: var(--type-title); text-wrap: pretty; }
3011
- .postcard__label { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; opacity: 0.85; }
3019
+ .postcard__caption { margin: 0; font: var(--postcard-caption-font, var(--type-title)); text-wrap: pretty; }
3020
+ .postcard__label { font: var(--postcard-label-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; opacity: 0.85; }
3012
3021
  .postcard.is-flipped > .postcard__front { transform: rotateY(-180deg); }
3013
3022
  .postcard.is-flipped > .postcard__back { transform: rotateY(0); }
3014
3023
  @media (prefers-reduced-motion: reduce) {
@@ -3064,7 +3073,7 @@ button { font: inherit; cursor: pointer; }
3064
3073
  display: grid;
3065
3074
  grid-template-rows: minmax(0, 1fr) auto;
3066
3075
  overflow: clip;
3067
- box-shadow: var(--shadow-sheet);
3076
+ box-shadow: var(--modal-shadow, var(--shadow-sheet));
3068
3077
  }
3069
3078
  .modal__lift {
3070
3079
  padding: var(--card-pad);
@@ -3078,15 +3087,15 @@ button { font: inherit; cursor: pointer; }
3078
3087
  .modal__drawer {
3079
3088
  height: var(--drawer-h);
3080
3089
  box-sizing: border-box;
3081
- padding: var(--sp-5);
3090
+ padding: var(--modal-drawer-pad, var(--sp-5));
3082
3091
  overflow: clip;
3083
3092
  display: grid;
3084
3093
  align-content: center;
3085
3094
  justify-items: center;
3086
- gap: var(--sp-1);
3095
+ gap: var(--modal-drawer-gap, var(--sp-1));
3087
3096
  text-align: center;
3088
- background: var(--card-3-bg);
3089
- color: var(--card-3-ink);
3097
+ background: var(--modal-drawer-bg, var(--card-3-bg));
3098
+ color: var(--modal-drawer-ink, var(--card-3-ink));
3090
3099
  --accent: var(--card-3-ink); /* the one solid button inverts on the dark drawer */
3091
3100
  --on-accent: var(--card-3-bg);
3092
3101
  }
@@ -3102,14 +3111,14 @@ button { font: inherit; cursor: pointer; }
3102
3111
  }
3103
3112
  @keyframes modal-open {
3104
3113
  0%, 50% { height: 0; padding-block: 0; } /* inside the card */
3105
- 100% { height: var(--drawer-h); padding-block: var(--sp-5); } /* 2. out below, the lift up, together */
3114
+ 100% { height: var(--drawer-h); padding-block: var(--modal-drawer-pad, var(--sp-5)); } /* 2. out below, the lift up, together — the same knob the rule reads: a keyframe that pinned the value would make the knob dead */
3106
3115
  }
3107
3116
  }
3108
3117
 
3109
- .btn-trio { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); }
3118
+ .btn-trio { display: flex; align-items: center; gap: var(--btn-trio-gap, var(--sp-3)); margin-top: var(--sp-3); }
3110
3119
  .btn-trio__dots {
3111
- width: var(--sp-5);
3112
- height: var(--sp-1);
3120
+ width: var(--btn-trio-dots-w, var(--sp-5));
3121
+ height: var(--btn-trio-dots-h, var(--sp-1));
3113
3122
  background: radial-gradient(circle, currentColor var(--sp-0), transparent calc(var(--sp-0) + 0.5px)) 0 0 / calc(var(--sp-5) / 2) 100% repeat-x;
3114
3123
  opacity: 0.4;
3115
3124
  }
@@ -3130,31 +3139,31 @@ button { font: inherit; cursor: pointer; }
3130
3139
  display: none; /* explicit closed state — never display on the base rule */
3131
3140
  margin: 0;
3132
3141
  inset: auto;
3133
- border: var(--border-w) solid var(--action-border);
3134
- border-radius: var(--r-card);
3142
+ border: var(--border-w) solid var(--menu-border, var(--action-border));
3143
+ border-radius: var(--menu-radius, var(--r-card));
3135
3144
  --menu-pad: var(--sp-2); /* published: a row's radius is concentric with the panel's */
3136
3145
  padding: var(--menu-pad);
3137
3146
  min-width: 12rem;
3138
- background: var(--action-bg);
3139
- color: var(--action-ink);
3140
- box-shadow: var(--shadow-float);
3147
+ background: var(--menu-bg, var(--action-bg));
3148
+ color: var(--menu-ink, var(--action-ink));
3149
+ box-shadow: var(--menu-shadow, var(--shadow-float));
3141
3150
  }
3142
3151
  .menu:popover-open { display: grid; }
3143
3152
  .menu:not([popover]) { display: grid; position: relative; } /* inline usage (no attribute) */
3144
3153
  .menu__item {
3145
3154
  display: flex;
3146
3155
  align-items: center;
3147
- gap: var(--sp-3);
3148
- min-height: var(--tap);
3156
+ gap: var(--menu-item-gap, var(--sp-3));
3157
+ min-height: var(--menu-item-min-h, var(--tap));
3149
3158
  width: 100%;
3150
- padding: 0 var(--sp-3);
3159
+ padding: var(--menu-item-pad-y, 0) var(--menu-item-pad-x, var(--sp-3));
3151
3160
  border: 0;
3152
3161
  /* concentric with the panel: the card radius minus the panel's padding, so the
3153
3162
  highlight's curve follows the outer corner (Lh 2026-09-16) */
3154
3163
  border-radius: calc(var(--r-card) - var(--menu-pad, var(--sp-2)));
3155
3164
  background: transparent;
3156
3165
  color: inherit;
3157
- font: var(--type-title);
3166
+ font: var(--menu-item-font, var(--type-title));
3158
3167
  text-align: left;
3159
3168
  cursor: pointer;
3160
3169
  }
@@ -3182,12 +3191,12 @@ button { font: inherit; cursor: pointer; }
3182
3191
  /* ---- banner: a chip that opens into a tinted card, no JS ----
3183
3192
  <details class="banner"><summary class="chip chip--on">Save $30 on orders over $70 <svg class="icon icon--xs"/></summary>
3184
3193
  <div class="card card--3 banner__body">…</div></details> */
3185
- .banner { display: grid; gap: var(--sp-2); justify-items: start; }
3194
+ .banner { display: grid; gap: var(--banner-gap, var(--sp-2)); justify-items: start; }
3186
3195
  .banner > summary { list-style: none; cursor: pointer; }
3187
3196
  .banner > summary::-webkit-details-marker { display: none; }
3188
3197
  .banner > summary .icon { transition: rotate 0.2s ease; }
3189
3198
  .banner[open] > summary .icon { rotate: 180deg; }
3190
- .banner__body { width: 100%; padding: var(--sp-4) var(--sp-5); --card-gap: var(--sp-1); }
3199
+ .banner__body { width: 100%; padding: var(--banner-body-pad-y, var(--sp-4)) var(--banner-body-pad-x, var(--sp-5)); --card-gap: var(--sp-1); }
3191
3200
  .banner::details-content { width: 100%; } /* Chromium wraps the content in this pseudo; justify-items: start shrink-wrapped it, so the body never reached the width */
3192
3201
  @supports (interpolate-size: allow-keywords) {
3193
3202
  @media (prefers-reduced-motion: no-preference) {
@@ -3205,9 +3214,9 @@ button { font: inherit; cursor: pointer; }
3205
3214
  bottom: var(--sp-5);
3206
3215
  max-width: var(--screen-w);
3207
3216
  margin-inline: auto;
3208
- padding-inline: var(--sp-4);
3217
+ padding-inline: var(--float-bar-pad-x, var(--sp-4));
3209
3218
  display: flex;
3210
- gap: var(--sp-2);
3219
+ gap: var(--float-bar-gap, var(--sp-2));
3211
3220
  align-items: center;
3212
3221
  z-index: 30;
3213
3222
  }
@@ -3222,29 +3231,29 @@ button { font: inherit; cursor: pointer; }
3222
3231
  display: flex;
3223
3232
  justify-content: space-around;
3224
3233
  align-items: center;
3225
- min-height: var(--bar-h);
3226
- padding: var(--sp-1);
3227
- border-radius: var(--r-chip);
3228
- border: var(--border-w) solid var(--action-border);
3229
- background: color-mix(in srgb, var(--action-bg) 82%, transparent);
3230
- color: var(--action-ink);
3234
+ min-height: var(--float-bar-pill-min-h, var(--bar-h));
3235
+ padding: var(--float-bar-pill-pad, var(--sp-1));
3236
+ border-radius: var(--float-bar-pill-radius, var(--r-chip));
3237
+ border: var(--border-w) solid var(--float-bar-pill-border, var(--action-border));
3238
+ background: var(--float-bar-pill-bg, color-mix(in srgb, var(--action-bg) 82%, transparent));
3239
+ color: var(--float-bar-pill-ink, var(--action-ink));
3231
3240
  backdrop-filter: blur(var(--sp-3));
3232
3241
  -webkit-backdrop-filter: blur(var(--sp-3));
3233
- box-shadow: var(--shadow-float);
3242
+ box-shadow: var(--float-bar-pill-shadow, var(--shadow-float));
3234
3243
  }
3235
3244
  .float-bar__item {
3236
3245
  flex: 1;
3237
3246
  min-width: 0;
3238
3247
  display: grid;
3239
3248
  justify-items: center;
3240
- gap: var(--sp-0);
3241
- min-height: var(--tap-lg);
3242
- padding: var(--sp-1);
3249
+ gap: var(--float-bar-item-gap, var(--sp-0));
3250
+ min-height: var(--float-bar-item-min-h, var(--tap-lg));
3251
+ padding: var(--float-bar-item-pad, var(--sp-1));
3243
3252
  border: 0;
3244
- border-radius: var(--r-chip);
3253
+ border-radius: var(--float-bar-item-radius, var(--r-chip));
3245
3254
  background: transparent;
3246
3255
  color: inherit;
3247
- font: var(--type-caption);
3256
+ font: var(--float-bar-item-font, var(--type-caption));
3248
3257
  text-decoration: none;
3249
3258
  opacity: 0.6;
3250
3259
  overflow: hidden;
@@ -3262,15 +3271,15 @@ button { font: inherit; cursor: pointer; }
3262
3271
  .callout {
3263
3272
  position: absolute;
3264
3273
  display: grid;
3265
- gap: var(--sp-0);
3274
+ gap: var(--callout-gap, var(--sp-0));
3266
3275
  width: max-content;
3267
3276
  max-width: 15rem;
3268
- padding: var(--sp-3) var(--sp-4);
3269
- border: var(--border-w) solid var(--action-border);
3270
- border-radius: var(--r-card);
3271
- background: var(--action-bg);
3272
- color: var(--action-ink);
3273
- box-shadow: var(--shadow-float);
3277
+ padding: var(--callout-pad-y, var(--sp-3)) var(--callout-pad-x, var(--sp-4));
3278
+ border: var(--border-w) solid var(--callout-border, var(--action-border));
3279
+ border-radius: var(--callout-radius, var(--r-card));
3280
+ background: var(--callout-bg, var(--action-bg));
3281
+ color: var(--callout-ink, var(--action-ink));
3282
+ box-shadow: var(--callout-shadow, var(--shadow-float));
3274
3283
  text-align: center;
3275
3284
  }
3276
3285
  .callout::after {
@@ -3287,16 +3296,16 @@ button { font: inherit; cursor: pointer; }
3287
3296
  border-top: 0;
3288
3297
  border-left: 0;
3289
3298
  }
3290
- .callout__title { font: var(--type-title); }
3291
- .callout__sub { font: var(--type-caption); opacity: 0.7; }
3299
+ .callout__title { font: var(--callout-title-font, var(--type-title)); }
3300
+ .callout__sub { font: var(--callout-sub-font, var(--type-caption)); opacity: 0.7; }
3292
3301
  /* ---- deck: swipe cards; the last child is on top ----
3293
3302
  <div class="deck"><article class="card">…</article><article class="card">…</article><article class="card card--fold">front</article></div>
3294
3303
  <div class="deck__actions"><button class="icon-btn">×</button><button class="icon-btn icon-btn--invert">✓</button></div> */
3295
- .deck { display: grid; place-items: center; padding: var(--sp-4) var(--sp-4) var(--sp-6); }
3304
+ .deck { display: grid; place-items: center; padding: var(--deck-pad-top, var(--sp-4)) var(--deck-pad-x, var(--sp-4)) var(--deck-pad-bottom, var(--sp-6)); }
3296
3305
  .deck > .card { grid-area: 1 / 1; width: min(100%, 20rem); min-height: 12.5rem; transition: rotate 0.3s ease, translate 0.3s ease; }
3297
3306
  .deck > .card:nth-last-child(2) { rotate: -4deg; translate: calc(-1 * var(--sp-2)) var(--sp-2); opacity: 0.85; }
3298
3307
  .deck > .card:nth-last-child(3) { rotate: 5deg; translate: var(--sp-3) var(--sp-4); opacity: 0.6; }
3299
3308
  .deck > .card:nth-last-child(n + 4) { display: none; }
3300
- .deck__actions { display: flex; justify-content: center; gap: var(--sp-8); }
3309
+ .deck__actions { display: flex; justify-content: center; gap: var(--deck-actions-gap, var(--sp-8)); }
3301
3310
  .deck__actions > .icon-btn { width: var(--bar-h); height: var(--bar-h); }
3302
3311