@usableapp/cardds 0.7.103 → 0.7.104

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 (60) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/css/actions.css +102 -36
  3. package/css/base.css +139 -15
  4. package/css/card.css +308 -79
  5. package/css/choice.css +25 -33
  6. package/css/finish.css +28 -27
  7. package/css/forms.css +6 -14
  8. package/css/journey.css +4 -4
  9. package/css/layover.css +26 -24
  10. package/css/lists.css +8 -6
  11. package/css/media.css +13 -74
  12. package/css/numbers.css +37 -32
  13. package/css/palette.css +1 -1
  14. package/css/people.css +3 -8
  15. package/css/post.css +23 -19
  16. package/css/sheet.css +84 -56
  17. package/css/stack.css +39 -215
  18. package/css/stamp.css +41 -29
  19. package/css/step.css +1 -107
  20. package/css/sticky.css +43 -41
  21. package/css/theme.template.css +12 -0
  22. package/css/tokens.css +100 -26
  23. package/dist/actions/Chip.d.ts +1 -1
  24. package/dist/actions/Chip.js +3 -2
  25. package/dist/cardds.css +1053 -868
  26. package/dist/cards/Card.d.ts +3 -3
  27. package/dist/cards/Card.js +1 -1
  28. package/dist/cards/CardFeed.js +7 -9
  29. package/dist/catalog.json +1 -1
  30. package/dist/layover/Snackbar.js +2 -2
  31. package/dist/lists/Bubble.d.ts +1 -1
  32. package/dist/lists/Bubble.js +1 -1
  33. package/dist/lists/Row.js +5 -7
  34. package/dist/numbers/Picker.js +11 -18
  35. package/dist/people/Avatar.d.ts +1 -1
  36. package/dist/people/Avatar.js +3 -2
  37. package/dist/post/PostDesk.js +6 -9
  38. package/dist/post/PostLetter.js +4 -4
  39. package/dist/pressable.d.ts +9 -0
  40. package/dist/pressable.js +6 -0
  41. package/dist/room.d.ts +5 -0
  42. package/dist/room.js +9 -0
  43. package/dist/scaffold/Panes.js +8 -14
  44. package/dist/sheets/Sheet.d.ts +1 -1
  45. package/dist/sheets/Sheet.js +2 -3
  46. package/dist/sheets/SheetSteps.js +3 -3
  47. package/dist/sheets/StackSheet.d.ts +1 -1
  48. package/dist/sheets/sheetStepsPile.js +2 -3
  49. package/dist/sheets/stackCard.js +67 -115
  50. package/dist/state.d.ts +21 -0
  51. package/dist/state.js +28 -1
  52. package/dist/sticky/Sticky.js +3 -7
  53. package/dist/templates/Home.d.ts +1 -1
  54. package/dist/templates/Splash.js +2 -2
  55. package/dist/templates/Walkthrough.js +4 -5
  56. package/docs/reference.md +71 -9
  57. package/lib/catalog.mjs +23 -7
  58. package/lint/rules.mjs +7 -5
  59. package/lint/values.mjs +144 -0
  60. package/package.json +6 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
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.104 — 2026-10-21
6
+
7
+ Every value a component shows comes from a token, so what the gallery shows is what a theme gets (Lh 2026-10-20). Also: the gallery shows every foundation token, the catalog finds a piece by a plain intention, the number pieces are ruled (Stat and Tile stat drop as twins of NumberBand and KvCell), and CSS and JS written twice are written once.
8
+
9
+ Components: all (tokens and the gallery changed; the components a screen sees move only where the Fixed and Changed lines say)
10
+
11
+ ### Fixed
12
+ - **A paper sheet's content is the theme's paper, not a fixed white.** Before → after: `Sheet paper`, a `StackSheet paper`, `Home`'s sheets and their foot read `--color-paper` straight, a base only a project's own colours set, so on every stock palette the content was `#fff` and its foot's words `#141414` (on `vers` a white body under champagne fields and buttons) → `--action-bg`, the paper of the theme's fields and buttons, and the words of its side: wireframe white as before, `clay` cream, `orchard` bone, `vers` champagne. The foot now declares its side, so a finish's button shadow on it is the onlight ink (it was the dark sheet's light ink, unseen on the paper).
13
+ - **A sticky note and a stamp take their colours from the theme.** Before → after: the nine note tones, the stamp's paper and ink, the remove disc, the note's edge, shadow and fold, and a chip on a note were colours written in the css (`#f3efe2`, `#fffdf8`, `#141414`, black shadows, a white chip) → tokens a theme sets: `--pastel-cotton` … `--pastel-water`, `--stamp-paper`, the onlight and ondark inks, shadows mixed from the onlight ink, the chip on `--stamp-paper`. The same values by default. The knobs (`--sticky-<tone>-bg`, `--stamp-paper-bg`, …) keep their names.
14
+ - **A sheet's grip while reloading, the words on a dim stage, and every other colour a component wrote** take the inks (`--ink-onlight` / `--ink-ondark`) instead of black and white.
15
+
16
+ ### Changed
17
+ - **`find` answers a plain intention with its piece** (the catalog's search, the CLI's `find` and the MCP's `find_components`). Before → after: "the user switches something on or off" listed ChipPick, MapArea, StampOn and no Toggle; "choose a or b" no BtnGroup or Toggle; "read a number at a glance" FloatBar, MapArea, Sticky; "chat" no Thread → Toggle first; BtnGroup, MapPick, Toggle; Progress, Ring; Thread in the first three. Function words (`at`, `or`, `user`, `something`…) match no text; a word that says a value of the roles' vocabulary (`choose` → pick, `fill` → write, `read` → show, `date` → time, `number`) finds the components whose role has it, and "or" between two words is a pick; a note or description that says three words of the request in a row ("a or b", "at a glance") ranks first among equals. New notes in docs/roles.md for BtnGroup, Check, Toggle, Thread, Progress and Ring say what each is for in a request's words.
18
+ - **No length in px.** `--border-w`, `--hairline-w`, `--rule-w`, `--scroll-w`, the focus ring, `--r-chip`, the shadows and every edge a component drew are rem (the same size at a 16px root; on a narrow phone, where the root is 15px, they shrink with everything else). New `--px` (one device pixel in rem) and `--browser-text` (1rem, the text size under which iOS zooms into a field).
19
+ - **Every size a component wrote is a multiple of a base.** A page's picture, a turned card's middle, the placeholders, the chip grid, the composer, the modal drawer, the menu, the callout, the three-up tile grid, the waveform, the stack's steps, the bars, the map's streets: `calc(var(--tap-base) * n)` with the same default.
20
+ - **New tokens for what components wrote in place:** easing (`--ease`, `--ease-in`, `--ease-out`, `--ease-in-out`, `--ease-linear`, `--ease-settle`), times (`--motion-spin`, `--motion-blink`, `--motion-flash`, `--motion-shake`, `--motion-pulse`, `--motion-run`, `--motion-stagger`), angles (`--tilt-base`: every tilt is a multiple of it; 0deg lays everything straight), layers (`--z-under-2` … `--z-over-3`), fades (`--fade-faint` … `--fade-most`), type (below), the pastels and the paper shadows. All keep today's values.
21
+ - **Every kind of type has all its tokens, and every size is set from them** (Lh 2026-10-20: "the type tokens are not complete for every kind and every size"). Before → after: the eight kinds wrote their weight and leading as numbers inside `--type-*` (only display, h1 and h2 had a tracking), and fourteen pieces composed their own `font:` with a number weight and leading on a size of the scale (the xl button, a day's number, a calendar day, a tile's value, a large row's title, the quote mark, a ring's value, a data band, a stamp's words and name, the composer, a field) → each kind has `--face-<kind>`, `--fw-<kind>`, `--fs-<kind>`, `--lh-<kind>`, `--track-<kind>`, and `--type-<kind>` is made of them; two scales, weight (`--fw-regular` 400 … `--fw-black` 900) and leading (`--lh-mark` 0.55 … `--lh-ruled` 1.7), give every other piece its step. Same values. Gallery `Text · Kinds` shows each kind's five tokens resolved, and the two scales.
22
+ - **The gate.** `npm run check` (`lint/values.mjs`, before the tests; `tests/tokens.spec.js` holds it at none) fails on any colour, px, rem, time, angle, easing, layer, fade, scale or type value written in a component's css or in an inline `style` of `src/` or a story, naming the file and the line: a `font:` shorthand's weight and leading, a knob's fallback (`z-index: var(--x, 5)`, `opacity: var(--k, .7)`), `calc()` in a leading or a tracking, `scale`, `filter: opacity()`, a fade in %, a unit in capitals and every CSS colour name included (docs/reference.md "A component brings no value of its own").
23
+ - **What the wider gate found** took tokens with the same values: `--ease-step` (`steps(1)`: a code cell's caret, a secret digit), `--scale-lift` 1.06, `--scale-pick` 1.7, `--scale-arrive` 0.6, `--z-pile` 7 (a sticky pile's top note); a snackbar and a writing sticky answer sit `--z-over` above `--z-top`, a stack sheet with no `--level` at `--z-over`.
24
+ - **What moves on a palette or a phone, besides the paper sheet.** On a coloured palette a stamp's ink and every shadow mixed from it are the palette's onlight ink, not `#141414` (orchard `#2a241c`). On a phone of 360 wide or less (root 15px) an edge of `--border-w` or `--hairline-w` is a little thinner and may draw one device pixel less. The sizes now made of `--tap-base` (the stack's steps, a page's picture, the bars, the placeholders, the composer, the menu, the callout, the modal drawer, the three-up tiles) grow with the member's text size through `setTextScale`, as every control already did; at 100% they are as before.
25
+
26
+ ### Internal (nothing a consumer sees)
27
+ - **CSS written twice is written once.** No class, knob or default changed (the style snapshot of 492 stories is identical). A sticky pile turns its notes in the board's own four rules (one selector list, `--sticky-board-turn-1…4` as before); a `TrackStep num`'s dot is one rule, its first width, height, padding and background were dead; comments that sat twice (a stack sheet's moves, a paper foot, the centred group's cap, a step's page) are written once beside their rules, with a pointer in the other file, and two orphaned paper-body comments and an empty `@media` left in stack.css by the rule-15 move went to the rules in sheet.css. jscpd over `css/`: 1.07% of lines duplicated (5 clones) before, 0.18% (2 clones: the vendor slider pair and the centred group's place, both allowed) after.
28
+ - **The twins check sees two more shapes** (tests/twins.spec.js): one modifier on two blocks that declares one set but for the knobs' names (`.sticky--tilt-left` and `.stamp--tilt-left`; `RENAMED_ALLOWED`), two keyframes of one shape on two properties (`sheet-title-run` on `text-indent`, `stack-sheet-run` on `translate`; `KEYFRAMES_ALLOWED`), and two rules of two declarations when they read a knob (`.card--away` and the sent post desk's card). Each pair it finds today is written down with why it stays two.
29
+ - **Mechanics that several components wrote out each are written once** (nothing a screen sees or sets changes; the motion, its timing and every `on…` fire as before). In `src/state.ts`: `sheetMs` (`--motion-sheet` in ms, 350 where it reads as none: Rows' and CardFeed's reorder glide, Snackbar's and Splash's leave), `stopMoves` + `glide` (a reordered row or feed card glides to its place), `swipeOf` (Walkthrough's, PostDesk's and SheetSteps' swipe, each with its own axis, distance and ratio), `visibleBottom` (Sheet's and SheetSteps' keyboard cut), `useSettled` (Picker's and Panes' rail coming to rest; Panes still drops the wait on every render). Also once: the open sheet's head pull and a peeked head's lift (`pullHead` in stackCard.ts), a Chip's and an Avatar's element (`tagged`, pressable.ts), the room a PostDesk and a Sticky hold (`holdRoomWhile`, room.ts). Left as they are: a motion's end (Card drops `onLeft` when its move is cancelled, Modal calls `onClosed` anyway, a sheet waits on `transitionend` and a timeout), the page slide (`slideTo` runs its update at once, `slideScreen` awaits the route and the new Screen), and how a snap rail scrolls to the chosen one (Panes honours reduced motion, Picker does not). One helper for any of them would change what one of them does.
30
+ - **The gate.** `npm run dup` (jscpd 4.3.0 over `src/`, `.jscpd.json`: 30 tokens, 2 lines) runs in `node scripts/ci.mjs` and fails above 0.62% duplicated lines. Left: 0.02%, a calendar's weekday row written in `Calendar` and in `CalendarWeeks` (0.67% before the fold); the catalog reads a component's classes from its own file, and a shared helper moved `calendar__wd` in `CalendarWeeks`' list. Not counted, each with its reason in `.jscpd.json`: `cardGrounds.ts` and `inkGrounds.ts` (each is sent whole to a page by `page.evaluate`, so they cannot share their contrast math) and static `import` lines.
31
+
5
32
  ## 0.7.103 — 2026-10-20
6
33
 
7
34
  The `Components:` line of a release names what a css change really moved.
package/css/actions.css CHANGED
@@ -26,17 +26,6 @@
26
26
  min-height: var(--header-h);
27
27
  }
28
28
 
29
- /* ---- top bar: back + screen title ---- */
30
- .topbar .t-h1 {
31
- text-transform: uppercase;
32
- flex: 1;
33
- min-width: 0;
34
- overflow: hidden;
35
- text-overflow: ellipsis;
36
- white-space: nowrap;
37
- padding-block: 0.15em; /* Thai marks survive the clip — see card.css, the head rule */
38
- margin-block: -0.15em;
39
- }
40
29
  .topbar__actions { margin-left: auto; display: flex; gap: var(--sp-2); }
41
30
  /* at most TWO trailing controls (Lh 2026-09-22): the title must keep its line; a third and
42
31
  beyond is not drawn — more moves belong in a Menu behind one button, or on the base */
@@ -61,7 +50,6 @@
61
50
  /* fixed, so it covers the top of the screen it sits in — reserve its height
62
51
  there, from the same header-h token. The breathing under it is published:
63
52
  a .step cancels it to touch the bar. */
64
- .screen:has(> .appbar) { --header-gap: var(--sp-2); padding-top: calc(var(--header-h) + var(--header-gap)); }
65
53
  .appbar__brand { flex: 1; display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
66
54
  .appbar__text { display: grid; min-width: 0; flex: 1; } /* pushes trailing button to the right edge */
67
55
  /* the mark + the words as ONE button (AppBar brandButton — the member's name opens their menu, Lh 2026-09-23): the same row, a tap target */
@@ -71,7 +59,7 @@
71
59
  margin: 0; text-align: start;
72
60
  border-radius: var(--appbar-who-radius, var(--r-chip));
73
61
  }
74
- .appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.35; } /* clipped lines: room for Thai marks */
62
+ .appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: var(--lh-caption); } /* clipped lines: room for Thai marks */
75
63
  .appbar__title { font: var(--type-label); }
