@usableapp/cardds 0.7.106 → 0.7.108

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 (110) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/bin/cardds.mjs +5 -3
  3. package/cardds.css +1 -0
  4. package/css/actions.css +9 -9
  5. package/css/base.css +6 -9
  6. package/css/card.css +19 -186
  7. package/css/choice.css +26 -1
  8. package/css/finish.css +4 -4
  9. package/css/journey.css +7 -3
  10. package/css/layover.css +4 -14
  11. package/css/lists.css +6 -18
  12. package/css/media.css +1 -7
  13. package/css/palette.css +1 -1
  14. package/css/parts.css +380 -0
  15. package/css/sheet.css +12 -139
  16. package/css/stack.css +15 -26
  17. package/css/stamp.css +1 -1
  18. package/css/step.css +3 -3
  19. package/css/theme.template.css +1 -1
  20. package/css/tokens.css +4 -4
  21. package/dist/actions/BarNote.d.ts +2 -2
  22. package/dist/actions/BarNote.js +2 -2
  23. package/dist/actions/Btn.d.ts +2 -2
  24. package/dist/actions/Btn.js +1 -1
  25. package/dist/actions/BtnRow.d.ts +1 -1
  26. package/dist/actions/BtnRow.js +1 -1
  27. package/dist/actions/IconBtn.d.ts +1 -1
  28. package/dist/actions/IconBtn.js +1 -1
  29. package/dist/cardds.css +500 -426
  30. package/dist/cards/Card.d.ts +6 -22
  31. package/dist/cards/Card.js +11 -22
  32. package/dist/cards/CardFeed.d.ts +1 -1
  33. package/dist/cards/CardFeed.js +5 -5
  34. package/dist/cards/CardParts.d.ts +2 -13
  35. package/dist/cards/CardParts.js +2 -13
  36. package/dist/cards/cardGo.js +1 -1
  37. package/dist/cards/cardGrounds.js +2 -2
  38. package/dist/catalog.json +1 -1
  39. package/dist/choice/Calendar.d.ts +12 -3
  40. package/dist/choice/Calendar.js +17 -5
  41. package/dist/choice/CalendarWeeks.d.ts +10 -2
  42. package/dist/choice/CalendarWeeks.js +18 -4
  43. package/dist/choice/HourGrid.d.ts +36 -0
  44. package/dist/choice/HourGrid.js +21 -0
  45. package/dist/choice/calendarDay.d.ts +7 -2
  46. package/dist/choice/calendarDay.js +6 -3
  47. package/dist/choice/span.d.ts +34 -0
  48. package/dist/choice/span.js +80 -0
  49. package/dist/forms/Field.d.ts +1 -1
  50. package/dist/forms/Field.js +1 -1
  51. package/dist/index.d.ts +8 -5
  52. package/dist/index.js +7 -5
  53. package/dist/journey/JourneyBadge.d.ts +2 -1
  54. package/dist/lists/Kv.d.ts +4 -2
  55. package/dist/lists/Kv.js +3 -3
  56. package/dist/lists/Row.d.ts +2 -28
  57. package/dist/lists/Row.js +2 -17
  58. package/dist/media/MapArea.d.ts +2 -2
  59. package/dist/media/MapArea.js +1 -1
  60. package/dist/media/Tile.d.ts +3 -7
  61. package/dist/media/Tile.js +5 -5
  62. package/dist/parts/Body.d.ts +7 -0
  63. package/dist/parts/Body.js +11 -0
  64. package/dist/parts/End.d.ts +10 -0
  65. package/dist/parts/End.js +7 -0
  66. package/dist/parts/Foot.d.ts +17 -0
  67. package/dist/parts/Foot.js +11 -0
  68. package/dist/parts/Head.d.ts +15 -0
  69. package/dist/parts/Head.js +34 -0
  70. package/dist/parts/Lead.d.ts +10 -0
  71. package/dist/parts/Lead.js +9 -0
  72. package/dist/parts/Words.d.ts +13 -0
  73. package/dist/parts/Words.js +20 -0
  74. package/dist/parts/headCtx.d.ts +7 -0
  75. package/dist/parts/headCtx.js +9 -0
  76. package/dist/scaffold/EmptyState.d.ts +1 -1
  77. package/dist/scaffold/EmptyState.js +1 -1
  78. package/dist/scaffold/Screen.js +1 -1
  79. package/dist/sheets/Modal.d.ts +3 -7
  80. package/dist/sheets/Modal.js +3 -7
  81. package/dist/sheets/Sheet.d.ts +2 -2
  82. package/dist/sheets/Sheet.js +8 -9
  83. package/dist/sheets/SheetStack.d.ts +3 -3
  84. package/dist/sheets/SheetStack.js +4 -4
  85. package/dist/sheets/SheetSteps.d.ts +1 -1
  86. package/dist/sheets/SheetSteps.js +2 -2
  87. package/dist/sheets/StackBack.d.ts +1 -1
  88. package/dist/sheets/StackBack.js +1 -1
  89. package/dist/sheets/StackSheet.js +7 -7
  90. package/dist/sheets/sheetMeasure.js +3 -3
  91. package/dist/sheets/sheetStepsPile.js +2 -2
  92. package/dist/sheets/stackCard.js +6 -6
  93. package/dist/templates/CallSheet.js +6 -5
  94. package/dist/templates/Walkthrough.js +3 -2
  95. package/docs/reference.md +76 -37
  96. package/lib/catalog.mjs +1 -1
  97. package/lib/mcp.mjs +1 -1
  98. package/lint/rules.mjs +14 -7
  99. package/package.json +2 -1
  100. package/scripts/codemod-parts.mjs +403 -0
  101. package/dist/cards/CardFoot.d.ts +0 -15
  102. package/dist/cards/CardFoot.js +0 -9
  103. package/dist/cards/CardHead.d.ts +0 -21
  104. package/dist/cards/CardHead.js +0 -39
  105. package/dist/cards/Stat.d.ts +0 -19
  106. package/dist/cards/Stat.js +0 -14
  107. package/dist/sheets/SheetBody.d.ts +0 -5
  108. package/dist/sheets/SheetBody.js +0 -9
  109. package/dist/sheets/SheetHead.d.ts +0 -15
  110. package/dist/sheets/SheetHead.js +0 -6
package/dist/cardds.css CHANGED
@@ -289,7 +289,7 @@
289
289
  --card-1-dark: 0; --card-2-dark: 0; --card-3-dark: 1; --card-4-dark: 0; --card-5-dark: 1;
290
290
  --accent-dark: 1; /* the brand fill: a primary button, a picked chip, a picked day */
291
291
  --action-dark: 0; /* the paper fill of neutral controls: a button, a chip, a field, a plain tile */
292
- --neg-dark: 1; /* a falling Delta's fill */
292
+ --neg-dark: 1; /* the fill of a falling number (--neg) */
293
293
 
294
294
  /* app background ("earth-toned surface or shiny color, up to the app") */
