@usableapp/cardds 0.7.105 → 0.7.107

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 (100) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/bin/cardds.mjs +6 -2
  3. package/cardds.css +1 -0
  4. package/css/actions.css +16 -9
  5. package/css/base.css +6 -9
  6. package/css/card.css +21 -173
  7. package/css/finish.css +4 -4
  8. package/css/journey.css +4 -1
  9. package/css/layover.css +4 -14
  10. package/css/lists.css +3 -18
  11. package/css/media.css +1 -7
  12. package/css/parts.css +380 -0
  13. package/css/sheet.css +21 -140
  14. package/css/stack.css +15 -26
  15. package/css/stamp.css +1 -1
  16. package/css/step.css +3 -3
  17. package/css/theme.template.css +1 -1
  18. package/css/tokens.css +5 -4
  19. package/dist/actions/BarNote.d.ts +2 -2
  20. package/dist/actions/BarNote.js +2 -2
  21. package/dist/actions/Btn.d.ts +2 -2
  22. package/dist/actions/Btn.js +1 -1
  23. package/dist/actions/BtnRow.d.ts +1 -1
  24. package/dist/actions/BtnRow.js +1 -1
  25. package/dist/actions/Chip.d.ts +7 -3
  26. package/dist/actions/Chip.js +5 -5
  27. package/dist/actions/IconBtn.d.ts +1 -1
  28. package/dist/actions/IconBtn.js +1 -1
  29. package/dist/actions/IconBtnGroup.d.ts +1 -1
  30. package/dist/cardds.css +486 -410
  31. package/dist/cards/Card.d.ts +6 -22
  32. package/dist/cards/Card.js +11 -22
  33. package/dist/cards/CardFeed.d.ts +1 -1
  34. package/dist/cards/CardFeed.js +5 -5
  35. package/dist/cards/CardParts.d.ts +0 -12
  36. package/dist/cards/CardParts.js +0 -12
  37. package/dist/cards/cardGo.js +1 -1
  38. package/dist/cards/cardGrounds.js +2 -2
  39. package/dist/catalog.json +1 -1
  40. package/dist/forms/Field.d.ts +1 -1
  41. package/dist/forms/Field.js +1 -1
  42. package/dist/index.d.ts +6 -5
  43. package/dist/index.js +6 -5
  44. package/dist/lists/Row.d.ts +7 -22
  45. package/dist/lists/Row.js +6 -4
  46. package/dist/media/MapArea.d.ts +7 -3
  47. package/dist/media/MapArea.js +4 -3
  48. package/dist/media/Tile.d.ts +3 -7
  49. package/dist/media/Tile.js +5 -5
  50. package/dist/numbers/Spinner.d.ts +1 -1
  51. package/dist/numbers/Spinner.js +1 -1
  52. package/dist/parts/Body.d.ts +7 -0
  53. package/dist/parts/Body.js +11 -0
  54. package/dist/parts/End.d.ts +10 -0
  55. package/dist/parts/End.js +7 -0
  56. package/dist/parts/Foot.d.ts +17 -0
  57. package/dist/parts/Foot.js +11 -0
  58. package/dist/parts/Head.d.ts +15 -0
  59. package/dist/parts/Head.js +34 -0
  60. package/dist/parts/Lead.d.ts +10 -0
  61. package/dist/parts/Lead.js +9 -0
  62. package/dist/parts/Words.d.ts +13 -0
  63. package/dist/parts/Words.js +20 -0
  64. package/dist/parts/headCtx.d.ts +7 -0
  65. package/dist/parts/headCtx.js +9 -0
  66. package/dist/scaffold/EmptyState.d.ts +1 -1
  67. package/dist/scaffold/EmptyState.js +1 -1
  68. package/dist/scaffold/Screen.js +1 -1
  69. package/dist/sheets/Modal.d.ts +3 -7
  70. package/dist/sheets/Modal.js +3 -7
  71. package/dist/sheets/Sheet.d.ts +2 -2
  72. package/dist/sheets/Sheet.js +8 -9
  73. package/dist/sheets/SheetStack.d.ts +3 -3
  74. package/dist/sheets/SheetStack.js +4 -4
  75. package/dist/sheets/SheetSteps.d.ts +1 -1
  76. package/dist/sheets/SheetSteps.js +2 -2
  77. package/dist/sheets/StackBack.d.ts +1 -1
  78. package/dist/sheets/StackBack.js +1 -1
  79. package/dist/sheets/StackSheet.js +7 -7
  80. package/dist/sheets/sheetMeasure.js +3 -3
  81. package/dist/sheets/sheetStepsPile.js +2 -2
  82. package/dist/sheets/stackCard.js +6 -6
  83. package/dist/templates/CallSheet.js +6 -5
  84. package/dist/templates/Walkthrough.js +3 -2
  85. package/docs/reference.md +75 -36
  86. package/lib/catalog.mjs +2 -0
  87. package/lib/mcp.mjs +1 -1
  88. package/lint/rules.mjs +22 -8
  89. package/package.json +2 -1
  90. package/scripts/codemod-parts.mjs +324 -0
  91. package/dist/cards/CardFoot.d.ts +0 -15
  92. package/dist/cards/CardFoot.js +0 -9
  93. package/dist/cards/CardHead.d.ts +0 -21
  94. package/dist/cards/CardHead.js +0 -39
  95. package/dist/cards/Stat.d.ts +0 -19
  96. package/dist/cards/Stat.js +0 -14
  97. package/dist/sheets/SheetBody.d.ts +0 -5
  98. package/dist/sheets/SheetBody.js +0 -9
  99. package/dist/sheets/SheetHead.d.ts +0 -15
  100. 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);
@@ -333,6 +333,7 @@
333
333
  --ink-tint: color-mix(in srgb, currentColor 12%, transparent); /* hover, rails, rests */
334
334
  --ink-rail: color-mix(in srgb, currentColor 30%, transparent); /* todo steps, ghost edges */
335
335
  --accent-soft: color-mix(in srgb, var(--accent) 22%, transparent); /* calendar marks */
336
+ --accent-wash: color-mix(in srgb, var(--accent) 22%, var(--action-bg)); /* the same wash laid on the paper of fields and buttons, opaque: a soft chip reads the same on any card (Chip soft) */
336
337
  --accent-mid: color-mix(in srgb, var(--accent) 50%, transparent);
337
338
  /* PASTELS (Lh 2026-10-20: a component never brings a colour of its own; every colour is the theme's): the nine pastel papers of a
338
339
  sticky note, a note chooser's swatch and a stamp's picture ground (src/sticky/tones.ts names them), and a stamp's paper. Light
@@ -394,11 +395,11 @@
394
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,
395
396
  marks, dots: currentColor), --card-muted (secondary words), the four signals, --on-accent (the ink on the accent fill, whose
396
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
397
- 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
398
399
  tile) takes its ground's side: `--dark: inherit`. inkGrounds() measures it; docs/reference.md "Ink follows its ground" */
399
400
  :where(body, [data-palette], [data-mode], .card, .tile, .modal__drawer, .postcard__front, .postcard__back, .sticky, .post-paper,
400
- .float-bar__pill, .snackbar__body, .delta, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .map__pin, .sheet--paper > :is(.sheet__body, .card__foot),
401
- .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)) {
402
403
  --card-ink: color-mix(in srgb, var(--ink-ondark) calc(var(--dark) * 100%), var(--ink-onlight));
403
404
  --card-muted: color-mix(in srgb, var(--ink-ondark-muted) calc(var(--dark) * 100%), var(--ink-onlight-muted));
404
405
  --on-accent: color-mix(in srgb, var(--ink-ondark) calc(var(--accent-dark) * 100%), var(--ink-onlight));
@@ -701,8 +702,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
701
702
 
702
703
  /* ---- typography levels ---- */
703
704
  /* --fs-self: each level publishes its own size, for a consumer that scales a level without knowing which */
704
- .t-display { font: var(--type-display); letter-spacing: var(--track-display); margin: 0; text-wrap: balance; --fs-self: var(--fs-display); }
705
- .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); }
706
707
  .t-h2 { font: var(--type-h2); letter-spacing: var(--track-h2); margin: 0; --fs-self: var(--fs-h2); }
707
708
  .t-title { font: var(--type-title); margin: 0; --fs-self: var(--fs-title); }
708
709
  .t-body { font: var(--type-body); margin: 0; }
@@ -756,9 +757,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
756
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) */
757
758
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
758
759
  (the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
759
- /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
760
- h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
761
- .page > .card__head > .t-h1 { font-size: var(--fs-h2); }
762
760
  .page .cta-pack {
763
761
  display: flex;
764
762
  align-items: start; /* buttons keep their own height, never the row's */
@@ -851,12 +849,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
851
849
 
852
850
  /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
853
851
  /* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
854
- <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>
855
853
  The heading and the far-right thing share the first row; a caption sits on
856
854
  a row below (order 1), whatever the DOM order — so an author writes the
857
855
  words in reading order. */
858
- .card__head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
859
- .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; }
860
858
  /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
861
859
  .sheet-stack__base > .screen__centre { padding-inline: 0; }
862
860
  /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
@@ -894,7 +892,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
894
892
  the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
895
893
  .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
896
894
  .step > .empty-state { flex: none; }
897
- .sheet__body > .empty-state:only-child { flex: none; }
895
+ .body > .empty-state:only-child { flex: none; }
898
896
  /* ---- where it sits in what actions.css draws (docs/reference.md rule 15) ---- */
899
897
  /* ---- top bar: back + screen title ---- */
900
898
  .topbar .t-h1 {
@@ -967,87 +965,25 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
967
965
  .card--4 { --card-bg: var(--card-4-bg); --dark: var(--card-4-dark); }
968
966
  .card--5 { --card-bg: var(--card-5-bg); --dark: var(--card-5-dark); }
969
967
 
970
- /* header row: title left, floating action right — centered, since the
971
- trailing control (chip/badge/icon-btn/link) is almost always taller than
972
- a single line of title text; top-aligning them left the control visually
973
- low against the title's optical center. A head with a genuinely tall,
974
- multi-line title can opt back out with .card__head--top. */
975
- .card__head {
976
- display: flex;
977
- flex-wrap: var(--card-head-wrap, wrap); /* the caption takes a line of its own — no wrapper div */
978
- align-items: center;
979
- justify-content: space-between;
980
- column-gap: var(--card-head-col-gap, var(--sp-3));
981
- row-gap: var(--card-head-row-gap, var(--sp-0));
982
- min-height: var(--card-head-min-h, auto);
983
- }
984
- .card__head--top { align-items: flex-start; }
985
968
 
986
969
  .card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
987
- .card__end { text-align: right; display: grid; gap: var(--card-end-gap, var(--sp-0)); justify-items: end; } /* trailing value + caption */
988
970
  .card__media > img { width: 100%; height: auto; object-fit: cover; }
989
971
 
990
- /* ---- the card skeleton: head · content · foot ----
991
- Every card is a head (the heading left, hints / actions / a more-menu at
992
- the far right), its content, and an optional foot of action buttons.
993
- The head's leading child — the heading, or the div that wraps heading and
994
- caption — takes the room, so a heading with nothing at the far right runs
995
- all the way across; whatever trails it hugs its own width. Only the
996
- heading (or the div that wraps it) is that leading child: a control that
997
- leads a custom head — a chip, an avatar, a tile badge — keeps its own
998
- width, else the rule stretched a chip into a bar and an avatar into an
999
- ellipse (two agents and the wallet demo caught it, 2026-09-10). */
1000
- .card__head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
1001
-
1002
- /* Skeleton head — the heading in a card's OWN head, and only there.
1003
- The heading wraps, no squeeze, up to three lines and cut there with an
1004
- ellipsis (Lh 2026-10-21: "มันควรจะ max สามบรรทัด" — a request's title ran to
1005
- four lines on a phone; reverses 2026-09-16's "no clamp"). The count is the knob
1006
- --card-head-title-lines. The group that holds heading ·
1007
- caption is the head's own wrapping row (above) — no wrapper element, no
1008
- height, only the row gap. */
1009
- .card__head :is(h1, h2, h3) {
1010
- overflow-wrap: anywhere;
1011
- display: -webkit-box;
1012
- -webkit-box-orient: vertical;
1013
- -webkit-line-clamp: var(--card-head-title-lines, 3);
1014
- line-clamp: var(--card-head-title-lines, 3);
1015
- overflow: hidden;
1016
- padding-block-start: var(--_mark-room); /* a stacked Thai mark rises over the first line's box: room inside the clip, given back outside */
1017
- margin-block-start: calc(-1 * var(--_mark-room));
1018
- --_mark-room: 0.2em;
1019
- }
1020
-
1021
- /* 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
1022
- 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,
1023
- stamp.css); the head only places it.
1024
- <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><span class="stamp">…</span></div> */
1025
- .card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
1026
- .card__head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
1027
-
1028
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
1029
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,
1030
974
  so a theme that sets that knob still wins. */
1031
- .card > .card__head:first-child > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); }
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 */
1032
976
 