76
64
  .appbar__sub { font: var(--type-caption); color: var(--on-surface-muted); }
77
65
  /* the bar's trailing controls (one or two: a labelled switch chip, a wallet button — home,
@@ -86,10 +74,11 @@
86
74
  border: var(--border-w) solid var(--icon-btn-border, var(--outline)); /* see .btn — same reason */
87
75
  background: var(--icon-btn-bg, var(--action-bg));
88
76
  color: var(--icon-btn-ink, var(--action-ink));
77
+ box-shadow: var(--icon-btn-shadow, none);
89
78
  display: grid;
90
79
  place-items: center;
91
80
  font-size: var(--fs-lg);
92
- line-height: 1;
81
+ line-height: var(--lh-solid);
93
82
  }
94
83
  .icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
95
84
  .icon-btn--sm { width: var(--icon-btn-sm-size, var(--tap-sm)); height: var(--icon-btn-sm-size, var(--tap-sm)); font-size: var(--fs-title); }
@@ -154,11 +143,11 @@
154
143
  /* quiet keeps a faint outline so it still reads as a button on colored palettes;
155
144
  it is ink on whatever it sits on — inherit, or on the dark slot (card--3) it
156
145
  would take the light-card action ink and vanish */
157
- .btn--quiet { background: transparent; color: inherit; border-color: color-mix(in srgb, currentColor 35%, transparent); }
146
+ .btn--quiet { background: transparent; color: inherit; border-color: var(--btn-quiet-border, color-mix(in srgb, currentColor 35%, transparent)); }
158
147
  .btn--block { width: 100%; }