295
295
  --surface: var(--color-surface, #f2f2f0);
@@ -395,11 +395,11 @@
395
395
  (card.css, media.css…) and is named in this list, which gives it the inks every piece on it reads: --card-ink (words, icons,
396
396
  marks, dots: currentColor), --card-muted (secondary words), the four signals, --on-accent (the ink on the accent fill, whose
397
397
  side is --accent-dark) and --accent-ink (the brand as a glyph). A piece with no fill of its own reads only these, so a dark
398
- surface turns everything on it to ondark with no rule per piece. A piece with no fill that stands in for a surface (a stat
398
+ surface turns everything on it to ondark with no rule per piece. A piece with no fill that stands in for a surface (an upload
399
399
  tile) takes its ground's side: `--dark: inherit`. inkGrounds() measures it; docs/reference.md "Ink follows its ground" */
400
400
  :where(body, [data-palette], [data-mode], .card, .tile, .modal__drawer, .postcard__front, .postcard__back, .sticky, .post-paper,
401
- .float-bar__pill, .snackbar__body, .delta, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .map__pin, .sheet--paper > :is(.sheet__body, .card__foot),
402
- .postcard__band, .track__step, .toggle, .chip, .card__face, .card--bands > :is(.card__head, .card__foot), .card--bands > .card__face > :is(.card__head, .card__foot)) {
401
+ .float-bar__pill, .snackbar__body, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .map__pin, .sheet--paper > :is(.body, .foot),
402
+ .postcard__band, .track__step, .toggle, .chip, .card__face, .card--bands > :is(.head, .foot), .card--bands > .card__face > :is(.head, .foot)) {
403
403
  --card-ink: color-mix(in srgb, var(--ink-ondark) calc(var(--dark) * 100%), var(--ink-onlight));
404
404
  --card-muted: color-mix(in srgb, var(--ink-ondark-muted) calc(var(--dark) * 100%), var(--ink-onlight-muted));
405
405
  --on-accent: color-mix(in srgb, var(--ink-ondark) calc(var(--accent-dark) * 100%), var(--ink-onlight));
@@ -521,7 +521,7 @@
521
521
  /* where the highlights land — written as the component's own rule, its knob still first, so a project's knob wins (a palette never DECLARES a
522
522
  knob). Each fill says its side; the words on it follow */
523
523
  [data-palette="orchard"] .badge:not(.badge--soft) { background: var(--badge-bg, var(--highlight-1)); color: var(--badge-ink, var(--ink-ondark)); }
524
- [data-palette="orchard"] .tile-badge:not(.tile-badge--plain) { background: var(--tile-badge-bg, var(--highlight-2)); --dark: 0; }
524
+ [data-palette="orchard"] .tile-badge:not(.tile-badge--plain, .card__media > .tile-badge--corner) { background: var(--tile-badge-bg, var(--highlight-2)); --dark: 0; } /* a badge on a media stays the paper (journey.css) */
525
525
  [data-palette="orchard"] .calendar__day--mark { background: var(--calendar-day-mark-bg, color-mix(in srgb, var(--highlight-2) 15%, transparent)); }
526
526
  /* fields are WHITE boxes on every ground (Lh 2026-09-25): a light ground, so the words typed in them are onlight, even on the moss card */
527
527
  [data-palette="orchard"] .field { background: var(--field-bg, #ffffff); color: var(--ink-onlight); }
@@ -702,8 +702,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
702
702
 
703
703
  /* ---- typography levels ---- */
704
704
  /* --fs-self: each level publishes its own size, for a consumer that scales a level without knowing which */
705
- .t-display { font: var(--type-display); letter-spacing: var(--track-display); margin: 0; text-wrap: balance; --fs-self: var(--fs-display); }
706
- .t-h1 { font: var(--type-h1); letter-spacing: var(--track-h1); margin: 0; --fs-self: var(--fs-h1); }
705
+ .t-display { --_type: var(--type-display); font: var(--type-display); letter-spacing: var(--track-display); margin: 0; text-wrap: balance; --fs-self: var(--fs-display); }
706
+ .t-h1 { --_type: var(--type-h1); font: var(--type-h1); letter-spacing: var(--track-h1); margin: 0; --fs-self: var(--fs-h1); }
707
707
  .t-h2 { font: var(--type-h2); letter-spacing: var(--track-h2); margin: 0; --fs-self: var(--fs-h2); }
708
708
  .t-title { font: var(--type-title); margin: 0; --fs-self: var(--fs-title); }
709
709
  .t-body { font: var(--type-body); margin: 0; }
@@ -757,9 +757,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
757
757
  .page > .t-body { color: var(--page-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
758
758
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
759
759
  (the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
760
- /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
761
- h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
762
- .page > .card__head > .t-h1 { font-size: var(--fs-h2); }
763
760
  .page .cta-pack {
764
761
  display: flex;
765
762
  align-items: start; /* buttons keep their own height, never the row's */
@@ -852,12 +849,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
852
849
 
853
850
  /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
854
851
  /* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
855
- <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
852
+ <div class="head"><h2 class="words__title t-h2">…</h2><span class="words__meta t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
856
853
  The heading and the far-right thing share the first row; a caption sits on
857
854
  a row below (order 1), whatever the DOM order — so an author writes the
858
855
  words in reading order. */
859
- .card__head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
860
- .card__head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
856
+ .head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
857
+ .head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
861
858
  /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
862
859
  .sheet-stack__base > .screen__centre { padding-inline: 0; }
863
860
  /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
@@ -895,7 +892,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
895
892
  the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
896
893
  .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
897
894
  .step > .empty-state { flex: none; }
898
- .sheet__body > .empty-state:only-child { flex: none; }
895
+ .body > .empty-state:only-child { flex: none; }
899
896
  /* ---- where it sits in what actions.css draws (docs/reference.md rule 15) ---- */
900
897
  /* ---- top bar: back + screen title ---- */
901
898
  .topbar .t-h1 {
@@ -968,80 +965,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
968
965
  .card--4 { --card-bg: var(--card-4-bg); --dark: var(--card-4-dark); }
969
966
  .card--5 { --card-bg: var(--card-5-bg); --dark: var(--card-5-dark); }
970
967
 
971
- /* header row: title left, floating action right — centered, since the
972
- trailing control (chip/badge/icon-btn/link) is almost always taller than
973
- a single line of title text; top-aligning them left the control visually
974
- low against the title's optical center. A head with a genuinely tall,
975
- multi-line title can opt back out with .card__head--top. */
976
- .card__head {
977
- display: flex;
978
- flex-wrap: var(--card-head-wrap, wrap); /* the caption takes a line of its own — no wrapper div */
979
- align-items: center;
980
- justify-content: space-between;
981
- column-gap: var(--card-head-col-gap, var(--sp-3));
982
- row-gap: var(--card-head-row-gap, var(--sp-0));
983
- min-height: var(--card-head-min-h, auto);
984
- }
985
- .card__head--top { align-items: flex-start; }
986
968
 
987
969
  .card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
988
- .card__end { text-align: right; display: grid; gap: var(--card-end-gap, var(--sp-0)); justify-items: end; } /* trailing value + caption */
989
970
  .card__media > img { width: 100%; height: auto; object-fit: cover; }
990
971
 
991
- /* ---- the card skeleton: head · content · foot ----
992
- Every card is a head (the heading left, hints / actions / a more-menu at
993
- the far right), its content, and an optional foot of action buttons.
994
- The head's leading child — the heading, or the div that wraps heading and
995
- caption — takes the room, so a heading with nothing at the far right runs
996
- all the way across; whatever trails it hugs its own width. Only the
997
- heading (or the div that wraps it) is that leading child: a control that
998
- leads a custom head — a chip, an avatar, a tile badge — keeps its own
999
- width, else the rule stretched a chip into a bar and an avatar into an
1000
- ellipse (two agents and the wallet demo caught it, 2026-09-10). */
1001
- .card__head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
1002
-
1003
- /* Skeleton head — the heading in a card's OWN head, and only there.
1004
- The heading wraps, no squeeze, up to three lines and cut there with an
1005
- ellipsis (Lh 2026-10-21: "มันควรจะ max สามบรรทัด" — a request's title ran to
1006
- four lines on a phone; reverses 2026-09-16's "no clamp"). The count is the knob
1007
- --card-head-title-lines. The group that holds heading ·
1008
- caption is the head's own wrapping row (above) — no wrapper element, no
1009
- height, only the row gap. */
1010
- .card__head :is(h1, h2, h3) {
1011
- overflow-wrap: anywhere;
1012
- display: -webkit-box;
1013
- -webkit-box-orient: vertical;
1014
- -webkit-line-clamp: var(--card-head-title-lines, 3);
1015
- line-clamp: var(--card-head-title-lines, 3);
1016
- overflow: hidden;
1017
- padding-block-start: var(--_mark-room); /* a stacked Thai mark rises over the first line's box: room inside the clip, given back outside */
1018
- margin-block-start: calc(-1 * var(--_mark-room));
1019
- --_mark-room: 0.2em;
1020
- }
1021
-
1022
- /* a Stamp in a card's head (`Card end={<Stamp …/>}`): the words stand at the left, the stamp at the right BESIDE BOTH LINES — the heading and the
1023
- caption pair up beside it, not the stamp beside the heading alone with the caption left under it. The stamp is its own size (--stamp-size,
1024
- stamp.css); the head only places it.
1025
- <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><span class="stamp">…</span></div> */
1026
- .card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
1027
- .card__head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
1028
-
1029
- /* a head that LEADS with a date (Lh 2026-10-21, timebank's request card): a `CardHead` whose first child is a JourneyBadge and whose second is a
1030
- ChipRow. The date stands at the left and the chips beside it, the two as tall as each other; the heading and its caption take the whole
1031
- width under them, so the heading sits right over the words of the middle. The date is always as tall as the chips. The gap under the date row is --card-head-lead-gap; the date and the chips place themselves (journey.css, actions.css).
1032
- <div class="card__head"><span class="tile-badge tile-badge--stretch">…</span><div class="chip-row">…</div><div><h2 class="t-h1">…</h2>…</div></div> */
1033
- .card__head:has(> .tile-badge:first-child + .chip-row) {
1034
- display: grid;
1035
- grid-template-columns: auto minmax(0, 1fr);
1036
- align-items: center;
1037
- row-gap: var(--card-head-lead-gap, var(--sp-3));
1038
- }
1039
- .card__head:has(> .tile-badge:first-child + .chip-row) > :nth-child(n + 3) { grid-column: 1 / -1; }
1040
-
1041
972
  /* a JourneyBadge at the edge of the card's own first head (the date box in the top-left or top-right corner) keeps its corner concentric
1042
973
  with the card's: the card's corner (--r-card) less the room the padding leaves round it. journey.css reads it behind the theme's --tile-badge-radius,
1043
974
  so a theme that sets that knob still wins. */
1044
- :is(.card > .card__head:first-child, .card__face > .card__turn + .card__head) > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); } /* a turning card's face: its head follows the unseen turn button */
975
+ :is(.card > .head:first-child, .card__face > .card__turn + .head) > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); } /* a turning card's face: its head follows the unseen turn button */
1045
976
 
1046
977
  /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are paper bands edge to
1047
978
  edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
@@ -1053,20 +984,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1053
984
  overflow: hidden;
1054
985
  }
1055
986
  .card--bands.card--turns { background: none; overflow: visible; } /* the turning box keeps --dark: the faces' ink is computed from it */
1056
- :is(.card--bands, .card--bands > .card__face) > :is(.card__head, .card__foot) {
1057
- --card-bg: var(--card-1-bg);
1058
- --dark: var(--card-1-dark);
1059
- background: var(--card-bg);
1060
- color: var(--card-ink);
1061
- margin-inline: calc(-1 * var(--card-pad));
1062
- padding-inline: var(--card-pad);
1063
- }
1064
- :is(.card--bands, .card--bands > .card__face) > .card__head { margin-top: calc(-1 * var(--card-pad)); padding-block: var(--card-pad); }
1065
- :is(.card--bands, .card--bands > .card__face) > .card__head { padding-block-end: var(--card-band-seam, var(--sp-2)); }
1066
- /* the seam: the heading and the words under it read as ONE paragraph that changes colour at the band's edge (Lh 2026-10-21), so the room on
1067
- each side of the head band's lower edge is --card-band-seam, not the card's padding and gap */
1068
- :is(.card--bands, .card--bands > .card__face) > .card__head + * { margin-block-start: calc(var(--card-band-seam, var(--sp-2)) - var(--card-gap)); }
1069
- :is(.card--bands, .card--bands > .card__face) > .card__foot { margin-bottom: calc(-1 * var(--card-pad)); padding-block: var(--sp-3); } /* the foot band is slim */
987
+ /* on a dark tone (3, 5) the lightened middle is a mid grey (#adadad, #bbbbbb in wireframe) that no muted ink reads on (3.2:1 and 3.8:1;
988
+ the ink gate, 2026-10-22, from the caption under a code on a `Card back`): there the middle's secondary words take the full ink */
989
+ :is(.card--bands, .card--bands > .card__face):is(.card--3, .card--5, .card--3 > *, .card--5 > *) { --card-muted: var(--card-ink); }
1070
990
 
1071
991
  /* ---- two faces: the WHOLE card turns (`Card back`, Lh 2026-10-19). The article is the turning box (.flip, base.css) and only that;
1072
992
  each face is the card's paper — head · middle · foot — and both lie in one grid cell. The head and the foot are the same on both
@@ -1088,10 +1008,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1088
1008
  position: relative;
1089
1009
  }
1090
1010
  .card__face > * { min-width: 0; }
1091
- .card__middle { flex: 1 0 auto; display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, auto); overflow: hidden; }
1011
+ .card__middle { flex: 1 0 auto; display: flex; flex-direction: column; gap: var(--card-gap); height: var(--card-middle-h, auto); overflow: hidden; } /* a flex column, not a grid: the middle's height is the flexed one, so one thing on a face (a QR, a code) may take the whole middle with height: 100% (a grid's auto row never stretched to it, and the thing stood at the top of an empty band) */
1092
1012
  .card__turn { position: absolute; inset: 0; width: var(--px); height: var(--px); padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
1093
1013
  .card__face:has(> .card__turn:focus-visible) { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
1094
- .card__head > .chip-row > .chip > .icon { --icon-display: inline-block; --icon-align: -0.15em; --icon-margin: 0 var(--chip-sm-gap, var(--sp-1)) 0 0; }
1014
+ .head > .chip-row > .chip > .icon { --icon-display: inline-block; --icon-align: -0.15em; --icon-margin: 0 var(--chip-sm-gap, var(--sp-1)) 0 0; }
1095
1015
 
1096
1016
  /* ---- bleed: a scrolling track that runs to the container's edges ----
1097
1017
  For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
@@ -1131,19 +1051,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1131
1051
  is genuinely first or last needs the corner clipped for it */
1132
1052
  .card:has(> :is(.bleed, .map--bleed):first-child), .card:has(> :is(.bleed, .map--bleed):last-child) { overflow: clip; }
1133
1053
 
1134
- /* foot: the card's action buttons on one line. One button takes the width,
1135
- up to four share it in even parts — so the gaps read even too — and from
1136
- three on they drop their side padding so the labels keep the room. */
1137
- .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; color: var(--card-foot-ink, inherit); }
1138
- .card__foot:has(> :nth-child(3)) > .btn { --btn-pad-x: var(--sp-2); }
1139
- /* a small link under the buttons (CardFoot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
1140
- whole short row of its own, centred — it never shares the buttons' line */
1141
- .card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
1142
- .card__foot-link { flex: 1 0 100%; display: flex; justify-content: center; }
1143
- /* round (CardFoot round): the foot holds a few round IconBtns, each its own size (they never share the line), as ONE group in the middle with
1144
- a gap that does not follow the card's width. The gap is wider than the Btns' (a miss between two round targets): --card-foot-gap sets it */
1145
- .card__foot--round { justify-content: center; gap: var(--card-foot-gap, var(--sp-6)); }
1146
-
1147
1054
  /* media placeholder for wireframes — currentColor so it survives any palette */
1148
1055
  .ph {
1149
1056
  background:
@@ -1260,7 +1167,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1260
1167
  align-items: center;
1261
1168
  gap: var(--card-row-gap, var(--sp-3));
1262
1169
  }
1263
- .card--row .card__body { flex: 1; display: grid; gap: var(--sp-0); }
1264
1170
 
1265
1171
  /* ---- a short window of past entries (`CardList show`, Lh 2026-10-19): the list scrolls IN ITSELF, a few rows in sight and the next one cut
1266
1172
  in half to say there is more. The height is counted in rows by the component (`--_card-list-h`, px: rows wrap to any height); the room
@@ -1277,7 +1183,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1277
1183
  }
1278
1184
 
1279
1185
  /* ---- the cross that takes a row off the list (`Card row onRemove`): small, at the row's top right corner, inside it ----
1280
- <label class="card card--row"><input type="radio">…<div class="card__body">…</div><button class="card__remove" aria-label="Remove"><svg class="icon">x</svg></button></label>
1186
+ <label class="card card--row"><input type="radio">…<span class="words">…</span><button class="card__remove" aria-label="Remove"><svg class="icon">x</svg></button></label>
1281
1187
  Smaller than any button (a ring half a thing's size, `--tap-base` / 2 = 24) and it grows with the size of things; the touch target is an
1282
1188
  unseen ring round it, never under --tap-floor (44). It stands in the row's flow, so the title wraps before it, tucked into the corner
1283
1189
  by the row's own padding; it sits over the row's radio (which covers the whole card), so a touch on it removes and never picks. */
@@ -1305,21 +1211,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1305
1211
  .card__remove > .icon { --icon-size: 62%; }
1306
1212
  .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
1307
1213
 
1308
- /* leading thumb/icon in a row */
1309
- .card__thumb {
1310
- width: var(--card-thumb-size, var(--tap-sm)); height: var(--card-thumb-size, var(--tap-sm));
1311
- flex: none;
1312
- border-radius: var(--card-thumb-radius, var(--r-control));
1313
- border: var(--border-w) solid currentColor;
1314
- display: grid;
1315
- place-items: center;
1316
- font: var(--card-thumb-font, var(--type-label));
1317
- }
1318
-
1319
1214
  /* ---- card feed: a queue of cards, all in sight — one big, the rest small (Lh 2026-10-09, the whole queue 2026-10-15) ----
1320
1215
  <div class="card-feed">
1321
- <article class="card feed-card"><div class="card__head feed-card__head">thumb or lead · h2 · caption</div>
1322
- <div class="feed-card__more"><div class="feed-card__rest">content · <div class="card__foot">…</div></div></div></article>
1216
+ <article class="card feed-card"><div class="head">lead · h2 · caption</div>
1217
+ <div class="feed-card__more"><div class="feed-card__rest">content · <div class="foot">…</div></div></div></article>
1323
1218
  <article class="card feed-card feed-card--small" role="button" tabindex="0">… <div class="feed-card__more" inert>…</div></article>
1324
1219
  <p class="t-caption card-feed__end" role="note">the end</p>
1325
1220
  </div>
@@ -1330,9 +1225,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1330
1225
  shut content in the scrollable overflow of whatever holds the feed — and hidden once the row has shut. */
1331
1226
  .card-feed { display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; gap: var(--card-feed-gap, var(--card-gap-rest)); }
1332
1227
  .feed-card { row-gap: 0; }
1333
- .feed-card__head { display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; min-height: var(--feed-card-head-h, var(--tap)); }
1334
- .feed-card__head:has(> :is(.card__thumb, .feed-card__lead)) { grid-template-columns: auto minmax(0, 1fr); }
1335
- .feed-card__head:has(> .t-caption) > :is(.card__thumb, .feed-card__lead) { grid-row: span 2; }
1228
+ .feed-card > .head:has(> .t-caption) > .feed-card__lead { grid-row: span 2; }
1336
1229
  .feed-card__lead { display: grid; align-self: center; } /* a Stamp here is the small one, 48 × 60 (stamp.css) */
1337
1230
  .card-feed__end { text-align: center; }
1338
1231
  .feed-card__more { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: 1fr; }
@@ -1349,22 +1242,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1349
1242
  .feed-card--small .feed-card__rest { transition: visibility 0s var(--motion-sheet); }
1350
1243
  }
1351
1244
 
1352
- /* ---- inline stat block (revenue-style) ---- */
1353
- .stat { display: grid; gap: var(--stat-gap, var(--sp-1)); }
1354
- .stat--centre { text-align: center; justify-items: center; } /* the one number of a sheet or a stage, under a centred avatar */
1355
- .stat__row { display: flex; align-items: center; justify-content: space-between; gap: var(--stat-row-gap, var(--sp-3)); }
1356
-
1357
- .delta {
1358
- font: var(--delta-font, var(--type-caption));
1359
- font-weight: var(--fw-bold);
1360
- padding: var(--delta-pad-y, var(--sp-1)) var(--delta-pad-x, var(--sp-2));
1361
- border-radius: var(--delta-radius, var(--r-chip));
1362
- background: var(--delta-bg, var(--card-3-bg));
1363
- --dark: var(--card-3-dark);
1364
- color: var(--delta-ink, var(--card-ink));
1365
- }
1366
- .delta--neg { background: var(--delta-neg-bg, var(--neg)); --dark: var(--neg-dark); color: var(--delta-neg-ink, var(--card-ink)); }
1367
-
1368
1245
  /* ---- panes: a few panes side by side inside a card or a sheet, one in sight (Lh 2026-10-02) ----
1369
1246
  <div class="panes" role="group"><div class="panes__rail"><div class="panes__pane">…</div>…</div><span class="pager__at">…</span></div>
1370
1247
  The rail runs to the card's edges and snaps a pane at a time; each pane takes the inset back, so its content stands on the
@@ -1388,7 +1265,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1388
1265
 
1389
1266
  /* ---- cover: the image IS the card; text over a scrim ----
1390
1267
  <article class="card card--cover"><div class="ph" role="img"></div>
1391
- <div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
1268
+ <div class="head"><button class="icon-btn icon-btn--sm">♥</button></div>
1392
1269
  <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
1393
1270
  <div class="row">…meta…</div></article> */
1394
1271
  .card--cover {
@@ -1478,14 +1355,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1478
1355
  .sheet-stack > .card:nth-child(3) { margin-top: calc(2 * var(--stack-step)); }
1479
1356
  .sheet-stack > .card:nth-child(4) { margin-top: calc(3 * var(--stack-step)); }
1480
1357
  .sheet-stack > .card:nth-child(5) { margin-top: calc(4 * var(--stack-step)); }
1481
- /* stack headers: uniform — [back?] title left, icons right, one height,
1482
- same gap to content on every card */
1483
- .sheet-stack > .card > .card__head {
1484
- align-items: center;
1485
- justify-content: flex-start;
1486
- min-height: var(--tap);
1487
- }
1488
- .sheet-stack--display > .card > .card__head { align-items: start; min-height: 0; }
1489
1358
  /* ============================================================
1490
1359
  .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
1491
1360
 
@@ -1538,8 +1407,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1538
1407
  overscroll-behavior: contain;
1539
1408
  }
1540
1409
  .sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
1541
- .sheet-stack--tap > .card > .card__head { touch-action: none; }
1542
- .sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
1410
+ .sheet-stack--tap > .card.is-open:not(:has(> .body)) {
1543
1411
  padding-top: calc(var(--card-pad) + var(--_refresh, 0rem));
1544
1412
  transition: translate var(--motion-sheet) var(--ease), padding-top var(--motion-sheet) var(--ease);
1545
1413
  }
@@ -1575,56 +1443,18 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1575
1443
  only, the sheet's size is untouched. A pull on the head keeps them square until the finger lets go. */
1576
1444
  .sheet-stack--flood > .card { transition: translate var(--motion-sheet) var(--ease), border-radius var(--motion-sheet) var(--ease); }
1577
1445
  .sheet-stack--flood > .card.is-open { border-top-left-radius: 0; border-top-right-radius: 0; }
1578
- :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head {
1579
- position: relative;
1580
- padding-left: 0;
1581
- transition: padding-left var(--motion-sheet) var(--ease);
1582
- }
1583
- /* ---- a stacked sheet with a SheetBody has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
1584
- pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a CardFoot, the CTA) at the bottom.
1585
- The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no SheetBody
1446
+ /* ---- a stacked sheet with a Body has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
1447
+ pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a Foot, the CTA) at the bottom.
1448
+ The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no Body
1586
1449
  keeps the old way: the whole open card scrolls. ---- */
1587
- .sheet-stack--tap > .card:has(> .sheet__body) { display: flex; flex-direction: column; gap: var(--card-gap); }
1588
- .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
1589
- /* a paper stack's foot is paper to the card's edges, and it covers the tinted flex gap above it so the
1590
- button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
1591
- where it was — the padding gives back what the bleed takes. */
1592
- .sheet-stack > .card.sheet--paper > .card__foot {
1593
- background: var(--action-bg);
1594
- color: var(--card-ink);
1595
- margin-top: calc(-1 * var(--card-gap));
1596
- margin-inline: calc(-1 * var(--card-pad));
1597
- margin-bottom: calc(-1 * var(--card-pad));
1598
- padding-top: var(--card-gap);
1599
- padding-inline: var(--card-pad);
1600
- padding-bottom: var(--card-pad);
1601
- }
1602
- .stack-sheet > .card__head { flex-wrap: nowrap; }
1450
+ .sheet-stack--tap > .card:has(> .body) { display: flex; flex-direction: column; gap: var(--card-gap); }
1451
+ .sheet-stack--tap > .card:has(> .body).is-open { overflow: clip; }
1603
1452
  /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
1604
1453
  tappable"): a SMALL DISC — its ring kept, 32px instead of 44, the chevron in it at 20 — so the head gives it 32px + a gap
1605
1454
  (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
1606
- .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back:not(.sheet-stack__back--end)) { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
1607
- :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) var(--ease); }
1608
- .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) { padding-right: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
1609
1455
  /* a stacked sheet is a control until it is open — tapped, never read into: no text selection (a drag, a double tap, a long press),
1610
1456
  no grey flash, no iOS callout; its head stays one when open. Only the OPEN sheet's body is text to select (Lh 2026-09-23) */
1611
- .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
1612
- /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
1613
- .sheet > .card__foot {
1614
- position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: var(--z-over); /* the sheet's border box: the same height the sheet's own % reads */
1615
- align-items: end;
1616
- align-content: end;
1617
- padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
1618
- background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
1619
- pointer-events: none;
1620
- translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
1621
- transition: translate var(--motion-sheet) var(--ease), visibility 0s;
1622
- }
1623
- .sheet.is-dragging > .card__foot { transition: none; }
1624
- /* at peek and away the foot is NOT held on the box's edge: it stands on the sheet's own bottom edge (no counter-slide), so it
1625
- LEAVES AND ARRIVES WITH ITS SHEET — solid the whole way, hidden only once the slide is over. It used to stay in place and
1626
- fade: the body slid through the strip behind a half-transparent button (timebank, Lh 2026-10-01: "ตัวหนังสือลอดใต้ปุ่ม"). */
1627
- :is(.sheet--peek, .sheet--away) > .card__foot { translate: 0 0; visibility: hidden; transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--ease-linear) var(--motion-sheet); }
1457
+ .sheet-stack--tap > .card { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
1628
1458
  /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
1629
1459
  /* a WALK (Pages of Page) — Lh 2026-09-23, third call: no top; the picture and the words are ONE run that scrolls up together
1630
1460
  (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
@@ -1653,7 +1483,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1653
1483
  .postcard__front .postcard__turn > .ph { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border: 0; border-radius: 0; min-height: 0; }
1654
1484
 
1655
1485
  /* ---- where it sits in what stack.css draws: a sheet pulled to refresh holds still (docs/reference.md rule 15) ---- */
1656
- .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
1486
+ .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .body)) { transition: none; }
1657
1487
 
1658
1488
  /* ---- where it sits in what step.css draws: the reading block, a walk of pages (docs/reference.md rule 15) ---- */
1659
1489
  .step:has(> .sheet--peek) > .pages { padding-bottom: var(--sheet-peek); }
@@ -1685,7 +1515,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1685
1515
 
1686
1516
  <div class="sheet-stack sheet-stack--tap"> (drop --tap for a static stack)
1687
1517
  <article class="card card--2">
1688
- <div class="card__head">
1518
+ <div class="head">
1689
1519
  <button class="icon-btn icon-btn--sm sheet-stack__back" aria-label="Back">…</button>
1690
1520
  <h2 class="t-h2 t-caps">Shopping</h2>
1691
1521
  <span class="icon-row">…</span> (optional trailing icons)
@@ -1740,7 +1570,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1740
1570
 
1741
1571
  /* NOT a skeleton head: a peek is one line at its own size, and the display
1742
1572
  stack deliberately runs a second line under the next card. */
1743
- .sheet-stack > .card > .card__head :is(h1, h2, h3) {
1573
+ .sheet-stack > .card > .head :is(h1, h2, h3) {
1744
1574
  font-size: var(--fs-h1);
1745
1575
  line-height: var(--lh-solid);
1746
1576
  letter-spacing: var(--track-h2);
@@ -1752,7 +1582,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1752
1582
  /* display stack: the giant title IS the peek — line one shows, line two is
1753
1583
  clipped by the next card (recents / library lists) */
1754
1584
  .sheet-stack--display { --stack-step: calc(var(--tap-base) * 13 / 6); }
1755
- .sheet-stack--display > .card > .card__head :is(h1, h2, h3) {
1585
+ .sheet-stack--display > .card > .head :is(h1, h2, h3) {
1756
1586
  font: var(--type-display);
1757
1587
  letter-spacing: var(--track-display);
1758
1588
  /* the FIRST line sits on the centre of the peek strip (--stack-step tall,
@@ -1766,7 +1596,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1766
1596
  NOT: there, every card is full height and full content always, and a
1767
1597
  peek is simply the strip of it the next card doesn't cover — the way a
1768
1598
  card in a real deck is never a shorter card, just a mostly hidden one. */
1769
- .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.card__head) { display: none; }
1599
+ .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.head) { display: none; }
1770
1600
 
1771
1601
  /* ---- .sheet-stack--tap — tap a peeked sheet open; a back button returns it. How its sheets move (one property: translate) is
1772
1602
  told over their rules, where they sit, in card.css. ---- */
@@ -1914,7 +1744,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1914
1744
  slide read as a second thing moving while the sheet itself moved; Lh,
1915
1745
  2026-09-11). Only the sheet travels; the button appears where it is. */
1916
1746
  /* a sheet's OWN head and back: the card is the stack's child or, with one host wrapper, the wrapper's. A card standing in the sheet's body wears its own head */
1917
- :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head > .sheet-stack__back {
1747
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .head > .sheet-stack__back {
1918
1748
  position: absolute;
1919
1749
  left: 0;
1920
1750
  top: 50%;
@@ -1929,35 +1759,24 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1929
1759
  min-height: 0;
1930
1760
  padding: 0;
1931
1761
  }
1932
- .sheet-stack--tap > .card.is-open > .card__head > .sheet-stack__back {
1762
+ .sheet-stack--tap > .card.is-open > .head > .sheet-stack__back {
1933
1763
  opacity: 1;
1934
1764
  visibility: visible;
1935
1765
  }
1936
1766
 
1937
- .sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
1938
- .sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
1939
- /* a paper stack's foot is paper (card.css paints it): its ink reads the action's side */
1940
- .sheet-stack > .card.sheet--paper > .card__foot { --dark: var(--action-dark); }
1767
+ .sheet-stack > .card:has(> .body) > :not(.body) { flex-shrink: 0; }
1941
1768
 
1942
1769
  /* ---- StackSheet's tabs (Lh 2026-09-23): the head's end is a row of small icon buttons, the sheet's pages — the chosen one
1943
1770
  filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
1944
1771
  .stack-sheet__tabs { display: flex; gap: var(--stack-sheet-tabs-gap, var(--sp-2)); flex: none; } /* room between the tabs: with none, the chosen tab's disc touched its neighbour's (Lh 2026-09-24) */
1945
- /* the head is ONE line, always (Lh 2026-09-24): the tabs keep their size, the TITLE gives — it never wraps, it runs sideways
1946
- (StackSheet measures --_over, the width it overflows by, and sets .is-long), one way: it waits, runs to the end, waits, and starts again from the first letter — like a song title */
1947
- .stack-sheet__title {
1948
- flex: 1; min-width: 0; overflow-x: clip; overflow-y: visible; white-space: nowrap; overflow-wrap: normal; /* clip SIDEWAYS only:
1949
- hidden clipped the Thai tone marks above a line-height-1 line (เพื่อน lost its ไม้เอก — Lh 2026-09-24) */
1950
- translate: 0 var(--stack-sheet-title-shift, -0.1em); /* FC Pride's line box carries more room above its letters than below: the
1951
- letters sat ~0.1em under the middle of the back disc and the tabs — lifted onto it (Lh 2026-09-24) */
1952
- }
1772
+ /* the title's run (the title's own rules are parts.css's, in a stack sheet) */
1953
1773
  .stack-sheet__run { display: inline-block; }
1954
- .stack-sheet__title.is-long .stack-sheet__run {
1774
+ .stack-sheet > .head > .words__title.is-long > .stack-sheet__run {
1955
1775
  animation: stack-sheet-run var(--stack-sheet-run-time, var(--_run, var(--motion-run))) var(--ease-in-out) infinite; /* ONE way: start → end, then back to the start at once (Lh 2026-09-24) */ /* --_run: from the overflow, 30px a second + the pauses */
1956
1776
  }
1957
1777
  @keyframes stack-sheet-run { 0%, 20% { translate: 0; } 85%, 100% { translate: calc(-1 * var(--_over)); } }
1958
1778
  @media (prefers-reduced-motion: reduce) {
1959
- .stack-sheet__title.is-long .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
1960
- .stack-sheet__title.is-long { text-overflow: ellipsis; }
1779
+ .stack-sheet > .head > .words__title.is-long > .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
1961
1780
  .stack-sheet__tab { transition: none; } /* the disc is simply there while open, and gone while closed */
1962
1781
  }
1963
1782
  .stack-sheet__tab {
@@ -1991,23 +1810,23 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1991
1810
  /* a CLOSED sheet shows no chosen disc either (Lh 2026-10-04): arriving on the home, every tab of every sheet is the icon
1992
1811
  alone. The fill fades in with the rise — the tab that was pressed, or the one the sheet opens on — and fades out as the
1993
1812
  sheet folds. The class stays (which page it is); only the paint waits for .is-open. */
1994
- .sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab.icon-btn--invert {
1813
+ .sheet-stack--tap > .card:not(.is-open) > .head .stack-sheet__tab.icon-btn--invert {
1995
1814
  border-color: transparent; background-color: transparent; box-shadow: none; color: inherit;
1996
1815
  }
1997
1816
  /* the last tab's ICON lines up with the inset, not its button's edge: an unchosen tab shows the glyph only, so the button's own
1998
1817
  margin around it reaches into the card's padding — and an open head (back · title · three tabs) keeps one line (Lh 2026-09-23) */
1999
- .card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
1818
+ .head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
2000
1819
 
2001
1820
  /* AT THE END (Lh 2026-09-29, the home): after the tabs, chevron down — the open sheet folds down. The head's end gives it its room
2002
1821
  while open; the last tab then sits on the inset (no reach into the padding) */
2003
1822
  .sheet-stack--tap .sheet-stack__back--end { left: auto; right: 0; }
2004
- .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
1823
+ .sheet-stack--tap > .card.is-open > .head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
2005
1824
  .sheet-stack--tap .sheet-stack__back > .icon { --icon-size: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
2006
1825
  .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--_back-size) - var(--tap-sm)) / 2); }
2007
1826
 
2008
1827
  /* the open sheet's title folds it (SheetStack.tsx): it says so to a pointer */
2009
- .sheet-stack--tap > .card.is-open > .card__head > :is(h1, h2, h3) { cursor: pointer; }
2010
- .sheet-stack--tap > .card.is-open > :not(.card__head) { -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); } /* the open sheet reads as the rest of the screen: not selectable (Lh 2026-09-25; it was text) */
1828
+ .sheet-stack--tap > .card.is-open > .head > :is(h1, h2, h3) { cursor: pointer; }
1829
+ .sheet-stack--tap > .card.is-open > :not(.head) { -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); } /* the open sheet reads as the rest of the screen: not selectable (Lh 2026-09-25; it was text) */
2011
1830
 
2012
1831
  /* ---- where it sits in what base.css draws (docs/reference.md rule 15) ---- */
2013
1832
  /* min-height: 0 — a flex item's floor is its content, and the stack's content
@@ -2054,8 +1873,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2054
1873
  the listed states, and the state it lands on becomes the one a tap
2055
1874
  returns to.
2056
1875
  <button class="sheet__handle" type="button" aria-label="…"></button>
2057
- <div class="card__head"><h2 class="t-h2">…</h2>…</div>
2058
- <div class="sheet__body">…controls, or small content cards…</div>
1876
+ <div class="head"><h2 class="words__title t-h2">…</h2>…</div>
1877
+ <div class="body">…controls, or small content cards…</div>
2059
1878
  </article>
2060
1879
  Written top-down: the handle, the title row (what peek shows), then
2061
1880
  the body. The handle and the head stay where they are at every
@@ -2127,38 +1946,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2127
1946
  .sheet--away { --_y: 100%; --_cover: var(--_box); visibility: hidden; transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--ease-linear) var(--motion-sheet); }
2128
1947
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
2129
1948
 
2130
- /* the body — everything under the head, the one thing in a sheet that
2131
- scrolls. Its scrollport runs to the card's side and bottom edges (reading
2132
- --card-pad, the way .bleed does) so content scrolls clear of the edge
2133
- rather than stopping inside the padding, and a .bleed strip inside it
2134
- still reaches the edges. The card's own grid rhythm continues inside. */
2135
- .sheet__body {
2136
- flex: 1;
2137
- min-height: 0;
2138
- display: grid;
2139
- gap: var(--card-gap);
2140
- align-content: start;
2141
- margin-inline: calc(-1 * var(--card-pad));
2142
- padding-inline: var(--card-pad);
2143
- overflow-y: auto;
2144
- overscroll-behavior: contain;
2145
- /* the part of the sheet that slid under the box's bottom edge cannot be
2146
- seen, and the scroller has to know that: scroll-padding is exactly "the
2147
- obscured part of my scrollport", so a focused field, scrollIntoView and
2148
- anchor navigation all land a control in the visible part, never under
2149
- the cut. A length from the box's container units (--_box), since the
2150
- cut is a share of the SHEET's height, not of the body's. */
2151
- scroll-padding-bottom: var(--_cover);
2152
- /* …and its content has to be able to LEAVE that part: the same length as bottom padding, so the last row
2153
- scrolls up to the visible edge at half and 3q as it does at full — everything between the head and the
2154
- foot can be reached at any state (Lh, 2026-09-21). Padding, not a shorter box: nothing that shows moves
2155
- when the state changes, only how far the body scrolls; a child that fills the body (a composer) fills
2156
- the part that shows. */
2157
- padding-bottom: var(--_cover);
2158
- }
2159
1949
  /* ---- the sheet's foot: the button that FINISHES the sheet, on the sheet (Lh, 2026-09-20) ----
2160
1950
  A screen with a sheet has no bar in front of it: the button that completes what the sheet asks — the
2161
- page's conclusion — is a .card__foot, the sheet's last child, on the sheet's own material. The sheet is
1951
+ page's conclusion — is a .foot, the sheet's last child, on the sheet's own material. The sheet is
2162
1952
  full height at every state, so its real bottom edge is under the cut unless it stands at full. So the
2163
1953
  foot is a layer as tall as the sheet, its buttons at its bottom, slid UP by the very share the sheet
2164
1954
  slid down (--_y, + the drag): the two cancel, and the buttons stand on the box's bottom edge at half,
@@ -2166,18 +1956,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2166
1956
  length — so a box that resizes (the keyboard) moves them as one, with nothing to catch up. The layer
2167
1957
  takes no taps, its buttons do; the strip behind them is the sheet's ground, --_foot tall, and the body
2168
1958
  stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
2169
- One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
1959
+ One row, ≤ 2 buttons (parts.css). <div class="foot"><button class="btn btn--primary btn--block">…</button></div> */
2170
1960
  /* …and the link row under the button, when there is one (its line + the gap over it) */
2171
- .sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--sheet-foot-h, calc(var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)))) + var(--card-pad) + var(--_safe-b-open)); } /* --sheet-foot-h: the foot's rows as the Sheet measured them (a button grows with the text size) */
2172
- .sheet > .card__foot > * { pointer-events: auto; }
2173
- .sheet:has(> .card__foot) > .sheet__body { margin-bottom: calc(var(--_foot) - var(--card-pad)); } /* the body stops above the strip (the card's own padding is the rest) */
2174
- /* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
2175
- box is never smaller than its padding — so the pinned parts must not give way to it: the body runs past
2176
- the sheet's clipped edge instead, where nothing shows anyway. At peek and away no part of the body shows. */
2177
- .sheet > :not(.sheet__body) { flex-shrink: 0; }
2178
- :is(.sheet--peek, .sheet--away) > .sheet__body { padding-bottom: 0; }
2179
- .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
2180
- .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover) + var(--_safe-b-open)); }
1961
+ .sheet:has(> .foot--link) { --_foot: calc(var(--card-gap) + var(--sheet-foot-h, calc(var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)))) + var(--card-pad) + var(--_safe-b-open)); } /* --sheet-foot-h: the foot's rows as the Sheet measured them (a button grows with the text size) */
1962
+ /* the body's padding can be taller than its own box, so the pinned parts must not give way to it (parts.css) */
1963
+ .sheet > :not(.body) { flex-shrink: 0; }
2181
1964
 