1033
977
  /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are paper bands edge to
1034
978
  edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
1035
- clips, so the bands keep its corners. A two-faced card (below) carries the look on each face. */
979
+ clips, so the bands keep its corners. A two-faced card (below) carries the look on each face, never on the turning box: a clip
980
+ there cut the face off at the card's edges while it turned, its near edge being nearer the eye than the box (2026-10-21). */
1036
981
  :is(.card--bands, .card--bands > .card__face) {
1037
982
  background: var(--card-middle-bg, color-mix(in srgb, var(--card-bg) 35%, var(--card-1-bg)));
1038
983
  --dark: var(--card-1-dark); /* the middle is mostly paper: its words take the paper's side */
1039
984
  overflow: hidden;
1040
985
  }
1041
- :is(.card--bands, .card--bands > .card__face) > :is(.card__head, .card__foot) {
1042
- --card-bg: var(--card-1-bg);
1043
- --dark: var(--card-1-dark);
1044
- background: var(--card-bg);
1045
- color: var(--card-ink);
1046
- margin-inline: calc(-1 * var(--card-pad));
1047
- padding-inline: var(--card-pad);
1048
- }
1049
- :is(.card--bands, .card--bands > .card__face) > .card__head { margin-top: calc(-1 * var(--card-pad)); padding-block: var(--card-pad); }
1050
- :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 */
986
+ .card--bands.card--turns { background: none; overflow: visible; } /* the turning box keeps --dark: the faces' ink is computed from it */
1051
987
 
1052
988
  /* ---- two faces: the WHOLE card turns (`Card back`, Lh 2026-10-19). The article is the turning box (.flip, base.css) and only that;
1053
989
  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
@@ -1069,10 +1005,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1069
1005
  position: relative;
1070
1006
  }
1071
1007
  .card__face > * { min-width: 0; }
1072
- .card__middle { flex: 1 0 auto; display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, auto); overflow: hidden; }
1008
+ .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) */
1073
1009
  .card__turn { position: absolute; inset: 0; width: var(--px); height: var(--px); padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
1074
1010
  .card__face:has(> .card__turn:focus-visible) { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
1075
- .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; }
1011
+ .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; }
1076
1012
 
1077
1013
  /* ---- bleed: a scrolling track that runs to the container's edges ----
1078
1014
  For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
@@ -1093,7 +1029,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1093
1029
  nothing jumps. Chromium/Safari paint it through the ::-webkit-scrollbar
1094
1030
  parts; Firefox gets its thin bar in the same ink. */
1095
1031
  .bleed {
1096
- margin-inline: calc(-1 * var(--card-pad, 0rem));
1032
+ margin-inline: calc(-1 * var(--card-pad, 0rem)); /* the edge a bleeding child takes back from what holds it; .map--bleed (sheet.css) reads the same --card-pad */
1097
1033
  padding-inline: var(--card-pad, 0rem);
1098
1034
  padding-block-end: var(--sp-2);
1099
1035
  scroll-padding-inline: var(--card-pad, 0rem);
@@ -1110,20 +1046,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1110
1046
  }
1111
1047
  /* a bleeding child sits over the container's rounded corner, so a strip that
1112
1048
  is genuinely first or last needs the corner clipped for it */
1113
- .card:has(> .bleed:first-child), .card:has(> .bleed:last-child) { overflow: clip; }
1114
-
1115
- /* foot: the card's action buttons on one line. One button takes the width,
1116
- up to four share it in even parts — so the gaps read even too — and from
1117
- three on they drop their side padding so the labels keep the room. */
1118
- .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; color: var(--card-foot-ink, inherit); }
1119
- .card__foot:has(> :nth-child(3)) > .btn { --btn-pad-x: var(--sp-2); }
1120
- /* a small link under the buttons (CardFoot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
1121
- whole short row of its own, centred — it never shares the buttons' line */
1122
- .card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
1123
- .card__foot-link { flex: 1 0 100%; display: flex; justify-content: center; }
1124
- /* 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
1125
- 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 */
1126
- .card__foot--round { justify-content: center; gap: var(--card-foot-gap, var(--sp-6)); }
1049
+ .card:has(> :is(.bleed, .map--bleed):first-child), .card:has(> :is(.bleed, .map--bleed):last-child) { overflow: clip; }
1127
1050
 
1128
1051
  /* media placeholder for wireframes — currentColor so it survives any palette */
1129
1052
  .ph {
@@ -1241,7 +1164,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1241
1164
  align-items: center;
1242
1165
  gap: var(--card-row-gap, var(--sp-3));
1243
1166
  }
1244
- .card--row .card__body { flex: 1; display: grid; gap: var(--sp-0); }
1245
1167
 
1246
1168
  /* ---- 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
1247
1169
  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
@@ -1258,7 +1180,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1258
1180
  }
1259
1181
 
1260
1182
  /* ---- the cross that takes a row off the list (`Card row onRemove`): small, at the row's top right corner, inside it ----
1261
- <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>
1183
+ <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>
1262
1184
  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
1263
1185
  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
1264
1186
  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. */
@@ -1286,21 +1208,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1286
1208
  .card__remove > .icon { --icon-size: 62%; }
1287
1209
  .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
1288
1210
 
1289
- /* leading thumb/icon in a row */
1290
- .card__thumb {
1291
- width: var(--card-thumb-size, var(--tap-sm)); height: var(--card-thumb-size, var(--tap-sm));
1292
- flex: none;
1293
- border-radius: var(--card-thumb-radius, var(--r-control));
1294
- border: var(--border-w) solid currentColor;
1295
- display: grid;
1296
- place-items: center;
1297
- font: var(--card-thumb-font, var(--type-label));
1298
- }
1299
-
1300
1211
  /* ---- card feed: a queue of cards, all in sight — one big, the rest small (Lh 2026-10-09, the whole queue 2026-10-15) ----
1301
1212
  <div class="card-feed">
1302
- <article class="card feed-card"><div class="card__head feed-card__head">thumb or lead · h2 · caption</div>
1303
- <div class="feed-card__more"><div class="feed-card__rest">content · <div class="card__foot">…</div></div></div></article>
1213
+ <article class="card feed-card"><div class="head">lead · h2 · caption</div>
1214
+ <div class="feed-card__more"><div class="feed-card__rest">content · <div class="foot">…</div></div></div></article>
1304
1215
  <article class="card feed-card feed-card--small" role="button" tabindex="0">… <div class="feed-card__more" inert>…</div></article>
1305
1216
  <p class="t-caption card-feed__end" role="note">the end</p>
1306
1217
  </div>
@@ -1311,9 +1222,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1311
1222
  shut content in the scrollable overflow of whatever holds the feed — and hidden once the row has shut. */
1312
1223
  .card-feed { display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; gap: var(--card-feed-gap, var(--card-gap-rest)); }
1313
1224
  .feed-card { row-gap: 0; }
1314
- .feed-card__head { display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; min-height: var(--feed-card-head-h, var(--tap)); }
1315
- .feed-card__head:has(> :is(.card__thumb, .feed-card__lead)) { grid-template-columns: auto minmax(0, 1fr); }
1316
- .feed-card__head:has(> .t-caption) > :is(.card__thumb, .feed-card__lead) { grid-row: span 2; }
1225
+ .feed-card > .head:has(> .t-caption) > .feed-card__lead { grid-row: span 2; }
1317
1226
  .feed-card__lead { display: grid; align-self: center; } /* a Stamp here is the small one, 48 × 60 (stamp.css) */
1318
1227
  .card-feed__end { text-align: center; }
1319
1228
  .feed-card__more { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: 1fr; }
@@ -1330,22 +1239,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1330
1239
  .feed-card--small .feed-card__rest { transition: visibility 0s var(--motion-sheet); }
1331
1240
  }
1332
1241
 
1333
- /* ---- inline stat block (revenue-style) ---- */
1334
- .stat { display: grid; gap: var(--stat-gap, var(--sp-1)); }
1335
- .stat--centre { text-align: center; justify-items: center; } /* the one number of a sheet or a stage, under a centred avatar */
1336
- .stat__row { display: flex; align-items: center; justify-content: space-between; gap: var(--stat-row-gap, var(--sp-3)); }
1337
-
1338
- .delta {
1339
- font: var(--delta-font, var(--type-caption));
1340
- font-weight: var(--fw-bold);
1341
- padding: var(--delta-pad-y, var(--sp-1)) var(--delta-pad-x, var(--sp-2));
1342
- border-radius: var(--delta-radius, var(--r-chip));
1343
- background: var(--delta-bg, var(--card-3-bg));
1344
- --dark: var(--card-3-dark);
1345
- color: var(--delta-ink, var(--card-ink));
1346
- }
1347
- .delta--neg { background: var(--delta-neg-bg, var(--neg)); --dark: var(--neg-dark); color: var(--delta-neg-ink, var(--card-ink)); }
1348
-
1349
1242
  /* ---- panes: a few panes side by side inside a card or a sheet, one in sight (Lh 2026-10-02) ----
1350
1243
  <div class="panes" role="group"><div class="panes__rail"><div class="panes__pane">…</div>…</div><span class="pager__at">…</span></div>
1351
1244
  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
@@ -1369,7 +1262,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1369
1262
 
1370
1263
  /* ---- cover: the image IS the card; text over a scrim ----
1371
1264
  <article class="card card--cover"><div class="ph" role="img"></div>
1372
- <div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
1265
+ <div class="head"><button class="icon-btn icon-btn--sm">♥</button></div>
1373
1266
  <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
1374
1267
  <div class="row">…meta…</div></article> */