159
148
  .btn--form { justify-self: center; align-self: center; margin-inline: auto; padding-inline: var(--btn-form-pad-x, calc(var(--sp-base) * 14.5)); /* 58px: "ส่งรหัส OTP" at 213px, 20% longer than at 40px sides (Lh 2026-09-25) */ } /* a form's button: its words + wide sides, centred in a grid (a step) or a block (Lh 2026-09-25) */
160
149
  /* xl: the one giant call to action in a tinted card ("Taxi!") */
161
- .btn--xl { min-height: var(--btn-xl-h); font: 800 var(--fs-3xl)/1 var(--font-display); letter-spacing: var(--track-h1); border-radius: var(--r-card); padding-inline: var(--sp-8); }
150
+ .btn--xl { min-height: var(--btn-xl-h); font: var(--fw-heavy) var(--fs-3xl) / var(--lh-solid) var(--font-display); letter-spacing: var(--track-h1); border-radius: var(--r-card); padding-inline: var(--sp-8); }
162
151
  /* disabled keeps the slot — a control that vanishes moves everything else,
163
152
  and a pager whose "previous" disappeared on page one would shift the bar
164
153
  under the thumb on every page */
@@ -183,9 +172,10 @@
183
172
  padding: var(--chip-pad-y, var(--sp-2)) var(--chip-pad-x, var(--sp-3));
