@usableapp/cardds 0.7.106 → 0.7.108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/bin/cardds.mjs +5 -3
  3. package/cardds.css +1 -0
  4. package/css/actions.css +9 -9
  5. package/css/base.css +6 -9
  6. package/css/card.css +19 -186
  7. package/css/choice.css +26 -1
  8. package/css/finish.css +4 -4
  9. package/css/journey.css +7 -3
  10. package/css/layover.css +4 -14
  11. package/css/lists.css +6 -18
  12. package/css/media.css +1 -7
  13. package/css/palette.css +1 -1
  14. package/css/parts.css +380 -0
  15. package/css/sheet.css +12 -139
  16. package/css/stack.css +15 -26
  17. package/css/stamp.css +1 -1
  18. package/css/step.css +3 -3
  19. package/css/theme.template.css +1 -1
  20. package/css/tokens.css +4 -4
  21. package/dist/actions/BarNote.d.ts +2 -2
  22. package/dist/actions/BarNote.js +2 -2
  23. package/dist/actions/Btn.d.ts +2 -2
  24. package/dist/actions/Btn.js +1 -1
  25. package/dist/actions/BtnRow.d.ts +1 -1
  26. package/dist/actions/BtnRow.js +1 -1
  27. package/dist/actions/IconBtn.d.ts +1 -1
  28. package/dist/actions/IconBtn.js +1 -1
  29. package/dist/cardds.css +500 -426
  30. package/dist/cards/Card.d.ts +6 -22
  31. package/dist/cards/Card.js +11 -22
  32. package/dist/cards/CardFeed.d.ts +1 -1
  33. package/dist/cards/CardFeed.js +5 -5
  34. package/dist/cards/CardParts.d.ts +2 -13
  35. package/dist/cards/CardParts.js +2 -13
  36. package/dist/cards/cardGo.js +1 -1
  37. package/dist/cards/cardGrounds.js +2 -2
  38. package/dist/catalog.json +1 -1
  39. package/dist/choice/Calendar.d.ts +12 -3
  40. package/dist/choice/Calendar.js +17 -5
  41. package/dist/choice/CalendarWeeks.d.ts +10 -2
  42. package/dist/choice/CalendarWeeks.js +18 -4
  43. package/dist/choice/HourGrid.d.ts +36 -0
  44. package/dist/choice/HourGrid.js +21 -0
  45. package/dist/choice/calendarDay.d.ts +7 -2
  46. package/dist/choice/calendarDay.js +6 -3
  47. package/dist/choice/span.d.ts +34 -0
  48. package/dist/choice/span.js +80 -0
  49. package/dist/forms/Field.d.ts +1 -1
  50. package/dist/forms/Field.js +1 -1
  51. package/dist/index.d.ts +8 -5
  52. package/dist/index.js +7 -5
  53. package/dist/journey/JourneyBadge.d.ts +2 -1
  54. package/dist/lists/Kv.d.ts +4 -2
  55. package/dist/lists/Kv.js +3 -3
  56. package/dist/lists/Row.d.ts +2 -28
  57. package/dist/lists/Row.js +2 -17
  58. package/dist/media/MapArea.d.ts +2 -2
  59. package/dist/media/MapArea.js +1 -1
  60. package/dist/media/Tile.d.ts +3 -7
  61. package/dist/media/Tile.js +5 -5
  62. package/dist/parts/Body.d.ts +7 -0
  63. package/dist/parts/Body.js +11 -0
  64. package/dist/parts/End.d.ts +10 -0
  65. package/dist/parts/End.js +7 -0
  66. package/dist/parts/Foot.d.ts +17 -0
  67. package/dist/parts/Foot.js +11 -0
  68. package/dist/parts/Head.d.ts +15 -0
  69. package/dist/parts/Head.js +34 -0
  70. package/dist/parts/Lead.d.ts +10 -0
  71. package/dist/parts/Lead.js +9 -0
  72. package/dist/parts/Words.d.ts +13 -0
  73. package/dist/parts/Words.js +20 -0
  74. package/dist/parts/headCtx.d.ts +7 -0
  75. package/dist/parts/headCtx.js +9 -0
  76. package/dist/scaffold/EmptyState.d.ts +1 -1
  77. package/dist/scaffold/EmptyState.js +1 -1
  78. package/dist/scaffold/Screen.js +1 -1
  79. package/dist/sheets/Modal.d.ts +3 -7
  80. package/dist/sheets/Modal.js +3 -7
  81. package/dist/sheets/Sheet.d.ts +2 -2
  82. package/dist/sheets/Sheet.js +8 -9
  83. package/dist/sheets/SheetStack.d.ts +3 -3
  84. package/dist/sheets/SheetStack.js +4 -4
  85. package/dist/sheets/SheetSteps.d.ts +1 -1
  86. package/dist/sheets/SheetSteps.js +2 -2
  87. package/dist/sheets/StackBack.d.ts +1 -1
  88. package/dist/sheets/StackBack.js +1 -1
  89. package/dist/sheets/StackSheet.js +7 -7
  90. package/dist/sheets/sheetMeasure.js +3 -3
  91. package/dist/sheets/sheetStepsPile.js +2 -2
  92. package/dist/sheets/stackCard.js +6 -6
  93. package/dist/templates/CallSheet.js +6 -5
  94. package/dist/templates/Walkthrough.js +3 -2
  95. package/docs/reference.md +76 -37
  96. package/lib/catalog.mjs +1 -1
  97. package/lib/mcp.mjs +1 -1
  98. package/lint/rules.mjs +14 -7
  99. package/package.json +2 -1
  100. package/scripts/codemod-parts.mjs +403 -0
  101. package/dist/cards/CardFoot.d.ts +0 -15
  102. package/dist/cards/CardFoot.js +0 -9
  103. package/dist/cards/CardHead.d.ts +0 -21
  104. package/dist/cards/CardHead.js +0 -39
  105. package/dist/cards/Stat.d.ts +0 -19
  106. package/dist/cards/Stat.js +0 -14
  107. package/dist/sheets/SheetBody.d.ts +0 -5
  108. package/dist/sheets/SheetBody.js +0 -9
  109. package/dist/sheets/SheetHead.d.ts +0 -15
  110. package/dist/sheets/SheetHead.js +0 -6
package/CHANGELOG.md CHANGED
@@ -2,6 +2,127 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap. Each release section after 0.7.97 carries one `Components:` line, written by `node scripts/components-changed.mjs <previous tag>`: the components the release changed, or `all` when a file changed that any component can read (a token, the build), or `none`; check only the screens that import those.
4
4
 