2182
1965
  /* grab handle — first child of the sheet, but it does not take a row: it sits in the
2183
1966
  card's top padding, the same air as a stack sheet's four dots (stack.css --_grip-y),
@@ -2203,22 +1986,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2203
1986
  touch-action: none;
2204
1987
  cursor: grab;
2205
1988
  }
2206
- /* paper: the head keeps the sheet's own colour, the content is the theme's paper, --action-bg, the ground of every field (Lh 2026-10-05: white; Lh 2026-10-20: the theme's, never a fixed white).
2207
- The body and the foot read as paper; the handle and the head stay on the card.
2208
- The flex gap under the head is outside the body, so it stays the sheet's tint.
2209
- The first field would sit on that colour seam. The paper's inset is --sp-6 (24px at
2210
- the default spacing), the body's own top padding, and only when the sheet is paper.
2211
- A plain sheet keeps padding-top 0. A sheet in a stack wears the same class and the same
2212
- inset: the stack's own gap under the head is the card's colour, so the row still needs this paper.
2213
- The paper takes the side of the controls' paper (--action-dark): on a dark sheet its words still turn to onlight. */
2214
- .sheet--paper > .sheet__body {
2215
- --card-bg: var(--action-bg);
2216
- --dark: var(--action-dark);
2217
- background: var(--card-bg);
2218
- color: var(--card-ink);
2219
- padding-top: var(--sp-6);
2220
- }
2221
- .sheet--paper > .card__foot { --card-bg: var(--action-bg); --dark: var(--action-dark); --card-foot-ink: var(--card-ink); }
2222
1989
  .sheet__handle--dots {
2223
1990
  --_dot: var(--sp-1);
2224
1991
  --_step: calc(var(--_dot) * 2.5);
@@ -2258,42 +2025,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2258
2025
  .sheet__handle:focus-visible { outline: var(--focus); outline-offset: var(--sp-2); }
2259
2026
  .sheet.is-dragging .sheet__handle { cursor: grabbing; }
2260
2027
 
2261
- /* the head — a title row at least --tap tall, either kind, so the peek strip
2262
- (--sheet-peek) is the same for every sheet:
2263
- - in a step it is a plain card head (card.css): the heading left, the
2264
- contextual thing at the far right
2265
- - .sheet__head is the stage sheet's three-column head: a round button,
2266
- the centred title and sub, a round button
2267
- - a label .row with its .check / .toggle may be the head when the sheet's
2268
- whole ask is that switch: peek shows it (a row is --tap tall plus its own
2269
- padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
2270
- .sheet > .card__head { --card-head-min-h: var(--sheet-head-h); }
2271
- /* a sheet's title is ONE line, always (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน" wrapped to two lines at 390px) — the
2272
- thing at its end keeps its place beside it; a caption still takes its own row. A title too long for the line RUNS sideways
2273
- like a song title (Lh 2026-10-03, as the StackSheet's — it was cut with …, and the end of it could never be read): the Sheet
2274
- measures --_over, the width it overflows by, and sets .is-long; one way — it waits, runs to its end, waits, and starts again
2275
- from the first letter. The move is text-indent: the heading is the app's own element, there is no inner box to slide.
2276
- Before the Sheet has measured, and where motion is off, it is cut with …. */
2277
- .sheet > .card__head:not(:has(> :is(.t-caption, .t-body))) { --card-head-wrap: nowrap; }
2278
- .sheet > .card__head > :is(h1, h2, h3):first-child, .sheet__title { white-space: nowrap; overflow-x: clip; overflow-y: visible; text-overflow: ellipsis; min-width: 0; } /* clip SIDEWAYS only: hidden cut the Thai tone marks over the line */
2279
- .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long {
2280
- text-overflow: clip;
2281
- animation: sheet-title-run var(--sheet-title-run-time, var(--_run, var(--motion-run))) var(--ease-in-out) infinite; /* --_run: from the overflow, 30px a second + the pauses */
2282
- }
2283
- @keyframes sheet-title-run { 0%, 20% { text-indent: 0; } 85%, 100% { text-indent: calc(-1 * var(--_over)); } }
2284
- @media (prefers-reduced-motion: reduce) {
2285
- .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long { animation: none; text-overflow: ellipsis; }
2286
- }
2287
- .sheet__head {
2288
- display: grid;
2289
- grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
2290
- align-items: center;
2291
- gap: var(--sp-3);
2292
- min-height: var(--tap);
2293
- text-align: center;
2294
- }
2295
- .sheet__title { font: var(--sheet-title-font, var(--type-h2)); letter-spacing: var(--track-h2); margin: 0; }
2296
- .sheet__sub { font: var(--sheet-sub-font, var(--type-caption)); color: var(--sheet-sub-ink, var(--card-muted)); }
2297
2028
 
2298
2029
  /* ============================================================
2299
2030
  .sheet-stage — the overlay stage: a page-height box the sheet
@@ -2440,11 +2171,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2440
2171
  }
2441
2172
 
2442
2173
  /* ---- the map IN FLOW (Lh 2026-10-17): `.map--inline` — a box in a card, or the whole of a sheet's body ----
2443
- <div class="sheet__body"><div class="map map--inline"><div>…the engine…</div><label class="map-pick" …>…</label>…
2174
+ <div class="body"><div class="map map--inline"><div>…the engine…</div><label class="map-pick" …>…</label>…
2444
2175
  <span class="map__ring" …></span></div></div>
2445
2176
  Choosing where to meet without leaving the sheet: the open sheet becomes the map and its head stays on top (a stage
2446
2177
  behind a half sheet covers the head and changes the look). No stage, so no band and no bar: the ring is at the centre
2447
- of the map's OWN box and the hint at its own top. In a card it is a rounded media box; as a SheetBody's only child it
2178
+ of the map's OWN box and the hint at its own top. In a card it is a rounded media box; as a sheet's Body's only child it
2448
2179
  fills the body (the part above the foot), however far the sheet slid. */
2449
2180
  .map--inline {
2450
2181
  --_top: 0rem;
@@ -2455,8 +2186,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2455
2186
  overflow: hidden;
2456
2187
  }
2457
2188
  .map--bleed { margin-inline: calc(-1 * var(--card-pad, 0rem)); border-radius: 0; } /* edge to edge is a band, and a band has no corner; --map-radius is for the box in the inset */
2458
- .sheet__body:has(> .map--inline:only-child) { display: flex; flex-direction: column; }
2459
- .sheet__body > .map--inline:only-child { flex: 1 0 auto; }
2189
+ /* a map that is a body's only child fills it; the body turns to a column for it (parts.css) */
2190
+ .body > .map--inline:only-child { flex: 1 0 auto; }
2460
2191
 
2461
2192
  /* ---- map pick: a place on the map the member taps to choose it (Lh 2026-10-17) ----
2462
2193
  <label class="map-pick" style="top: 30%; left: 40%"><input type="radio" name="meet" value="library">
@@ -2545,7 +2276,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2545
2276
  the form's own spacing, from the top (never spread or centred: the keyboard needs the room under it); the next steps wait in a pile at the
2546
2277
  bottom of the body, each its title line. Every step has a thin rule on top. A step opens and folds as a 1fr ↔ 0fr grid row, so the pile rises and
2547
2278
  falls — no height is written. Nothing fades: done and next read at full ink. */
2548
- .sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
2549
2279
  .sheet-steps { flex-shrink: 0; min-height: calc(100% + var(--kb, 0rem)); /* the keyboard's height more: the pile of next steps stays under the keyboard, it never rises with it */ display: flex; flex-direction: column; touch-action: pan-y; position: relative; }
2550
2280
  .sheet-steps__holder { position: absolute; top: 0; left: 0; width: var(--browser-text); height: var(--browser-text); padding: 0; border: 0; opacity: 0; pointer-events: none; font-size: var(--browser-text); } /* holds the cursor (and the phone's keyboard) while a step folds; font 1rem = the browser's own size, so iOS does not zoom to it */
2551
2281
  .sheet-steps__track { list-style: none; margin: 0; padding: 0 0 var(--sheet-steps-end, var(--sp-6)); flex: 1; /* room under the LAST step: scrolled to the end it never touches the foot's button (Lh 2026-09-28, #153) */ display: flex; flex-direction: column; }
@@ -2581,7 +2311,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2581
2311
  .sheet-step__check { margin-inline-start: auto; }
2582
2312
  /* a step's child that bleeds (ChipRow bleed, MapArea bleed, a Banner) reaches the SHEET's edges (Lh 2026-10-19, the ask sheet's
2583
2313
  จุดนัด step: the map edge to edge under the step's buttons). The fold's clip is the step body's box, which sat inside the sheet's
2584
- inset, so the body takes that inset back and pads by it again, the way .sheet__body does; the inner keeps its own box and its own
2314
+ inset, so the body takes that inset back and pads by it again, the way a sheet's .body does; the inner keeps its own box and its own
2585
2315
  inset (--sheet-step-inner-pad-x: the inset BEYOND the sheet's) and publishes the whole, the sheet's plus its own, as --card-pad,
2586
2316
  so a .bleed cancels it exactly. The sum is computed on the body, before the inner's --card-pad takes over; outside a card there is
2587
2317
  no --card-pad and the step keeps its own inset (0px, as .bleed reads it). The inner reads the knob itself, not the sum: a theme's
@@ -2598,61 +2328,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2598
2328
  /* ==== the step bar answers a press, as PRESS in actions.css answers every other control (#62: a block is written in its own file) ==== */
2599
2329
  .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
2600
2330
 
2601
- /* ==== a body that holds only an EmptyState stands it at --empty-state-at of its room, as a Step does (step.css) (#62: a block is written in its own file) ==== */
2602
- .sheet__body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
2603
- .sheet__body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
2604
- .sheet__body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
2605
2331
 
2606
- /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
2607
- /* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private.
2608
- A paper body adds its own --sp-6 on top of this (the rule below): the stack's gap under the head is the
2609
- card's colour, so without the paper's inset the first row sits on that seam. */
2610
- .sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0rem); transition: padding-top var(--motion-sheet) var(--ease); }
2611
- /* the head's band is as slim as it can be (Lh 2026-10-06: 20 over · 48 head · 20 under read as a heavy bar): the paper starts
2612
- --sp-3 under the head, not a whole --card-gap (--sp-2 at first; Lh added 4 the same day). The body is pulled up by the difference — the card's gap is left alone, the
2613
- stack's step and the body's own rows read it. */
2614
- .sheet-stack > .card.sheet--paper > .sheet__body {
2615
- margin-top: calc(var(--sp-3) - var(--card-gap));
2616
- padding-top: calc(var(--sp-6) + var(--_refresh, 0rem));
2617
- transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
2618
- }
2619
- /* a pull follows the finger at once, on paper too: the rule above is as strong as the stack's own `is-refreshing` one and comes later */
2620
- .sheet-stack > .card.sheet--paper > .sheet__body.is-refreshing { transition: none; }
2621
- /* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
2622
- cover the corners, and a paper body showed there beside the head. Clear, the card's own colour
2623
- shows in that pocket. Open, the body is paper, and the first row sits --sp-6 down, the same paper
2624
- inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
2625
- would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
2626
- into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
2627
- sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
2628
- at the moment the sheet stops. Rising, the body is paper at once. */
2629
- .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
2630
- background-color: transparent;
2631
- transition:
2632
- padding-top var(--motion-sheet) var(--ease),
2633
- background-color var(--motion-quick) var(--ease) calc(var(--stagger, 0) * var(--motion-stagger) + var(--motion-sheet) - var(--motion-quick));
2634
- }
2635
- @media (prefers-reduced-motion: reduce) { .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
2636
- transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
2637
- } }
2638
2332
  /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