184
173
  font: var(--type-label);
185
174
  font-size: var(--chip-font-size, var(--fs-sm));
186
- min-height: var(--chip-h);
175
+ min-height: var(--chip-min-h, var(--chip-h));
187
176
  text-decoration: none;
188
- justify-self: start; /* as a grid child it hugs its text, never stretches */
177
+ justify-content: var(--chip-justify, normal);
178
+ justify-self: var(--chip-self, start); /* as a grid child it hugs its text, never stretches */
189
179
  }
190
180
  .dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); translate: 0 var(--dot-drop, 0.09em); } /* signal dot: chips, legends.
191
181
  The drop: a line box is centred on its ascender room (Thai marks), the letters beside the dot sit lower — the dot rides down to the
@@ -202,8 +192,8 @@
202
192
  /* muted: an untouched default — the words wash out, the edge stays; emphasis is the difference from the neighbours (docs/reference.md 13) */
203
193
  .chip--muted { color: var(--chip-muted-ink, var(--card-muted)); } /* the chip's own fill's muted words: a muted chip on the dark card is still a light chip */
204
194
 
205
- .chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
206
- .chip-row.bleed { flex-wrap: nowrap; } /* actions.css loads after card.css — the strip stays one line */
195
+ .chip-row { display: flex; gap: var(--sp-2); }
196
+ .chip-row:not(.bleed) { flex-wrap: wrap; } /* a strip (.bleed, card.css) stays one line */
207
197
  .chip-row.bleed > .chip { flex: none; white-space: nowrap; } /* a chip of the strip keeps its words on one line at its own width: the strip scrolls, a chip never shrinks to fit it */
208
198
 
209
199
  /* ---- dropdown: the standard for every select — a pill trigger + .menu popover ----
@@ -232,10 +222,9 @@
232
222
  /* muted: the value is still the default (docs/reference.md 13) */
233
223
  .dropdown--muted .dropdown__btn { color: var(--dropdown-muted-ink, var(--action-muted)); }
234
224
  .dropdown__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
235
- .dropdown__btn > .icon { transition: rotate var(--motion-quick) ease; }
236
- .dropdown:has(.menu:popover-open) .dropdown__btn > .icon { rotate: 180deg; }
237
- .dropdown:has(.menu.\:popover-open) .dropdown__btn > .icon { rotate: 180deg; } /* the polyfill's open (layover.css) */
238
- .dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards (= .menu--end, layover.css) */
225
+ .dropdown__btn > .icon { --icon-motion: rotate var(--motion-quick) var(--ease); }
226
+ .dropdown:has(.menu:popover-open) .dropdown__btn > .icon { --icon-turn: 180deg; }
227
+ .dropdown:has(.menu.\:popover-open) .dropdown__btn > .icon { --icon-turn: 180deg; } /* the polyfill's open (layover.css) */
239
228
  .filter-row { display: flex; gap: var(--sp-2); }
240
229
  .filter-row > .dropdown { flex: 1; min-width: 0; }
241
230
 
@@ -381,7 +370,7 @@
381
370
  cursor: default;
382
371
  }
383
372
  .action-note__words { display: block; } /* ONE run of words: an icon inside sits in the line, the spaces around it kept */
384
- .action-note__words > .icon { display: inline-block; vertical-align: -0.2em; width: 1.25em; height: 1.25em; margin-inline: 0.15em; color: var(--action-note-icon-ink, inherit); } /* a project may colour the icon apart from the words — e.g. the system blue of the control it points at (Lh 2026-09-24) */
373
+ .action-note__words > .icon { --icon-display: inline-block; --icon-align: -0.2em; --icon-size: 1.25em; --icon-margin: 0 0.15em; --icon-ink: var(--action-note-icon-ink, inherit); } /* a project may colour the icon apart from the words — e.g. the system blue of the control it points at (Lh 2026-09-24) */
385
374
 
386
375
  /* PRESS (Lh 2026-09-25; every tappable thing, 2026-09-27): every CONTROL answers a tap, INFORMATION never does (no pointer, no
387
376
  press). Two answers, by size: GIVE — a control the size of a finger shrinks under it (--press-scale) and springs back; DIM —
@@ -397,34 +386,27 @@
397
386
  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, .map-pick) {
398
387
  cursor: pointer;
399
388
  touch-action: manipulation;
400
- transition: scale var(--motion-press) ease-out;
389
+ transition: scale var(--motion-press) var(--ease-out);
401
390
  }
402
391
  :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .feed-card--small, .toggle-field, .post-box__edge) {
403
392
  cursor: pointer;
404
393
  touch-action: manipulation;
405
- transition: opacity var(--motion-press) ease-out;
394
+ transition: opacity var(--motion-press) var(--ease-out);
406
395
  }
407
396
  /* a CHOICE's fill fades in as it is picked (a plain button's colour never changes on a tap, so it does not fade) */