1375
1268
  .card--cover {
@@ -1459,14 +1352,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1459
1352
  .sheet-stack > .card:nth-child(3) { margin-top: calc(2 * var(--stack-step)); }
1460
1353
  .sheet-stack > .card:nth-child(4) { margin-top: calc(3 * var(--stack-step)); }
1461
1354
  .sheet-stack > .card:nth-child(5) { margin-top: calc(4 * var(--stack-step)); }
1462
- /* stack headers: uniform — [back?] title left, icons right, one height,
1463
- same gap to content on every card */
1464
- .sheet-stack > .card > .card__head {
1465
- align-items: center;
1466
- justify-content: flex-start;
1467
- min-height: var(--tap);
1468
- }
1469
- .sheet-stack--display > .card > .card__head { align-items: start; min-height: 0; }
1470
1355
  /* ============================================================
1471
1356
  .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
1472
1357
 
@@ -1519,8 +1404,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1519
1404
  overscroll-behavior: contain;
1520
1405
  }
1521
1406
  .sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
1522
- .sheet-stack--tap > .card > .card__head { touch-action: none; }
1523
- .sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
1407
+ .sheet-stack--tap > .card.is-open:not(:has(> .body)) {
1524
1408
  padding-top: calc(var(--card-pad) + var(--_refresh, 0rem));
1525
1409
  transition: translate var(--motion-sheet) var(--ease), padding-top var(--motion-sheet) var(--ease);
1526
1410
  }
@@ -1556,56 +1440,18 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1556
1440
  only, the sheet's size is untouched. A pull on the head keeps them square until the finger lets go. */
1557
1441
  .sheet-stack--flood > .card { transition: translate var(--motion-sheet) var(--ease), border-radius var(--motion-sheet) var(--ease); }
1558
1442
  .sheet-stack--flood > .card.is-open { border-top-left-radius: 0; border-top-right-radius: 0; }
1559
- :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head {
1560
- position: relative;
1561
- padding-left: 0;
1562
- transition: padding-left var(--motion-sheet) var(--ease);
1563
- }
1564
- /* ---- a stacked sheet with a SheetBody has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
1565
- pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a CardFoot, the CTA) at the bottom.
1566
- The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no SheetBody
1443
+ /* ---- a stacked sheet with a Body has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
1444
+ pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a Foot, the CTA) at the bottom.
1445
+ The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no Body
1567
1446
  keeps the old way: the whole open card scrolls. ---- */
1568
- .sheet-stack--tap > .card:has(> .sheet__body) { display: flex; flex-direction: column; gap: var(--card-gap); }
1569
- .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
1570
- /* a paper stack's foot is paper to the card's edges, and it covers the tinted flex gap above it so the
1571
- button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
1572
- where it was — the padding gives back what the bleed takes. */
1573
- .sheet-stack > .card.sheet--paper > .card__foot {
1574
- background: var(--action-bg);
1575
- color: var(--card-ink);
1576
- margin-top: calc(-1 * var(--card-gap));
1577
- margin-inline: calc(-1 * var(--card-pad));
1578
- margin-bottom: calc(-1 * var(--card-pad));
1579
- padding-top: var(--card-gap);
1580
- padding-inline: var(--card-pad);
1581
- padding-bottom: var(--card-pad);
1582
- }
1583
- .stack-sheet > .card__head { flex-wrap: nowrap; }
1447
+ .sheet-stack--tap > .card:has(> .body) { display: flex; flex-direction: column; gap: var(--card-gap); }
1448
+ .sheet-stack--tap > .card:has(> .body).is-open { overflow: clip; }
1584
1449
  /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
1585
1450
  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
1586
1451
  (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
1587
- .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))); }
1588
- :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); }
1589
- .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))); }
1590
1452
  /* 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),
1591
1453
  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) */
1592
- .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
1593
- /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
1594
- .sheet > .card__foot {
1595
- 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 */
1596
- align-items: end;
1597
- align-content: end;
1598
- padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
1599
- background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
1600
- pointer-events: none;
1601
- translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
1602
- transition: translate var(--motion-sheet) var(--ease), visibility 0s;
1603
- }
1604
- .sheet.is-dragging > .card__foot { transition: none; }
1605
- /* 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
1606
- LEAVES AND ARRIVES WITH ITS SHEET — solid the whole way, hidden only once the slide is over. It used to stay in place and
1607
- fade: the body slid through the strip behind a half-transparent button (timebank, Lh 2026-10-01: "ตัวหนังสือลอดใต้ปุ่ม"). */
1608
- :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); }
1454
+ .sheet-stack--tap > .card { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
1609
1455
  /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
1610
1456
  /* 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
1611
1457
  (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
@@ -1634,7 +1480,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1634
1480
  .postcard__front .postcard__turn > .ph { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border: 0; border-radius: 0; min-height: 0; }
1635
1481
 
1636
1482
  /* ---- where it sits in what stack.css draws: a sheet pulled to refresh holds still (docs/reference.md rule 15) ---- */
1637
- .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
1483
+ .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .body)) { transition: none; }
1638
1484
 
1639
1485
  /* ---- where it sits in what step.css draws: the reading block, a walk of pages (docs/reference.md rule 15) ---- */
1640
1486
  .step:has(> .sheet--peek) > .pages { padding-bottom: var(--sheet-peek); }
@@ -1666,7 +1512,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1666
1512
 
1667
1513
  <div class="sheet-stack sheet-stack--tap"> (drop --tap for a static stack)
1668
1514
  <article class="card card--2">
1669
- <div class="card__head">
1515
+ <div class="head">
1670
1516
  <button class="icon-btn icon-btn--sm sheet-stack__back" aria-label="Back">…</button>
1671
1517
  <h2 class="t-h2 t-caps">Shopping</h2>
1672
1518
  <span class="icon-row">…</span> (optional trailing icons)
@@ -1721,7 +1567,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1721
1567
 
1722
1568
  /* NOT a skeleton head: a peek is one line at its own size, and the display
1723
1569
  stack deliberately runs a second line under the next card. */
1724
- .sheet-stack > .card > .card__head :is(h1, h2, h3) {
1570
+ .sheet-stack > .card > .head :is(h1, h2, h3) {
1725
1571
  font-size: var(--fs-h1);
1726
1572
  line-height: var(--lh-solid);
1727
1573
  letter-spacing: var(--track-h2);
@@ -1733,7 +1579,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1733
1579
  /* display stack: the giant title IS the peek — line one shows, line two is
1734
1580
  clipped by the next card (recents / library lists) */
1735
1581
  .sheet-stack--display { --stack-step: calc(var(--tap-base) * 13 / 6); }
1736
- .sheet-stack--display > .card > .card__head :is(h1, h2, h3) {
1582
+ .sheet-stack--display > .card > .head :is(h1, h2, h3) {
1737
1583
  font: var(--type-display);
1738
1584
  letter-spacing: var(--track-display);
1739
1585
  /* the FIRST line sits on the centre of the peek strip (--stack-step tall,
@@ -1747,7 +1593,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1747
1593
  NOT: there, every card is full height and full content always, and a
1748
1594
  peek is simply the strip of it the next card doesn't cover — the way a
1749
1595
  card in a real deck is never a shorter card, just a mostly hidden one. */
1750
- .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.card__head) { display: none; }
1596
+ .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.head) { display: none; }
1751
1597
 
1752
1598
  /* ---- .sheet-stack--tap — tap a peeked sheet open; a back button returns it. How its sheets move (one property: translate) is
1753
1599
  told over their rules, where they sit, in card.css. ---- */
@@ -1895,7 +1741,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1895
1741
  slide read as a second thing moving while the sheet itself moved; Lh,
1896
1742
  2026-09-11). Only the sheet travels; the button appears where it is. */
1897
1743
  /* 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 */
1898
- :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head > .sheet-stack__back {
1744
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .head > .sheet-stack__back {
1899
1745
  position: absolute;
1900
1746
  left: 0;
1901
1747
  top: 50%;
@@ -1910,35 +1756,24 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1910
1756
  min-height: 0;
1911
1757
  padding: 0;
1912
1758
  }
1913
- .sheet-stack--tap > .card.is-open > .card__head > .sheet-stack__back {
1759
+ .sheet-stack--tap > .card.is-open > .head > .sheet-stack__back {
1914
1760
  opacity: 1;
1915
1761
  visibility: visible;
1916
1762
  }
1917
1763
 
1918
- .sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
1919
- .sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
1920
- /* a paper stack's foot is paper (card.css paints it): its ink reads the action's side */
1921
- .sheet-stack > .card.sheet--paper > .card__foot { --dark: var(--action-dark); }
1764
+ .sheet-stack > .card:has(> .body) > :not(.body) { flex-shrink: 0; }
1922
1765
 
1923
1766
  /* ---- 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
1924
1767
  filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
1925
1768
  .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) */
1926
- /* the head is ONE line, always (Lh 2026-09-24): the tabs keep their size, the TITLE gives — it never wraps, it runs sideways
1927
- (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 */
1928
- .stack-sheet__title {
1929
- flex: 1; min-width: 0; overflow-x: clip; overflow-y: visible; white-space: nowrap; overflow-wrap: normal; /* clip SIDEWAYS only:
1930
- hidden clipped the Thai tone marks above a line-height-1 line (เพื่อน lost its ไม้เอก — Lh 2026-09-24) */
1931
- translate: 0 var(--stack-sheet-title-shift, -0.1em); /* FC Pride's line box carries more room above its letters than below: the
1932
- letters sat ~0.1em under the middle of the back disc and the tabs — lifted onto it (Lh 2026-09-24) */
1933
- }
1769
+ /* the title's run (the title's own rules are parts.css's, in a stack sheet) */
1934
1770
  .stack-sheet__run { display: inline-block; }
1935
- .stack-sheet__title.is-long .stack-sheet__run {
1771
+ .stack-sheet > .head > .words__title.is-long > .stack-sheet__run {
1936
1772
  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 */
1937
1773
  }
1938
1774
  @keyframes stack-sheet-run { 0%, 20% { translate: 0; } 85%, 100% { translate: calc(-1 * var(--_over)); } }
1939
1775
  @media (prefers-reduced-motion: reduce) {
1940
- .stack-sheet__title.is-long .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
1941
- .stack-sheet__title.is-long { text-overflow: ellipsis; }
1776
+ .stack-sheet > .head > .words__title.is-long > .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
1942
1777
  .stack-sheet__tab { transition: none; } /* the disc is simply there while open, and gone while closed */
1943
1778
  }
1944
1779
  .stack-sheet__tab {
@@ -1972,23 +1807,23 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1972
1807
  /* a CLOSED sheet shows no chosen disc either (Lh 2026-10-04): arriving on the home, every tab of every sheet is the icon
1973
1808
  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
1974
1809
  sheet folds. The class stays (which page it is); only the paint waits for .is-open. */
1975
- .sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab.icon-btn--invert {
1810
+ .sheet-stack--tap > .card:not(.is-open) > .head .stack-sheet__tab.icon-btn--invert {
1976
1811
  border-color: transparent; background-color: transparent; box-shadow: none; color: inherit;
1977
1812
  }
1978
1813
  /* 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
1979
1814
  margin around it reaches into the card's padding — and an open head (back · title · three tabs) keeps one line (Lh 2026-09-23) */
1980
- .card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
1815
+ .head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
1981
1816
 
1982
1817
  /* 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
1983
1818
  while open; the last tab then sits on the inset (no reach into the padding) */
1984
1819
  .sheet-stack--tap .sheet-stack__back--end { left: auto; right: 0; }
1985
- .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
1820
+ .sheet-stack--tap > .card.is-open > .head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
1986
1821
  .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) */
1987
1822
  .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--_back-size) - var(--tap-sm)) / 2); }
1988
1823
 
1989
1824
  /* the open sheet's title folds it (SheetStack.tsx): it says so to a pointer */
1990
- .sheet-stack--tap > .card.is-open > .card__head > :is(h1, h2, h3) { cursor: pointer; }
1991
- .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) */
1825
+ .sheet-stack--tap > .card.is-open > .head > :is(h1, h2, h3) { cursor: pointer; }
1826
+ .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) */
1992
1827
 