5
+ ## 0.7.108 — 2026-10-22
6
+
7
+ `KvGrid centre` (Lh 2026-10-21, the code on timebank's request card): the one number at the centre of the room it stands in, a turning card's back. A `JourneyBadge corner` on a `CardMedia` is the card's paper, its corner concentric with the media's; a theme's two badge knobs still win. From when to when (timebank, Lh 2026-10-21): the asker says which days and which hours they need, the helper answers within them.
8
+
9
+ Components: all (css/palette.css: orchard's badge rule now leaves the media corner badge out, a file any component can read; what a consumer sees change is KvGrid, KvCell, JourneyBadge, CardMedia, Card, Calendar, CalendarWeeks and HourGrid (new), and `scripts/codemod-parts.mjs`, the migration tool, under Fixed)
10
+
11
+ ### Added
12
+ - **`KvGrid centre`: one cell at the centre of the room it stands in, a turning card's back** (Lh 2026-10-21, timebank's request card: the six-digit code the helper types, big, mono, selectable, centred in the middle band over its muted caption; the place `Stat centre` held, as `Head centre` is named). Before → after: a lone `KvCell` in a `KvGrid` on a `Card back` stood left-aligned at the top of the middle band, and the only way to centre it was a hand-written grid around a `Text level="stat"` (cardds's own story did that) → `<KvGrid centre>` is as tall as its holder (`min-height: 100%`, not `height`: in a plain card, a grid of auto rows, a percent height resolved short and the cell overflowed it) and stands its cells at the middle of it, each value and caption centred (`.kv-grid--centre`, `align-content` · `text-align` · `justify-items`); on a plain card, which has no height of its own, the grid hugs its cell and only centres across. The value is the consumer's: a code is `<Text level="stat" mono selectable as="span">490734</Text>`. No new knob (`--kv-cell-gap` sets the space between the value and the caption); `.card__middle` untouched, no rule in lists.css writes another piece's properties. A consumer writes `<KvGrid centre>` in a `Card back` and nothing else changes. Stories: `Kv/KvGridCentre` (new); `Card/TurnsCode` and `Card/TurnsLongTitle` draw their back with it in place of an inline-style grid (inline `style` left in stories: 37, each a knob, a story-only layout, a `display: contents` remount wrapper, the palette demo's surface or the type-scale table; the QR back `TURN_BACK` keeps its three). Style snapshot: only those two stories moved (two elements more on each back) and the new one; the other 498 identical. Tests: tests/kv-grid-centre.spec.js (2); 537 → 538 classes (`.kv-grid--centre`), 719 knobs unchanged. The catalog answers "one big code number centred in the middle of a card back" with KvGrid, then KvCell.
13
+
14
+ | before | after |
15
+ | --- | --- |
16
+ | `back={<div style={{ display: 'grid', justifyItems: 'center', alignContent: 'center', height: '100%' }}><Text level="stat" mono>490734</Text><Text level="caption">…</Text></div>}` | `back={<KvGrid centre><KvCell value={<Text level="stat" mono selectable as="span">490734</Text>} label="…" /></KvGrid>}` |
17
+ - **A run of days on `Calendar` and `CalendarWeeks`: `span` + `onSpanChange`, or `defaultSpan`.** Before → after: a calendar held ONE day (`selected`), so a request for several days was one calendar per day or a field typed by hand → `span={[first, last]}` covers a run: one drag across the days covers them (the first cell under the finger is one end, the last the other, whichever way it went), or a tap on the first day and a tap on the last; the whole run is ONE highlight, the same colour throughout as a finger dragged over words (Lh 2026-10-21: no picked first or last day), a pill that bridges the grid's gaps; a covered cell carries `aria-pressed`. The app is asked once, when the finger lifts or the second tap closes the run; a third tap starts over. A disabled day is never covered: the run stops before it, so the helper's answer stays inside the asked days (the app marks the asked days `soft` and disables the rest; `Calendar/SpanWithin`). On `Calendar` the span is `[day, day]` (numbers, as `selected`); on `CalendarWeeks` it is `[key, key]` and a drag covers along a row while a finger moving up or down still scrolls the box (`touch-action: pan-y`); two taps cover across the weeks. A key on a cell is a tap. `selected` is not read while `span` is given. Knob `--calendar-span-bg` (the highlight, 25% accent by default: the helper's run must read over the asked days' 12% marks). Gallery `Calendar/Span` · `Calendar/SpanWithin` · `CalendarWeeks/Span`.
18
+ - **`HourGrid`: the hours of a day from when to when.** Before → after: an hour was one `ChipPick` in a `ChipGrid` (one slot, no span) or a `Picker` (one value) → `<HourGrid hours={…} label="น." span={[from, to]} onSpanChange={…} />` lays the hours out in Calendar's own 44px round cells, `--hour-grid-cols` to a row (6), a label over them, and covers a run by the same drag or two taps; `selected` + `onSelect` picks one hour instead. It knows no clock: the app builds `hours: { key, num, disabled?, mark?: 'soft' | 'strong' }[]`, as `CalendarWeeks` builds its days. On a sheet, never in a Card. Gallery `HourGrid/Default` · `Within` · `OneHour` · `DaysThenHours` (the when step of an ask: the weeks, then the hours).
19
+ - **A request card's date and time say the span.** Nothing new to import: the card's date box (`JourneyBadge stretch`) takes the run (`label="พฤ–อา" value="18–21" sub="มิ.ย."`) and the clock chip its hours (`09:00–12:00`), the app's words as before. New knob `--tile-badge-stretch-w` (the stretch badge's width, `calc(var(--fs-lg) * 3.25)` as before): two days need more than one, so the app sets it on that badge; a stretch badge's lines no longer break at a dash. Gallery `Card/DateSpanHead` shows the request card so headed.
20
+ - **One mechanic, three holders.** The span is written once (`src/choice/span.ts`, `useSpan`): `Calendar`, `CalendarWeeks` and `HourGrid` number their cells and spread its handlers; the cell is still `calendarDay` (`.calendar__day`), so every `--calendar-day-*` knob reaches the hours too. The grid's columns are now `--_cols` (7 unless the holder says otherwise). `tests/gallery-span.spec.js` holds the drag, the two taps, the clip at a disabled cell, the unbroken highlight and the keyboard.
21
+
22
+ ### Changed
23
+ - **A badge on the corner of a picture is the paper, its corner concentric with the media's** (ruled Lh 2026-10-21, chosen from three screenshots side by side: the distance on a request card is information, and a highlight-filled badge reads as a control). Before → after: a `JourneyBadge corner` on a `CardMedia` took the badge's own look, the dark `--card-3-bg` fill with the badge corner (`--r-badge`, 13 at 375), and under orchard the highlight yellow; the stories that wanted the paper set three properties by hand in `style` (`--tile-badge-bg`, `--dark`, `--tile-badge-radius`), which an app on the package could not → `.card__media > .tile-badge--corner` (journey.css) is `--card-1-bg` with the paper's ink (`--dark: var(--card-1-dark)`) and its corner is `--r-media` less the badge's inset `--sp-3` (8.8 at 375), so the two arcs share a centre; orchard's highlight rule leaves this badge out. A theme's `--tile-badge-bg` and `--tile-badge-radius` still win (read first, as before). Nothing changes for a consumer unless it set those knobs by hand on such a badge, which it may now drop. A badge in a head (`Tile · JourneyBadgeGate`, the date on `Card · Turns`) is unchanged: the head's concentric rule and orchard's highlight hold there. Stories that moved (the style snapshot; only the badge's background, its ink and the ink's border colour, and its radius): `Card · Turns`, `TurnsTwoLineTitle`, `TurnsCode`, `TurnsControlled`, `TurnsRoundFoot` (the radius only, 21.6 → 8.8: they had the paper by hand, with the card's corner less its padding; the inline style is gone), `CardMedia · CardMediaRated`, `Placeholder · Md`, `Mosaic · PhotoCard`, `Tile · JourneyBadgeCorner` (dark fill, white ink, 13 → white paper, onlight ink, 8.8). `Card · PaperBands` and `TurnsLongTitle` have no corner badge and did not move. Inline `style` objects left in the stories: 37 (was 38). Test: `tests/gallery-inline.spec.js` reads the badge's computed background and radius against the resolved tokens, on orchard too, and the head badge's highlight.
24
+
25
+ ### Fixed
26
+ - **On a dark tone's two-tone card the middle's secondary words take the full ink** (the ink gate, 2026-10-22, found by the caption under the code on `Card back`). Before → after: a `Card paper="bands"` on tone 3 or 5 lightens its middle to a mid grey (#adadad, #bbbbbb in wireframe) and declared the paper's side for it, so a muted word there (`Text muted`, a `KvCell` label, a `Words meta`) read at 3.2:1 and 3.8:1, under the 4.5:1 words need → the middle of a tone 3 or 5 bands card declares `--card-muted` as `--card-ink`, so its secondary words are the full ink there; tones 1, 2 and 4 are unchanged, and a theme's `--card-middle-bg` is read as before. Components: Card. The ink gate (tests/ink.spec.js) passes on every palette; no story as written moved.
27
+ - **`npx cardds codemod-parts` moves a `Row`'s retired props, imports a part once, and keeps a conditional foot conditional** (timebank #372 ran 0.7.107's version and found the three). Before → after: (1) a `Row` still on the props 0.7.106 retired (`title`, `meta`, `lead`, `leadIcon`, `body`, `end`, `endValue`, `endCaption`) was left as it was, and `--check` said nothing of it → it is written in parts, in the 0.7.106 table's order (`<Row><Lead icon="gift" /><Words title meta>{body}</Words><End value caption />{end}</Row>`; `lead` and `end` stand bare, as the table says; `lg`, `done`, `as`, `htmlFor`, `href`, `more`, `open`, `defaultOpen`, `onOpenChange` stay on the Row), and `--check` names `Row title`, `Row meta`, `Row lead`, `Row leadIcon`, `Row end`, `Row body`, `Row endValue`, `Row endCaption` per line. (2) `Head`, `Words` and `Foot` were added to an `import type { … } from 'cardds'` as well as to the value import, so a file with both failed with TS2300 → a part goes to the value import only; the type import has its retired type names renamed (`CardFootProps` → `FootProps`, and every use in the file), and a file with only a type import gets a value import after it. (3) `foot={cond ? x : undefined}` became an always-drawn `<Foot>{cond ? x : undefined}</Foot>`, an empty foot when `cond` is false → `{cond ? <Foot>{x}</Foot> : null}` (also `: null`, and `cond && x` → `{cond && <Foot>{x}</Foot>}`); a plain `foot={value}` is still `<Foot>{value}</Foot>`, since the script cannot know whether `value` may be undefined. A consumer that migrated its screens with 0.7.107's version runs `npx cardds codemod-parts --check <files…>` again to find the Rows it left, then the rewrite on those files. tests/codemod-parts.spec.js, fixtures `row-`, `type-import-`, `foot-if-`.
28
+
29
+ ## 0.7.107 — 2026-10-21
30
+
31
+ Every outer holds the same rows (Lh 2026-10-21): a `Card`, a `Sheet`, a stack sheet and a `Modal` are the paper and its behaviour, and inside each stand a `Head`, a `Body` and a `Foot`; a line is `Lead` · `Words` · `End`, the parts a `Row` and a `Card row` are both made of. Eleven parts and seven `Card` props are gone with no alias; `npx cardds codemod-parts <files>` moves a screen. Also: `Stat`, `StatRow`, `Delta` and `Tile stat` removed (a number is a `NumberBand` or a `KvCell`); the gallery's nav is one grouping by `src/` folder in a derived order; a scoped playwright run takes ports of its own.
32
+
33
+ Components: all (`css/tokens.css` lost `.delta` from its surface list; a new css file, `css/parts.css`, holds the parts and every outer's rule over them; any screen that imported a retired name fails the typecheck, see the table)
34
+
35
+ **Moving a screen.** Run the first row; it rewrites the rest. `--check` exits 1 naming each tag, prop, import or story it would still rewrite. It reads the TypeScript compiler from the project.
36
+
37
+ | before | after |
38
+ | --- | --- |
39
+ | a screen on the retired names | `npx cardds codemod-parts <files…>` (`--check` to list what is left) |
40
+ | `CardHead` · `SheetHead` · `Card heading caption end level` | `<Head><Words title meta level />{end}</Head>`, the first child (`SheetHead` → `Head centre`) |
41
+ | `SheetBody` · `ModalBody` | `<Body>` |
42
+ | `CardFoot` · `Card foot footLink footRound` | `<Foot link round>…</Foot>`, the last child |
43
+ | `RowLead` · `CardThumb` | `<Lead>` |
44
+ | `RowBody` · `CardBody` (in a `Card row`) | `<Words>` |
45
+ | `RowEnd` · `CardEnd` | `<End>` |
46
+ | `--card-head-*` · `--sheet-title-font` · `--sheet-sub-*` | `--head-*` · `--words-title-font` · `--words-meta-*` |
47
+ | `--card-foot-*` | `--foot-*` |
48
+ | `--row-lead-*` · `--card-thumb-*` · `--row-body-gap` · `--row-title-font` · `--row-meta-*` · `--row-end-gap` · `--card-end-gap` | `--lead-*` · `--words-gap` · `--words-title-font` · `--words-meta-*` · `--end-gap` |
49
+ | `.card__head` · `.sheet__head` · `.sheet__body` · `.modal__lift` · `.card__foot` · `.row__lead` · `.row__body` · `.row__end` | `.head` · `.body` · `.foot` · `.lead` · `.words` · `.end` |
50
+
51
+ The four entries on the parts under Changed below say each move in full: what it was, what governs it now, the stories that moved page, and the counts.
52
+
53
+ ### Removed
54
+ - **`Stat`, `StatRow`, `Delta` and `Tile stat` are gone: a number is a `NumberBand` or a `KvCell`** (ruled Lh 2026-10-20, carried in 0.7.104 as "no new caller"; removed now). Before → after: a label, a big number and a caption stood in a `Stat` (a `StatRow` put a `Delta` "+7.5%" beside the label), and a value with its icon and label in a cell was a `Tile stat` → the number that is a card of its own is a `NumberBand` (`label` · `num` · `foot`); a number in a cell, alone or beside others, is a `KvCell` in a `KvGrid` (`icon` · `value` · `label`); a change beside it is words in the cell's label, not a pill. `Tile` loses `stat` and `value` (`.tile--stat`, `.tile__value`); `.stat`, `.stat--centre`, `.stat__row`, `.delta`, `.delta--neg` and the knobs `--stat-gap`, `--stat-row-gap`, `--tile-stat-gap`, `--tile-stat-min-h`, `--tile-value-font` and the eight `--delta-*` are gone (738 → 725 knobs). The root token `--neg-dark` stays (a theme may set it; nothing in cardds reads it now). A screen on the old names fails the typecheck (`Stat`, `StatRow`, `Delta` are not exported; `stat` and `value` are no props of `Tile`). Migration:
55
+
56
+ | before | after |
57
+ | --- | --- |
58
+ | `<Stat><Text level="label" muted>Today</Text><Text level="stat">6.5 h</Text></Stat>` in a card or a sheet | `<KvGrid><KvCell value="6.5 h" label="Today" /></KvGrid>` |
59
+ | a `Stat` that was the whole card | `<NumberBand label={<>Hours<br />given</>} num="18" foot="…" />` |
60
+ | `<StatRow><Text level="label">Gross revenue</Text><Delta>+7.5%</Delta></StatRow>` | `label="Gross revenue · +7.5% vs last week"` on the `KvCell` |
61
+ | `<Tile stat icon="droplet" value="38 %" sub="Humidity" />` | `<KvCell icon="droplet" value="38 %" label="Humidity" />` |
62
+ | `Stat grow` (the room beside a `Ring`) · `Stat centre` (the one number centred under an avatar) | nothing on `KvCell` or `NumberBand` does this; a screen that needs it reports the gap |
63
+
64
+ Stories gone: `Stat/*` (four) and `Tile/StatOnEachTone` (the ink check reads `Kv/KvGridKami` on every tone). Migrated to a `KvCell`: `Avatar/WithIconXL`, `BtnGroup/Week`, `Home`'s wallet tabs, `Kv/Wallet`, `Ring/Default`, `Ring/InAHead`, `Tile/Dashboard`, `Track/OnADarkCard`.
65
+
66
+ ### Changed
67
+ - **One thing on a turning card's face may take the whole middle** (Lh 2026-10-21, the QR on timebank's request card: bigger, centred, the whole room). Before → after: the middle of a `Card back` face was a grid whose one row hugged its content, so a child given `height: 100%` (a QR with its caption, a code) stood at the top of an empty band, and nothing an app wrote could centre it or fill the room → the middle is a flex column of the same gap, so a child's `height: 100%` is the middle's height and it may centre or fill. Nothing for a consumer to change; a face whose middle is words stands as before (style snapshot: only the five `Card · Turns*` stories moved, and only by this). The story's back now draws the QR as a square as big as the room above its caption (`min(100%, 100cqh)` in a size container), centred. Components: Card. Stories `Card/Turns*`.
68
+ - **A line is made of one set of parts, `Lead` · `Words` · `End`, whether it is a `Row` or a `Card row`; the six parts each whole had of its own are gone** (Lh 2026-10-21, option A of three: the inner set is one, named without an outer prefix; the outer governs an inner's look only through the inner's knobs or a selector naming both, in the inner's file). Before → after: a row's parts were `RowLead`, `RowBody`, `RowEnd` (`.row__lead`, `.row__body` > `.row__title` · `.row__meta`, `.row__end`, lists.css) and a row card's were `CardThumb`, `CardBody`, `CardEnd` (`.card__thumb`, `.card__body`, `.card__end`, card.css), two names and two sets of knobs for one shape → the three parts live in `src/parts/` and draw `.lead`, `.words` > `.words__title` · `.words__meta`, `.end` (css/parts.css, after lists.css), the same props as before (`Lead icon`; `Words title meta`, and `level` reserved for a head's words; `End value caption`), and both wholes take them as direct children in that order: `<Row><Lead icon="gift" /><Words title="…" meta="…" /><End value="+1.0 h" caption="Tue" /></Row>`, `<Card row><Lead>01</Lead><Words><Text level="title">…</Text></Words><End>…</End></Card>`. A row card's lead keeps its 44 box in the card's ink and label type (`.card--row > .lead`, and a `FeedCard`'s icon lead the same), a row's its 48 box; the row's words keep `min-width: 0` and its end `flex: none` (`.row > .words`, `.row > .end`). No story's look moved: the style snapshot compares the computed look only now, so an element renamed (or a div that became a span) with the same values is no move, and a story shown on another page is named as a move of page. The catalog's `parts` is the folder rule, an export of `src/parts/`, and a part's `partOf` became `fits`, the wholes the stories stand it in, read from the previews (`cardds get Row` → `parts: Lead · Words · End`, `get Lead` → `fits: Card · Row`, `get End` → `Card · CardHead · Row`); `CardHead`, `CardFoot`, `SheetBody` are no parts until their PRs. The gallery has a `parts` group (End, Lead, Words, their row-card stories lifted from the Card page), and the `CardBody` page became `CardList` and `CardMedia`. Migration, run `npx cardds codemod-parts <files…>` on a screen (`--check` exits 1 naming what is left), or by hand:
69
+
70
+ | before | after |
71
+ | --- | --- |
72
+ | `<RowLead icon="gift" />` | `<Lead icon="gift" />` |
73
+ | `<RowBody title="…" meta="…" />` · `<RowBody><Wave /></RowBody>` | `<Words title="…" meta="…" />` · `<Words><Wave /></Words>` |
74
+ | `<RowEnd value="+1.0 h" caption="Tue" />` | `<End value="+1.0 h" caption="Tue" />` |
75
+ | `<CardThumb>01</CardThumb>` in a `Card row` (or a `FeedCard`'s `icon`) | `<Lead>01</Lead>` |
76
+ | `<CardBody>…</CardBody>` in a `Card row` | `<Words>…</Words>` (a `CardBody` outside a `Card row` the codemod leaves and names: it was never one) |
77
+ | `<CardEnd>…</CardEnd>` in a row card or a head's `end` | `<End>…</End>` |
78
+ | `--row-lead-size` · `--row-lead-radius` · `--row-lead-border` | `--lead-size` · `--lead-radius` · `--lead-border` (and `--lead-font`, the row card's label type) |
79
+ | `--card-thumb-size` · `--card-thumb-radius` · `--card-thumb-font` | the `.card--row > .lead` defaults of `--lead-size` · `--lead-radius` · `--lead-font` (set the `--lead-*` knob on the list or the card) |
80
+ | `--row-body-gap` · `--row-title-font` | `--words-gap` · `--words-title-font` |
81
+ | `--row-meta-font` · `--row-meta-ink` · `--row-meta-gap` | `--words-meta-font` · `--words-meta-ink` · `--words-meta-gap` |
82
+ | `--row-end-gap` · `--card-end-gap` | `--end-gap` |
83
+
84
+ A screen on the old names fails the typecheck (the six are not exported). 725 → 722 knobs; 545 → 542 classes. Stories moved page, the same look: `Card/CardBodyTitleOnly` → `Words/WordsTitleOnly`, `Card/CardThumbInitialAndIcon` → `Lead/LeadInitialAndIcon`, `Card/CardEndInHead` → `End/EndInHead`, `CardListLinks` · `CardListPick` · `CardListWindow` → the `CardList` page, `CardMediaRated` · `CardMediaStrip` → the `CardMedia` page.
85
+ - **`Head` is the one head of a card, a sheet and a stack sheet** (Lh 2026-10-21, option A: every outer holds the same rows, a consumer changes the outer and keeps what is inside; the second of the inner parts). Before → after: a card's head was a `CardHead` (`heading` · `caption` · `end` · `level` · `top`, or children), written for you by `Card heading caption end level`, a stack sheet's head was a `CardHead` with children, and the stage sheet's was a `SheetHead` (`lead` · `title` · `sub` · `trail`), three heads for one row → one `Head` in `src/parts/` (`top`, `centre`, `long`, children): a lead (`Lead`, `StackBack`, `JourneyBadge`), the `Words` (`title`, `meta`, `level`), chips, an end (an `IconBtn`, a `Chip`, an `End`). `<Card foot={…}><Head><Words title="Sunday market" meta="Saturday" /><IconBtn sm icon="ellipsis" label="More" /></Head>…</Card>`. `Words` in a `Head` writes its title and its meta as the head's own children, flat: `h2.words__title.t-h2` (`h1` with `level="h1"`) then `span.words__meta.t-caption.t-muted`, so the caption still takes a row of its own under the title and the end. The stack's keyboard way in is kept: on the open sheet the title is a tab stop (`tabIndex` 0) and Enter or Space folds it; a title wider than its line still runs sideways once (`.is-long`, the Sheet's `text-indent`, the StackSheet's `translate`). `Card` no longer has `heading`, `caption`, `end`, `level`; it finds its `Head` child by type (a component of your own that wraps a `Head` is not found, as it was not with `CardHead`). `CardHead` and `SheetHead` are gone, no alias. `npx cardds codemod-parts <files…>` rewrites a screen (`--check` lists what still writes the old forms), and a story named `CardHeadX` or `SheetHeadX` is `HeadX`.
86
+
87
+ | before | after |
88
+ | --- | --- |
89
+ | `<CardHead heading="Recent" caption={date} end={<Chip on>3 new</Chip>} />` | `<Head><Words title="Recent" meta={date} /><Chip on>3 new</Chip></Head>` |
90
+ | `<CardHead level="h1" heading="Welcome" top />` | `<Head top><Words title="Welcome" level="h1" /></Head>` |
91
+ | `<CardHead><StackBack /><Text level="h2" caps>Shopping</Text><IconRow>…</IconRow></CardHead>` | `<Head>` and `</Head>` round the same children |
92
+ | `<Card heading="Alone" caption="…" end={<Chip>1</Chip>} foot={…}>` | `<Card foot={…}><Head><Words title="Alone" meta="…" /><Chip>1</Chip></Head>…</Card>` |
93
+ | `<SheetHead lead={a} title="Create a room" sub="Settings" trail={b} />` | `<Head centre>{a}<Words title="Create a room" meta="Settings" />{b}</Head>` |
94
+ | `--card-head-col-gap` · `--card-head-row-gap` · `--card-head-lead-gap` · `--card-head-title-lines` | `--head-col-gap` · `--head-row-gap` · `--head-lead-gap` · `--head-title-lines` |
95
+ | `--sheet-title-font` · `--sheet-sub-font` · `--sheet-sub-ink` | `--words-title-font` · `--words-meta-font` · `--words-meta-ink` |
96
+ | `--card-head-min-h` · `--card-head-wrap` (a sheet set them on its head) | the sheet still sets them, now private to `parts.css` (`--head-min-h`, `--head-wrap`) |
97
+ | `.card__head`, `.card__head--top`, `.sheet__head`, `.sheet__title`, `.sheet__sub`, `.feed-card__head`, `.stack-sheet__title` | `.head`, `.head--top`, `.head--centre`, `.words__title`, `.words__meta`, `.feed-card > .head`, `.stack-sheet > .head > .words__title` |
98
+
99
+ The title's type is one knob, `--words-title-font`, which a head reads (default the level's own face) and three outers set: the stack for its one-line peek (`--fs-h1` over `--lh-solid`, or `--type-display`), a `Page` for its one `h1` (the card heading's size), and a theme anywhere; a sheet's title is the default, so a theme that set `--sheet-title-font` sets `--words-title-font` on the sheet. Every rule that names a head moved into `css/parts.css` under its outer (`.sheet-stack > .card > .head`, `.sheet > .head`, `.stack-sheet > .head`, `.card--bands > .head`, `.feed-card > .head`), so the rule-15 gate lists no override. The stack sheet's title keeps the overflow it computed (`hidden`, from the head's three-line clamp, which outranked the `overflow-x: clip` of `.stack-sheet__title` on every sheet); those two dead declarations are gone. 722 → 717 knobs and 542 → 538 classes, both re-pinned. Gallery: the pages `CardHead`, `SheetHead` and `StackHead` are one `Head` page in `parts` (the page says it fits a Card, a Sheet, a SheetStack, a Modal and a Page; `cardds get Head` prints the elements the stories stand it in directly, `fits: Card · Page · Sheet` once a modal's head stands in its Body, and a whole's `parts` list its rows first, then a line's parts); stories `CardHeadCustom`, `CardHeadTop` are `HeadCustom`, `HeadTop` (Card), `SheetHeadLeadTrail`, `SheetHeadTitleOnly` are `HeadLeadTrail`, `HeadTitleOnly` (Sheet), `LongTitle` and `StackBackRevealed` and `IconRowInRow` stand on the page unchanged, and `Picker · Words` is `Picker · WordValues` (a story named `Words` shadowed the part).
100
+
101
+ The style snapshot of the 499 stories: no element of a card, a sheet or a stack head moved (a box-for-box comparison of every element's x, y, width and height names none, and the snapshot reports no style). Six stories stand on another page with the same look (`CardHead · CardHeadCustom`, `CardHeadTop`, `IconRowInRow` and `SheetHead · LongTitle`, `StackHead · StackBackRevealed` are on `Head`; `Picker · Words` is `WordValues`). Three stories changed shape with the stage sheet's head, whose title and sub were wrapped in a `div` that `Head` does not draw: `SheetStage · Map` 36 → 35 elements, `SheetHead · SheetHeadLeadTrail` and `SheetHeadTitleOnly` are `Head · HeadLeadTrail` and `HeadTitleOnly`. In them the head is the same height (60.5 with a sub, 48 without), the buttons and the title's text do not move; the title's own box is 4.4px taller (the head's room for Thai marks, `padding-block-start` 0.2em given back by a negative margin), the sub's box is the column wide and as tall as the body's line (219 × 33 at y 484.5, against 55 × 22 at y 491.5) with its text centred on the same middle, about 1.5px higher.
102
+ - **`Body` is the one content row of a sheet, a stack sheet and a modal** (Lh 2026-10-21, option A: every outer holds the same rows; the third of the inner parts). Before → after: a sheet's content stood in a `SheetBody` (`.sheet__body`, the scroller) and a modal's in a `ModalBody` (`.modal__lift`), two pieces with the same job → one `Body` (`.body`, `src/parts/Body.tsx`): a grid column from the top that scrolls, governed by each outer in `css/parts.css` with the outer in the selector (`.sheet > .body` stops above the foot's strip and keeps a focused control above the cut, `.sheet-stack > .card > .body` follows the pull to refresh, `.modal > .body` is padded and shrinks under the modal's height). A `Modal` with no `Body` is still the confirm; `ModalAsk` stays one piece. Run `npx cardds codemod-parts <files>`: it renames both tags, folds their imports into one `Body`, and renames a story named `SheetBody…`.
103
+
104
+ | before | after |
105
+ | --- | --- |
106
+ | `<Sheet><Head>…</Head><SheetBody>…</SheetBody></Sheet>` | `<Sheet><Head>…</Head><Body>…</Body></Sheet>` |
107
+ | `<Modal open><ModalBody>…</ModalBody><ModalAsk …/></Modal>` | `<Modal open><Body>…</Body><ModalAsk …/></Modal>` |
108
+ | `.sheet__body`, `.modal__lift` (a theme or a test that named them) | `.body`; in one outer only: `.sheet > .body`, `.modal > .body` |
109
+ | (none) | `--body-gap` (the card's gap) · `--body-pad-top` (none; a paper sheet's `--sp-6`) |
110
+
111
+ A screen on the old names fails the typecheck. `cardds check` now names a control written straight in a `Sheet` or a `Modal`, outside its `Body` (`field-off-sheet`: a sheet's children are its rows); a control in its `Head` (a `Dropdown` at the head's end) stays legal, and a `Body` in a `Card` is the card's (`field-in-card`). `cardds get Sheet` lists `parts: Head · Body` and `get Modal` `Body`; `get Body` prints `fits: Modal · Sheet` and `get Head` `Card · Page · Sheet` (a modal's head stands in its Body now), the elements the stories stand each in directly, so a stack sheet's parts are its `Card`'s and `get SheetStack` lists none. 717 → 719 knobs; 538 → 537 classes (the two retired become one). The style snapshot of the 499 stories names no story that moved; one stands on another page with the same look, `SheetBody · SheetBodyScrolls` → `Body · BodyScrolls`.
112
+ - **`Foot` is the one foot of a card, a sheet and a stack sheet, and `Card` has no foot props** (Lh 2026-10-21, option A: every outer holds the same rows; the fourth of the inner parts). Before → after: a card's buttons stood in a `CardFoot` (`.card__foot`, `link`, `round`), or `Card foot footLink footRound` wrote one for you as its last child → one `Foot` (`.foot`, `src/parts/Foot.tsx`, the same `link` and `round`), written as the card's or the sheet's last child; `Card`'s `foot`, `footLink` and `footRound` are gone. A turning card finds its `Foot` child by type and draws it on both faces, as it did its `CardFoot`. Each outer governs the foot in `css/parts.css` under its name: `.sheet > .foot` (the layer that stands on the box's bottom edge at every state), `.sheet-stack > .card.sheet--paper > .foot` (the paper band), and `.card--bands` (one rule with the head's band now, so the twin allowance went). The data props of the templates stay: `StackSheet foot`, `Home sheets[].foot`, `CallSheet` `foot` and `footLink`, `FeedCard foot` render a `Foot`. Run `npx cardds codemod-parts <files>`: it renames `CardFoot`, writes a `Card`'s foot props as a `<Foot>` last child (a fragment's children unwrapped), drops a `footRound` that drew no foot, and renames a `CardFoot…` story.
113
+
114
+ | before | after |
115
+ | --- | --- |
116
+ | `<CardFoot><Btn>Later</Btn><Btn primary>Join</Btn></CardFoot>` | `<Foot><Btn>Later</Btn><Btn primary>Join</Btn></Foot>` |
117
+ | `<Card foot={<Btn primary>Offer help</Btn>} footLink={<Link sm>see all</Link>}>…</Card>` | `<Card>…<Foot link={<Link sm>see all</Link>}><Btn primary>Offer help</Btn></Foot></Card>` |
118
+ | `<Card footRound foot={<><IconBtn size="xl" …/><IconBtn size="xl" …/></>}>` | `<Card>…<Foot round><IconBtn size="xl" …/><IconBtn size="xl" …/></Foot></Card>` |
119
+ | `--card-foot-gap` · `--card-foot-ink` · `--card-foot-link-gap` | `--foot-gap` · `--foot-ink` · `--foot-link-gap` |
120
+ | `.card__foot`, `.card__foot--link`, `.card__foot--round`, `.card__foot-link` | `.foot`, `.foot--link`, `.foot--round`, `.foot__link` |
121
+
122
+ A screen on the old names fails the typecheck. `cardds get Card` lists `parts: Head · Foot · Lead · Words · End` and `get Sheet` `Head · Body · Foot`; `get Foot` prints `fits: Card · Sheet` (a stack sheet's foot stands in its `Card`). A `Words` in a stack sheet's head now calls a consumer's own `onKeyDown` before the fold. 719 knobs (three renamed). The style snapshot of the 499 stories names no story that moved; four stand on another page with the same look, `CardFoot · CardFootTwo`, `CardFootRound`, `CardFootWordsWide`, `CardFootFourAnswers` → `Foot · FootTwo`, `FootRound`, `FootWordsWide`, `FootFourAnswers`; one story is renamed with the same look, `IconBtnGroup · InACardFoot` → `IconBtnGroup · InAFoot`.
123
+ - **The migrated stories' numbers are a cell's size.** Before → after: the wallet's balance was `Text stat xl` and the others `Text stat`, each with its label above → a `KvCell` value (`--kv-cell-val-font`, h2) with its label under it. The style snapshot names the seven stories above as moved and nothing else (501 → 496 stories).
124
+ - **The gallery's nav: one grouping, by `src/` folder, in a derived order** (Lh 2026-10-21, chosen from a picture of three options). Before → after: the nav had three groupings behind a `catalog · frame · role` switch (kept in `localStorage`), a hand-written `NAV` of 18 groups and 107 names, and inside every group the pages stood in the `NAV` order, so the folder view interleaved a folder's pages by another grouping's index (scaffold read ActionBar, LayerHistory, TopBar, AppBar, Page, Pager, …) → the switch is gone and the nav draws Foundations, then one group a `src/` folder in alphabetical order, then `screens` last (Splash, Walkthrough, Home, Install, SignIn, SignUp, Requests in the app's order of first use, the one list left in `gallery/src/entries.ts`, then PostScreens, StampScreens, StickyScreens, StickyWelcome). Inside a folder the order is derived (`gallery/src/rank.mjs`): a part follows its whole (Card, then CardBody, CardFeed, CardFoot, CardHead; SheetStack, then StackHead, before SheetStage), everything else alphabetical. `Screen` (src/scaffold) has a page of its own under `scaffold`, so its three stories (Scrolling, Fill, Lead) are shown; its four palette stories are gone (the Foundations panel shows every palette, and a palette nested in a story failed the ink gate in dark mode); `Palette` is a link under Foundations, where its tokens are read; the `not a component` group is gone. The search matches the folder name where it matched the `NAV` group. No story's style moved (the style snapshot of 0.7.106 holds; the three Screen stories are added).
125
+
5
126
  ## 0.7.106 — 2026-10-21
6
127
 
7
128
  The map edge to edge under a step's controls (`MapArea bleed`, and a step whose bleeding child reaches the sheet's edges); `Chip soft`; a card's head led by its date; `Row` in parts (`RowLead`, `RowBody`, `RowEnd`, and the props that drew them are gone); a turning two-tone card no longer cut at its edges; the catalog names a whole's parts.
package/bin/cardds.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  // `npx cardds init` — set a project up the ONE way (Lh 2026-09-29): the project's theme.css, the check script, the ESLint rules.
3
3
  // `npx cardds check [dir…]` — the rules of lint/rules.mjs over the project's source; exit 1 on an error.
4
4
  // `npx cardds guide` — the few rules no checker sees, as text (the MCP server hands the same text over as its instructions).
5
+ // `npx cardds codemod-parts [--check] <files…>` — rewrite a screen from the retired parts (CardHead, SheetBody, CardFoot…) to Head · Body · Foot · Lead · Words · End.
5
6
  // `npx cardds list | get <Name> | find [words…] [--represents a,b] [--shape s] [--interact k|none] | scaffold [id]` — the component catalog (lib/catalog.mjs) for an AI session; `npx cardds mcp` serves the same over MCP.
6
7
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, copyFileSync } from 'node:fs';
7
8
  import { dirname, join, relative, resolve } from 'node:path';
@@ -107,7 +108,7 @@ const asText = {
107
108
  ...(c.composes.length ? [`composes: ${c.composes.join(' · ')}`] : []),
108
109
  ...(c.composedBy.length ? [`composed by: ${c.composedBy.join(' · ')}`] : []),
109
110
  ...(c.parts?.length ? [`parts: ${c.parts.join(' · ')} — its children, each its own export`] : []),
110
- ...(c.partOf ? [`part of: ${c.partOf}`] : []),
111
+ ...(c.fits?.length ? [`fits: ${c.fits.join(' · ')} — the wholes it is a child of`] : []),
111
112
  ...(c.params ? ['\nparameters', ...(c.params.length ? [] : [' (none)']), ...c.params.map((p) => ` ${p.name}${p.optional && !p.default ? '?' : ''}: ${p.type}${p.default ? ` = ${p.default}` : ''}`)] : [
112
113
  '\nprops',
113
114
  ...c.props.map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type}${p.slot ? ' [slot]' : ''}${p.description ? ` — ${one(p.description)}` : ''}`)]),
@@ -144,6 +145,7 @@ function ask(cmd, argv) {
144
145
  return 0;
145
146
  }
146
147
 
147
- const code = cmd === 'check' ? check(args) : cmd === 'init' ? init() : cmd === 'guide' ? (process.stdout.write(GUIDE), 0) : ['list', 'get', 'find', 'scaffold', 'mcp'].includes(cmd) ? await ask(cmd, args)
148
- : (console.log('usage: cardds init | guide | check [dir…] | list | get <Name> [--html] | find [words…] [--represents a,b] [--shape s] [--interact k|none] | scaffold [id] [--html [--css <href>]] | mcp (list get find scaffold mcp: --root <path> --json)'), cmd ? 2 : 0);
148
+ const codemod = (a) => import('../scripts/codemod-parts.mjs').then((m) => m.main(a), (e) => { console.error(`cardds codemod-parts: ${e.code === 'ERR_MODULE_NOT_FOUND' ? 'it reads the TypeScript compiler: install `typescript` in the project' : e.message}`); return 2; });
149
+ const code = cmd === 'check' ? check(args) : cmd === 'codemod-parts' ? await codemod(args) : cmd === 'init' ? init() : cmd === 'guide' ? (process.stdout.write(GUIDE), 0) : ['list', 'get', 'find', 'scaffold', 'mcp'].includes(cmd) ? await ask(cmd, args)
150
+ : (console.log('usage: cardds init | guide | check [dir…] | codemod-parts [--check] <files…> | list | get <Name> [--html] | find [words…] [--represents a,b] [--shape s] [--interact k|none] | scaffold [id] [--html [--css <href>]] | mcp (list get find scaffold mcp: --root <path> --json)'), cmd ? 2 : 0);
149
151
  process.exitCode = code; // not process.exit: a big answer piped to a reader is cut off by an exit before it is flushed
package/cardds.css CHANGED
@@ -14,6 +14,7 @@
14
14
  @import url("css/people.css"); /* avatar, avatar stack, chooser row */
15
15
  @import url("css/choice.css"); /* check, toggle, chip grid, days, calendar, mood, pin, composer */
16
16
  @import url("css/lists.css"); /* row, rows, key-value, kv grid, timeline, legend, link */
17
+ @import url("css/parts.css"); /* the parts of a line: lead, words, end — a Row's and a row card's children */
17
18
  @import url("css/sticky.css"); /* sticky note, its field, tone chooser, thread of posts, pile */
18
19
  @import url("css/stamp.css"); /* stamp, empty place, album page, a stamp stuck on a surface */
19
20
  @import url("css/post.css"); /* the post: postmark, a postcard read (letter), written (desk), kept (box) */
package/css/actions.css CHANGED
@@ -106,7 +106,7 @@
106
106
  /* a button may carry a WORD beside (or instead of) its icon — two named sides, ชาย · หญิง (Lh 2026-10-02): the label's look, one line */
107
107
  .icon-btn-group > .icon-btn { display: flex; align-items: center; justify-content: center; gap: var(--sp-2); padding-inline: var(--sp-2); font: var(--type-label); font-size: var(--fs-sm); white-space: nowrap; }
108
108
  .icon-btn-group > .icon-btn + .icon-btn { border-inline-start: var(--border-w) solid var(--icon-btn-group-rule, var(--icon-btn-group-border, var(--outline))); }
109
- .card__foot > .icon-btn-group { flex: 1 1 0; }
109
+ .foot > .icon-btn-group { flex: 1 1 0; }
110
110
  /* --vertical: the buttons one over the other, ONE button wide — at a row's end, where a panel across would take the words' room */
111
111
  .icon-btn-group--vertical { flex-direction: column; flex: none; width: var(--tap); }
112
112
  .icon-btn-group--vertical > .icon-btn { flex: 1 0 auto; min-height: var(--tap); }
@@ -116,7 +116,7 @@
116
116
  does not stretch it: a foot's `flex: 1 1 0` is undone for it below, as the foot undoes it for `.btn--form` (card.css). */
117
117
  .icon-btn-group--form { flex: 0 1 auto; justify-self: center; margin-inline: auto; } /* no align-self: a sheet's foot stands its buttons on the bottom edge by `align-items: end` */
118
118
  .icon-btn-group--form > .icon-btn { flex: 0 1 auto; width: auto; min-width: var(--icon-btn-group-form-min-w, var(--tap)); }
119
- .card__foot > :is(.icon-btn-group--form, .icon-btn-line) { flex: 0 1 auto; }
119
+ .foot > :is(.icon-btn-group--form, .icon-btn-line) { flex: 0 1 auto; }
120
120
  /* --labelled: the label's words, then the panel, ONE line in the middle (the words are read, not pressed: no frame, plain ink); too narrow → the words wrap
121
121
  above the panel, both centred */
122
122
  .icon-btn-line { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--icon-btn-group-gap, var(--sp-3)); min-width: 0; justify-self: center; margin-inline: auto; }
@@ -367,7 +367,7 @@
367
367
 
368
368
 
369
369
  /* ---- action note (Lh 2026-09-24): the main button's place, the main button's size, but ON the ground — no fill, no border, no
370
- rounding — one line of muted words, an inline icon allowed; nothing to press. In a CardFoot or an ActionBar row it takes the
370
+ rounding — one line of muted words, an inline icon allowed; nothing to press. In a Foot or an ActionBar row it takes the
371
371
  button's share (the whole width), so a foot or a bar keeps its height whether it holds a button or the note. ---- */
372
372
  .action-note {
373
373
  flex: 1 1 0;
@@ -437,7 +437,7 @@
437
437
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
438
438
  a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):not(.link):is(:disabled, [aria-disabled="true"]),
439
439
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
440
- .sheet-stack--tap > .card > .card__head > :is(h1, h2, h3):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); } /* the keyboard's way into a stack sheet */
440
+ .sheet-stack--tap > .card > .head > :is(h1, h2, h3):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); } /* the keyboard's way into a stack sheet */
441
441
  @media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
442
442
 
443
443
  /* ==== the chip a choice draws: picked (choice.css drives it with its input), and --sign (#62: a block is written in its own file) ==== */
@@ -459,14 +459,14 @@
459
459
 
460
460
  /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
461
461
  /* a head's ChipRow takes a line of its own under the title: at most two lines of chips, a chip's words cut with an ellipsis */
462
- .card__head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * var(--chip-sm-line, var(--lh-body)) + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
462
+ .head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * var(--chip-sm-line, var(--lh-body)) + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
463
463
  /* …beside a date that leads the head (card.css) they stand in the date's row, every chip in sight: the meeting place may take a line of its own */
464
- .card__head:has(> .tile-badge:first-child + .chip-row) > .chip-row { grid-column: 2; grid-row: 1; max-height: none; }
465
- .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
466
- .card__foot > .btn { flex: 1 1 0; min-width: 0; }
464
+ .head:has(> .tile-badge:first-child + .chip-row) > .chip-row { grid-column: 2; grid-row: 1; max-height: none; }
465
+ .head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
466
+ .foot > .btn { flex: 1 1 0; min-width: 0; }
467
467
  /* …unless the button says its own size: `Btn form` is as wide as its words in a foot too, centred (Lh 2026-10-10: one button filling
468
468
  a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
469
- .card__foot > .btn--form { flex: 0 1 auto; }
469
+ .foot > .btn--form { flex: 0 1 auto; }
470
470
  /* the page dots under the card sit centred under it — the indicator belongs
471
471
  with the page that changes, never in the bar (Lh, 2026-09-12) */
472
472
  .screen__centre > .pager__at { justify-self: center; }
package/css/base.css CHANGED
@@ -133,8 +133,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
133
133
 
134
134
  /* ---- typography levels ---- */
135
135
  /* --fs-self: each level publishes its own size, for a consumer that scales a level without knowing which */
136
- .t-display { font: var(--type-display); letter-spacing: var(--track-display); margin: 0; text-wrap: balance; --fs-self: var(--fs-display); }
137
- .t-h1 { font: var(--type-h1); letter-spacing: var(--track-h1); margin: 0; --fs-self: var(--fs-h1); }
136
+ .t-display { --_type: var(--type-display); font: var(--type-display); letter-spacing: var(--track-display); margin: 0; text-wrap: balance; --fs-self: var(--fs-display); }
137
+ .t-h1 { --_type: var(--type-h1); font: var(--type-h1); letter-spacing: var(--track-h1); margin: 0; --fs-self: var(--fs-h1); }
138
138
  .t-h2 { font: var(--type-h2); letter-spacing: var(--track-h2); margin: 0; --fs-self: var(--fs-h2); }
139
139
  .t-title { font: var(--type-title); margin: 0; --fs-self: var(--fs-title); }
140
140
  .t-body { font: var(--type-body); margin: 0; }
@@ -188,9 +188,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
188
188
  .page > .t-body { color: var(--page-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
189
189
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
190
190
  (the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
191
- /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
192
- h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
193
- .page > .card__head > .t-h1 { font-size: var(--fs-h2); }
194
191
  .page .cta-pack {
195
192
  display: flex;
196
193
  align-items: start; /* buttons keep their own height, never the row's */
@@ -283,12 +280,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
283
280
 
284
281
  /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
285
282
  /* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
286
- <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
283
+ <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>
287
284
  The heading and the far-right thing share the first row; a caption sits on
288
285
  a row below (order 1), whatever the DOM order — so an author writes the
289
286
  words in reading order. */
290
- .card__head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
291
- .card__head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
287
+ .head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
288
+ .head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
292
289
  /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
293
290
  .sheet-stack__base > .screen__centre { padding-inline: 0; }
294
291
  /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
@@ -326,7 +323,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
326
323
  the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
327
324
  .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
328
325
  .step > .empty-state { flex: none; }
329
- .sheet__body > .empty-state:only-child { flex: none; }
326
+ .body > .empty-state:only-child { flex: none; }
330
327
  /* ---- where it sits in what actions.css draws (docs/reference.md rule 15) ---- */
331
328
  /* ---- top bar: back + screen title ---- */
332
329
  .topbar .t-h1 {