408
397
  :where(.chip--pick, button.chip, .tile--pick, button.tile, .day, .segment__item, .calendar__day, .float-bar__item, .picker__item, .check,
409
398
  .icon-btn[role="tab"], .avatar-pick__item > .avatar, .map-pick > .map-pick__mark, .map-pick > .map-pick__name) {
410
399
  transition-property: scale, background-color, color, border-color, opacity;
411
400
  transition-duration: var(--motion-press);
412
- transition-timing-function: ease-out;
401
+ transition-timing-function: var(--ease-out);
413
402
  }
414
403
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
415
404
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
416
405
  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, .map-pick):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
417
406
  :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .feed-card--small, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
418
- .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
419
- /* a pile of notes pressed whole (Lh 2026-10-11): the button lies invisible UNDER the notes, so it is the notes that answer — DIM, as a card does (the group dims as one, the
420
- notes do not show through one another); the ring is the button's own, round the whole pile */
421
- .sticky-pile__press { cursor: pointer; touch-action: manipulation; }
422
- .sticky-pile__notes:has(> .sticky-pile__press) { transition: opacity var(--motion-press) ease-out; }
423
- .sticky-pile__notes:has(> .sticky-pile__press:active) { opacity: var(--press-dim); }
424
- .sticky-pile__press:focus-visible { outline: var(--focus); outline-offset: 0; }
425
407
  /* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back, a post letter's sheet over the other) must not turn see-through:
426
408
  it darkens a touch instead (--press-shade) */
427
- :where(.sheet-stack--tap > .card:not(.is-open), .postcard__turn, .post-letter__sheet:not(:has(.icon-btn:active))) { cursor: pointer; touch-action: manipulation; transition: filter var(--motion-press) ease-out; }
409
+ :where(.sheet-stack--tap > .card:not(.is-open), .postcard__turn, .post-letter__sheet:not(:has(.icon-btn:active))) { cursor: pointer; touch-action: manipulation; transition: filter var(--motion-press) var(--ease-out); }
428
410
  :where(.sheet-stack--tap > .card:not(.is-open), .postcard__turn, .post-letter__sheet:not(:has(.icon-btn:active))):active { filter: brightness(var(--press-shade)); }
429
411
  /* the focus ring: on the control, or on the label that wraps a hidden input (chip, tile, file button, a row that is a label, a card that is a label round its radio) */
430
412
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
@@ -443,3 +425,87 @@
443
425
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
444
426
  .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 */
445
427
  @media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
428
+
429
+ /* ==== the chip a choice draws: picked (choice.css drives it with its input), and --sign (#62: a block is written in its own file) ==== */
430
+ .chip--pick { position: relative; } /* the input covers the whole chip, on a row as in a grid */
431
+ .chip--pick:has(:checked) { background: var(--accent); --dark: var(--accent-dark); color: var(--card-ink); border-color: transparent; }
432
+ /* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
433
+ .chip--sign { padding-right: var(--sp-2); }
434
+ .chip--sign::after {
435
+ content: "";
436
+ width: var(--icon-xs);
437
+ height: var(--icon-xs);
438
+ flex: none;
439
+ background: currentColor;
440
+ mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
441
+ -webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
442
+ }
443
+ .chip--sign:has(:checked)::after { rotate: 45deg; }
444
+ @media (prefers-reduced-motion: no-preference) { .chip--sign::after { transition: rotate var(--motion-slide); } }
445
+
446
+ /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
447
+ /* 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 */
448
+ .card__head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * 1.5 + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
449
+ .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: var(--lh-body); }
450
+ .card__foot > .btn { flex: 1 1 0; min-width: 0; }
451
+ /* …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
452
+ a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
453
+ .card__foot > .btn--form { flex: 0 1 auto; }
454
+ /* the page dots under the card sit centred under it — the indicator belongs
455
+ with the page that changes, never in the bar (Lh, 2026-09-12) */
456
+ .screen__centre > .pager__at { justify-self: center; }
457
+ .panes > .pager__at { justify-self: center; }
458
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
459
+ .screen--fill:has(> .sheet-stack--flood) > :is(.appbar, .topbar):first-child {
460
+ position: absolute;
461
+ inset: var(--safe-top) 0 auto;
462
+ max-width: none;
463
+ margin: 0;
464
+ z-index: var(--z-flat); /* under every sheet (--level 1-5); the base clears it by margin */
465
+ }
466
+ .screen--fill:has(> .sheet-stack--flood > .stack-card.is-open, > .sheet-stack--flood > :not(.stack-card) > .stack-card.is-open) > :is(.appbar, .topbar):first-child {
467
+ visibility: hidden;
468
+ transition: visibility 0s var(--ease-linear) var(--motion-sheet); /* gone once the sheet has covered it; back at once on a fold (the rule above has no delay) */
469
+ }
470
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
471
+ /* the step waiting for its answer: its buttons take no taps (the app guards its own handler too) */
472
+ .sheet-step--busy .btn { pointer-events: none; opacity: var(--fade-half); }
473
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
474
+ /* ============================================================
475
+ TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
476
+ in a .screen--fill the content runs to the screen's bottom edge, and the bar lies over it at the foot — it covers what is
477
+ under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
478
+ (On a scrolling screen the bar is a sticky last row instead, actions.css.)
479
+ ============================================================ */
480
+ .screen--fill > .action-bar {
481
+ position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
482
+ max-width: none;
483
+ margin: 0;
484
+ /* room between its top edge and its first row (the walk's dots): since it has a ground, a 0 here put the dots against the words
485
+ running under it (Lh 2026-09-25) — the bar's own top padding */
486
+ padding-top: var(--action-bar-fill-pad-top, var(--sp-3));
487
+ /* its GROUND (Lh 2026-09-24, timebank card 141 — was "no ground of its own"): words that run under the layer mid-scroll
488
+ showed through the dots, the controls and the buttons. The layer lies on the screen's own surface now; a project sets
489
+ another (or `transparent`, the old see-through) with the knob */
490
+ background: var(--action-bar-bg, var(--surface));
491
+ }
492
+ /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
493
+ .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
494
+ /* the TOP is in the column too (Lh 2026-09-23): an .appbar in a fill screen is its first row, not fixed over it — the screen
495
+ reserves nothing for it; the breathing under it stays the one it had (--sp-2), not the screen's gap */
496
+ .screen--fill > .appbar {
497
+ position: relative; inset: auto; top: auto;
498
+ flex: none;
499
+ max-width: none;
500
+ margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge; its own padding is the inset */
501
+ margin-bottom: calc(var(--sp-2) - var(--screen-gap));
502
+ }
503
+ /* …and a STEP touches it, as it touches a TopBar: a full sheet's top edge on the bar's bottom edge (0.6.5 left 8px of base between
504
+ them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
505
+ .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
506
+ /* ---- where it sits in what forms.css draws (docs/reference.md rule 15) ---- */
507
+ .field--action > .icon-btn { margin-left: auto; }
508
+ /* ---- where it sits in what sticky.css draws (docs/reference.md rule 15) ---- */
509
+ .sticky__foot > .chip { display: block; flex: none; min-width: 0; max-width: var(--sticky-foot-chip-max-w, 62%); overflow: hidden; text-overflow: ellipsis; text-align: start; }
510
+ /* ---- where it sits in what layover.css draws (docs/reference.md rule 15) ---- */
511
+ .modal__drawer > .btn-row { justify-self: stretch; margin-top: var(--modal-drawer-answers-gap, var(--sp-5)); white-space: nowrap; }
package/css/base.css CHANGED
@@ -75,6 +75,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
75
75
 