2639
2333
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
2640
2334
 
2641
- /* ---- where it sits in what stack.css draws: a body pulled to refresh holds still (docs/reference.md rule 15) ---- */
2642
- .sheet-stack > .card > .sheet__body.is-refreshing { transition: none; }
2643
-
2644
2335
  /* ---- css/step.css — .step: one screen, one task — card, optional sheet, bar outside ---- */
2645
2336
  /* ============================================================
2646
2337
  cardds/step.css — .step: one screen, one task.
2647
2338
  What the member reads (words on the base — not a card, Lh 2026-09-16), what the member does when the
2648
2339
  step asks for it (a sheet, sheet.css). The move: a page of words has
2649
2340
  an .action-bar (actions.css) OUTSIDE the step, below it — the base's;
2650
- a step with a sheet has none: the sheet's own .card__foot finishes it
2341
+ a step with a sheet has none: the sheet's own .foot finishes it
2651
2342
  (see "LAYERS" below).
2652
2343
 
2653
2344
  <section class="step">
2654
2345
  <section class="page">…the words…</section> ← first
2655
- <article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
2346
+ <article class="card sheet sheet--half">…the controls… <div class="foot">…</div></article> ← last, optional
2656
2347
  </section>
2657
2348
  <div class="action-bar">…</div> ← the screen's last child, only when there is no sheet
2658
2349
 
@@ -2704,7 +2395,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2704
2395
  .screen > .step:first-child, .screen--lead > .step { --_lead: var(--screen-lead, 15cqh); } /* top="lead" says it; a Step first says it too */
