@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/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)); }
package/css/sheet.css CHANGED
@@ -34,8 +34,8 @@
34
34
  the listed states, and the state it lands on becomes the one a tap
35
35
  returns to.
36
36
  <button class="sheet__handle" type="button" aria-label="…"></button>
37
- <div class="card__head"><h2 class="t-h2">…</h2>…</div>
38
- <div class="sheet__body">…controls, or small content cards…</div>
37
+ <div class="head"><h2 class="words__title t-h2">…</h2>…</div>
38
+ <div class="body">…controls, or small content cards…</div>
39
39
  </article>
40
40
  Written top-down: the handle, the title row (what peek shows), then
41
41
  the body. The handle and the head stay where they are at every
@@ -107,38 +107,9 @@
107
107
  .sheet--away { --_y: 100%; --_cover: var(--_box); visibility: hidden; transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--ease-linear) var(--motion-sheet); }
108
108
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
109
109
 
110
- /* the body — everything under the head, the one thing in a sheet that
111
- scrolls. Its scrollport runs to the card's side and bottom edges (reading
112
- --card-pad, the way .bleed does) so content scrolls clear of the edge
113
- rather than stopping inside the padding, and a .bleed strip inside it
114
- still reaches the edges. The card's own grid rhythm continues inside. */
115
- .sheet__body {
116
- flex: 1;
117
- min-height: 0;
118
- display: grid;
119
- gap: var(--card-gap);
120
- align-content: start;
121
- margin-inline: calc(-1 * var(--card-pad));
122
- padding-inline: var(--card-pad);
123
- overflow-y: auto;
124
- overscroll-behavior: contain;
125
- /* the part of the sheet that slid under the box's bottom edge cannot be
126
- seen, and the scroller has to know that: scroll-padding is exactly "the
127
- obscured part of my scrollport", so a focused field, scrollIntoView and
128
- anchor navigation all land a control in the visible part, never under
129
- the cut. A length from the box's container units (--_box), since the
130
- cut is a share of the SHEET's height, not of the body's. */
131
- scroll-padding-bottom: var(--_cover);
132
- /* …and its content has to be able to LEAVE that part: the same length as bottom padding, so the last row
133
- scrolls up to the visible edge at half and 3q as it does at full — everything between the head and the
134
- foot can be reached at any state (Lh, 2026-09-21). Padding, not a shorter box: nothing that shows moves
135
- when the state changes, only how far the body scrolls; a child that fills the body (a composer) fills
136
- the part that shows. */
137
- padding-bottom: var(--_cover);
138
- }
139
110
  /* ---- the sheet's foot: the button that FINISHES the sheet, on the sheet (Lh, 2026-09-20) ----
140
111
  A screen with a sheet has no bar in front of it: the button that completes what the sheet asks — the
141
- page's conclusion — is a .card__foot, the sheet's last child, on the sheet's own material. The sheet is
112
+ page's conclusion — is a .foot, the sheet's last child, on the sheet's own material. The sheet is
142
113
  full height at every state, so its real bottom edge is under the cut unless it stands at full. So the
143
114
  foot is a layer as tall as the sheet, its buttons at its bottom, slid UP by the very share the sheet
144
115
  slid down (--_y, + the drag): the two cancel, and the buttons stand on the box's bottom edge at half,
@@ -146,18 +117,11 @@
146
117
  length — so a box that resizes (the keyboard) moves them as one, with nothing to catch up. The layer
147
118
  takes no taps, its buttons do; the strip behind them is the sheet's ground, --_foot tall, and the body
148
119
  stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
149
- One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
120
+ One row, ≤ 2 buttons (parts.css). <div class="foot"><button class="btn btn--primary btn--block">…</button></div> */
150
121
  /* …and the link row under the button, when there is one (its line + the gap over it) */
151
- .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) */
152
- .sheet > .card__foot > * { pointer-events: auto; }
153
- .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) */
154
- /* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
155
- box is never smaller than its padding — so the pinned parts must not give way to it: the body runs past
156
- the sheet's clipped edge instead, where nothing shows anyway. At peek and away no part of the body shows. */
157
- .sheet > :not(.sheet__body) { flex-shrink: 0; }
158
- :is(.sheet--peek, .sheet--away) > .sheet__body { padding-bottom: 0; }
159
- .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
160
- .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover) + var(--_safe-b-open)); }
122
+ .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) */
123
+ /* the body's padding can be taller than its own box, so the pinned parts must not give way to it (parts.css) */
124
+ .sheet > :not(.body) { flex-shrink: 0; }
161
125
 