76
76
  /* icon from icons.svg sprite: <svg class="icon"><use href="icons.svg#bell"/></svg> */
77
77
  .icon { width: var(--icon-size, var(--icon)); height: var(--icon-size, var(--icon)); flex: none; }
78
+ /* what a component that holds an icon may set on it (#62: a holder adjusts its child only through the child's knobs): unset, each
79
+ is the icon as it is. No fill: a drawing's own fill="none" is an attribute any declared fill would beat */
80
+ .icon {
81
+ display: var(--icon-display, block); vertical-align: var(--icon-align, baseline); margin: var(--icon-margin, 0);
82
+ padding: var(--icon-pad, 0); border-radius: var(--icon-radius, 0); background: var(--icon-bg, none); color: var(--icon-ink, inherit);
83
+ rotate: var(--icon-turn, none); translate: var(--icon-shift, none); transition: var(--icon-motion, all 0s); animation: var(--icon-spin, none);
84
+ }
78
85
  .icon--sm { width: var(--icon-sm-size, var(--icon-sm)); height: var(--icon-sm-size, var(--icon-sm)); }
79
86
  .icon--xs { width: var(--icon-xs-size, var(--icon-xs)); height: var(--icon-xs-size, var(--icon-xs)); } /* inside captions, chips, links */
80
87
  .icon--lg { width: var(--icon-lg-size, var(--icon-lg)); height: var(--icon-lg-size, var(--icon-lg)); } /* empty states */
@@ -123,12 +130,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
123
130
  position: relative; /* the box a .card--centre or a .screen__centre floats in (card.css) */
124
131
  container-type: size; /* a share of the SCREEN's height (a no-bar step's lead, step.css) reads it as cqh — a real phone's height, or the gallery's frame */
125
132
  }
126
- /* min-height: 0 — a flex item's floor is its content, and the stack's content
127
- is its own full-height sheets, so without it the stack grew to the whole
128
- screen and its open sheet ran past the bottom edge by the bars' height,
129
- unreachable by scrolling (timebank home, 2026-09-13). The stack IS the room
130
- left under the bars; every sheet fills exactly that. */
131
- .screen--fill > .sheet-stack { flex: 1; min-height: 0; }
132
133
 
133
134
  /* ---- typography levels ---- */
134
135
  /* --fs-self: each level publishes its own size, for a consumer that scales a level without knowing which */
@@ -138,7 +139,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
138
139
  .t-title { font: var(--type-title); margin: 0; --fs-self: var(--fs-title); }
139
140
  .t-body { font: var(--type-body); margin: 0; }
140
141
  .t-label { font: var(--type-label); }
141
- .t-caption { font: var(--type-caption); }
142
+ .t-caption { font: var(--type-caption); color: var(--t-caption-ink, inherit); }
142
143
  .t-stat { font: var(--type-stat); letter-spacing: var(--track-display); margin: 0; font-variant-numeric: tabular-nums; }
143
144
  .t-stat--mono { font-family: var(--font-mono); letter-spacing: 0; } /* clocks, ETAs, codes */
144
145
  .t-stat--xl { font-size: var(--fs-hero); } /* the one number on a sheet */
@@ -155,9 +156,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
155
156
  --_shape: 0.75em 0.3em 0.9em 0.35em / 0.4em 0.95em 0.35em 0.8em; /* four unlike corners: a stroke drawn by hand has no two ends alike */
156
157
  background-color: var(--mark-bg, var(--_bg));