1993
1828
  /* ---- where it sits in what base.css draws (docs/reference.md rule 15) ---- */
1994
1829
  /* min-height: 0 — a flex item's floor is its content, and the stack's content
@@ -2035,8 +1870,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2035
1870
  the listed states, and the state it lands on becomes the one a tap
2036
1871
  returns to.
2037
1872
  <button class="sheet__handle" type="button" aria-label="…"></button>
2038
- <div class="card__head"><h2 class="t-h2">…</h2>…</div>
2039
- <div class="sheet__body">…controls, or small content cards…</div>
1873
+ <div class="head"><h2 class="words__title t-h2">…</h2>…</div>
1874
+ <div class="body">…controls, or small content cards…</div>
2040
1875
  </article>
2041
1876
  Written top-down: the handle, the title row (what peek shows), then
2042
1877
  the body. The handle and the head stay where they are at every
@@ -2108,38 +1943,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2108
1943
  .sheet--away { --_y: 100%; --_cover: var(--_box); visibility: hidden; transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--ease-linear) var(--motion-sheet); }
2109
1944
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
2110
1945
 
2111
- /* the body — everything under the head, the one thing in a sheet that
2112
- scrolls. Its scrollport runs to the card's side and bottom edges (reading
2113
- --card-pad, the way .bleed does) so content scrolls clear of the edge
2114
- rather than stopping inside the padding, and a .bleed strip inside it
2115
- still reaches the edges. The card's own grid rhythm continues inside. */
2116
- .sheet__body {
2117
- flex: 1;
2118
- min-height: 0;
2119
- display: grid;
2120
- gap: var(--card-gap);
2121
- align-content: start;
2122
- margin-inline: calc(-1 * var(--card-pad));
2123
- padding-inline: var(--card-pad);
2124
- overflow-y: auto;
2125
- overscroll-behavior: contain;
2126
- /* the part of the sheet that slid under the box's bottom edge cannot be
2127
- seen, and the scroller has to know that: scroll-padding is exactly "the
2128
- obscured part of my scrollport", so a focused field, scrollIntoView and
2129
- anchor navigation all land a control in the visible part, never under
2130
- the cut. A length from the box's container units (--_box), since the
2131
- cut is a share of the SHEET's height, not of the body's. */
2132
- scroll-padding-bottom: var(--_cover);
2133
- /* …and its content has to be able to LEAVE that part: the same length as bottom padding, so the last row
2134
- scrolls up to the visible edge at half and 3q as it does at full — everything between the head and the
2135
- foot can be reached at any state (Lh, 2026-09-21). Padding, not a shorter box: nothing that shows moves
2136
- when the state changes, only how far the body scrolls; a child that fills the body (a composer) fills
2137
- the part that shows. */
2138
- padding-bottom: var(--_cover);
2139
- }
2140
1946
  /* ---- the sheet's foot: the button that FINISHES the sheet, on the sheet (Lh, 2026-09-20) ----
2141
1947
  A screen with a sheet has no bar in front of it: the button that completes what the sheet asks — the
2142
- page's conclusion — is a .card__foot, the sheet's last child, on the sheet's own material. The sheet is
1948
+ page's conclusion — is a .foot, the sheet's last child, on the sheet's own material. The sheet is
2143
1949
  full height at every state, so its real bottom edge is under the cut unless it stands at full. So the
2144
1950
  foot is a layer as tall as the sheet, its buttons at its bottom, slid UP by the very share the sheet
2145
1951
  slid down (--_y, + the drag): the two cancel, and the buttons stand on the box's bottom edge at half,
@@ -2147,18 +1953,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2147
1953
  length — so a box that resizes (the keyboard) moves them as one, with nothing to catch up. The layer
2148
1954
  takes no taps, its buttons do; the strip behind them is the sheet's ground, --_foot tall, and the body
2149
1955
  stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
2150
- One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
1956
+ One row, ≤ 2 buttons (parts.css). <div class="foot"><button class="btn btn--primary btn--block">…</button></div> */
2151
1957
  /* …and the link row under the button, when there is one (its line + the gap over it) */
2152
- .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) */
2153
- .sheet > .card__foot > * { pointer-events: auto; }
2154
- .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) */
2155
- /* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
2156
- box is never smaller than its padding — so the pinned parts must not give way to it: the body runs past
2157
- the sheet's clipped edge instead, where nothing shows anyway. At peek and away no part of the body shows. */
2158
- .sheet > :not(.sheet__body) { flex-shrink: 0; }
2159
- :is(.sheet--peek, .sheet--away) > .sheet__body { padding-bottom: 0; }
2160
- .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
2161
- .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover) + var(--_safe-b-open)); }
1958
+ .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) */
1959
+ /* the body's padding can be taller than its own box, so the pinned parts must not give way to it (parts.css) */
1960
+ .sheet > :not(.body) { flex-shrink: 0; }
2162
1961
 
2163
1962
  /* grab handle — first child of the sheet, but it does not take a row: it sits in the
2164
1963
  card's top padding, the same air as a stack sheet's four dots (stack.css --_grip-y),
@@ -2184,22 +1983,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2184
1983
  touch-action: none;
2185
1984
  cursor: grab;
2186
1985
  }
2187
- /* 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).
2188
- The body and the foot read as paper; the handle and the head stay on the card.
2189
- The flex gap under the head is outside the body, so it stays the sheet's tint.
2190
- The first field would sit on that colour seam. The paper's inset is --sp-6 (24px at
2191
- the default spacing), the body's own top padding, and only when the sheet is paper.
2192
- A plain sheet keeps padding-top 0. A sheet in a stack wears the same class and the same
2193
- inset: the stack's own gap under the head is the card's colour, so the row still needs this paper.
2194
- The paper takes the side of the controls' paper (--action-dark): on a dark sheet its words still turn to onlight. */
2195
- .sheet--paper > .sheet__body {
2196
- --card-bg: var(--action-bg);
2197
- --dark: var(--action-dark);
2198
- background: var(--card-bg);
2199
- color: var(--card-ink);
2200
- padding-top: var(--sp-6);
2201
- }
2202
- .sheet--paper > .card__foot { --card-bg: var(--action-bg); --dark: var(--action-dark); --card-foot-ink: var(--card-ink); }
2203
1986
  .sheet__handle--dots {
2204
1987
  --_dot: var(--sp-1);
2205
1988
  --_step: calc(var(--_dot) * 2.5);
@@ -2239,42 +2022,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2239
2022
  .sheet__handle:focus-visible { outline: var(--focus); outline-offset: var(--sp-2); }
2240
2023
  .sheet.is-dragging .sheet__handle { cursor: grabbing; }
2241
2024
 
2242
- /* the head — a title row at least --tap tall, either kind, so the peek strip
2243
- (--sheet-peek) is the same for every sheet:
2244
- - in a step it is a plain card head (card.css): the heading left, the
2245
- contextual thing at the far right
2246
- - .sheet__head is the stage sheet's three-column head: a round button,
2247
- the centred title and sub, a round button
2248
- - a label .row with its .check / .toggle may be the head when the sheet's
2249
- whole ask is that switch: peek shows it (a row is --tap tall plus its own
2250
- padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
2251
- .sheet > .card__head { --card-head-min-h: var(--sheet-head-h); }
2252
- /* a sheet's title is ONE line, always (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน" wrapped to two lines at 390px) — the
2253
- 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
2254
- 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
2255
- measures --_over, the width it overflows by, and sets .is-long; one way — it waits, runs to its end, waits, and starts again
2256
- from the first letter. The move is text-indent: the heading is the app's own element, there is no inner box to slide.
2257
- Before the Sheet has measured, and where motion is off, it is cut with …. */
2258
- .sheet > .card__head:not(:has(> :is(.t-caption, .t-body))) { --card-head-wrap: nowrap; }
2259
- .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 */
2260
- .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long {
2261
- text-overflow: clip;
2262
- 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 */
2263
- }
2264
- @keyframes sheet-title-run { 0%, 20% { text-indent: 0; } 85%, 100% { text-indent: calc(-1 * var(--_over)); } }
2265
- @media (prefers-reduced-motion: reduce) {
2266
- .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long { animation: none; text-overflow: ellipsis; }
2267
- }
2268
- .sheet__head {
2269
- display: grid;
2270
- grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
2271
- align-items: center;
2272
- gap: var(--sp-3);
2273
- min-height: var(--tap);
2274
- text-align: center;
2275
- }
2276
- .sheet__title { font: var(--sheet-title-font, var(--type-h2)); letter-spacing: var(--track-h2); margin: 0; }
2277
- .sheet__sub { font: var(--sheet-sub-font, var(--type-caption)); color: var(--sheet-sub-ink, var(--card-muted)); }
2278
2025
 
2279
2026
  /* ============================================================
2280
2027
  .sheet-stage — the overlay stage: a page-height box the sheet
@@ -2421,11 +2168,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2421
2168
  }
2422
2169
 
2423
2170
  /* ---- the map IN FLOW (Lh 2026-10-17): `.map--inline` — a box in a card, or the whole of a sheet's body ----
2424
- <div class="sheet__body"><div class="map map--inline"><div>…the engine…</div><label class="map-pick" …>…</label>…
2171
+ <div class="body"><div class="map map--inline"><div>…the engine…</div><label class="map-pick" …>…</label>…
2425
2172
  <span class="map__ring" …></span></div></div>
2426
2173
  Choosing where to meet without leaving the sheet: the open sheet becomes the map and its head stays on top (a stage
2427
2174
  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
2428
- 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
2175
+ 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
2429
2176
  fills the body (the part above the foot), however far the sheet slid. */
2430
2177
  .map--inline {
2431
2178
  --_top: 0rem;
@@ -2435,8 +2182,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2435
2182
  border-radius: var(--map-radius, var(--r-media));
2436
2183
  overflow: hidden;
2437
2184
  }
2438
- .sheet__body:has(> .map--inline:only-child) { display: flex; flex-direction: column; }
2439
- .sheet__body > .map--inline:only-child { flex: 1 0 auto; }
2185
+ .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 */
2186
+ /* a map that is a body's only child fills it; the body turns to a column for it (parts.css) */
2187
+ .body > .map--inline:only-child { flex: 1 0 auto; }
2440
2188
 
2441
2189
  /* ---- map pick: a place on the map the member taps to choose it (Lh 2026-10-17) ----
2442
2190
  <label class="map-pick" style="top: 30%; left: 40%"><input type="radio" name="meet" value="library">
@@ -2525,7 +2273,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2525
2273
  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
2526
2274
  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
2527
2275
  falls — no height is written. Nothing fades: done and next read at full ink. */
2528
- .sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
2529
2276
  .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; }