2705
2396
 
2706
2397
  /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
2707
- move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
2398
+ move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .foot
2708
2399
  (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
2709
2400
  own screens are composed (the guide); the framework does not police it — a component never changes
2710
2401
  because another one is there. */
@@ -2881,7 +2572,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2881
2572
  align-self: stretch;
2882
2573
  max-block-size: calc((2 * var(--fs-caption) + var(--fs-lg)) * var(--tile-badge-line, var(--lh-badge)) + 2 * var(--sp-3));
2883
2574
  container-type: size;
2884
- inline-size: calc(var(--fs-lg) * 3.25);
2575
+ inline-size: var(--tile-badge-stretch-w, calc(var(--fs-lg) * 3.25)); /* a run of days ("18–21") needs more: the app sets the knob on that badge */
2885
2576
  grid-template-columns: auto auto;
2886
2577
  align-content: center;
2887
2578
  justify-content: center;
@@ -2890,17 +2581,21 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2890
2581
  row-gap: 0;
2891
2582
  padding-block: var(--sp-3);
2892
2583
  }
2893
- .tile-badge--stretch > * { line-height: inherit; }
2584
+ .tile-badge--stretch > * { line-height: inherit; white-space: nowrap; } /* a value never breaks at its dash */
2894
2585
  .tile-badge--stretch > :first-child { grid-column: 1 / -1; }
2895
2586
  @container (min-height: 3.5rem) { /* just under the three lines' own height, the most the badge grows to */
2896
2587
  .tile-badge--stretch > * { grid-column: 1 / -1; }
2897
2588
  }
2898
2589
 
2899
2590
  /* where it sits in a head that leads with the date (card.css): the first cell, as tall as the chips beside it however many rows, so no cap */
2900
- .card__head:has(> .tile-badge:first-child + .chip-row) > .tile-badge--stretch:first-child { grid-column: 1; grid-row: 1; max-block-size: none; }
2591
+ .head:has(> .tile-badge:first-child + .chip-row) > .tile-badge--stretch:first-child { grid-column: 1; grid-row: 1; max-block-size: none; }
2901
2592
 
2902
2593
  /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */
2903
2594
  .tile-badge--corner { position: absolute; right: var(--sp-3); bottom: var(--sp-3); }
2595
+ /* on a CardMedia (ruled Lh 2026-10-21, three screenshots side by side): the card's paper, its corner concentric with the media's — the media's
2596
+ corner less the inset above. Not the theme's highlight: the distance on a request card is information, and a highlight fill reads as a
2597
+ control. Both knobs still win (palette.css leaves this badge out of orchard's highlight rule for the same reason) */
2598
+ .card__media > .tile-badge--corner { --_badge-radius: calc(var(--r-media) - var(--sp-3)); background: var(--tile-badge-bg, var(--card-1-bg)); --dark: var(--card-1-dark); }
2904
2599
 
2905
2600
  /* ---- note row: icon + caption (transit hints, fine print) ---- */
2906
2601
  .note-row {
@@ -3020,7 +2715,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3020
2715
  /* a button may carry a WORD beside (or instead of) its icon — two named sides, ชาย · หญิง (Lh 2026-10-02): the label's look, one line */
3021
2716
  .icon-btn-group > .icon-btn { display: flex; align-items: center; justify-content: center; gap: var(--sp-2); padding-inline: var(--sp-2); font: var(--type-label); font-size: var(--fs-sm); white-space: nowrap; }
3022
2717
  .icon-btn-group > .icon-btn + .icon-btn { border-inline-start: var(--border-w) solid var(--icon-btn-group-rule, var(--icon-btn-group-border, var(--outline))); }
3023
- .card__foot > .icon-btn-group { flex: 1 1 0; }
2718
+ .foot > .icon-btn-group { flex: 1 1 0; }
3024
2719
  /* --vertical: the buttons one over the other, ONE button wide — at a row's end, where a panel across would take the words' room */
3025
2720
  .icon-btn-group--vertical { flex-direction: column; flex: none; width: var(--tap); }
3026
2721
  .icon-btn-group--vertical > .icon-btn { flex: 1 0 auto; min-height: var(--tap); }
@@ -3030,7 +2725,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3030
2725
  does not stretch it: a foot's `flex: 1 1 0` is undone for it below, as the foot undoes it for `.btn--form` (card.css). */
3031
2726
  .icon-btn-group--form { flex: 0 1 auto; justify-self: center; margin-inline: auto; } /* no align-self: a sheet's foot stands its buttons on the bottom edge by `align-items: end` */
3032
2727
  .icon-btn-group--form > .icon-btn { flex: 0 1 auto; width: auto; min-width: var(--icon-btn-group-form-min-w, var(--tap)); }
3033
- .card__foot > :is(.icon-btn-group--form, .icon-btn-line) { flex: 0 1 auto; }
2728
+ .foot > :is(.icon-btn-group--form, .icon-btn-line) { flex: 0 1 auto; }
3034
2729
  /* --labelled: the label's words, then the panel, ONE line in the middle (the words are read, not pressed: no frame, plain ink); too narrow → the words wrap
3035
2730
  above the panel, both centred */
3036
2731
  .icon-btn-line { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--icon-btn-group-gap, var(--sp-3)); min-width: 0; justify-self: center; margin-inline: auto; }
@@ -3281,7 +2976,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3281
2976
 
3282
2977
 
3283
2978
  /* ---- action note (Lh 2026-09-24): the main button's place, the main button's size, but ON the ground — no fill, no border, no
3284
- rounding — one line of muted words, an inline icon allowed; nothing to press. In a CardFoot or an ActionBar row it takes the
2979
+ rounding — one line of muted words, an inline icon allowed; nothing to press. In a Foot or an ActionBar row it takes the
3285
2980
  button's share (the whole width), so a foot or a bar keeps its height whether it holds a button or the note. ---- */
3286
2981
  .action-note {
3287
2982
  flex: 1 1 0;
@@ -3351,7 +3046,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3351
3046
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
3352
3047
  a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):not(.link):is(:disabled, [aria-disabled="true"]),
3353
3048
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
3354
- .sheet-stack--tap > .card > .card__head > :is(h1, h2, h3):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); } /* the keyboard's way into a stack sheet */
3049
+ .sheet-stack--tap > .card > .head > :is(h1, h2, h3):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); } /* the keyboard's way into a stack sheet */
3355
3050
  @media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
3356
3051
 
3357
3052
  /* ==== the chip a choice draws: picked (choice.css drives it with its input), and --sign (#62: a block is written in its own file) ==== */
@@ -3373,14 +3068,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3373
3068
 
3374
3069
  /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
3375
3070
  /* a head's ChipRow takes a line of its own under the title: at most two lines of chips, a chip's words cut with an ellipsis */
3376
- .card__head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * var(--chip-sm-line, var(--lh-body)) + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
3071
+ .head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * var(--chip-sm-line, var(--lh-body)) + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
3377
3072
  /* …beside a date that leads the head (card.css) they stand in the date's row, every chip in sight: the meeting place may take a line of its own */
3378
- .card__head:has(> .tile-badge:first-child + .chip-row) > .chip-row { grid-column: 2; grid-row: 1; max-height: none; }
3379
- .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3380
- .card__foot > .btn { flex: 1 1 0; min-width: 0; }
3073
+ .head:has(> .tile-badge:first-child + .chip-row) > .chip-row { grid-column: 2; grid-row: 1; max-height: none; }
3074
+ .head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3075
+ .foot > .btn { flex: 1 1 0; min-width: 0; }
3381
3076
  /* …unless the button says its own size: `Btn form` is as wide as its words in a foot too, centred (Lh 2026-10-10: one button filling
3382
3077
  a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
3383
- .card__foot > .btn--form { flex: 0 1 auto; }
3078
+ .foot > .btn--form { flex: 0 1 auto; }
3384
3079
  /* the page dots under the card sit centred under it — the indicator belongs
3385
3080
  with the page that changes, never in the bar (Lh, 2026-09-12) */
3386
3081
  .screen__centre > .pager__at { justify-self: center; }
@@ -3921,7 +3616,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3921
3616
  <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> */
3922
3617
  .calendar { display: grid; gap: var(--calendar-gap, var(--sp-3)); }
3923
3618
  .calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--calendar-head-gap, var(--sp-2)); }
