@usableapp/cardds 0.7.106 → 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 (95) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/bin/cardds.mjs +5 -3
  3. package/cardds.css +1 -0
  4. package/css/actions.css +9 -9
  5. package/css/base.css +6 -9
  6. package/css/card.css +16 -186
  7. package/css/finish.css +4 -4
  8. package/css/journey.css +1 -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 +12 -139
  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 +4 -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/IconBtn.d.ts +1 -1
  26. package/dist/actions/IconBtn.js +1 -1
  27. package/dist/cardds.css +461 -422
  28. package/dist/cards/Card.d.ts +6 -22
  29. package/dist/cards/Card.js +11 -22
  30. package/dist/cards/CardFeed.d.ts +1 -1
  31. package/dist/cards/CardFeed.js +5 -5
  32. package/dist/cards/CardParts.d.ts +0 -12
  33. package/dist/cards/CardParts.js +0 -12
  34. package/dist/cards/cardGo.js +1 -1
  35. package/dist/cards/cardGrounds.js +2 -2
  36. package/dist/catalog.json +1 -1
  37. package/dist/forms/Field.d.ts +1 -1
  38. package/dist/forms/Field.js +1 -1
  39. package/dist/index.d.ts +6 -5
  40. package/dist/index.js +6 -5
  41. package/dist/lists/Row.d.ts +2 -28
  42. package/dist/lists/Row.js +2 -17
  43. package/dist/media/MapArea.d.ts +2 -2
  44. package/dist/media/MapArea.js +1 -1
  45. package/dist/media/Tile.d.ts +3 -7
  46. package/dist/media/Tile.js +5 -5
  47. package/dist/parts/Body.d.ts +7 -0
  48. package/dist/parts/Body.js +11 -0
  49. package/dist/parts/End.d.ts +10 -0
  50. package/dist/parts/End.js +7 -0
  51. package/dist/parts/Foot.d.ts +17 -0
  52. package/dist/parts/Foot.js +11 -0
  53. package/dist/parts/Head.d.ts +15 -0
  54. package/dist/parts/Head.js +34 -0
  55. package/dist/parts/Lead.d.ts +10 -0
  56. package/dist/parts/Lead.js +9 -0
  57. package/dist/parts/Words.d.ts +13 -0
  58. package/dist/parts/Words.js +20 -0
  59. package/dist/parts/headCtx.d.ts +7 -0
  60. package/dist/parts/headCtx.js +9 -0
  61. package/dist/scaffold/EmptyState.d.ts +1 -1
  62. package/dist/scaffold/EmptyState.js +1 -1
  63. package/dist/scaffold/Screen.js +1 -1
  64. package/dist/sheets/Modal.d.ts +3 -7
  65. package/dist/sheets/Modal.js +3 -7
  66. package/dist/sheets/Sheet.d.ts +2 -2
  67. package/dist/sheets/Sheet.js +8 -9
  68. package/dist/sheets/SheetStack.d.ts +3 -3
  69. package/dist/sheets/SheetStack.js +4 -4
  70. package/dist/sheets/SheetSteps.d.ts +1 -1
  71. package/dist/sheets/SheetSteps.js +2 -2
  72. package/dist/sheets/StackBack.d.ts +1 -1
  73. package/dist/sheets/StackBack.js +1 -1
  74. package/dist/sheets/StackSheet.js +7 -7
  75. package/dist/sheets/sheetMeasure.js +3 -3
  76. package/dist/sheets/sheetStepsPile.js +2 -2
  77. package/dist/sheets/stackCard.js +6 -6
  78. package/dist/templates/CallSheet.js +6 -5
  79. package/dist/templates/Walkthrough.js +3 -2
  80. package/docs/reference.md +75 -36
  81. package/lib/catalog.mjs +1 -1
  82. package/lib/mcp.mjs +1 -1
  83. package/lint/rules.mjs +14 -7
  84. package/package.json +2 -1
  85. package/scripts/codemod-parts.mjs +324 -0
  86. package/dist/cards/CardFoot.d.ts +0 -15
  87. package/dist/cards/CardFoot.js +0 -9
  88. package/dist/cards/CardHead.d.ts +0 -21
  89. package/dist/cards/CardHead.js +0 -39
  90. package/dist/cards/Stat.d.ts +0 -19
  91. package/dist/cards/Stat.js +0 -14
  92. package/dist/sheets/SheetBody.d.ts +0 -5
  93. package/dist/sheets/SheetBody.js +0 -9
  94. package/dist/sheets/SheetHead.d.ts +0 -15
  95. package/dist/sheets/SheetHead.js +0 -6