2530
2277
  .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 */
2531
2278
  .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; }
@@ -2559,8 +2306,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2559
2306
  }
2560
2307
  .sheet-step__summary { color: var(--sheet-step-summary-ink, var(--card-muted)); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2561
2308
  .sheet-step__check { margin-inline-start: auto; }
2562
- .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
2563
- .sheet-step__inner { display: grid; gap: var(--card-gap); padding-block: var(--card-gap); padding-inline: var(--sheet-step-inner-pad-x, var(--sheet-step-bar-pad-x, var(--sp-3))); } /* the open step's content in the frame of its title's words (the bar's own inset), so it reads as that step's (Lh 2026-09-28, #153) */
2309
+ /* 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
2310
+ จุดนัด 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
2311
+ 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
2312
+ 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,
2313
+ 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
2314
+ 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
2315
+ unitless 0 (SheetSteps · Ruled) is a length there but not in a calc, so the step stays right and only the bleed is lost. */
2316
+ .sheet-step__body { --_step-pad: var(--sheet-step-inner-pad-x, var(--sheet-step-bar-pad-x, var(--sp-3))); --_pad: calc(var(--card-pad, 0rem) + var(--_step-pad)); margin-inline: calc(-1 * var(--card-pad, 0rem)); padding-inline: var(--card-pad, 0rem); min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
2317
+ .sheet-step__inner { --card-pad: var(--_pad); display: grid; gap: var(--card-gap); padding-block: var(--card-gap); padding-inline: var(--_step-pad); } /* the open step's content in the frame of its title's words (the bar's own inset), so it reads as that step's (Lh 2026-09-28, #153) */
2564
2318
  .sheet-step__inner > * { min-width: 0; }
2565
2319
  .sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s var(--ease-linear) var(--motion-sheet); }
2566
2320
  .sheet-step__go { position: absolute; inset: min(0rem, (var(--_row-h) - var(--tap-sm)) / 2) 0 auto; min-height: max(100%, var(--tap-sm)); /* a thin bar still takes a 44px tap */ margin: 0; cursor: pointer; }
@@ -2571,61 +2325,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2571
2325
  /* ==== the step bar answers a press, as PRESS in actions.css answers every other control (#62: a block is written in its own file) ==== */
2572
2326
  .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
2573
2327
 
2574
- /* ==== 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) ==== */
2575
- .sheet__body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
2576
- .sheet__body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
2577
- .sheet__body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
2578
2328
 
2579
- /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
2580
- /* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private.
2581
- A paper body adds its own --sp-6 on top of this (the rule below): the stack's gap under the head is the
2582
- card's colour, so without the paper's inset the first row sits on that seam. */
2583
- .sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0rem); transition: padding-top var(--motion-sheet) var(--ease); }
2584
- /* 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
2585
- --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
2586
- stack's step and the body's own rows read it. */
2587
- .sheet-stack > .card.sheet--paper > .sheet__body {
2588
- margin-top: calc(var(--sp-3) - var(--card-gap));
2589
- padding-top: calc(var(--sp-6) + var(--_refresh, 0rem));
2590
- transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
2591
- }
2592
- /* 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 */
2593
- .sheet-stack > .card.sheet--paper > .sheet__body.is-refreshing { transition: none; }
2594
- /* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
2595
- cover the corners, and a paper body showed there beside the head. Clear, the card's own colour
2596
- shows in that pocket. Open, the body is paper, and the first row sits --sp-6 down, the same paper
2597
- inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
2598
- would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
2599
- into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
2600
- sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
2601
- at the moment the sheet stops. Rising, the body is paper at once. */
2602
- .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
2603
- background-color: transparent;
2604
- transition:
2605
- padding-top var(--motion-sheet) var(--ease),
2606
- background-color var(--motion-quick) var(--ease) calc(var(--stagger, 0) * var(--motion-stagger) + var(--motion-sheet) - var(--motion-quick));
2607
- }
2608
- @media (prefers-reduced-motion: reduce) { .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
2609
- transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
2610
- } }
2611
2329
  /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
2612
2330
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
2613
2331
 
2614
- /* ---- where it sits in what stack.css draws: a body pulled to refresh holds still (docs/reference.md rule 15) ---- */
2615
- .sheet-stack > .card > .sheet__body.is-refreshing { transition: none; }
2616
-
2617
2332
  /* ---- css/step.css — .step: one screen, one task — card, optional sheet, bar outside ---- */
2618
2333
  /* ============================================================
2619
2334
  cardds/step.css — .step: one screen, one task.
2620
2335
  What the member reads (words on the base — not a card, Lh 2026-09-16), what the member does when the
2621
2336
  step asks for it (a sheet, sheet.css). The move: a page of words has
2622
2337
  an .action-bar (actions.css) OUTSIDE the step, below it — the base's;
2623
- a step with a sheet has none: the sheet's own .card__foot finishes it
2338
+ a step with a sheet has none: the sheet's own .foot finishes it
2624
2339
  (see "LAYERS" below).
2625
2340
 
2626
2341
  <section class="step">
2627
2342
  <section class="page">…the words…</section> ← first
2628
- <article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
2343
+ <article class="card sheet sheet--half">…the controls… <div class="foot">…</div></article> ← last, optional
2629
2344
  </section>
2630
2345
  <div class="action-bar">…</div> ← the screen's last child, only when there is no sheet
2631
2346
 
@@ -2677,7 +2392,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2677
2392
  .screen > .step:first-child, .screen--lead > .step { --_lead: var(--screen-lead, 15cqh); } /* top="lead" says it; a Step first says it too */
2678
2393
 
2679
2394
  /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
2680
- move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
2395
+ move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .foot
2681
2396
  (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
2682
2397
  own screens are composed (the guide); the framework does not police it — a component never changes
2683
2398
  because another one is there. */
@@ -2850,6 +2565,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2850
2565
  over the number and the month on one line; tall (a two-line title) = three lines, day · number · month. It grows no taller than
2851
2566
  its three lines (2026-10-21, timebank: beside a four-line title it was a tall empty block); held there, it stands at the head's top. */
2852
2567
  .tile-badge--stretch {
2568
+ line-height: var(--tile-badge-line, var(--lh-badge)); /* the knob's own property; the cap below reads it too */
2853
2569
  align-self: stretch;
2854
2570
  max-block-size: calc((2 * var(--fs-caption) + var(--fs-lg)) * var(--tile-badge-line, var(--lh-badge)) + 2 * var(--sp-3));
2855
2571
  container-type: size;
@@ -2861,7 +2577,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2861
2577
  column-gap: var(--sp-1);
2862
2578
  row-gap: 0;
2863
2579
  padding-block: var(--sp-3);
2864
- line-height: var(--tile-badge-line, var(--lh-badge));
2865
2580
  }
2866
2581
  .tile-badge--stretch > * { line-height: inherit; }
2867
2582
  .tile-badge--stretch > :first-child { grid-column: 1 / -1; }
@@ -2869,6 +2584,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2869
2584
  .tile-badge--stretch > * { grid-column: 1 / -1; }
2870
2585
  }
2871
2586
 
2587
+ /* 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 */
2588
+ .head:has(> .tile-badge:first-child + .chip-row) > .tile-badge--stretch:first-child { grid-column: 1; grid-row: 1; max-block-size: none; }
2589
+
2872
2590
  /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */
2873
2591
  .tile-badge--corner { position: absolute; right: var(--sp-3); bottom: var(--sp-3); }
2874
2592
 
@@ -2990,7 +2708,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2990
2708
  /* 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 */
2991
2709
  .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; }
2992
2710
  .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))); }
2993
- .card__foot > .icon-btn-group { flex: 1 1 0; }
2711
+ .foot > .icon-btn-group { flex: 1 1 0; }
2994
2712
  /* --vertical: the buttons one over the other, ONE button wide — at a row's end, where a panel across would take the words' room */
2995
2713
  .icon-btn-group--vertical { flex-direction: column; flex: none; width: var(--tap); }
2996
2714
  .icon-btn-group--vertical > .icon-btn { flex: 1 0 auto; min-height: var(--tap); }
@@ -3000,7 +2718,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3000
2718
  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). */