3924
- .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; }
3619
+ .calendar__grid { display: grid; grid-template-columns: repeat(var(--_cols, 7), 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; container-type: inline-size; } /* a container: the span band below is measured in its width */
3925
3620
  .calendar__wd { font: var(--calendar-wd-font, var(--type-caption)); color: var(--calendar-wd-ink, var(--card-muted)); padding-block: var(--calendar-wd-pad-y, var(--sp-2)); }
3926
3621
  .calendar__day {
3927
3622
  width: 100%;
@@ -3942,6 +3637,31 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3942
3637
  .calendar__day:disabled { opacity: var(--fade-dim); }
3943
3638
  .calendar__day[aria-current="date"] { text-decoration: underline; text-underline-offset: 0.2em; }
3944
3639
 
3640
+ /* ---- span: a run of cells covered by one drag or two taps (Calendar, CalendarWeeks, HourGrid; src/choice/span.ts is the mechanic). The whole
3641
+ run is ONE highlight, as a finger drags over words (Lh 2026-10-21: no picked first or last day, the same colour throughout): each covered
3642
+ cell's own ::before is a slice of the band, a column and a gap wide between so the slices meet across the grid's gaps, the slice at an end
3643
+ stops at its column's edge with the pill's corner; a one-cell run is a pill the column wide. The cells keep their ink. A grid that is a
3644
+ span takes the touch (no scroll starts on it); the weeks' box keeps its vertical scroll, so there a drag covers a row and two taps cover across rows */
3645
+ .calendar--span .calendar__grid { touch-action: none; -webkit-user-select: none; user-select: none; }
3646
+ .calendar--weeks.calendar--span .calendar__grid { touch-action: pan-y; }
3647
+ .calendar__grid { --_col: calc((100cqw - (var(--_cols, 7) - 1) * var(--calendar-grid-gap, var(--sp-1))) / var(--_cols, 7)); --_span-w: calc(var(--_col) + var(--calendar-grid-gap, var(--sp-1))); }
3648
+ :is(.calendar__day--span-start, .calendar__day--span-in, .calendar__day--span-end, .calendar__day--span-one) { position: relative; isolation: isolate; }
3649
+ .calendar__day--span-start::before, .calendar__day--span-in::before, .calendar__day--span-end::before, .calendar__day--span-one::before { /* flat, not :is(): the knob walk reads each alternative's element */
3650
+ content: "";
3651
+ position: absolute;
3652
+ inset-block: 0;
3653
+ inset-inline-start: calc(50% - var(--_span-w) / 2);
3654
+ width: var(--_span-w);
3655
+ z-index: var(--z-under);
3656
+ background: var(--calendar-span-bg, color-mix(in srgb, var(--accent) 25%, transparent));
3657
+ }
3658
+ .calendar__day--span-start::before { inset-inline-start: calc(50% - var(--_col) / 2); width: calc(var(--_span-w) - var(--calendar-grid-gap, var(--sp-1)) / 2); border-start-start-radius: var(--r-chip); border-end-start-radius: var(--r-chip); }
3659
+ .calendar__day--span-end::before { width: calc(var(--_span-w) - var(--calendar-grid-gap, var(--sp-1)) / 2); border-start-end-radius: var(--r-chip); border-end-end-radius: var(--r-chip); }
3660
+ .calendar__day--span-one::before { inset-inline-start: calc(50% - var(--_col) / 2); width: var(--_col); border-radius: var(--r-chip); }
3661
+ /* ---- hour grid: the hours of a day in Calendar's cells, `--hour-grid-cols` to a row, a label over them
3662
+ <div class="calendar hour-grid"><span class="calendar__label">น.</span><div class="calendar__grid"><button class="calendar__day">9</button>…</div></div> */
3663
+ .hour-grid > .calendar__grid { --_cols: var(--hour-grid-cols, 6); }
3664
+
3945
3665
  /* ---- calendar weeks: a run of week rows that scrolls week by week, `show` rows in sight; the head sticks at the top of the box
3946
3666
  <div class="calendar calendar--weeks"><div class="calendar__weeks-head"><span class="calendar__label">June 2026</span><div class="calendar__grid">…wd×7</div></div>
3947
3667
  <div class="calendar__grid calendar__week"><button class="calendar__day">…</button>×7</div>…</div>
@@ -4161,14 +3881,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4161
3881
  /* ---- css/lists.css — row, rows, key-value, kv grid, timeline, legend, link ---- */
4162
3882
  /* ============================================================
4163
3883
  cardds/lists.css — frameless rows and pairs INSIDE a card:
4164
- row (lead · body · end) · key-value rows · kv grid ·
4165
- timeline · legend · link
3884
+ row (its parts, lead · words · end, are parts.css) · key-value
3885
+ rows · kv grid · timeline · legend · link
4166
3886
  A row inside a card has no boundary of its own; .rows draws
4167
3887
  hairlines between rows. A row that IS a card = .card--row.
4168
3888
  ============================================================ */
4169
3889
 
4170
3890
  /* ---- row: leading visual · title + meta · trailing control ----
4171
- <div class="row"><span class="avatar">A</span><div class="row__body"><span class="row__title">Alice Kau</span><span class="row__meta">Owner</span></div><button class="icon-btn icon-btn--sm">›</button></div> */
3891
+ <div class="row"><span class="avatar">A</span><span class="words"><span class="words__title">Alice Kau</span><span class="words__meta">Owner</span></span><button class="icon-btn icon-btn--sm">›</button></div> */
4172
3892
  .row {
4173
3893
  display: flex;
4174
3894
  align-items: center;
@@ -4181,21 +3901,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4181
3901
  /* no padding on a row (Lh 2026-09-16): padding is for room, not for spreading.
4182
3902
  Space BETWEEN rows is .rows' gap; a lone row as card content takes the card's
4183
3903
  gap. (.row--flush, which zeroed the old padding, is gone — every row is flush.) */
4184
- .row__lead {
4185
- width: var(--row-lead-size, var(--tap));
4186
- height: var(--row-lead-size, var(--tap));
4187
- flex: none;
4188
- border-radius: var(--row-lead-radius, var(--r-control));
4189
- border: var(--border-w) solid var(--row-lead-border, var(--outline));
4190
- display: grid;
4191
- place-items: center;
4192
- }
4193
- .row__body { flex: 1; min-width: 0; display: grid; gap: var(--row-body-gap, var(--sp-0)); }
4194
- .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) */
4195
- .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)); }
4196
- .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
4197
3904
  .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 */
4198
- .row--lg .row__title { font: var(--fw-bold) var(--fs-lg) / var(--lh-h1) var(--font-display); }
4199
3905
  /* a row that EXPANDS (Lh 2026-09-22): the row is a <details>' summary, what it shows comes out under it —
4200
3906
  who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
4201
3907
  <details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
@@ -4210,7 +3916,6 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4210
3916
  pointer-events off here, aria-disabled from the markup. The tick is the .check's checked look
4211
3917
  at --tap-sm, but not an input: nothing to press. */
4212
3918
  .row--done { pointer-events: none; cursor: default; }
4213
- .row--done .row__title { color: var(--card-muted); }
4214
3919
  .row__done {
4215
3920
  width: var(--row-done-size, var(--tap-sm));
4216
3921
  height: var(--row-done-size, var(--tap-sm));
@@ -4251,6 +3956,9 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4251
3956
  <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> */
4252
3957
  .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)); }
4253
3958
  .kv-grid > .kv-cell + .kv-cell { border-left: var(--border-w) solid var(--hairline); padding-left: var(--sp-4); }
3959
+ /* `centre` (Lh 2026-10-21, the code on timebank's request card, as `Stat centre` and `Head centre` were): the grid is as tall as its holder, its cells stand in the middle of it, each value and caption centred */
3960
+ .kv-grid--centre { min-height: 100%; align-content: center; text-align: center; } /* min-height, not height: in a plain card (a grid of auto rows) a percent height resolved 8px short and the cell overflowed it; a percent min-height is nothing there, and the holder's height on a turning card's face */
3961
+ .kv-grid--centre > .kv-cell { justify-items: center; }
4254
3962
  .kv-cell { display: grid; gap: var(--kv-cell-gap, var(--sp-0)); align-content: start; min-width: 0; }
4255
3963
  .kv-cell > .icon { --icon-margin: 0 0 var(--sp-1); }