157
158
  background-image:
158
- linear-gradient(100deg, var(--_pool), transparent 0.55em), /* ink pools where the pen lands */
159
- linear-gradient(280deg, var(--_pool), transparent 0.4em), /* and a little where it lifts */
160
- linear-gradient(181deg, transparent 18%, var(--_pool) 52%, transparent 88%); /* the nib presses harder along its middle */
159
+ linear-gradient(calc(90deg + var(--tilt-base) * 10), var(--_pool), transparent 0.55em), /* ink pools where the pen lands */
160
+ linear-gradient(calc(270deg + var(--tilt-base) * 10), var(--_pool), transparent 0.4em), /* and a little where it lifts */
161
+ linear-gradient(calc(180deg + var(--tilt-base)), transparent 18%, var(--_pool) 52%, transparent 88%); /* the nib presses harder along its middle */
161
162
  color: var(--mark-ink, inherit);
162
163
  padding-inline: var(--mark-pad-x, 0.3em);
163
164
  border-radius: var(--mark-radius, var(--_shape));
@@ -171,7 +172,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
171
172
  /* read, not shown (Lh 2026-09-23): words a screen reader needs — a page's h1 the design leaves out — kept in the page, taking no room.
172
173
  The 1px is the one the platform needs to keep it in the accessibility tree, not a length of the design. */
173
174
  .screen :is(input, textarea, [contenteditable="true"]), .screen .t-select { -webkit-user-select: text; user-select: text; -webkit-touch-callout: default; }