162
126
  /* grab handle — first child of the sheet, but it does not take a row: it sits in the
163
127
  card's top padding, the same air as a stack sheet's four dots (stack.css --_grip-y),
@@ -183,22 +147,6 @@
183
147
  touch-action: none;
184
148
  cursor: grab;
185
149
  }
186
- /* 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).
187
- The body and the foot read as paper; the handle and the head stay on the card.
188
- The flex gap under the head is outside the body, so it stays the sheet's tint.
189
- The first field would sit on that colour seam. The paper's inset is --sp-6 (24px at
190
- the default spacing), the body's own top padding, and only when the sheet is paper.
191
- A plain sheet keeps padding-top 0. A sheet in a stack wears the same class and the same
192
- inset: the stack's own gap under the head is the card's colour, so the row still needs this paper.
193
- The paper takes the side of the controls' paper (--action-dark): on a dark sheet its words still turn to onlight. */
194
- .sheet--paper > .sheet__body {
195
- --card-bg: var(--action-bg);
196
- --dark: var(--action-dark);
197
- background: var(--card-bg);
198
- color: var(--card-ink);
199
- padding-top: var(--sp-6);
200
- }
201
- .sheet--paper > .card__foot { --card-bg: var(--action-bg); --dark: var(--action-dark); --card-foot-ink: var(--card-ink); }
202
150
  .sheet__handle--dots {
203
151
  --_dot: var(--sp-1);
204
152
  --_step: calc(var(--_dot) * 2.5);
@@ -238,42 +186,6 @@
238
186
  .sheet__handle:focus-visible { outline: var(--focus); outline-offset: var(--sp-2); }
239
187
  .sheet.is-dragging .sheet__handle { cursor: grabbing; }
240
188
 
241
- /* the head — a title row at least --tap tall, either kind, so the peek strip
242
- (--sheet-peek) is the same for every sheet:
243
- - in a step it is a plain card head (card.css): the heading left, the
244
- contextual thing at the far right
245
- - .sheet__head is the stage sheet's three-column head: a round button,
246
- the centred title and sub, a round button
247
- - a label .row with its .check / .toggle may be the head when the sheet's
248
- whole ask is that switch: peek shows it (a row is --tap tall plus its own
249
- padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
250
- .sheet > .card__head { --card-head-min-h: var(--sheet-head-h); }
251
- /* a sheet's title is ONE line, always (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน" wrapped to two lines at 390px) — the
252
- 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
253
- 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
254
- measures --_over, the width it overflows by, and sets .is-long; one way — it waits, runs to its end, waits, and starts again
255
- from the first letter. The move is text-indent: the heading is the app's own element, there is no inner box to slide.
256
- Before the Sheet has measured, and where motion is off, it is cut with …. */
257
- .sheet > .card__head:not(:has(> :is(.t-caption, .t-body))) { --card-head-wrap: nowrap; }
258
- .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 */
259
- .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long {
260
- text-overflow: clip;
261
- 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 */
262
- }
263
- @keyframes sheet-title-run { 0%, 20% { text-indent: 0; } 85%, 100% { text-indent: calc(-1 * var(--_over)); } }
264
- @media (prefers-reduced-motion: reduce) {
265
- .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long { animation: none; text-overflow: ellipsis; }
266
- }
267
- .sheet__head {
268
- display: grid;
269
- grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
270
- align-items: center;
271
- gap: var(--sp-3);
272
- min-height: var(--tap);
273
- text-align: center;
274
- }
275
- .sheet__title { font: var(--sheet-title-font, var(--type-h2)); letter-spacing: var(--track-h2); margin: 0; }
276
- .sheet__sub { font: var(--sheet-sub-font, var(--type-caption)); color: var(--sheet-sub-ink, var(--card-muted)); }
277
189
 
278
190
  /* ============================================================
279
191
  .sheet-stage — the overlay stage: a page-height box the sheet
@@ -420,11 +332,11 @@
420
332
  }
421
333
 
422
334
  /* ---- the map IN FLOW (Lh 2026-10-17): `.map--inline` — a box in a card, or the whole of a sheet's body ----
423
- <div class="sheet__body"><div class="map map--inline"><div>…the engine…</div><label class="map-pick" …>…</label>…
335
+ <div class="body"><div class="map map--inline"><div>…the engine…</div><label class="map-pick" …>…</label>…
424
336
  <span class="map__ring" …></span></div></div>
425
337
  Choosing where to meet without leaving the sheet: the open sheet becomes the map and its head stays on top (a stage
426
338
  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
427
- 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
339
+ 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
428
340
  fills the body (the part above the foot), however far the sheet slid. */
429
341
  .map--inline {
430
342
  --_top: 0rem;
@@ -434,8 +346,9 @@
434
346
  border-radius: var(--map-radius, var(--r-media));
435
347
  overflow: hidden;
436
348
  }
437
- .sheet__body:has(> .map--inline:only-child) { display: flex; flex-direction: column; }
438
- .sheet__body > .map--inline:only-child { flex: 1 0 auto; }
349
+ .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 */
350
+ /* a map that is a body's only child fills it; the body turns to a column for it (parts.css) */
351
+ .body > .map--inline:only-child { flex: 1 0 auto; }
439
352
 
440
353
  /* ---- map pick: a place on the map the member taps to choose it (Lh 2026-10-17) ----
441
354
  <label class="map-pick" style="top: 30%; left: 40%"><input type="radio" name="meet" value="library">
@@ -524,7 +437,6 @@
524
437
  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
525
438
  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
526
439
  falls — no height is written. Nothing fades: done and next read at full ink. */
527
- .sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
528
440
  .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; }