package/css/finish.css CHANGED
@@ -82,7 +82,7 @@
82
82
  0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
83
83
  }
84
84
  /* a peeked stack tab is the icon alone: no sheen and no shadow until its sheet is open (the fill fades with the sheet) */
85
- [data-finish="lit"] .sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab {
85
+ [data-finish="lit"] .sheet-stack--tap > .card:not(.is-open) > .head .stack-sheet__tab {
86
86
  background-image: none;
87
87
  box-shadow: none;
88
88
  }
@@ -121,7 +121,7 @@
121
121
  }
122
122
  /* a dark card (the brand tone, a cover): multiply would only blacken it, so the grain lightens, fainter */
123
123
  [data-finish="print"] :is(.card--3, .card--cover)::after { mix-blend-mode: screen; opacity: calc(var(--print-grain, 0.32) * 0.56); }
124
- [data-finish="print"] .card:not(.card--row, .card--cover, .feed-card--small, .sheet-stack > *, .sheet-stack > :not(.sheet-stack__base) > *) > .card__head {
124
+ [data-finish="print"] .card:not(.card--row, .card--cover, .feed-card--small, .sheet-stack > *, .sheet-stack > :not(.sheet-stack__base) > *) > .head {
125
125
  padding-bottom: var(--sp-2);
126
126
  border-bottom: var(--rule-w) dotted color-mix(in srgb, currentColor 45%, transparent);
127
127
  }
@@ -132,7 +132,7 @@
132
132
  [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 */
133
133
  [data-finish="print"] .btn--primary { box-shadow: 0.125rem 0.125rem 0 0 color-mix(in srgb, var(--card-ink) 60%, transparent); }
134
134
  [data-finish="print"] :is(.btn, .chip):active { box-shadow: none; translate: 0.125rem 0.125rem; }
135
- [data-finish="print"] .card__thumb { border-style: dashed; }
135
+ [data-finish="print"] .card--row > .lead { border-style: dashed; }
136
136
 
137
137
  /* ============================================================
138
138
  sticker — a die-cut sticker on a sheet: --sticker-cut of white paper left round the print, a short hard
@@ -167,4 +167,4 @@
167
167
  [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; }
168
168
  [data-finish="ink"] .btn--primary { border-color: var(--on-surface); box-shadow: 0.1875rem 0.1875rem 0 0 var(--on-surface); }
169
169
  [data-finish="ink"] .toggle { box-shadow: 0.125rem 0.125rem 0 0 var(--on-surface); }
170
- [data-finish="ink"] .card__thumb { border-width: var(--ink-w, 0.15625rem); }
170
+ [data-finish="ink"] .card--row > .lead { border-width: var(--ink-w, 0.15625rem); }
package/css/journey.css CHANGED
@@ -74,7 +74,7 @@
74
74
  }
75
75
 
76
76
  /* 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 */
77
- .card__head:has(> .tile-badge:first-child + .chip-row) > .tile-badge--stretch:first-child { grid-column: 1; grid-row: 1; max-block-size: none; }
77
+ .head:has(> .tile-badge:first-child + .chip-row) > .tile-badge--stretch:first-child { grid-column: 1; grid-row: 1; max-block-size: none; }
78
78
 
79
79
  /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */
80
80
  .tile-badge--corner { position: absolute; right: var(--sp-3); bottom: var(--sp-3); }
package/css/layover.css CHANGED
@@ -8,14 +8,14 @@
8
8
  /* ---- the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
9
9
  <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)
10
10
  <article class="card modal">
11
- <div class="modal__lift">…the card's content (a head, rows, a timeline)…</div>
11
+ <div class="body">…the card's content (a head, rows, a timeline)…</div>
12
12
  <div class="modal__drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
13
13
  <div class="btn-trio">…</div>
14
14
  </div>
15
15
  </article>
16
16
  </div>
17
17
  .modal is the card — no border (the shadow is its edge on the scrim);
18
- .modal__lift the light part that holds the thing being asked about (it
18
+ .body the light part that holds the thing being asked about (it
19
19
  scrolls when tall); .modal__drawer the dark end that asks. The ground is
20
20
  whatever the stage shows (dim it or not). Two beats (2 × --motion-sheet) when
21
21
  the stage appears, and the same two backwards when it closes:
@@ -47,16 +47,6 @@
47
47
  overflow: clip;
48
48
  box-shadow: var(--modal-shadow, var(--shadow-sheet));
49
49
  }
50
- .modal__lift {
51
- padding: var(--card-pad);
52
- display: grid;
53
- gap: var(--card-gap);
54
- align-content: start;
55
- flex: 0 1 auto;
56
- min-height: 0;
57
- overflow-y: auto; /* tall content scrolls inside; the drawer stays */
58
- overscroll-behavior: contain;
59
- }
60
50
  .modal__drawer {
61
51
  flex-shrink: 0;
62
52
  height: var(--drawer-h);
@@ -81,8 +71,8 @@
81
71
  --accent: var(--card-ink);
82
72
  --accent-dark: calc(1 - var(--dark));
83
73
  }
84
- /* the CONFIRM: a Modal with no ModalBody: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
85
- .modal:not(:has(> .modal__lift)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
74
+ /* the CONFIRM: a Modal with no Body: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
75
+ .modal:not(:has(> .body)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
86
76
  .modal__drawer .t-caption { --t-caption-ink: var(--modal-drawer-caption-ink, var(--card-muted)); }
87
77
  .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 */
88
78
  /* 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
package/css/lists.css CHANGED
@@ -1,13 +1,13 @@
1
1
  /* ============================================================
2
2
  cardds/lists.css — frameless rows and pairs INSIDE a card:
3
- row (lead · body · end) · key-value rows · kv grid ·
4
- timeline · legend · link
3
+ row (its parts, lead · words · end, are parts.css) · key-value
4
+ rows · kv grid · timeline · legend · link
5
5
  A row inside a card has no boundary of its own; .rows draws
6
6
  hairlines between rows. A row that IS a card = .card--row.
7
7
  ============================================================ */
8
8
 
9
9
  /* ---- row: leading visual · title + meta · trailing control ----
10
- <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> */
10
+ <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> */
11
11
  .row {
12
12
  display: flex;
13
13
  align-items: center;
@@ -20,21 +20,7 @@
20
20
  /* no padding on a row (Lh 2026-09-16): padding is for room, not for spreading.
21
21
  Space BETWEEN rows is .rows' gap; a lone row as card content takes the card's
22
22
  gap. (.row--flush, which zeroed the old padding, is gone — every row is flush.) */
23
- .row__lead {
24
- width: var(--row-lead-size, var(--tap));
25
- height: var(--row-lead-size, var(--tap));
26
- flex: none;
27
- border-radius: var(--row-lead-radius, var(--r-control));
28
- border: var(--border-w) solid var(--row-lead-border, var(--outline));
29
- display: grid;
30
- place-items: center;
31
- }
32
- .row__body { flex: 1; min-width: 0; display: grid; gap: var(--row-body-gap, var(--sp-0)); }
33
- .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) */
34
- .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)); }
35
- .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
36
23
  .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 */