3001
2719
  .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` */
3002
2720
  .icon-btn-group--form > .icon-btn { flex: 0 1 auto; width: auto; min-width: var(--icon-btn-group-form-min-w, var(--tap)); }
3003
- .card__foot > :is(.icon-btn-group--form, .icon-btn-line) { flex: 0 1 auto; }
2721
+ .foot > :is(.icon-btn-group--form, .icon-btn-line) { flex: 0 1 auto; }
3004
2722
  /* --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
3005
2723
  above the panel, both centred */
3006
2724
  .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; }
@@ -3080,12 +2798,17 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3080
2798
  .dot--error { background: var(--signal-error); }
3081
2799
  .dot--info { background: var(--signal-info); }
3082
2800
  /* --sm: a small LABEL (a name on a note, a tag in a tight head) — low and tight; never a tap target */
3083
- .chip--sm { min-height: 0; gap: var(--chip-sm-gap, var(--sp-1)); padding: var(--chip-sm-pad-y, var(--sp-0)) var(--chip-sm-pad-x, var(--sp-2)); }
2801
+ /* one height wherever it stands (Lh 2026-10-21): the body's line, so a head that cuts its words with an ellipsis keeps the Thai marks over them, and a foot's or a middle's chip reads the same */
2802
+ .chip--sm { min-height: 0; gap: var(--chip-sm-gap, var(--sp-1)); padding: var(--chip-sm-pad-y, var(--sp-0)) var(--chip-sm-pad-x, var(--sp-2)); line-height: var(--chip-sm-line, var(--lh-body)); }
2803
+ /* soft: the accent washed into the chip's ground, no edge — a status to see, quieter than on (was the inbox Ribbon, Lh 2026-10-21) */
2804
+ .chip--soft { background: var(--chip-soft-bg, var(--accent-wash)); color: var(--chip-soft-ink, var(--card-ink)); border-color: transparent; } /* the wash on the chip's own paper, opaque (--accent-wash): see-through, a dark tone came through under the light side's ink (ink.spec) */
3084
2805
  .chip--on { background: var(--chip-on-bg, var(--accent)); --dark: var(--accent-dark); color: var(--chip-on-ink, var(--card-ink)); border-color: transparent; }
3085
2806
  /* muted: an untouched default — the words wash out, the edge stays; emphasis is the difference from the neighbours (docs/reference.md 13) */
3086
2807
  .chip--muted { color: var(--chip-muted-ink, var(--card-muted)); } /* the chip's own fill's muted words: a muted chip on the dark card is still a light chip */
3087
2808
 
3088
2809
  .chip-row { display: flex; gap: var(--sp-2); }
2810
+ /* the words that lead the chips (ChipRow `lead`, "นัดที่" before a place, Lh 2026-10-21): the chips' own type and size, at the middle of their height */
2811
+ .chip-row__lead { align-self: center; font: var(--type-label); font-size: var(--chip-font-size, var(--fs-sm)); color: var(--chip-row-lead-ink, inherit); }
3089
2812
  .chip-row:not(.bleed) { flex-wrap: wrap; } /* a strip (.bleed, card.css) stays one line */
3090
2813
  .chip-row.bleed > .chip { flex: none; white-space: nowrap; } /* a chip of the strip keeps its words on one line at its own width: the strip scrolls, a chip never shrinks to fit it */
3091
2814
 
@@ -3246,7 +2969,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3246
2969
 
3247
2970
 
3248
2971
  /* ---- 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
3249
- rounding — one line of muted words, an inline icon allowed; nothing to press. In a CardFoot or an ActionBar row it takes the
2972
+ rounding — one line of muted words, an inline icon allowed; nothing to press. In a Foot or an ActionBar row it takes the
3250
2973
  button's share (the whole width), so a foot or a bar keeps its height whether it holds a button or the note. ---- */
3251
2974
  .action-note {
3252
2975
  flex: 1 1 0;
@@ -3316,7 +3039,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3316
3039
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
3317
3040
  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"]),
3318
3041
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
3319
- .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 */
3042
+ .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 */
3320
3043
  @media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
3321
3044
 
3322
3045
  /* ==== the chip a choice draws: picked (choice.css drives it with its input), and --sign (#62: a block is written in its own file) ==== */
@@ -3338,12 +3061,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3338
3061
 
3339
3062
  /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
3340
3063
  /* 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 */
3341
- .card__head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * 1.5 + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
3342
- .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: var(--lh-body); }
3343
- .card__foot > .btn { flex: 1 1 0; min-width: 0; }
3064
+ .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; }
3065
+ /* …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 */
3066
+ .head:has(> .tile-badge:first-child + .chip-row) > .chip-row { grid-column: 2; grid-row: 1; max-height: none; }
3067
+ .head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3068
+ .foot > .btn { flex: 1 1 0; min-width: 0; }
3344
3069
  /* …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
3345
3070
  a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
3346
- .card__foot > .btn--form { flex: 0 1 auto; }
3071
+ .foot > .btn--form { flex: 0 1 auto; }
3347
3072
  /* the page dots under the card sit centred under it — the indicator belongs
3348
3073
  with the page that changes, never in the bar (Lh, 2026-09-12) */
3349
3074
  .screen__centre > .pager__at { justify-self: center; }
@@ -4124,14 +3849,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4124
3849
  /* ---- css/lists.css — row, rows, key-value, kv grid, timeline, legend, link ---- */
4125
3850
  /* ============================================================
4126
3851
  cardds/lists.css — frameless rows and pairs INSIDE a card:
4127
- row (lead · body · end) · key-value rows · kv grid ·
4128
- timeline · legend · link
3852
+ row (its parts, lead · words · end, are parts.css) · key-value
3853
+ rows · kv grid · timeline · legend · link
4129
3854
  A row inside a card has no boundary of its own; .rows draws
4130
3855
  hairlines between rows. A row that IS a card = .card--row.
4131
3856
  ============================================================ */
4132
3857
 
4133
3858
  /* ---- row: leading visual · title + meta · trailing control ----
4134
- <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> */
3859
+ <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> */
4135
3860
  .row {
4136
3861
  display: flex;
4137
3862
  align-items: center;
@@ -4144,21 +3869,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4144
3869
  /* no padding on a row (Lh 2026-09-16): padding is for room, not for spreading.
4145
3870
  Space BETWEEN rows is .rows' gap; a lone row as card content takes the card's
4146
3871
  gap. (.row--flush, which zeroed the old padding, is gone — every row is flush.) */
4147
- .row__lead {
4148
- width: var(--row-lead-size, var(--tap));
4149
- height: var(--row-lead-size, var(--tap));
4150
- flex: none;
4151
- border-radius: var(--row-lead-radius, var(--r-control));
4152
- border: var(--border-w) solid var(--row-lead-border, var(--outline));
4153
- display: grid;
4154
- place-items: center;
4155
- }
4156
- .row__body { flex: 1; min-width: 0; display: grid; gap: var(--row-body-gap, var(--sp-0)); }
4157
- .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) */
4158
- .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)); }
4159
- .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
4160
3872
  .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 */
4161
- .row--lg .row__title { font: var(--fw-bold) var(--fs-lg) / var(--lh-h1) var(--font-display); }
4162
3873
  /* a row that EXPANDS (Lh 2026-09-22): the row is a <details>' summary, what it shows comes out under it —
4163
3874
  who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
4164
3875
  <details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
@@ -4173,7 +3884,6 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4173
3884
  pointer-events off here, aria-disabled from the markup. The tick is the .check's checked look
4174
3885
  at --tap-sm, but not an input: nothing to press. */
4175
3886
  .row--done { pointer-events: none; cursor: default; }
4176
- .row--done .row__title { color: var(--card-muted); }
4177
3887
  .row__done {
4178
3888
  width: var(--row-done-size, var(--tap-sm));
4179
3889
  height: var(--row-done-size, var(--tap-sm));
@@ -4336,6 +4046,388 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4336
4046
  /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
4337
4047
  .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) */
4338
4048
 
4049
+ /* ---- css/parts.css — the parts of a line: lead, words, end — a Row's and a row card's children ---- */
4050
+ /* ============================================================
4051
+ cardds/parts.css — the parts of a LINE: lead · words · end.
4052
+ A Row (lists.css) and a row card (.card--row, card.css) are
4053
+ both made of them, as their direct children in that order.
4054
+ An outer governs a part only through the part's knobs or a
4055
+ selector naming both, written here (Lh 2026-10-21).
4056
+ <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>
4057
+ ============================================================ */
4058
+
4059
+ /* ---- lead: the outlined box at the start of a line ---- */
4060
+ .lead {
4061
+ width: var(--lead-size, var(--tap));
4062
+ height: var(--lead-size, var(--tap));
4063
+ flex: none;
4064
+ border-radius: var(--lead-radius, var(--r-control));
4065
+ border: var(--border-w) solid var(--lead-border, var(--outline));
4066
+ display: grid;
4067
+ place-items: center;
4068
+ }
4069
+ /* 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 */
4070
+ :is(.card--row, .feed-card > .head) > .lead {
4071
+ width: var(--lead-size, var(--tap-sm));
4072
+ height: var(--lead-size, var(--tap-sm));
4073
+ border-color: var(--lead-border, currentColor);
4074
+ font: var(--lead-font, var(--type-label));
4075
+ }
4076
+ .feed-card > .head:has(> .t-caption) > .lead { grid-row: span 2; }
4077
+
4078
+ /* ---- words: the title over its meta, taking the room between the lead and the end ---- */
4079
+ .words { flex: 1; display: grid; gap: var(--words-gap, var(--sp-0)); }
4080
+ .row > .words { min-width: 0; }
4081
+ .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) */
4082
+ .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)); }
4083
+ .row--lg > .words > .words__title { font: var(--fw-bold) var(--fs-lg) / var(--lh-h1) var(--font-display); }
4084
+ .row--done > .words > .words__title { color: var(--card-muted); }
4085
+
4086
+ /* ---- end: the value over its caption, right-aligned at the end of a line ---- */
4087
+ .end { margin-left: auto; text-align: right; display: grid; gap: var(--end-gap, var(--sp-0)); justify-items: end; }
4088
+ .row > .end { flex: none; }
4089
+
4090
+ /* ---- head: the first row of a card, a sheet and a stack sheet. A lead, the Words, chips, an end, as its direct children.
4091
+ <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>
4092
+ 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
4093
+ 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
4094
+ naming both, written below under the outer's name (Lh 2026-10-21). ---- */
4095
+ /* header row: title left, floating action right — centered, since the
4096
+ trailing control (chip/badge/icon-btn/link) is almost always taller than
4097
+ a single line of title text; top-aligning them left the control visually
4098
+ low against the title's optical center. A head with a genuinely tall,
4099
+ multi-line title can opt back out with .head--top. */
4100
+ .head {
4101
+ display: flex;
4102
+ flex-wrap: var(--head-wrap, wrap); /* the caption takes a line of its own — no wrapper div */
4103
+ align-items: center;
4104
+ justify-content: space-between;
4105
+ column-gap: var(--head-col-gap, var(--sp-3));
4106
+ row-gap: var(--head-row-gap, var(--sp-0));
4107
+ min-height: var(--head-min-h, auto);
4108
+ }
4109
+ .head--top { align-items: flex-start; }
4110
+
4111
+ /* ---- the card skeleton: head · content · foot ----
4112
+ Every card is a head (the heading left, hints / actions / a more-menu at
4113
+ the far right), its content, and an optional foot of action buttons.
4114
+ The head's leading child — the heading, or the div that wraps heading and
4115
+ caption — takes the room, so a heading with nothing at the far right runs
4116
+ all the way across; whatever trails it hugs its own width. Only the
4117
+ heading (or the div that wraps it) is that leading child: a control that
4118
+ leads a custom head — a chip, an avatar, a tile badge — keeps its own
4119
+ width, else the rule stretched a chip into a bar and an avatar into an
4120
+ ellipse (two agents and the wallet demo caught it, 2026-09-10). */
4121
+ .head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
4122
+
4123
+ /* Skeleton head — the heading in a card's OWN head, and only there.
4124
+ The heading wraps, no squeeze, up to three lines and cut there with an
4125
+ ellipsis (Lh 2026-10-21: "มันควรจะ max สามบรรทัด" — a request's title ran to
4126
+ four lines on a phone; reverses 2026-09-16's "no clamp"). The count is the knob
4127
+ --head-title-lines. The group that holds heading ·
4128
+ caption is the head's own wrapping row (above) — no wrapper element, no
4129
+ height, only the row gap. */
4130
+ .head :is(h1, h2, h3) {
4131
+ overflow-wrap: anywhere;
4132
+ display: -webkit-box;
4133
+ -webkit-box-orient: vertical;
4134
+ -webkit-line-clamp: var(--head-title-lines, 3);
4135
+ line-clamp: var(--head-title-lines, 3);
4136
+ overflow: hidden;
4137
+ padding-block-start: var(--_mark-room); /* a stacked Thai mark rises over the first line's box: room inside the clip, given back outside */
4138
+ margin-block-start: calc(-1 * var(--_mark-room));
4139
+ --_mark-room: 0.2em;
4140
+ }
4141
+
4142
+ .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) */
4143
+ .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) */
4144
+ .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 */
4145
+
4146
+ /* a stage sheet's head (`Head centre`): a round button · the centred title over its meta · a round button. The title and the meta
4147
+ stand in the middle column, the buttons span both rows; the 1fr rows keep the pair centred when a button is the taller */
4148
+ .head--centre {
4149
+ display: grid;
4150
+ grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
4151
+ grid-template-rows: 1fr auto auto 1fr;
4152
+ align-items: center;
4153
+ column-gap: var(--sp-3);
4154
+ row-gap: 0;
4155
+ min-height: var(--tap);
4156
+ text-align: center;
4157
+ }
4158
+ .head--centre > .words__title { grid-area: 2 / 2; }
4159
+ .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 */
4160
+ .head--centre > :first-child:not(.words__title) { grid-area: 1 / 1 / -1 / 2; }
4161
+ .head--centre > :last-child:not(.words__title, .words__meta) { grid-area: 1 / 3 / -1 / 4; }
4162
+
4163
+ /* 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
4164
+ 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,
4165
+ stamp.css); the head only places it.
4166
+ <div class="head"><h2 class="words__title t-h2">…</h2><span class="words__meta t-caption t-muted">…</span><span class="stamp">…</span></div> */
4167
+ .head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
4168
+ .head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
4169
+
4170
+ /* 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
4171
+ 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
4172
+ 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).
4173
+ <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> */
4174
+ .head:has(> .tile-badge:first-child + .chip-row) {
4175
+ display: grid;
4176
+ grid-template-columns: auto minmax(0, 1fr);
4177
+ align-items: center;
4178
+ row-gap: var(--head-lead-gap, var(--sp-3));
4179
+ }
4180
+ .head:has(> .tile-badge:first-child + .chip-row) > :nth-child(n + 3) { grid-column: 1 / -1; }
4181
+
4182
+ /* in a card with paper bands: the head and the foot are paper bands edge to edge, one look */
4183
+ :is(.card--bands, .card--bands > .card__face) > :is(.head, .foot) {
4184
+ --card-bg: var(--card-1-bg);
4185
+ --dark: var(--card-1-dark);
4186
+ background: var(--card-bg);
4187
+ color: var(--card-ink);
4188
+ margin-inline: calc(-1 * var(--card-pad));
4189
+ padding-inline: var(--card-pad);
4190
+ }
4191
+ :is(.card--bands, .card--bands > .card__face) > .head { margin-top: calc(-1 * var(--card-pad)); padding-block: var(--card-pad); }
4192
+ :is(.card--bands, .card--bands > .card__face) > .head { padding-block-end: var(--card-band-seam, var(--sp-2)); }
4193
+ /* 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
4194
+ each side of the head band's lower edge is --card-band-seam, not the card's padding and gap */
4195
+ :is(.card--bands, .card--bands > .card__face) > .head + * { margin-block-start: calc(var(--card-band-seam, var(--sp-2)) - var(--card-gap)); }
4196
+
4197
+ /* in a sheet stack */
4198
+ /* stack headers: uniform — [back?] title left, icons right, one height,
4199
+ same gap to content on every card */
4200
+ .sheet-stack > .card > .head {
4201
+ align-items: center;
4202
+ justify-content: flex-start;
4203
+ min-height: var(--tap);
4204
+ }
4205
+ .sheet-stack--display > .card > .head { align-items: start; min-height: 0; }
4206
+ .sheet-stack--tap > .card > .head { touch-action: none; }
4207
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .head {
4208
+ position: relative;
4209
+ padding-left: 0;
4210
+ transition: padding-left var(--motion-sheet) var(--ease);
4211
+ }
4212
+ .stack-sheet > .head { flex-wrap: nowrap; }
4213
+ .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))); }
4214
+ :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); }
4215
+ .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))); }
4216
+ /* a stacked sheet is a control until it is open: its head is not text to select */
4217
+ .sheet-stack > .card > .head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
4218
+
4219
+ /* in a sheet */
4220
+ /* the head — a title row at least --tap tall, either kind, so the peek strip
4221
+ (--sheet-peek) is the same for every sheet:
4222
+ - in a step it is a plain card head (card.css): the heading left, the
4223
+ contextual thing at the far right
4224
+ - `Head centre` is the stage sheet's three-column head: a round button,
4225
+ the centred title and meta, a round button
4226
+ - a label .row with its .check / .toggle may be the head when the sheet's
4227
+ whole ask is that switch: peek shows it (a row is --tap tall plus its own
4228
+ padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
4229
+ .sheet > .head { --head-min-h: var(--sheet-head-h); }
4230
+ .sheet > .head:not(:has(> :is(.t-caption, .t-body))) { --head-wrap: nowrap; }
4231
+
4232
+ /* a sheet's title is ONE line, always (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน" wrapped to two lines at 390px) — the
4233
+ 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
4234
+ 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
4235
+ measures --_over, the width it overflows by, and sets .is-long; one way — it waits, runs to its end, waits, and starts again
4236
+ from the first letter. The move is text-indent: the heading is the app's own element, there is no inner box to slide.
4237
+ Before the Sheet has measured, and where motion is off, it is cut with …. */
4238
+ .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 */
4239
+ .sheet > .head > :is(.words__title, :is(h1, h2, h3):first-child).is-long {
4240
+ text-overflow: clip;
4241
+ 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 */
4242
+ }
4243
+ @keyframes sheet-title-run { 0%, 20% { text-indent: 0; } 85%, 100% { text-indent: calc(-1 * var(--_over)); } }
4244
+ @media (prefers-reduced-motion: reduce) {
4245
+ .sheet > .head > :is(.words__title, :is(h1, h2, h3):first-child).is-long { animation: none; text-overflow: ellipsis; }
4246
+ }
4247
+
4248
+ /* 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
4249
+ (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,
4250
+ 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 */
4251
+ .stack-sheet > .head > .words__title {
4252
+ flex: 1; min-width: 0; white-space: nowrap; overflow-wrap: normal;
4253
+ translate: 0 var(--stack-sheet-title-shift, -0.1em); /* FC Pride's line box carries more room above its letters than below: the
4254
+ letters sat ~0.1em under the middle of the back disc and the tabs — lifted onto it (Lh 2026-09-24) */
4255
+ }
4256
+ @media (prefers-reduced-motion: reduce) {
4257
+ .stack-sheet > .head > .words__title.is-long { text-overflow: ellipsis; }
4258
+ }
4259
+
4260
+ /* in a feed card */
4261
+ .feed-card > .head { display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; min-height: var(--feed-card-head-h, var(--tap)); }
4262
+ .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 */
4263
+
4264
+ /* ---- body: the content row, under the head and over the foot. What the member reads or fills in; in a sheet, a stack sheet
4265
+ and a modal the one part that scrolls. A grid column from the top, the card's rhythm inside.
4266
+ <div class="body">…controls, rows, small content cards…</div>
4267
+ Knobs --body-gap (the card's gap) and --body-pad-top (none; a paper sheet's inset). An outer governs a body through those
4268
+ knobs or a selector naming both, written below under the outer's name (Lh 2026-10-21). ---- */
4269
+ .body {
4270
+ display: grid;
4271
+ gap: var(--body-gap, var(--card-gap));
4272
+ align-content: start;
4273
+ min-height: 0;
4274
+ padding-top: var(--body-pad-top, 0rem);
4275
+ overflow-y: auto;
4276
+ overscroll-behavior: contain;
4277
+ }
4278
+ /* 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) */
4279
+ .body:has(> .map--inline:only-child) { display: flex; flex-direction: column; }
4280
+ .body:has(> .sheet-steps) { display: flex; flex-direction: column; }
4281
+ .body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
4282
+ .body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
4283
+ .body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
4284
+
4285
+ /* in a sheet and a stack sheet */
4286
+ /* everything under the head. Its scrollport runs to the card's side and bottom edges (reading --card-pad, the way .bleed does) so
4287
+ content scrolls clear of the edge rather than stopping inside the padding, and a .bleed strip inside it still reaches the edges.
4288
+ The outers are in a :where() so the body keeps the weight of one class, as the state rules below expect. */
4289
+ :where(.sheet, .sheet-stack .card) > .body {
4290
+ flex: 1;
4291
+ margin-inline: calc(-1 * var(--card-pad));
4292
+ padding-inline: var(--card-pad);
4293
+ /* the part of the sheet that slid under the box's bottom edge cannot be
4294
+ seen, and the scroller has to know that: scroll-padding is exactly "the
4295
+ obscured part of my scrollport", so a focused field, scrollIntoView and
4296
+ anchor navigation all land a control in the visible part, never under
4297
+ the cut. A length from the box's container units (--_box), since the
4298
+ cut is a share of the SHEET's height, not of the body's. */
4299
+ scroll-padding-bottom: var(--_cover);
4300
+ /* …and its content has to be able to LEAVE that part: the same length as bottom padding, so the last row
4301
+ scrolls up to the visible edge at half and 3q as it does at full — everything between the head and the
4302
+ foot can be reached at any state (Lh, 2026-09-21). Padding, not a shorter box: nothing that shows moves
4303
+ when the state changes, only how far the body scrolls; a child that fills the body (a composer) fills
4304
+ the part that shows. */
4305
+ padding-bottom: var(--_cover);
4306
+ }
4307
+ .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) */
4308
+ /* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
4309
+ box is never smaller than its padding: the body runs past the sheet's clipped edge instead, where nothing
4310
+ shows anyway (the sheet's other parts keep their size, sheet.css). At peek and away no part of the body shows. */
4311
+ :is(.sheet--peek, .sheet--away) > .body { padding-bottom: 0; }
4312
+ :where(.sheet, .sheet-stack .card) > .body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
4313
+ :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)); }
4314
+ /* 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).
4315
+ The body and the foot read as paper; the handle and the head stay on the card.
4316
+ The flex gap under the head is outside the body, so it stays the sheet's tint.
4317
+ The first field would sit on that colour seam. The paper's inset is --sp-6 (24px at
4318
+ the default spacing), the body's own top padding, and only when the sheet is paper.
4319
+ A plain sheet keeps padding-top 0. A sheet in a stack wears the same class and the same
4320
+ inset: the stack's own gap under the head is the card's colour, so the row still needs this paper.
4321
+ The paper takes the side of the controls' paper (--action-dark): on a dark sheet its words still turn to onlight. */
4322
+ .sheet--paper > .body {
4323
+ --card-bg: var(--action-bg);
4324
+ --dark: var(--action-dark);
4325
+ background: var(--card-bg);
4326
+ color: var(--card-ink);
4327
+ padding-top: var(--body-pad-top, var(--sp-6));
4328
+ }
4329
+
4330
+ /* in a sheet stack */
4331
+ /* 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.
4332
+ --_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
4333
+ card's colour, so without the paper's inset the first row sits on that seam. */
4334
+ .sheet-stack > .card > .body {
4335
+ --_cover: 0rem;
4336
+ padding-top: calc(var(--body-pad-top, 0rem) + var(--_refresh, 0rem));
4337
+ transition: padding-top var(--motion-sheet) var(--ease);
4338
+ }
4339
+ /* 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
4340
+ --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
4341
+ stack's step and the body's own rows read it. */
4342
+ .sheet-stack > .card.sheet--paper > .body {
4343
+ margin-top: calc(var(--sp-3) - var(--card-gap));
4344
+ padding-top: calc(var(--body-pad-top, var(--sp-6)) + var(--_refresh, 0rem));
4345
+ transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
4346
+ }
4347
+ /* 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 */
4348
+ .sheet-stack > .card.sheet--paper > .body.is-refreshing { transition: none; }
4349
+ /* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
4350
+ cover the corners, and a paper body showed there beside the head. Clear, the card's own colour
4351
+ shows in that pocket. Open, the body is paper, and the first row sits --sp-6 down, the same paper
4352
+ inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
4353
+ would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
4354
+ into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
4355
+ sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
4356
+ at the moment the sheet stops. Rising, the body is paper at once. */
4357
+ .sheet-stack > .card.sheet--paper:not(.is-open) > .body {
4358
+ background-color: transparent;
4359
+ transition:
4360
+ padding-top var(--motion-sheet) var(--ease),
4361
+ background-color var(--motion-quick) var(--ease) calc(var(--stagger, 0) * var(--motion-stagger) + var(--motion-sheet) - var(--motion-quick));
4362
+ }
4363
+ @media (prefers-reduced-motion: reduce) { .sheet-stack > .card.sheet--paper:not(.is-open) > .body {
4364
+ transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
4365
+ } }
4366
+ /* a body pulled to refresh holds still */
4367
+ .sheet-stack > .card > .body.is-refreshing { transition: none; }
4368
+
4369
+ /* in a modal: the light part that holds the thing being asked about, padded on every side, shrinking (and scrolling) under the
4370
+ modal's max-height while the ask keeps its height (layover.css) */
4371
+ .modal > .body {
4372
+ padding: var(--body-pad-top, var(--card-pad)) var(--card-pad) var(--card-pad);
4373
+ flex: 0 1 auto;
4374
+ }
4375
+
4376
+ /* ---- 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.
4377
+ 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
4378
+ side padding so the labels keep the room. <div class="foot"><button class="btn btn--primary btn--block">…</button></div>
4379
+ Knobs --foot-gap, --foot-ink, --foot-link-gap. An outer governs a foot through those knobs or a selector naming both, written
4380
+ below under the outer's name (Lh 2026-10-21). ---- */
4381
+ .foot { display: flex; gap: var(--foot-gap, var(--sp-2)); align-items: center; color: var(--foot-ink, inherit); }
4382
+ .foot:has(> :nth-child(3)) > .btn { --btn-pad-x: var(--sp-2); }
4383
+ /* a small link under the buttons (Foot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
4384
+ whole short row of its own, centred — it never shares the buttons' line */
4385
+ .foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--foot-link-gap, var(--sp-1)); }
4386
+ .foot__link { flex: 1 0 100%; display: flex; justify-content: center; }
4387
+ /* 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
4388
+ 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 */
4389
+ .foot--round { justify-content: center; gap: var(--foot-gap, var(--sp-6)); }
4390
+
4391
+ /* in a card with paper bands: the foot band (the head's look, above) is slim */
4392
+ :is(.card--bands, .card--bands > .card__face) > .foot { margin-bottom: calc(-1 * var(--card-pad)); padding-block: var(--sp-3); }
4393
+
4394
+ /* in a sheet stack */
4395
+ /* a paper stack's foot is paper to the card's edges, and it covers the tinted flex gap above it so the
4396
+ button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
4397
+ where it was — the padding gives back what the bleed takes. Its ink reads the action's side. */
4398
+ .sheet-stack > .card.sheet--paper > .foot {
4399
+ --dark: var(--action-dark);
4400
+ background: var(--action-bg);
4401
+ color: var(--card-ink);
4402
+ margin-top: calc(-1 * var(--card-gap));
4403
+ margin-inline: calc(-1 * var(--card-pad));
4404
+ margin-bottom: calc(-1 * var(--card-pad));
4405
+ padding-top: var(--card-gap);
4406
+ padding-inline: var(--card-pad);
4407
+ padding-bottom: var(--card-pad);
4408
+ }
4409
+
4410
+ /* 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).
4411
+ 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 */
4412
+ .sheet > .foot {
4413
+ 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 */
4414
+ align-items: end;
4415
+ align-content: end;
4416
+ padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
4417
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
4418
+ pointer-events: none;
4419
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
4420
+ transition: translate var(--motion-sheet) var(--ease), visibility 0s;
4421
+ }
4422
+ .sheet > .foot > * { pointer-events: auto; }
4423
+ .sheet.is-dragging > .foot { transition: none; }
4424
+ /* 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
4425
+ LEAVES AND ARRIVES WITH ITS SHEET — solid the whole way, hidden only once the slide is over. It used to stay in place and
4426
+ fade: the body slid through the strip behind a half-transparent button (timebank, Lh 2026-10-01: "ตัวหนังสือลอดใต้ปุ่ม"). */
4427
+ :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); }
4428
+ /* a paper sheet's foot is paper, as its body is: the words read the controls' side */
4429
+ .sheet--paper > .foot { --card-bg: var(--action-bg); --dark: var(--action-dark); color: var(--foot-ink, var(--card-ink)); }
4430
+
4339
4431
  /* ---- css/sticky.css — sticky note, its field, tone chooser, thread of posts, pile ---- */
4340
4432
  /* 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.
4341
4433
  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. */
@@ -5026,7 +5118,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5026
5118
  }
5027
5119
 
5028
5120
  /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
5029
- .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
5121
+ .head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
5030
5122
  /* ---- where it sits in what people.css draws (docs/reference.md rule 15) ---- */
5031
5123
  /* 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
5032
5124
  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)
@@ -5424,17 +5516,11 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5424
5516
  position: relative;
5425
5517
  }
5426
5518
  .tile__label { font: var(--tile-label-font, var(--type-title)); }
5427
- .tile__value { font: var(--tile-value-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
5428
5519
  .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 */
5429
5520
  .tile__text { display: grid; gap: var(--tile-text-gap, var(--sp-0)); }
5430
5521
  .tile--on { background: var(--tile-on-bg, var(--accent)); --dark: var(--accent-dark); color: var(--tile-on-ink, var(--card-ink)); border-color: transparent; }
5431
5522
  /* .tile--pick's hidden input and :has(:checked) fill are declared once in choice.css, alongside .chip--pick and .mood__opt */
5432
5523
  .tile--wide { grid-column: 1 / -1; }
5433
- /* 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 */
5434
- /* 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`
5435
- = 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,
5436
- Lh 2026-10-18). --tile-ink still overrides; a toned stat tile (.tile--N, below) takes its fill and its ink back */
5437
- .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; }
5438
5524
  /* upload: dashed, centred plus + label */