174
- .t-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
175
+ .t-sr { position: absolute; width: var(--px); height: var(--px); margin: calc(-1 * var(--px)); padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
175
176
 
176
177
  /* ---- page: words straight on the base, no card around them — a
177
178
  heading, a paragraph, maybe a .cta-pack. Was .section until 2026-09-16
@@ -190,8 +191,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
190
191
  /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
191
192
  h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
192
193
  .page > .card__head > .t-h1 { font-size: var(--fs-h2); }
193
- .page > :is(img, .ph) { max-height: var(--page-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; align-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
194
- .page > .ph { width: 100%; height: var(--page-media-h, 14rem); }
195
194
  .page .cta-pack {
196
195
  display: flex;
197
196
  align-items: start; /* buttons keep their own height, never the row's */
@@ -216,12 +215,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
216
215
  /* ---- the turn: one flip mechanic for everything with two faces (a Postcard, a Card's `back`).
217
216
  The faces share one grid cell and turn on Y at --motion-flip; where motion is off they swap at once.
218
217
  The owner sets the box's size and the faces' look; .is-flipped on .flip shows the back. */
219
- .flip { perspective: 60rem; display: grid; }
218
+ .flip { perspective: calc(var(--tap-base) * 20); display: grid; }
220
219
  .flip__face {
221
220
  grid-area: 1 / 1;
222
221
  backface-visibility: hidden;
223
222
  -webkit-backface-visibility: hidden;
224
- transition: transform var(--motion-flip) ease-in-out;
223
+ transition: transform var(--motion-flip) var(--ease-in-out);
225
224
  }
226
225
  .flip__face--back { transform: rotateY(180deg); }
227
226
  .flip.is-flipped > .flip__face--front { transform: rotateY(-180deg); }
@@ -229,3 +228,128 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
229
228
  @media (prefers-reduced-motion: reduce) {
230
229
  .flip__face { transition: none; }
231
230
  }
231
+
232
+ /* ==== the screen's parts and the room it keeps, once written in the files of what they hold (#62: a block is written in its own file) ==== */
233
+ /* the appbar is fixed over the top of the screen: the screen reserves its height, from the same header-h token. The breathing under it is published:
234
+ a .step cancels it to touch the bar (actions.css) */
235
+ .screen:has(> .appbar) { --header-gap: var(--sp-2); padding-top: calc(var(--header-h) + var(--header-gap)); }
236
+ .screen.screen--fill:has(> .appbar) { padding-top: var(--safe-top); --header-gap: 0rem; }
237
+ /* a plain screen is a column at least one phone tall when it ends on an .action-bar or holds a .screen__centre (step.css, card.css) */
238
+ .screen:not(.screen--fill):has(> .action-bar:last-child, > .screen__centre) { display: flex; flex-direction: column; min-height: 100dvh; }
239
+ /* the centred group: the place of a .card--centre (card.css), for a small group of things */
240
+ .screen--fill > .screen__centre {
241
+ position: absolute;
242
+ top: var(--screen-centre);
243
+ translate: 0 -50%;
244
+ /* the cap: as a .card--centre's (card.css, which says why) */
245
+ --_lead: 0rem;
246
+ max-height: calc(2 * min(var(--screen-centre) - var(--_lead) - var(--screen-gap), 100% - var(--screen-centre) - var(--bar-reserve)));
247
+ overflow-y: auto;
248
+ overscroll-behavior: contain;
249
+ }
250
+ /* ---- centred block: the same place, for one thing to read that is not a
251
+ lone card — an empty state, a card with its quiet second button and the
252
+ pager under it. The block runs edge to
253
+ edge and keeps the screen's inset as padding, and
254
+ its children stack at the screen's own rhythm (Lh, 2026-09-11):
255
+ <div class="screen screen--fill"><header class="topbar">…</header>
256
+ <div class="screen__centre"><article class="card">…</article><button class="btn btn--quiet btn--block">…</button></div>
257
+ </div> */
258
+ .screen__centre {
259
+ inset-inline: 0;
260
+ padding-inline: var(--screen-pad);
261
+ display: grid;
262
+ gap: var(--screen-gap);
263
+ align-content: start;
264
+ }
265
+ .screen__centre > * { min-width: 0; }
266
+
267
+ /* ---- the same group on a plain .screen: centred when short, as long as it
268
+ likes when not (Lh, 2026-09-12) ----
269
+ The member sees one content area — whatever is between the top bar and the
270
+ bar — and reads it as a whole page: a short card sits at its middle, a long
271
+ one simply makes the page longer, runs under the bar (a sticky last row,
272
+ actions.css) and scrolls up like any page — nothing is capped, nothing
273
+ shrinks, no scroll box inside a box.
274
+ So on a .screen that is NOT --fill the group is in flow: the column is at
275
+ least one phone tall (step.css), the group takes the room after the top bar and
276
+ centres its children in it. The screen's own inset stays the inset.
277
+ <div class="screen"><header class="topbar">…</header>
278
+ <div class="screen__centre"><article class="card">…</article><div class="pager__at">…</div></div>
279
+ </div><div class="action-bar">…</div> */
280
+ .screen:not(.screen--fill) > .screen__centre { flex: 1; padding-inline: 0; align-content: center; }
281
+
282
+ .screen__centre:has(> :is(.stamp, .stamp-place)) { justify-items: center; text-align: center; }
283
+
284
+ /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
285
+ /* 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>
287
+ The heading and the far-right thing share the first row; a caption sits on
288
+ a row below (order 1), whatever the DOM order — so an author writes the
289
+ 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; }
292
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
293
+ .sheet-stack__base > .screen__centre { padding-inline: 0; }
294
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
295
+ /* the lucide tip stops 2.2 of 24 short of the box's bottom: lowered so the tip IS the bottom-centre; filled with the surface so the
296
+ street pattern never shows through the pin's body */
297
+ .map__pin > .icon { fill: var(--surface); }
298
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
299
+ /* the reading block: a .page (base.css) — the same words-on-the-base
300
+ block as anywhere else (it was .step__card, and a .card before 2026-09-16;
301
+ Lh: a step reads like a page, not a card in a page — so it IS the page's
302
+ block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
303
+ between things, the tokens .card publishes for the head and .bleed) and
304
+ gets the flex rule: it hugs its content, and when the column runs out (a
305
+ short phone, large text) it shrinks and scrolls inside itself instead of
306
+ pushing the bar or the sheet */
307
+ .step > .page, .step > .pages > .page {
308
+ gap: var(--card-gap);
309
+ padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
310
+ color: var(--on-surface); /* align-content: start is .page's own (base.css) — not restated here */
311
+ }
312
+ /* a lone .page carries the margin a walk's .pages does not (its own .page children do, below) —
313
+ the screen's gap, moved in here from above the step; the inset, taken back from the bleed.
314
+ It ends with the bottom layer's room, as a walk's page does: it scrolls only when the layer would cover its words
315
+ (timebank onboard-04, 2026-09-27 — its last Link lay under the bar; Lh: same rule as the walk) */
316
+ .step > .page { margin: var(--_lead, 0) var(--screen-pad) 0; padding-bottom: var(--bottom-h, 0rem); }
317
+ /* --bleed: the page's box runs to the screen's edges, the inset is its own padding — what leans, slides or casts a shadow past
318
+ the inset (a postcard) is cut at the glass, not at the inset; a little room on top for a turned corner. Nothing pans sideways. */
319
+ .step > .page--bleed { margin-inline: 0; padding-inline: var(--screen-pad); padding-block-start: var(--sp-2); overflow-x: hidden; }
320
+ .step > .pages > .page {
321
+ padding-inline: var(--screen-pad); /* the words keep the inset INSIDE it */
322
+ margin-top: var(--_lead, 0rem); /* the lead is each PAGE's margin (15% with no top, the screen's gap under a bar): the words start there and scroll up to the box's edge */
323
+ padding-bottom: var(--bottom-h, 0rem); /* exactly the bottom layer (measured by Screen): a page scrolls ONLY when the layer would cover its words, and then just far enough to clear them — at 100% nothing scrolls (Lh 2026-09-23) */
324
+ }
325
+ /* a walk page that OPENS on a picture (Lh 2026-09-23): the picture takes the lead's room — as wide as the content (edge to edge), from the top edge, as tall as
326
+ the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
327
+ .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
328
+ .step > .empty-state { flex: none; }
329
+ .sheet__body > .empty-state:only-child { flex: none; }
330
+ /* ---- where it sits in what actions.css draws (docs/reference.md rule 15) ---- */
331
+ /* ---- top bar: back + screen title ---- */
332
+ .topbar .t-h1 {
333
+ text-transform: uppercase;
334
+ flex: 1;
335
+ min-width: 0;
336
+ overflow: hidden;
337
+ text-overflow: ellipsis;
338
+ white-space: nowrap;
339
+ padding-block: 0.15em; /* Thai marks survive the clip — see card.css, the head rule */
340
+ margin-block: -0.15em;
341
+ }
342
+ /* ---- where it sits in what media.css draws (docs/reference.md rule 15) ---- */
343
+ .postcard__band .icon-btn[aria-pressed="true"] .icon { fill: currentColor; }
344
+
345
+ /* ---- where it sits in what card.css draws: a walk's page (docs/reference.md rule 15) ---- */
346
+ .pages > .page { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
347
+
348
+ /* ---- where it sits in what step.css draws: the reading block (docs/reference.md rule 15) ---- */
349
+ .step:has(> .sheet--peek) > .page { padding-bottom: var(--sheet-peek); }
350
+ .step > .page {
351
+ flex: 0 1 auto;
352
+ min-height: 0;
353
+ overflow-y: auto;
354
+ overscroll-behavior: contain;
355
+ }