37
- .row--lg .row__title { font: var(--fw-bold) var(--fs-lg) / var(--lh-h1) var(--font-display); }
38
24
  /* a row that EXPANDS (Lh 2026-09-22): the row is a <details>' summary, what it shows comes out under it —
39
25
  who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
40
26
  <details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
@@ -49,7 +35,6 @@
49
35
  pointer-events off here, aria-disabled from the markup. The tick is the .check's checked look
50
36
  at --tap-sm, but not an input: nothing to press. */
51
37
  .row--done { pointer-events: none; cursor: default; }
52
- .row--done .row__title { color: var(--card-muted); }
53
38
  .row__done {
54
39
  width: var(--row-done-size, var(--tap-sm));
55
40
  height: var(--row-done-size, var(--tap-sm));
package/css/media.css CHANGED
@@ -43,17 +43,11 @@
43
43
  position: relative;
44
44
  }
45
45
  .tile__label { font: var(--tile-label-font, var(--type-title)); }
46
- .tile__value { font: var(--tile-value-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
47
46
  .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 */
48
47
  .tile__text { display: grid; gap: var(--tile-text-gap, var(--sp-0)); }
49
48
  .tile--on { background: var(--tile-on-bg, var(--accent)); --dark: var(--accent-dark); color: var(--tile-on-ink, var(--card-ink)); border-color: transparent; }
50
49
  /* .tile--pick's hidden input and :has(:checked) fill are declared once in choice.css, alongside .chip--pick and .mood__opt */
51
50
  .tile--wide { grid-column: 1 / -1; }
52
- /* 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 */
53
- /* 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`
54
- = 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,
55
- Lh 2026-10-18). --tile-ink still overrides; a toned stat tile (.tile--N, below) takes its fill and its ink back */
56
- .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; }
57
51
  /* upload: dashed, centred plus + label */
58
52
  .tile--upload {
59
53
  border-style: dashed;
@@ -72,7 +66,7 @@
72
66
  .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; }
73
67
 
74
68
  /* ---- wave: audio waveform stand-in for voice rows ----
75
- <span class="wave"></span> — sits in a .row__body, fills the width */
69
+ <span class="wave"></span> — sits in a .words, fills the width */
76
70
  .wave {
77
71
  /* only WHOLE bars show (Lh 2026-09-23 — a clipped last bar read thinner): the columns are as many whole bar-widths as fit
78
72
  (auto-fill), spread edge to edge; bars that do not fit go to rows of no height, clipped away */
package/css/parts.css ADDED
@@ -0,0 +1,380 @@
1
+ /* ============================================================
2
+ cardds/parts.css — the parts of a LINE: lead · words · end.
3
+ A Row (lists.css) and a row card (.card--row, card.css) are
4
+ both made of them, as their direct children in that order.
5
+ An outer governs a part only through the part's knobs or a
6
+ selector naming both, written here (Lh 2026-10-21).
7
+ <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>
8
+ ============================================================ */
9
+
10
+ /* ---- lead: the outlined box at the start of a line ---- */
11
+ .lead {
12
+ width: var(--lead-size, var(--tap));
13
+ height: var(--lead-size, var(--tap));
14
+ flex: none;
15
+ border-radius: var(--lead-radius, var(--r-control));
16
+ border: var(--border-w) solid var(--lead-border, var(--outline));
17
+ display: grid;
18
+ place-items: center;
19
+ }
20
+ /* 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 */
21
+ :is(.card--row, .feed-card > .head) > .lead {
22
+ width: var(--lead-size, var(--tap-sm));
23
+ height: var(--lead-size, var(--tap-sm));
24
+ border-color: var(--lead-border, currentColor);
25
+ font: var(--lead-font, var(--type-label));
26
+ }
27
+ .feed-card > .head:has(> .t-caption) > .lead { grid-row: span 2; }
28
+
29
+ /* ---- words: the title over its meta, taking the room between the lead and the end ---- */
30
+ .words { flex: 1; display: grid; gap: var(--words-gap, var(--sp-0)); }
31
+ .row > .words { min-width: 0; }
32
+ .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) */
33
+ .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)); }
34
+ .row--lg > .words > .words__title { font: var(--fw-bold) var(--fs-lg) / var(--lh-h1) var(--font-display); }
35
+ .row--done > .words > .words__title { color: var(--card-muted); }
36
+
37
+ /* ---- end: the value over its caption, right-aligned at the end of a line ---- */
38
+ .end { margin-left: auto; text-align: right; display: grid; gap: var(--end-gap, var(--sp-0)); justify-items: end; }
39
+ .row > .end { flex: none; }
40
+
41
+ /* ---- head: the first row of a card, a sheet and a stack sheet. A lead, the Words, chips, an end, as its direct children.
42
+ <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>
43
+ 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
44
+ 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
45
+ naming both, written below under the outer's name (Lh 2026-10-21). ---- */
46
+ /* header row: title left, floating action right — centered, since the
47
+ trailing control (chip/badge/icon-btn/link) is almost always taller than
48
+ a single line of title text; top-aligning them left the control visually
49
+ low against the title's optical center. A head with a genuinely tall,
50
+ multi-line title can opt back out with .head--top. */
51
+ .head {
52
+ display: flex;
53
+ flex-wrap: var(--head-wrap, wrap); /* the caption takes a line of its own — no wrapper div */
54
+ align-items: center;
55
+ justify-content: space-between;
56
+ column-gap: var(--head-col-gap, var(--sp-3));
57
+ row-gap: var(--head-row-gap, var(--sp-0));
58
+ min-height: var(--head-min-h, auto);
59
+ }
60
+ .head--top { align-items: flex-start; }
61
+
62
+ /* ---- the card skeleton: head · content · foot ----
63
+ Every card is a head (the heading left, hints / actions / a more-menu at
64
+ the far right), its content, and an optional foot of action buttons.
65
+ The head's leading child — the heading, or the div that wraps heading and
66
+ caption — takes the room, so a heading with nothing at the far right runs
67
+ all the way across; whatever trails it hugs its own width. Only the
68
+ heading (or the div that wraps it) is that leading child: a control that
69
+ leads a custom head — a chip, an avatar, a tile badge — keeps its own
70
+ width, else the rule stretched a chip into a bar and an avatar into an
71
+ ellipse (two agents and the wallet demo caught it, 2026-09-10). */
72
+ .head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
73
+
74
+ /* Skeleton head — the heading in a card's OWN head, and only there.
75
+ The heading wraps, no squeeze, up to three lines and cut there with an
76
+ ellipsis (Lh 2026-10-21: "มันควรจะ max สามบรรทัด" — a request's title ran to
77
+ four lines on a phone; reverses 2026-09-16's "no clamp"). The count is the knob
78
+ --head-title-lines. The group that holds heading ·
79
+ caption is the head's own wrapping row (above) — no wrapper element, no
80
+ height, only the row gap. */
81
+ .head :is(h1, h2, h3) {
82
+ overflow-wrap: anywhere;
83
+ display: -webkit-box;
84
+ -webkit-box-orient: vertical;
85
+ -webkit-line-clamp: var(--head-title-lines, 3);
86
+ line-clamp: var(--head-title-lines, 3);
87
+ overflow: hidden;
88
+ padding-block-start: var(--_mark-room); /* a stacked Thai mark rises over the first line's box: room inside the clip, given back outside */
89
+ margin-block-start: calc(-1 * var(--_mark-room));
90
+ --_mark-room: 0.2em;
91
+ }
92
+
93
+ .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) */
94
+ .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) */
95
+ .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 */
96
+
97
+ /* a stage sheet's head (`Head centre`): a round button · the centred title over its meta · a round button. The title and the meta
98
+ stand in the middle column, the buttons span both rows; the 1fr rows keep the pair centred when a button is the taller */
99
+ .head--centre {
100
+ display: grid;
101
+ grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
102
+ grid-template-rows: 1fr auto auto 1fr;
103
+ align-items: center;
104
+ column-gap: var(--sp-3);
105
+ row-gap: 0;
106
+ min-height: var(--tap);
107
+ text-align: center;
108
+ }
109
+ .head--centre > .words__title { grid-area: 2 / 2; }
110
+ .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 */
111
+ .head--centre > :first-child:not(.words__title) { grid-area: 1 / 1 / -1 / 2; }
112
+ .head--centre > :last-child:not(.words__title, .words__meta) { grid-area: 1 / 3 / -1 / 4; }
113
+
114
+ /* 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
115
+ 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,
116
+ stamp.css); the head only places it.
117
+ <div class="head"><h2 class="words__title t-h2">…</h2><span class="words__meta t-caption t-muted">…</span><span class="stamp">…</span></div> */
118
+ .head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
119
+ .head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
120
+
121
+ /* 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
122
+ 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
123
+ 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).
124
+ <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> */
125
+ .head:has(> .tile-badge:first-child + .chip-row) {
126
+ display: grid;
127
+ grid-template-columns: auto minmax(0, 1fr);
128
+ align-items: center;
129
+ row-gap: var(--head-lead-gap, var(--sp-3));
130
+ }
131
+ .head:has(> .tile-badge:first-child + .chip-row) > :nth-child(n + 3) { grid-column: 1 / -1; }
132
+
133
+ /* in a card with paper bands: the head and the foot are paper bands edge to edge, one look */
134
+ :is(.card--bands, .card--bands > .card__face) > :is(.head, .foot) {
135
+ --card-bg: var(--card-1-bg);
136
+ --dark: var(--card-1-dark);
137
+ background: var(--card-bg);
138
+ color: var(--card-ink);
139
+ margin-inline: calc(-1 * var(--card-pad));
140
+ padding-inline: var(--card-pad);
141
+ }
142
+ :is(.card--bands, .card--bands > .card__face) > .head { margin-top: calc(-1 * var(--card-pad)); padding-block: var(--card-pad); }
143
+ :is(.card--bands, .card--bands > .card__face) > .head { padding-block-end: var(--card-band-seam, var(--sp-2)); }
144
+ /* 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
145
+ each side of the head band's lower edge is --card-band-seam, not the card's padding and gap */
146
+ :is(.card--bands, .card--bands > .card__face) > .head + * { margin-block-start: calc(var(--card-band-seam, var(--sp-2)) - var(--card-gap)); }
147
+
148
+ /* in a sheet stack */
149
+ /* stack headers: uniform — [back?] title left, icons right, one height,
150
+ same gap to content on every card */
151
+ .sheet-stack > .card > .head {
152
+ align-items: center;
153
+ justify-content: flex-start;
154
+ min-height: var(--tap);
155
+ }
156
+ .sheet-stack--display > .card > .head { align-items: start; min-height: 0; }
157
+ .sheet-stack--tap > .card > .head { touch-action: none; }
158
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .head {
159
+ position: relative;
160
+ padding-left: 0;
161
+ transition: padding-left var(--motion-sheet) var(--ease);
162
+ }
163
+ .stack-sheet > .head { flex-wrap: nowrap; }
164
+ .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))); }
165
+ :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); }
166
+ .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))); }
167
+ /* a stacked sheet is a control until it is open: its head is not text to select */
168
+ .sheet-stack > .card > .head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
169
+
170
+ /* in a sheet */
171
+ /* the head — a title row at least --tap tall, either kind, so the peek strip
172
+ (--sheet-peek) is the same for every sheet:
173
+ - in a step it is a plain card head (card.css): the heading left, the
174
+ contextual thing at the far right
175
+ - `Head centre` is the stage sheet's three-column head: a round button,
176
+ the centred title and meta, a round button
177
+ - a label .row with its .check / .toggle may be the head when the sheet's
178
+ whole ask is that switch: peek shows it (a row is --tap tall plus its own
179
+ padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
180
+ .sheet > .head { --head-min-h: var(--sheet-head-h); }
181
+ .sheet > .head:not(:has(> :is(.t-caption, .t-body))) { --head-wrap: nowrap; }
182
+
183
+ /* a sheet's title is ONE line, always (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน" wrapped to two lines at 390px) — the
184
+ 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
185
+ 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
186
+ measures --_over, the width it overflows by, and sets .is-long; one way — it waits, runs to its end, waits, and starts again
187
+ from the first letter. The move is text-indent: the heading is the app's own element, there is no inner box to slide.
188
+ Before the Sheet has measured, and where motion is off, it is cut with …. */
189
+ .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 */
190
+ .sheet > .head > :is(.words__title, :is(h1, h2, h3):first-child).is-long {
191
+ text-overflow: clip;
192
+ 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 */
193
+ }
194
+ @keyframes sheet-title-run { 0%, 20% { text-indent: 0; } 85%, 100% { text-indent: calc(-1 * var(--_over)); } }
195
+ @media (prefers-reduced-motion: reduce) {
196
+ .sheet > .head > :is(.words__title, :is(h1, h2, h3):first-child).is-long { animation: none; text-overflow: ellipsis; }
197
+ }
198
+
199
+ /* 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
200
+ (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,
201
+ 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 */
202
+ .stack-sheet > .head > .words__title {
203
+ flex: 1; min-width: 0; white-space: nowrap; overflow-wrap: normal;
204
+ translate: 0 var(--stack-sheet-title-shift, -0.1em); /* FC Pride's line box carries more room above its letters than below: the
205
+ letters sat ~0.1em under the middle of the back disc and the tabs — lifted onto it (Lh 2026-09-24) */
206
+ }
207
+ @media (prefers-reduced-motion: reduce) {
208
+ .stack-sheet > .head > .words__title.is-long { text-overflow: ellipsis; }
209
+ }
210
+
211
+ /* in a feed card */
212
+ .feed-card > .head { display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; min-height: var(--feed-card-head-h, var(--tap)); }
213
+ .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 */
214
+
215
+ /* ---- body: the content row, under the head and over the foot. What the member reads or fills in; in a sheet, a stack sheet
216
+ and a modal the one part that scrolls. A grid column from the top, the card's rhythm inside.
217
+ <div class="body">…controls, rows, small content cards…</div>
218
+ Knobs --body-gap (the card's gap) and --body-pad-top (none; a paper sheet's inset). An outer governs a body through those
219
+ knobs or a selector naming both, written below under the outer's name (Lh 2026-10-21). ---- */
220
+ .body {
221
+ display: grid;
222
+ gap: var(--body-gap, var(--card-gap));
223
+ align-content: start;
224
+ min-height: 0;
225
+ padding-top: var(--body-pad-top, 0rem);
226
+ overflow-y: auto;
227
+ overscroll-behavior: contain;
228
+ }
229
+ /* 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) */
230
+ .body:has(> .map--inline:only-child) { display: flex; flex-direction: column; }
231
+ .body:has(> .sheet-steps) { display: flex; flex-direction: column; }
232
+ .body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
233
+ .body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
234
+ .body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
235
+
236
+ /* in a sheet and a stack sheet */
237
+ /* everything under the head. Its scrollport runs to the card's side and bottom edges (reading --card-pad, the way .bleed does) so
238
+ content scrolls clear of the edge rather than stopping inside the padding, and a .bleed strip inside it still reaches the edges.
239
+ The outers are in a :where() so the body keeps the weight of one class, as the state rules below expect. */
240
+ :where(.sheet, .sheet-stack .card) > .body {
241
+ flex: 1;
242
+ margin-inline: calc(-1 * var(--card-pad));
243
+ padding-inline: var(--card-pad);
244
+ /* the part of the sheet that slid under the box's bottom edge cannot be
245
+ seen, and the scroller has to know that: scroll-padding is exactly "the
246
+ obscured part of my scrollport", so a focused field, scrollIntoView and
247
+ anchor navigation all land a control in the visible part, never under
248
+ the cut. A length from the box's container units (--_box), since the
249
+ cut is a share of the SHEET's height, not of the body's. */
250
+ scroll-padding-bottom: var(--_cover);
251
+ /* …and its content has to be able to LEAVE that part: the same length as bottom padding, so the last row
252
+ scrolls up to the visible edge at half and 3q as it does at full — everything between the head and the
253
+ foot can be reached at any state (Lh, 2026-09-21). Padding, not a shorter box: nothing that shows moves
254
+ when the state changes, only how far the body scrolls; a child that fills the body (a composer) fills
255
+ the part that shows. */
256
+ padding-bottom: var(--_cover);
257
+ }
258
+ .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) */
259
+ /* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
260
+ box is never smaller than its padding: the body runs past the sheet's clipped edge instead, where nothing
261
+ shows anyway (the sheet's other parts keep their size, sheet.css). At peek and away no part of the body shows. */
262
+ :is(.sheet--peek, .sheet--away) > .body { padding-bottom: 0; }
263
+ :where(.sheet, .sheet-stack .card) > .body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
264
+ :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)); }
265
+ /* 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).
266
+ The body and the foot read as paper; the handle and the head stay on the card.
267
+ The flex gap under the head is outside the body, so it stays the sheet's tint.
268
+ The first field would sit on that colour seam. The paper's inset is --sp-6 (24px at
269
+ the default spacing), the body's own top padding, and only when the sheet is paper.
270
+ A plain sheet keeps padding-top 0. A sheet in a stack wears the same class and the same
271
+ inset: the stack's own gap under the head is the card's colour, so the row still needs this paper.
272
+ The paper takes the side of the controls' paper (--action-dark): on a dark sheet its words still turn to onlight. */
273
+ .sheet--paper > .body {
274
+ --card-bg: var(--action-bg);
275
+ --dark: var(--action-dark);
276
+ background: var(--card-bg);
277
+ color: var(--card-ink);
278
+ padding-top: var(--body-pad-top, var(--sp-6));
279
+ }
280
+
281
+ /* in a sheet stack */
282
+ /* 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.
283
+ --_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
284
+ card's colour, so without the paper's inset the first row sits on that seam. */
285
+ .sheet-stack > .card > .body {
286
+ --_cover: 0rem;
287
+ padding-top: calc(var(--body-pad-top, 0rem) + var(--_refresh, 0rem));
288
+ transition: padding-top var(--motion-sheet) var(--ease);
289
+ }
290
+ /* 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
291
+ --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
292
+ stack's step and the body's own rows read it. */
293
+ .sheet-stack > .card.sheet--paper > .body {
294
+ margin-top: calc(var(--sp-3) - var(--card-gap));
295
+ padding-top: calc(var(--body-pad-top, var(--sp-6)) + var(--_refresh, 0rem));
296
+ transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
297
+ }
298
+ /* 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 */
299
+ .sheet-stack > .card.sheet--paper > .body.is-refreshing { transition: none; }
300
+ /* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
301
+ cover the corners, and a paper body showed there beside the head. Clear, the card's own colour
302
+ shows in that pocket. Open, the body is paper, and the first row sits --sp-6 down, the same paper
303
+ inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
304
+ would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
305
+ into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
306
+ sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
307
+ at the moment the sheet stops. Rising, the body is paper at once. */
308
+ .sheet-stack > .card.sheet--paper:not(.is-open) > .body {
309
+ background-color: transparent;
310
+ transition:
311
+ padding-top var(--motion-sheet) var(--ease),
312
+ background-color var(--motion-quick) var(--ease) calc(var(--stagger, 0) * var(--motion-stagger) + var(--motion-sheet) - var(--motion-quick));
313
+ }
314
+ @media (prefers-reduced-motion: reduce) { .sheet-stack > .card.sheet--paper:not(.is-open) > .body {
315
+ transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
316
+ } }
317
+ /* a body pulled to refresh holds still */
318
+ .sheet-stack > .card > .body.is-refreshing { transition: none; }
319
+
320
+ /* in a modal: the light part that holds the thing being asked about, padded on every side, shrinking (and scrolling) under the
321
+ modal's max-height while the ask keeps its height (layover.css) */
322
+ .modal > .body {
323
+ padding: var(--body-pad-top, var(--card-pad)) var(--card-pad) var(--card-pad);
324
+ flex: 0 1 auto;
325
+ }
326
+
327
+ /* ---- 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.
328
+ 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
329
+ side padding so the labels keep the room. <div class="foot"><button class="btn btn--primary btn--block">…</button></div>
330
+ Knobs --foot-gap, --foot-ink, --foot-link-gap. An outer governs a foot through those knobs or a selector naming both, written
331
+ below under the outer's name (Lh 2026-10-21). ---- */
332
+ .foot { display: flex; gap: var(--foot-gap, var(--sp-2)); align-items: center; color: var(--foot-ink, inherit); }
333
+ .foot:has(> :nth-child(3)) > .btn { --btn-pad-x: var(--sp-2); }
334
+ /* a small link under the buttons (Foot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
335
+ whole short row of its own, centred — it never shares the buttons' line */
336
+ .foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--foot-link-gap, var(--sp-1)); }
337
+ .foot__link { flex: 1 0 100%; display: flex; justify-content: center; }
338
+ /* 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
339
+ 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 */
340
+ .foot--round { justify-content: center; gap: var(--foot-gap, var(--sp-6)); }
341
+
342
+ /* in a card with paper bands: the foot band (the head's look, above) is slim */
343
+ :is(.card--bands, .card--bands > .card__face) > .foot { margin-bottom: calc(-1 * var(--card-pad)); padding-block: var(--sp-3); }
344
+
345
+ /* in a sheet stack */
346
+ /* a paper stack's foot is paper to the card's edges, and it covers the tinted flex gap above it so the
347
+ button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
348
+ where it was — the padding gives back what the bleed takes. Its ink reads the action's side. */
349
+ .sheet-stack > .card.sheet--paper > .foot {
350
+ --dark: var(--action-dark);
351
+ background: var(--action-bg);
352
+ color: var(--card-ink);
353
+ margin-top: calc(-1 * var(--card-gap));
354
+ margin-inline: calc(-1 * var(--card-pad));
355
+ margin-bottom: calc(-1 * var(--card-pad));
356
+ padding-top: var(--card-gap);
357
+ padding-inline: var(--card-pad);
358
+ padding-bottom: var(--card-pad);
359
+ }
360
+
361
+ /* 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).
362
+ 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 */
363
+ .sheet > .foot {
364
+ 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 */
365
+ align-items: end;
366
+ align-content: end;
367
+ padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
368
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
369
+ pointer-events: none;
370
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
371
+ transition: translate var(--motion-sheet) var(--ease), visibility 0s;
372
+ }
373
+ .sheet > .foot > * { pointer-events: auto; }
374
+ .sheet.is-dragging > .foot { transition: none; }
375
+ /* 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
376
+ LEAVES AND ARRIVES WITH ITS SHEET — solid the whole way, hidden only once the slide is over. It used to stay in place and
377
+ fade: the body slid through the strip behind a half-transparent button (timebank, Lh 2026-10-01: "ตัวหนังสือลอดใต้ปุ่ม"). */
378
+ :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); }
379
+ /* a paper sheet's foot is paper, as its body is: the words read the controls' side */
380
+ .sheet--paper > .foot { --card-bg: var(--action-bg); --dark: var(--action-dark); color: var(--foot-ink, var(--card-ink)); }