529
441
  .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 */
530
442
  .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; }
@@ -558,8 +470,15 @@
558
470
  }
559
471
  .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; }
560
472
  .sheet-step__check { margin-inline-start: auto; }
561
- .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
562
- .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) */
473
+ /* 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
474
+ จุดนัด 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
475
+ 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
476
+ 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,
477
+ 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
478
+ 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
479
+ unitless 0 (SheetSteps · Ruled) is a length there but not in a calc, so the step stays right and only the bleed is lost. */
480
+ .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); }
481
+ .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) */
563
482
  .sheet-step__inner > * { min-width: 0; }
564
483
  .sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s var(--ease-linear) var(--motion-sheet); }
565
484
  .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; }
@@ -570,45 +489,7 @@
570
489
  /* ==== the step bar answers a press, as PRESS in actions.css answers every other control (#62: a block is written in its own file) ==== */
571
490
  .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
572
491
 
573
- /* ==== 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) ==== */
574
- .sheet__body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
575
- .sheet__body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
576
- .sheet__body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
577
492
 
578
- /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
579
- /* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private.
580
- A paper body adds its own --sp-6 on top of this (the rule below): the stack's gap under the head is the
581
- card's colour, so without the paper's inset the first row sits on that seam. */
582
- .sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0rem); transition: padding-top var(--motion-sheet) var(--ease); }
583
- /* 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
584
- --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
585
- stack's step and the body's own rows read it. */
586
- .sheet-stack > .card.sheet--paper > .sheet__body {
587
- margin-top: calc(var(--sp-3) - var(--card-gap));
588
- padding-top: calc(var(--sp-6) + var(--_refresh, 0rem));
589
- transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
590
- }
591
- /* 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 */
592
- .sheet-stack > .card.sheet--paper > .sheet__body.is-refreshing { transition: none; }
593
- /* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
594
- cover the corners, and a paper body showed there beside the head. Clear, the card's own colour
595
- shows in that pocket. Open, the body is paper, and the first row sits --sp-6 down, the same paper
596
- inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
597
- would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
598
- into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
599
- sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
600
- at the moment the sheet stops. Rising, the body is paper at once. */
601
- .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
602
- background-color: transparent;
603
- transition:
604
- padding-top var(--motion-sheet) var(--ease),
605
- background-color var(--motion-quick) var(--ease) calc(var(--stagger, 0) * var(--motion-stagger) + var(--motion-sheet) - var(--motion-quick));
606
- }
607
- @media (prefers-reduced-motion: reduce) { .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
608
- transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
609
- } }
610
493
  /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
611
494
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
612
495
 
613
- /* ---- where it sits in what stack.css draws: a body pulled to refresh holds still (docs/reference.md rule 15) ---- */
614
- .sheet-stack > .card > .sheet__body.is-refreshing { transition: none; }