4256
3964
  .kv-cell__val { font: var(--kv-cell-val-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
@@ -4373,6 +4081,388 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4373
4081
  /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
4374
4082
  .sheet-step__inner > .link { justify-self: center; } /* a link of its own row in a step (skip, "change number", "set PIN again"): centred under the step, as the skip (Lh 2026-09-25) */
4375
4083
 
4084
+ /* ---- css/parts.css — the parts of a line: lead, words, end — a Row's and a row card's children ---- */
4085
+ /* ============================================================
4086
+ cardds/parts.css — the parts of a LINE: lead · words · end.
4087
+ A Row (lists.css) and a row card (.card--row, card.css) are
4088
+ both made of them, as their direct children in that order.
4089
+ An outer governs a part only through the part's knobs or a
4090
+ selector naming both, written here (Lh 2026-10-21).
4091
+ <div class="row"><span class="lead"><svg class="icon"/></span><span class="words"><span class="words__title">Alice Kau</span><span class="words__meta">Owner</span></span><span class="end"><span class="t-title">+1.0 h</span><span class="t-caption t-muted">Tue</span></span></div>
4092
+ ============================================================ */
4093
+
4094
+ /* ---- lead: the outlined box at the start of a line ---- */
4095
+ .lead {
4096
+ width: var(--lead-size, var(--tap));
4097
+ height: var(--lead-size, var(--tap));
4098
+ flex: none;
4099
+ border-radius: var(--lead-radius, var(--r-control));
4100
+ border: var(--border-w) solid var(--lead-border, var(--outline));
4101
+ display: grid;
4102
+ place-items: center;
4103
+ }
4104
+ /* in a row card or a feed card's head: the small box in the card's own ink, a number or an initial in label type */
4105
+ :is(.card--row, .feed-card > .head) > .lead {
4106
+ width: var(--lead-size, var(--tap-sm));
4107
+ height: var(--lead-size, var(--tap-sm));
4108
+ border-color: var(--lead-border, currentColor);
4109
+ font: var(--lead-font, var(--type-label));
4110
+ }
4111
+ .feed-card > .head:has(> .t-caption) > .lead { grid-row: span 2; }
4112
+
4113
+ /* ---- words: the title over its meta, taking the room between the lead and the end ---- */
4114
+ .words { flex: 1; display: grid; gap: var(--words-gap, var(--sp-0)); }
4115
+ .row > .words { min-width: 0; }
4116
+ .words__title { font: var(--words-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) */
4117
+ .words__meta { font: var(--words-meta-font, var(--type-caption)); color: var(--words-meta-ink, var(--card-muted)); display: flex; align-items: center; gap: var(--words-meta-gap, var(--sp-1)); }
4118
+ .row--lg > .words > .words__title { font: var(--fw-bold) var(--fs-lg) / var(--lh-h1) var(--font-display); }
4119
+ .row--done > .words > .words__title { color: var(--card-muted); }
4120
+
4121
+ /* ---- end: the value over its caption, right-aligned at the end of a line ---- */
4122
+ .end { margin-left: auto; text-align: right; display: grid; gap: var(--end-gap, var(--sp-0)); justify-items: end; }
4123
+ .row > .end { flex: none; }
4124
+
4125
+ /* ---- head: the first row of a card, a sheet and a stack sheet. A lead, the Words, chips, an end, as its direct children.
4126
+ <div class="head"><h2 class="words__title t-h2">…</h2><span class="words__meta t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
4127
+ The Words of a head are its title and its meta, flat in the head (no .words box round them): the title and the end share the first
4128
+ row, the meta takes a row of its own under both (base.css `order: 1`). An outer governs a head through the head's knobs or a selector
4129
+ naming both, written below under the outer's name (Lh 2026-10-21). ---- */
4130
+ /* header row: title left, floating action right — centered, since the
4131
+ trailing control (chip/badge/icon-btn/link) is almost always taller than
4132
+ a single line of title text; top-aligning them left the control visually
4133
+ low against the title's optical center. A head with a genuinely tall,
4134
+ multi-line title can opt back out with .head--top. */
4135
+ .head {
4136
+ display: flex;
4137
+ flex-wrap: var(--head-wrap, wrap); /* the caption takes a line of its own — no wrapper div */
4138
+ align-items: center;
4139
+ justify-content: space-between;
4140
+ column-gap: var(--head-col-gap, var(--sp-3));
4141
+ row-gap: var(--head-row-gap, var(--sp-0));
4142
+ min-height: var(--head-min-h, auto);
4143
+ }
4144
+ .head--top { align-items: flex-start; }
4145
+
4146
+ /* ---- the card skeleton: head · content · foot ----
4147
+ Every card is a head (the heading left, hints / actions / a more-menu at
4148
+ the far right), its content, and an optional foot of action buttons.
4149
+ The head's leading child — the heading, or the div that wraps heading and
4150
+ caption — takes the room, so a heading with nothing at the far right runs
4151
+ all the way across; whatever trails it hugs its own width. Only the
4152
+ heading (or the div that wraps it) is that leading child: a control that
4153
+ leads a custom head — a chip, an avatar, a tile badge — keeps its own
4154
+ width, else the rule stretched a chip into a bar and an avatar into an
4155
+ ellipse (two agents and the wallet demo caught it, 2026-09-10). */
4156
+ .head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
4157
+
4158
+ /* Skeleton head — the heading in a card's OWN head, and only there.
4159
+ The heading wraps, no squeeze, up to three lines and cut there with an
4160
+ ellipsis (Lh 2026-10-21: "มันควรจะ max สามบรรทัด" — a request's title ran to
4161
+ four lines on a phone; reverses 2026-09-16's "no clamp"). The count is the knob
4162
+ --head-title-lines. The group that holds heading ·
4163
+ caption is the head's own wrapping row (above) — no wrapper element, no
4164
+ height, only the row gap. */
4165
+ .head :is(h1, h2, h3) {
4166
+ overflow-wrap: anywhere;
4167
+ display: -webkit-box;
4168
+ -webkit-box-orient: vertical;
4169
+ -webkit-line-clamp: var(--head-title-lines, 3);
4170
+ line-clamp: var(--head-title-lines, 3);
4171
+ overflow: hidden;
4172
+ padding-block-start: var(--_mark-room); /* a stacked Thai mark rises over the first line's box: room inside the clip, given back outside */
4173
+ margin-block-start: calc(-1 * var(--_mark-room));
4174
+ --_mark-room: 0.2em;
4175
+ }
4176
+
4177
+ .head > .words__title { font: var(--words-title-font, var(--_type, var(--type-h2))); } /* --_type: the level's own face, set by .t-h1 and .t-display (base.css) */
4178
+ .page > .head > h1.words__title { font-size: var(--fs-h2); } /* the page's ONE heading on the base (a screen with no TopBar) reads at the card heading's size — h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
4179
+ .head > .words__meta { display: block; align-items: initial; gap: initial; } /* a head's caption is text; the flex line with a Dot is a row's */
4180
+
4181
+ /* a stage sheet's head (`Head centre`): a round button · the centred title over its meta · a round button. The title and the meta
4182
+ stand in the middle column, the buttons span both rows; the 1fr rows keep the pair centred when a button is the taller */
4183
+ .head--centre {
4184
+ display: grid;
4185
+ grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
4186
+ grid-template-rows: 1fr auto auto 1fr;
4187
+ align-items: center;
4188
+ column-gap: var(--sp-3);
4189
+ row-gap: 0;
4190
+ min-height: var(--tap);
4191
+ text-align: center;
4192
+ }
4193
+ .head--centre > .words__title { grid-area: 2 / 2; }
4194
+ .head--centre > .words__meta { grid-area: 3 / 2; line-height: calc(var(--lh-body) * var(--fs-base)); } /* the meta stands on the body's line, as the sub did, an inline span in a line of the head's own type */
4195
+ .head--centre > :first-child:not(.words__title) { grid-area: 1 / 1 / -1 / 2; }
4196
+ .head--centre > :last-child:not(.words__title, .words__meta) { grid-area: 1 / 3 / -1 / 4; }
4197
+
4198
+ /* a Stamp in a card's head (a `Stamp` as the head's end): the words stand at the left, the stamp at the right BESIDE BOTH LINES — the heading and the
4199
+ caption pair up beside it, not the stamp beside the heading alone with the caption left under it. The stamp is its own size (--stamp-size,
4200
+ stamp.css); the head only places it.
4201
+ <div class="head"><h2 class="words__title t-h2">…</h2><span class="words__meta t-caption t-muted">…</span><span class="stamp">…</span></div> */
4202
+ .head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
4203
+ .head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
4204
+
4205
+ /* a head that LEADS with a date (Lh 2026-10-21, timebank's request card): a `Head` whose first child is a JourneyBadge and whose second is a
4206
+ ChipRow. The date stands at the left and the chips beside it, the two as tall as each other; the heading and its caption take the whole
4207
+ width under them, so the heading sits right over the words of the middle. The date is always as tall as the chips. The gap under the date row is --head-lead-gap; the date and the chips place themselves (journey.css, actions.css).
4208
+ <div class="head"><span class="tile-badge tile-badge--stretch">…</span><div class="chip-row">…</div><div><h2 class="words__title t-h1">…</h2>…</div></div> */
4209
+ .head:has(> .tile-badge:first-child + .chip-row) {
4210
+ display: grid;
4211
+ grid-template-columns: auto minmax(0, 1fr);
4212
+ align-items: center;
4213
+ row-gap: var(--head-lead-gap, var(--sp-3));
4214
+ }
4215
+ .head:has(> .tile-badge:first-child + .chip-row) > :nth-child(n + 3) { grid-column: 1 / -1; }
4216
+
4217
+ /* in a card with paper bands: the head and the foot are paper bands edge to edge, one look */
4218
+ :is(.card--bands, .card--bands > .card__face) > :is(.head, .foot) {
4219
+ --card-bg: var(--card-1-bg);
4220
+ --dark: var(--card-1-dark);
4221
+ background: var(--card-bg);
4222
+ color: var(--card-ink);
4223
+ margin-inline: calc(-1 * var(--card-pad));
4224
+ padding-inline: var(--card-pad);
4225
+ }
4226
+ :is(.card--bands, .card--bands > .card__face) > .head { margin-top: calc(-1 * var(--card-pad)); padding-block: var(--card-pad); }
4227
+ :is(.card--bands, .card--bands > .card__face) > .head { padding-block-end: var(--card-band-seam, var(--sp-2)); }
4228
+ /* the seam: the heading and the words under it read as ONE paragraph that changes colour at the band's edge (Lh 2026-10-21), so the room on
4229
+ each side of the head band's lower edge is --card-band-seam, not the card's padding and gap */
4230
+ :is(.card--bands, .card--bands > .card__face) > .head + * { margin-block-start: calc(var(--card-band-seam, var(--sp-2)) - var(--card-gap)); }
4231
+
4232
+ /* in a sheet stack */
4233
+ /* stack headers: uniform — [back?] title left, icons right, one height,
4234
+ same gap to content on every card */
4235
+ .sheet-stack > .card > .head {
4236
+ align-items: center;
4237
+ justify-content: flex-start;
4238
+ min-height: var(--tap);
4239
+ }
4240
+ .sheet-stack--display > .card > .head { align-items: start; min-height: 0; }
4241
+ .sheet-stack--tap > .card > .head { touch-action: none; }
4242
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .head {
4243
+ position: relative;
4244
+ padding-left: 0;
4245
+ transition: padding-left var(--motion-sheet) var(--ease);
4246
+ }
4247
+ .stack-sheet > .head { flex-wrap: nowrap; }
4248
+ .sheet-stack--tap > .card.is-open > .head:has(> .sheet-stack__back:not(.sheet-stack__back--end)) { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
4249
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) var(--ease); }
4250
+ .sheet-stack--tap > .card.is-open > .head:has(> .sheet-stack__back--end) { padding-right: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
4251
+ /* a stacked sheet is a control until it is open: its head is not text to select */
4252
+ .sheet-stack > .card > .head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
4253
+
4254
+ /* in a sheet */
4255
+ /* the head — a title row at least --tap tall, either kind, so the peek strip
4256
+ (--sheet-peek) is the same for every sheet:
4257
+ - in a step it is a plain card head (card.css): the heading left, the
4258
+ contextual thing at the far right
4259
+ - `Head centre` is the stage sheet's three-column head: a round button,
4260
+ the centred title and meta, a round button
4261
+ - a label .row with its .check / .toggle may be the head when the sheet's
4262
+ whole ask is that switch: peek shows it (a row is --tap tall plus its own
4263
+ padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
4264
+ .sheet > .head { --head-min-h: var(--sheet-head-h); }
4265
+ .sheet > .head:not(:has(> :is(.t-caption, .t-body))) { --head-wrap: nowrap; }
4266
+
4267
+ /* a sheet's title is ONE line, always (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน" wrapped to two lines at 390px) — the
4268
+ thing at its end keeps its place beside it; a caption still takes its own row. A title too long for the line RUNS sideways
4269
+ like a song title (Lh 2026-10-03, as the StackSheet's — it was cut with …, and the end of it could never be read): the Sheet
4270
+ measures --_over, the width it overflows by, and sets .is-long; one way — it waits, runs to its end, waits, and starts again
4271
+ from the first letter. The move is text-indent: the heading is the app's own element, there is no inner box to slide.
4272
+ Before the Sheet has measured, and where motion is off, it is cut with …. */
4273
+ .sheet > .head > :is(.words__title, :is(h1, h2, h3):first-child) { white-space: nowrap; overflow-x: clip; overflow-y: visible; text-overflow: ellipsis; min-width: 0; } /* clip SIDEWAYS only: hidden cut the Thai tone marks over the line */
4274
+ .sheet > .head > :is(.words__title, :is(h1, h2, h3):first-child).is-long {
4275
+ text-overflow: clip;
4276
+ animation: sheet-title-run var(--sheet-title-run-time, var(--_run, var(--motion-run))) var(--ease-in-out) infinite; /* --_run: from the overflow, 30px a second + the pauses */
4277
+ }
4278
+ @keyframes sheet-title-run { 0%, 20% { text-indent: 0; } 85%, 100% { text-indent: calc(-1 * var(--_over)); } }
4279
+ @media (prefers-reduced-motion: reduce) {
4280
+ .sheet > .head > :is(.words__title, :is(h1, h2, h3):first-child).is-long { animation: none; text-overflow: ellipsis; }
4281
+ }
4282
+
4283
+ /* in a stack sheet: the head is ONE line, always (Lh 2026-09-24) — the tabs keep their size, the TITLE gives. It never wraps, it runs sideways
4284
+ (StackSheet measures --_over, the width it overflows by, and sets .is-long; the run itself is stack.css's), one way: it waits, runs to the end,
4285
+ waits, and starts again from the first letter — like a song title. The head's line clamp (above) already cuts it, so no overflow is set here */
4286
+ .stack-sheet > .head > .words__title {
4287
+ flex: 1; min-width: 0; white-space: nowrap; overflow-wrap: normal;
4288
+ translate: 0 var(--stack-sheet-title-shift, -0.1em); /* FC Pride's line box carries more room above its letters than below: the
4289
+ letters sat ~0.1em under the middle of the back disc and the tabs — lifted onto it (Lh 2026-09-24) */
4290
+ }
4291
+ @media (prefers-reduced-motion: reduce) {
4292
+ .stack-sheet > .head > .words__title.is-long { text-overflow: ellipsis; }
4293
+ }
4294
+
4295
+ /* in a feed card */
4296
+ .feed-card > .head { display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; min-height: var(--feed-card-head-h, var(--tap)); }
4297
+ .feed-card > .head:has(> :is(.lead, .feed-card__lead)) { grid-template-columns: auto minmax(0, 1fr); } /* a Lead's own row of the head: parts.css */
4298
+
4299
+ /* ---- body: the content row, under the head and over the foot. What the member reads or fills in; in a sheet, a stack sheet
4300
+ and a modal the one part that scrolls. A grid column from the top, the card's rhythm inside.
4301
+ <div class="body">…controls, rows, small content cards…</div>
4302
+ Knobs --body-gap (the card's gap) and --body-pad-top (none; a paper sheet's inset). An outer governs a body through those
4303
+ knobs or a selector naming both, written below under the outer's name (Lh 2026-10-21). ---- */
4304
+ .body {
4305
+ display: grid;
4306
+ gap: var(--body-gap, var(--card-gap));
4307
+ align-content: start;
4308
+ min-height: 0;
4309
+ padding-top: var(--body-pad-top, 0rem);
4310
+ overflow-y: auto;
4311
+ overscroll-behavior: contain;
4312
+ }
4313
+ /* what a body holds alone: a map fills it, a SheetSteps runs its pile down it, an EmptyState stands at --empty-state-at of its room as in a Step (step.css) */
4314
+ .body:has(> .map--inline:only-child) { display: flex; flex-direction: column; }
4315
+ .body:has(> .sheet-steps) { display: flex; flex-direction: column; }
4316
+ .body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
4317
+ .body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
4318
+ .body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
4319
+
4320
+ /* in a sheet and a stack sheet */
4321
+ /* everything under the head. Its scrollport runs to the card's side and bottom edges (reading --card-pad, the way .bleed does) so
4322
+ content scrolls clear of the edge rather than stopping inside the padding, and a .bleed strip inside it still reaches the edges.
4323
+ The outers are in a :where() so the body keeps the weight of one class, as the state rules below expect. */
4324
+ :where(.sheet, .sheet-stack .card) > .body {
4325
+ flex: 1;
4326
+ margin-inline: calc(-1 * var(--card-pad));
4327
+ padding-inline: var(--card-pad);
4328
+ /* the part of the sheet that slid under the box's bottom edge cannot be
4329
+ seen, and the scroller has to know that: scroll-padding is exactly "the
4330
+ obscured part of my scrollport", so a focused field, scrollIntoView and
4331
+ anchor navigation all land a control in the visible part, never under
4332
+ the cut. A length from the box's container units (--_box), since the
4333
+ cut is a share of the SHEET's height, not of the body's. */
4334
+ scroll-padding-bottom: var(--_cover);
4335
+ /* …and its content has to be able to LEAVE that part: the same length as bottom padding, so the last row
4336
+ scrolls up to the visible edge at half and 3q as it does at full — everything between the head and the
4337
+ foot can be reached at any state (Lh, 2026-09-21). Padding, not a shorter box: nothing that shows moves
4338
+ when the state changes, only how far the body scrolls; a child that fills the body (a composer) fills
4339
+ the part that shows. */
4340
+ padding-bottom: var(--_cover);
4341
+ }
4342
+ .sheet:has(> .foot) > .body { margin-bottom: calc(var(--_foot) - var(--card-pad)); } /* the body stops above the foot's strip (the card's own padding is the rest) */
4343
+ /* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
4344
+ box is never smaller than its padding: the body runs past the sheet's clipped edge instead, where nothing
4345
+ shows anyway (the sheet's other parts keep their size, sheet.css). At peek and away no part of the body shows. */
4346
+ :is(.sheet--peek, .sheet--away) > .body { padding-bottom: 0; }
4347
+ :where(.sheet, .sheet-stack .card) > .body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
4348
+ :where(.sheet, .sheet-stack .card) > .body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover) + var(--_safe-b-open)); }
4349
+ /* paper: the head keeps the sheet's own colour, the content is the theme's paper, --action-bg, the ground of every field (Lh 2026-10-05: white; Lh 2026-10-20: the theme's, never a fixed white).
4350
+ The body and the foot read as paper; the handle and the head stay on the card.
4351
+ The flex gap under the head is outside the body, so it stays the sheet's tint.
4352
+ The first field would sit on that colour seam. The paper's inset is --sp-6 (24px at
4353
+ the default spacing), the body's own top padding, and only when the sheet is paper.
4354
+ A plain sheet keeps padding-top 0. A sheet in a stack wears the same class and the same
4355
+ inset: the stack's own gap under the head is the card's colour, so the row still needs this paper.
4356
+ The paper takes the side of the controls' paper (--action-dark): on a dark sheet its words still turn to onlight. */
4357
+ .sheet--paper > .body {
4358
+ --card-bg: var(--action-bg);
4359
+ --dark: var(--action-dark);
4360
+ background: var(--card-bg);
4361
+ color: var(--card-ink);
4362
+ padding-top: var(--body-pad-top, var(--sp-6));
4363
+ }
4364
+
4365
+ /* in a sheet stack */
4366
+ /* nothing slid under here: the stack's sheet is always whole. A pull in the content, already at the top: the words move down, the card stays.
4367
+ --_refresh is the finger, private. A paper body adds its own --sp-6 on top of this (the rule below): the stack's gap under the head is the
4368
+ card's colour, so without the paper's inset the first row sits on that seam. */
4369
+ .sheet-stack > .card > .body {
4370
+ --_cover: 0rem;
4371
+ padding-top: calc(var(--body-pad-top, 0rem) + var(--_refresh, 0rem));
4372
+ transition: padding-top var(--motion-sheet) var(--ease);
4373
+ }
4374
+ /* the head's band is as slim as it can be (Lh 2026-10-06: 20 over · 48 head · 20 under read as a heavy bar): the paper starts
4375
+ --sp-3 under the head, not a whole --card-gap (--sp-2 at first; Lh added 4 the same day). The body is pulled up by the difference — the card's gap is left alone, the
4376
+ stack's step and the body's own rows read it. */
4377
+ .sheet-stack > .card.sheet--paper > .body {
4378
+ margin-top: calc(var(--sp-3) - var(--card-gap));
4379
+ padding-top: calc(var(--body-pad-top, var(--sp-6)) + var(--_refresh, 0rem));
4380
+ transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
4381
+ }
4382
+ /* a pull follows the finger at once, on paper too: the rule above is as strong as the stack's own `is-refreshing` one and comes later */
4383
+ .sheet-stack > .card.sheet--paper > .body.is-refreshing { transition: none; }
4384
+ /* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
4385
+ cover the corners, and a paper body showed there beside the head. Clear, the card's own colour
4386
+ shows in that pocket. Open, the body is paper, and the first row sits --sp-6 down, the same paper
4387
+ inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
4388
+ would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
4389
+ into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
4390
+ sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
4391
+ at the moment the sheet stops. Rising, the body is paper at once. */
4392
+ .sheet-stack > .card.sheet--paper:not(.is-open) > .body {
4393
+ background-color: transparent;
4394
+ transition:
4395
+ padding-top var(--motion-sheet) var(--ease),
4396
+ background-color var(--motion-quick) var(--ease) calc(var(--stagger, 0) * var(--motion-stagger) + var(--motion-sheet) - var(--motion-quick));
4397
+ }
4398
+ @media (prefers-reduced-motion: reduce) { .sheet-stack > .card.sheet--paper:not(.is-open) > .body {
4399
+ transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
4400
+ } }
4401
+ /* a body pulled to refresh holds still */
4402
+ .sheet-stack > .card > .body.is-refreshing { transition: none; }
4403
+
4404
+ /* in a modal: the light part that holds the thing being asked about, padded on every side, shrinking (and scrolling) under the
4405
+ modal's max-height while the ask keeps its height (layover.css) */
4406
+ .modal > .body {
4407
+ padding: var(--body-pad-top, var(--card-pad)) var(--card-pad) var(--card-pad);
4408
+ flex: 0 1 auto;
4409
+ }
4410
+
4411
+ /* ---- foot: the last row of a card, a sheet and a stack sheet. What the member can do next: the action buttons on one line.
4412
+ One button takes the width, up to four share it in even parts — so the gaps read even too — and from three on they drop their
4413
+ side padding so the labels keep the room. <div class="foot"><button class="btn btn--primary btn--block">…</button></div>
4414
+ Knobs --foot-gap, --foot-ink, --foot-link-gap. An outer governs a foot through those knobs or a selector naming both, written
4415
+ below under the outer's name (Lh 2026-10-21). ---- */
4416
+ .foot { display: flex; gap: var(--foot-gap, var(--sp-2)); align-items: center; color: var(--foot-ink, inherit); }
4417
+ .foot:has(> :nth-child(3)) > .btn { --btn-pad-x: var(--sp-2); }
4418
+ /* a small link under the buttons (Foot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
4419
+ whole short row of its own, centred — it never shares the buttons' line */
4420
+ .foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--foot-link-gap, var(--sp-1)); }
4421
+ .foot__link { flex: 1 0 100%; display: flex; justify-content: center; }
4422
+ /* round (Foot round): the foot holds a few round IconBtns, each its own size (they never share the line), as ONE group in the middle with
4423
+ a gap that does not follow the card's width. The gap is wider than the Btns' (a miss between two round targets): --foot-gap sets it */
4424
+ .foot--round { justify-content: center; gap: var(--foot-gap, var(--sp-6)); }
4425
+
4426
+ /* in a card with paper bands: the foot band (the head's look, above) is slim */
4427
+ :is(.card--bands, .card--bands > .card__face) > .foot { margin-bottom: calc(-1 * var(--card-pad)); padding-block: var(--sp-3); }
4428
+
4429
+ /* in a sheet stack */
4430
+ /* a paper stack's foot is paper to the card's edges, and it covers the tinted flex gap above it so the
4431
+ button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
4432
+ where it was — the padding gives back what the bleed takes. Its ink reads the action's side. */
4433
+ .sheet-stack > .card.sheet--paper > .foot {
4434
+ --dark: var(--action-dark);
4435
+ background: var(--action-bg);
4436
+ color: var(--card-ink);
4437
+ margin-top: calc(-1 * var(--card-gap));
4438
+ margin-inline: calc(-1 * var(--card-pad));
4439
+ margin-bottom: calc(-1 * var(--card-pad));
4440
+ padding-top: var(--card-gap);
4441
+ padding-inline: var(--card-pad);
4442
+ padding-bottom: var(--card-pad);
4443
+ }
4444
+
4445
+ /* in a sheet: the button that FINISHES the sheet stands on the box's bottom edge at half, 3q and full alike (sheet.css says why).
4446
+ The layer is as tall as the sheet, its buttons at its bottom, slid UP by the share the sheet slid down; it takes no taps, its buttons do */
4447
+ .sheet > .foot {
4448
+ position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: var(--z-over); /* the sheet's border box: the same height the sheet's own % reads */
4449
+ align-items: end;
4450
+ align-content: end;
4451
+ padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
4452
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
4453
+ pointer-events: none;
4454
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
4455
+ transition: translate var(--motion-sheet) var(--ease), visibility 0s;
4456
+ }
4457
+ .sheet > .foot > * { pointer-events: auto; }
4458
+ .sheet.is-dragging > .foot { transition: none; }
4459
+ /* at peek and away the foot is NOT held on the box's edge: it stands on the sheet's own bottom edge (no counter-slide), so it
4460
+ LEAVES AND ARRIVES WITH ITS SHEET — solid the whole way, hidden only once the slide is over. It used to stay in place and
4461
+ fade: the body slid through the strip behind a half-transparent button (timebank, Lh 2026-10-01: "ตัวหนังสือลอดใต้ปุ่ม"). */
4462
+ :is(.sheet--peek, .sheet--away) > .foot { translate: 0 0; visibility: hidden; transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--ease-linear) var(--motion-sheet); }
4463
+ /* a paper sheet's foot is paper, as its body is: the words read the controls' side */
4464
+ .sheet--paper > .foot { --card-bg: var(--action-bg); --dark: var(--action-dark); color: var(--foot-ink, var(--card-ink)); }
4465
+
4376
4466
  /* ---- css/sticky.css — sticky note, its field, tone chooser, thread of posts, pile ---- */