5439
5525
  .tile--upload {
5440
5526
  border-style: dashed;
@@ -5453,7 +5539,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5453
5539
  .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; }
5454
5540
 
5455
5541
  /* ---- wave: audio waveform stand-in for voice rows ----
5456
- <span class="wave"></span> — sits in a .row__body, fills the width */
5542
+ <span class="wave"></span> — sits in a .words, fills the width */
5457
5543
  .wave {
5458
5544
  /* only WHOLE bars show (Lh 2026-09-23 — a clipped last bar read thinner): the columns are as many whole bar-widths as fit
5459
5545
  (auto-fill), spread edge to edge; bars that do not fit go to rows of no height, clipped away */
@@ -5605,14 +5691,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5605
5691
  /* ---- the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
5606
5692
  <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)
5607
5693
  <article class="card modal">
5608
- <div class="modal__lift">…the card's content (a head, rows, a timeline)…</div>
5694
+ <div class="body">…the card's content (a head, rows, a timeline)…</div>
5609
5695
  <div class="modal__drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
5610
5696
  <div class="btn-trio">…</div>
5611
5697
  </div>
5612
5698
  </article>
5613
5699
  </div>
5614
5700
  .modal is the card — no border (the shadow is its edge on the scrim);
5615
- .modal__lift the light part that holds the thing being asked about (it
5701
+ .body the light part that holds the thing being asked about (it
5616
5702
  scrolls when tall); .modal__drawer the dark end that asks. The ground is
5617
5703
  whatever the stage shows (dim it or not). Two beats (2 × --motion-sheet) when
5618
5704
  the stage appears, and the same two backwards when it closes:
@@ -5644,16 +5730,6 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5644
5730
  overflow: clip;
5645
5731
  box-shadow: var(--modal-shadow, var(--shadow-sheet));
5646
5732
  }
5647
- .modal__lift {
5648
- padding: var(--card-pad);
5649
- display: grid;
5650
- gap: var(--card-gap);
5651
- align-content: start;
5652
- flex: 0 1 auto;
5653
- min-height: 0;
5654
- overflow-y: auto; /* tall content scrolls inside; the drawer stays */
5655
- overscroll-behavior: contain;
5656
- }
5657
5733
  .modal__drawer {
5658
5734
  flex-shrink: 0;
5659
5735
  height: var(--drawer-h);
@@ -5678,8 +5754,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5678
5754
  --accent: var(--card-ink);
5679
5755
  --accent-dark: calc(1 - var(--dark));
5680
5756
  }