4377
4467
  /* cardds — sticky notes: the paper note, the field written on it, the tone chooser, notes stuck on a page, a thread of posts, the answer note.
4378
4468
  A note is pastel paper: its tone is one of the theme's nine pastels (--pastel-*, tokens.css), its ink the onlight set, whatever card it lies on. */
@@ -5063,7 +5153,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5063
5153
  }
5064
5154
 
5065
5155
  /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
5066
- .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
5156
+ .head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
5067
5157
  /* ---- where it sits in what people.css draws (docs/reference.md rule 15) ---- */
5068
5158
  /* a stamp in the stack is the stamp's small size (--stamp-sm-size: one rule in stamp.css, 48 × 60), lapped like the heads. Measured 2026-10-11 in a Row's
5069
5159
  lead with a title, a meta line and an end button: at 375 the meta line breaks in two, accepted by Lh (a lap of 16 would hold it; the lap stays 8)
@@ -5461,17 +5551,11 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5461
5551
  position: relative;
5462
5552
  }
5463
5553
  .tile__label { font: var(--tile-label-font, var(--type-title)); }
5464
- .tile__value { font: var(--tile-value-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
5465
5554
  .tile__sub { font: var(--tile-sub-font, var(--type-caption)); color: var(--tile-sub-ink, var(--card-muted)); } /* the muted words of the tile's ground; --dim faded the full ink to 2.4–3.4:1 */
5466
5555
  .tile__text { display: grid; gap: var(--tile-text-gap, var(--sp-0)); }
5467
5556
  .tile--on { background: var(--tile-on-bg, var(--accent)); --dark: var(--accent-dark); color: var(--tile-on-ink, var(--card-ink)); border-color: transparent; }
5468
5557
  /* .tile--pick's hidden input and :has(:checked) fill are declared once in choice.css, alongside .chip--pick and .mood__opt */
5469
5558
  .tile--wide { grid-column: 1 / -1; }
5470
- /* stat: READ, not pressed — no frame of its own inside a card (a frame says "tap me"): icon · value · words on the card's ground */
5471
- /* a stat tile has no fill of its own, so it has no ink of its own: it takes the ink of what it stands on (currentColor on `color`
5472
- = the inherited colour), as the card's heading does. It kept the plain tile's --action-ink, dark on a dark card (timebank, tone 3,
5473
- Lh 2026-10-18). --tile-ink still overrides; a toned stat tile (.tile--N, below) takes its fill and its ink back */
5474
- .tile--stat { --dark: inherit; color: var(--tile-ink, currentColor); border-color: transparent; background: none; padding-inline: 0; 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; }
5475
5559
  /* upload: dashed, centred plus + label */
5476
5560
  .tile--upload {
5477
5561
  border-style: dashed;
@@ -5490,7 +5574,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5490
5574
  .tile--3 { background: var(--tile-3-bg, var(--card-3-bg)); --dark: var(--card-3-dark); color: var(--tile-3-ink, var(--card-ink)); border-color: transparent; }
5491
5575
 
5492
5576
  /* ---- wave: audio waveform stand-in for voice rows ----
5493
- <span class="wave"></span> — sits in a .row__body, fills the width */
5577
+ <span class="wave"></span> — sits in a .words, fills the width */
5494
5578
  .wave {
5495
5579
  /* only WHOLE bars show (Lh 2026-09-23 — a clipped last bar read thinner): the columns are as many whole bar-widths as fit
5496
5580
  (auto-fill), spread edge to edge; bars that do not fit go to rows of no height, clipped away */
@@ -5642,14 +5726,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5642
5726
  /* ---- the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
5643
5727
  <div class="sheet-stage sheet-stage--dim"> (dim = the page under a scrim; a Modal on a clean ground is the stage without it — Lh 2026-09-22, `--ask` retired: the stage does the ground, the modal centres itself)
5644
5728
  <article class="card modal">
5645
- <div class="modal__lift">…the card's content (a head, rows, a timeline)…</div>
5729
+ <div class="body">…the card's content (a head, rows, a timeline)…</div>
5646
5730
  <div class="modal__drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
5647
5731
  <div class="btn-trio">…</div>
5648
5732
  </div>
5649
5733
  </article>
5650
5734
  </div>
5651
5735
  .modal is the card — no border (the shadow is its edge on the scrim);
5652
- .modal__lift the light part that holds the thing being asked about (it
5736
+ .body the light part that holds the thing being asked about (it
5653
5737
  scrolls when tall); .modal__drawer the dark end that asks. The ground is
5654
5738
  whatever the stage shows (dim it or not). Two beats (2 × --motion-sheet) when
5655
5739
  the stage appears, and the same two backwards when it closes:
@@ -5681,16 +5765,6 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5681
5765
  overflow: clip;
5682
5766
  box-shadow: var(--modal-shadow, var(--shadow-sheet));
5683
5767
  }
5684
- .modal__lift {
5685
- padding: var(--card-pad);
5686
- display: grid;
5687
- gap: var(--card-gap);
5688
- align-content: start;
5689
- flex: 0 1 auto;
5690
- min-height: 0;
5691
- overflow-y: auto; /* tall content scrolls inside; the drawer stays */
5692
- overscroll-behavior: contain;
5693
- }
5694
5768
  .modal__drawer {
5695
5769
  flex-shrink: 0;
5696
5770
  height: var(--drawer-h);
@@ -5715,8 +5789,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5715
5789
  --accent: var(--card-ink);
5716
5790
  --accent-dark: calc(1 - var(--dark));
5717
5791
  }
5718
- /* the CONFIRM: a Modal with no ModalBody: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
5719
- .modal:not(:has(> .modal__lift)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
5792
+ /* the CONFIRM: a Modal with no Body: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
5793
+ .modal:not(:has(> .body)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
5720
5794
  .modal__drawer .t-caption { --t-caption-ink: var(--modal-drawer-caption-ink, var(--card-muted)); }
5721
5795
  .modal__drawer > h2:focus { outline: none; } /* the question takes the cursor when the modal arrives (for a screen reader, the keyboard's way in) — it is not a control: no ring */
5722
5796
  /* the answers as WORD buttons (a BtnRow of Btns; Lh 2026-10-11): the row fills the ask with room over it and each button keeps its words on one
@@ -6083,7 +6157,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
6083
6157
  0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
6084
6158
  }
6085
6159
  /* a peeked stack tab is the icon alone: no sheen and no shadow until its sheet is open (the fill fades with the sheet) */
6086
- [data-finish="lit"] .sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab {
6160
+ [data-finish="lit"] .sheet-stack--tap > .card:not(.is-open) > .head .stack-sheet__tab {
6087
6161
  background-image: none;
6088
6162
  box-shadow: none;
6089
6163
  }
@@ -6122,7 +6196,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
6122
6196
  }
6123
6197
  /* a dark card (the brand tone, a cover): multiply would only blacken it, so the grain lightens, fainter */
6124
6198
  [data-finish="print"] :is(.card--3, .card--cover)::after { mix-blend-mode: screen; opacity: calc(var(--print-grain, 0.32) * 0.56); }
6125
- [data-finish="print"] .card:not(.card--row, .card--cover, .feed-card--small, .sheet-stack > *, .sheet-stack > :not(.sheet-stack__base) > *) > .card__head {
6199
+ [data-finish="print"] .card:not(.card--row, .card--cover, .feed-card--small, .sheet-stack > *, .sheet-stack > :not(.sheet-stack__base) > *) > .head {
6126
6200
  padding-bottom: var(--sp-2);
6127
6201
  border-bottom: var(--rule-w) dotted color-mix(in srgb, currentColor 45%, transparent);
6128
6202
  }
@@ -6133,7 +6207,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
6133
6207
  [data-finish="print"] .chip { border-color: var(--chip-border, var(--_ink)); } /* a holder that clears a chip's edge (a note's chips) clears it in print too */
6134
6208
  [data-finish="print"] .btn--primary { box-shadow: 0.125rem 0.125rem 0 0 color-mix(in srgb, var(--card-ink) 60%, transparent); }
6135
6209
  [data-finish="print"] :is(.btn, .chip):active { box-shadow: none; translate: 0.125rem 0.125rem; }
6136
- [data-finish="print"] .card__thumb { border-style: dashed; }
6210
+ [data-finish="print"] .card--row > .lead { border-style: dashed; }
6137
6211
 
6138
6212
  /* ============================================================
6139
6213
  sticker — a die-cut sticker on a sheet: --sticker-cut of white paper left round the print, a short hard
@@ -6168,5 +6242,5 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
6168
6242
  [data-finish="ink"] :is(.btn, .chip, .icon-btn:not(.icon-btn-group > *)) { border: var(--ink-w, 0.15625rem) solid currentColor; box-shadow: 0.1875rem 0.1875rem 0 0 currentColor; }
6169
6243
  [data-finish="ink"] .btn--primary { border-color: var(--on-surface); box-shadow: 0.1875rem 0.1875rem 0 0 var(--on-surface); }
6170
6244
  [data-finish="ink"] .toggle { box-shadow: 0.125rem 0.125rem 0 0 var(--on-surface); }
6171
- [data-finish="ink"] .card__thumb { border-width: var(--ink-w, 0.15625rem); }
6245
+ [data-finish="ink"] .card--row > .lead { border-width: var(--ink-w, 0.15625rem); }
6172
6246