5681
- /* the CONFIRM: a Modal with no ModalBody: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
5682
- .modal:not(:has(> .modal__lift)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
5757
+ /* the CONFIRM: a Modal with no Body: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
5758
+ .modal:not(:has(> .body)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
5683
5759
  .modal__drawer .t-caption { --t-caption-ink: var(--modal-drawer-caption-ink, var(--card-muted)); }
5684
5760
  .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 */
5685
5761
  /* 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
@@ -6046,7 +6122,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
6046
6122
  0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
6047
6123
  }
6048
6124
  /* a peeked stack tab is the icon alone: no sheen and no shadow until its sheet is open (the fill fades with the sheet) */
6049
- [data-finish="lit"] .sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab {
6125
+ [data-finish="lit"] .sheet-stack--tap > .card:not(.is-open) > .head .stack-sheet__tab {
6050
6126
  background-image: none;
6051
6127
  box-shadow: none;
6052
6128
  }
@@ -6085,7 +6161,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
6085
6161
  }
6086
6162
  /* a dark card (the brand tone, a cover): multiply would only blacken it, so the grain lightens, fainter */
6087
6163
  [data-finish="print"] :is(.card--3, .card--cover)::after { mix-blend-mode: screen; opacity: calc(var(--print-grain, 0.32) * 0.56); }
6088
- [data-finish="print"] .card:not(.card--row, .card--cover, .feed-card--small, .sheet-stack > *, .sheet-stack > :not(.sheet-stack__base) > *) > .card__head {
6164
+ [data-finish="print"] .card:not(.card--row, .card--cover, .feed-card--small, .sheet-stack > *, .sheet-stack > :not(.sheet-stack__base) > *) > .head {
6089
6165
  padding-bottom: var(--sp-2);
6090
6166
  border-bottom: var(--rule-w) dotted color-mix(in srgb, currentColor 45%, transparent);
6091
6167
  }
@@ -6096,7 +6172,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
6096
6172
  [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 */
6097
6173
  [data-finish="print"] .btn--primary { box-shadow: 0.125rem 0.125rem 0 0 color-mix(in srgb, var(--card-ink) 60%, transparent); }
6098
6174
  [data-finish="print"] :is(.btn, .chip):active { box-shadow: none; translate: 0.125rem 0.125rem; }
6099
- [data-finish="print"] .card__thumb { border-style: dashed; }
6175
+ [data-finish="print"] .card--row > .lead { border-style: dashed; }
6100
6176
 
6101
6177
  /* ============================================================
6102
6178
  sticker — a die-cut sticker on a sheet: --sticker-cut of white paper left round the print, a short hard
@@ -6131,5 +6207,5 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
6131
6207
  [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; }
6132
6208
  [data-finish="ink"] .btn--primary { border-color: var(--on-surface); box-shadow: 0.1875rem 0.1875rem 0 0 var(--on-surface); }
6133
6209
  [data-finish="ink"] .toggle { box-shadow: 0.125rem 0.125rem 0 0 var(--on-surface); }
6134
- [data-finish="ink"] .card__thumb { border-width: var(--ink-w, 0.15625rem); }
6210
+ [data-finish="ink"] .card--row > .lead { border-width: var(--ink-w, 0.15625rem); }
6135
6211