@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/dist/cardds.css CHANGED
@@ -46,8 +46,8 @@
46
46
 
47
47
  Every length is rem so the whole system scales with the root
48
48
  font size (base.css steps it down on narrow phones and it follows
49
- the user's text-size setting). Only borders, rules and focus
50
- rings stay in px — they must stay crisp, not scale.
49
+ the user's text-size setting). Borders, rules and focus rings
50
+ are rem too (Lh 2026-10-20: "units are always rem").
51
51
  4pt grid: 1rem = 16px → --sp-1 = 4px.
52
52
  ============================================================ */
53
53
 
@@ -69,7 +69,7 @@
69
69
  --tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
70
70
  --header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
71
71
  Text-holders follow the type instead: --badge and --icon-xs (inline with captions) are on --fs-base.
72
- Not on a base: --screen-w (the column), borders / rules / focus (px, crisp), shadows, motion. */
72
+ Not on a base: --screen-w (the column), borders / rules / focus (fixed rem), shadows, motion. */
73
73
 
74
74
  /* ---- spacing (4pt grid): the number in the name is the multiplier ---- */
75
75
  --sp-0: calc(var(--sp-base) * 0.5); /* 2 hairline gaps (title→meta) */
@@ -119,7 +119,10 @@
119
119
  --tile-h: calc(var(--tap-base) * 11 / 4); /* 132 tile / band height */
120
120
 
121
121
  /* ---- radius ---- */
122
- --r-chip: 999px; /* pills, chips, buttons — always fully round */
122
+ /* TILT (Lh 2026-10-20: no unit a component writes for itself): every paper laid at an angle (a note, a stamp, a letter, a card
123
+ sent away) turns by a multiple of this. A theme that wants everything straight sets it to 0deg */
124
+ --tilt-base: 1deg;
125
+ --r-chip: 62.4375rem; /* pills, chips, buttons — always fully round */
123
126
  --r-card: calc(var(--r-base) * 2); /* 41.6 the one card radius: every card, tile, menu, sheet, fab — but a row card (--r-card-row) */
124
127
  --r-card-row: calc(var(--r-base) * 0.625); /* 13 a row card's corner (Lh 2026-10-17: a small card's corner shrinks to keep the big card's PROPORTION). Measured at 375:
125
128
  whole cards stand 186–437 tall, corner/height 0.095–0.224 (Card · Skeleton 0.122); a two-line row stands 88, so its
@@ -128,13 +131,19 @@
128
131
  --r-control: calc(var(--r-base) * 0.875); /* 18.2 fields, thumbs, menu rows */
129
132
  --r-badge: calc(var(--r-base) * 0.625); /* 13 tile badge, bars */
130
133
 
131
- /* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
132
- --border-w: 1.5px;
133
- --hairline-w: 2px; /* the thinnest mark: a sheet's grab line (Lh 2026-10-06: 1px, then 2px the same day) */
134
- --rule-w: 2px; /* dotted connectors, picker rules */
135
- --scroll-w: 1px; /* the scrollbar of a .bleed track: a hairline of ink */
136
- --focus: 2px solid currentColor; /* :focus-visible ring */
137
- --shadow-card: 0 1px 2px rgb(0 0 0 / 0.06), 0 0.5rem 1.5rem rgb(0 0 0 / 0.08);
134
+ /* ---- borders / rules / elevation (rem, like every length) ---- */
135
+ /* one device-independent pixel in rem: what the platform needs to keep a hidden word in the accessibility tree, and the half-width
136
+ of a drawn line. The browser's own text size: a field under it makes iOS zoom in on focus */
137
+ --px: 0.0625rem;
138
+ --browser-text: 1rem;
139
+ /* the wireframe map's two street directions (.map) */
140
+ --street-1: 75deg; --street-2: -18deg;
141
+ --border-w: 0.09375rem;
142
+ --hairline-w: 0.125rem; /* the thinnest mark: a sheet's grab line (Lh 2026-10-06: 1px, then 2px the same day) */
143
+ --rule-w: 0.125rem; /* dotted connectors, picker rules */
144
+ --scroll-w: 0.0625rem; /* the scrollbar of a .bleed track: a hairline of ink */
145
+ --focus: 0.125rem solid currentColor; /* :focus-visible ring */
146
+ --shadow-card: 0 0.0625rem 0.125rem rgb(0 0 0 / 0.06), 0 0.5rem 1.5rem rgb(0 0 0 / 0.08);
138
147
  --shadow-float: 0 0.25rem 0.75rem rgb(0 0 0 / 0.14);
139
148
  --shadow-sheet: 0 -0.5rem 2rem rgb(0 0 0 / 0.16); /* sheets, lifted cards */
140
149
 
@@ -144,11 +153,44 @@
144
153
  --motion-quick: 0.2s; /* a small state flip that isn't a press: a chevron turning, a popover/banner opening */
145
154
  --motion-slide: 0.22s; /* a centred card sliding out and the next in (card.css) — quick, a page turn (Lh, 2026-09-12) */
146
155
  --motion-flip: 0.45s; /* a postcard turning over (media.css) — slow enough to read as a card in the hand */
156
+ --motion-spin: 0.9s; /* a busy ring's one turn (Spinner, a busy FormNote) */
157
+ --motion-blink: 1.1s; /* a code cell's caret, on and off once */
158
+ --motion-flash: 0.5s; /* a secret code's digit seen before its dot takes it */
159
+ --motion-shake: 0.4s; /* a wrong code shaking once */
160
+ --motion-pulse: 0.4s; /* a sheet's grip, ink to ink and back, while the app reloads */
161
+ --motion-run: 6s; /* a long title running sideways, until its own length is measured (--_run) */
162
+ --motion-stagger: 70ms; /* one stacked sheet after another as they tuck or untuck */
163
+ /* LAYERS (Lh 2026-10-20: no number a component writes for itself): the order of things INSIDE one piece, which keeps its own
164
+ stacking context. Under its ground, flat, over it, over that, the picked one on top */
165
+ --z-under-2: -2; --z-under: -1; --z-flat: 0; --z-over: 1; --z-over-2: 2; --z-over-3: 3;
166
+ --z-pile: 7; /* a sticky pile's top note: one over the six it can show, each note under it one lower (StickyPile) */
167
+ /* EASING (Lh 2026-10-20: a component writes no curve of its own): the six ways a thing moves, read by every transition and animation.
168
+ --ease-settle is the one a moved row or card lands with (Row, CardFeed: Web Animations read it) */
169
+ --ease: ease;
170
+ --ease-in: ease-in;
171
+ --ease-out: ease-out;
172
+ --ease-in-out: ease-in-out;
173
+ --ease-linear: linear;
174
+ --ease-settle: cubic-bezier(0.2, 0.8, 0.2, 1);
175
+ --ease-step: steps(1); /* on or off with nothing between: a code cell's caret blinking, a secret digit before its dot */
147
176
  --motion-press: 0.12s; /* a control giving under the finger and springing back (actions.css PRESS) */
148
177
  --press-scale: 0.96; /* how far a finger-sized control gives — a share of its size */
178
+ --scale-lift: 1.06; /* a stamp lifted off the paper while the finger holds it */
179
+ --scale-pick: 1.7; /* a mood's dot, the one picked */
180
+ --scale-arrive: 0.6; /* a stamp's remove button, the size it grows from */
149
181
  --press-dim: 0.6; /* how far a card-wide one (a row, a summary line, a card) fades under the finger */
150
182
  --press-shade: 0.93; /* how far something with a thing behind it (a peeked sheet, a postcard) darkens under the finger */
151
183
  --off: 0.4; /* a disabled control's fade */
184
+ /* FADES (Lh 2026-10-20: no opacity a component writes for itself): what each lets through. Nine uses had eight values; one name each
185
+ until the system settles on fewer */
186
+ --fade-faint: 0.25; /* a halo behind an avatar */
187
+ --fade-dim: 0.3; /* a sheet's grip at rest, a day that cannot be picked */
188
+ --fade-soft: 0.4; /* the dots between a trio's answers */
189
+ --fade-half: 0.5; /* a step's button while the step is busy */
190
+ --fade-hint: 0.55; /* a field's placeholder */
191
+ --fade-mild: 0.65; /* a route's line after its code */
192
+ --fade-near: 0.85; /* an icon row */
193
+ --fade-most: 0.9; /* a quote's mark */
152
194
 
153
195
  /* ---- z-index: the few layers that stack ABOVE ordinary content, low to high ---- */
154
196
  --z-bar: 30; /* .action-bar, .float-bar — above .sheet-stack sheets (--level 1-5) */
@@ -184,18 +226,31 @@
184
226
  --fs-hero: calc(var(--fs-base) * 2.5); /* 55 the one number on a sheet */
185
227
  --fs-giant: calc(var(--fs-base) * 2.9); /* 64 data bands */
186
228
 
187
- /* level weight size / line-height family */
188
- --type-display: 800 var(--fs-display) / 1.15 var(--font-display); /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
189
- --track-display: -0.02em;
190
- --type-h1: 900 var(--fs-h1) / 1.2 var(--font-display);
191
- --track-h1: -0.015em;
192
- --type-h2: 700 var(--fs-h2) / 1.25 var(--font-display);
193
- --track-h2: -0.01em;
194
- --type-title: 650 var(--fs-title) / 1.3 var(--font-ui);
195
- --type-body: 400 var(--fs-body) / 1.5 var(--font-ui);
196
- --type-label: 600 var(--fs-label) / 1.2 var(--font-ui); /* chips, buttons */
197
- --type-caption: 500 var(--fs-caption) / 1.35 var(--font-ui);
198
- --type-stat: 800 var(--fs-stat) / 1.05 var(--font-display); /* big numbers */
229
+ /* TYPE (Lh 2026-10-20: every kind and every size has its tokens; a component writes no type value of its own).
230
+ THE SCALES: weight and leading, each step named once. A kind below picks a step of each; a piece that needs a step alone reads it. */
231
+ --fw-regular: 400; --fw-medium: 500; --fw-semibold: 600; --fw-strong: 650; --fw-bold: 700; --fw-heavy: 800; --fw-black: 900;
232
+ --lh-mark: 0.55; /* a quotation mark three lines tall, set into the first line */
233
+ --lh-band: 0.9; /* a data band's giant number */
234
+ --lh-badge: 0.95; /* a date box's two lines (JourneyBadge) */
235
+ --lh-solid: 1; /* a line as tall as its letters: a number in a disc, an icon's glyph, one big number */
236
+ --lh-tight: 1.1; /* a day's number, a tile's value */
237
+ --lh-field: 1.4; /* the words in a field */
238
+ --lh-hand: 1.6; /* a letter written by hand */
239
+ --lh-ruled: 1.7; /* a ruled sheet: the line under every line of writing */
240
+ --track-loose: 0.02em; /* initials in an avatar */
241
+ --track-caps: 0.08em; /* a kicker in capitals */
242
+ /* THE EIGHT KINDS: each has its face, weight, size (--fs-* above), leading and tracking, and its style --type-<kind> is made of them */
243
+ --fw-display: var(--fw-heavy); --lh-display: 1.15; --track-display: -0.02em; /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
244
+ --fw-h1: var(--fw-black); --lh-h1: 1.2; --track-h1: -0.015em;
245
+ --fw-h2: var(--fw-bold); --lh-h2: 1.25; --track-h2: -0.01em;
246
+ --fw-title: var(--fw-strong); --lh-title: 1.3; --track-title: normal;
247
+ --fw-body: var(--fw-regular); --lh-body: 1.5; --track-body: normal;
248
+ --fw-label: var(--fw-semibold); --lh-label: 1.2; --track-label: normal; /* chips, buttons */
249
+ --fw-caption: var(--fw-medium); --lh-caption: 1.35; --track-caption: normal;
250
+ --fw-stat: var(--fw-heavy); --lh-stat: 1.05; --track-stat: normal; /* big numbers */
251
+ --face-display: var(--font-display); --face-h1: var(--font-display); --face-h2: var(--font-display); --face-stat: var(--font-display);
252
+ --face-title: var(--font-ui); --face-body: var(--font-ui); --face-label: var(--font-ui); --face-caption: var(--font-ui);
253
+ /* --type-<kind> is set where a palette or a mode is (below), so a weight or a face tuned there reaches the words inside */
199
254
 
200
255
  /* ============================================================
201
256
  COLOR ROLES — wireframe defaults (grayscale).
@@ -278,6 +333,12 @@
278
333
  --ink-rail: color-mix(in srgb, currentColor 30%, transparent); /* todo steps, ghost edges */
279
334
  --accent-soft: color-mix(in srgb, var(--accent) 22%, transparent); /* calendar marks */
280
335
  --accent-mid: color-mix(in srgb, var(--accent) 50%, transparent);
336
+ /* PASTELS (Lh 2026-10-20: a component never brings a colour of its own; every colour is the theme's): the nine pastel papers of a
337
+ sticky note, a note chooser's swatch and a stamp's picture ground (src/sticky/tones.ts names them), and a stamp's paper. Light
338
+ grounds in every palette: their words are the onlight set. A theme retunes them here; a knob (--sticky-<tone>-bg) retunes one place */
339
+ --pastel-cotton: #f3efe2; --pastel-blush: #f6d5da; --pastel-tea-leaf: #d5e8c5; --pastel-custard: #f6e9a0; --pastel-salmon: #f3c7b3;
340
+ --pastel-iceberg: #cfe0ee; --pastel-cocoa: #dccbbd; --pastel-apricot: #f6d49a; --pastel-water: #bcd6f3;
341
+ --stamp-paper: #fffdf8;
281
342
  --scrim: color-mix(in srgb, #000 50%, transparent); /* dim stage, cover gradient — ALWAYS black, never the brand (Lh 2026-09-24) */
282
343
  }
283
344
 
@@ -302,6 +363,19 @@
302
363
  sit on <body>: a custom property is resolved where it is declared. A signal's SOFT ground (what a Snackbar stands on) is its
303
364
  onlight shade washed 14% into the surface (tone 2's formula) */
304
365
  :root, [data-palette], [data-mode] {
366
+ /* each kind's style, from its tokens: set here so a palette or a mode that tunes a kind's weight, size, leading or face reaches .t-<kind> */
367
+ --type-display: var(--fw-display) var(--fs-display) / var(--lh-display) var(--face-display);
368
+ --type-h1: var(--fw-h1) var(--fs-h1) / var(--lh-h1) var(--face-h1);
369
+ --type-h2: var(--fw-h2) var(--fs-h2) / var(--lh-h2) var(--face-h2);
370
+ --type-title: var(--fw-title) var(--fs-title) / var(--lh-title) var(--face-title);
371
+ --type-body: var(--fw-body) var(--fs-body) / var(--lh-body) var(--face-body);
372
+ --type-label: var(--fw-label) var(--fs-label) / var(--lh-label) var(--face-label);
373
+ --type-caption: var(--fw-caption) var(--fs-caption) / var(--lh-caption) var(--face-caption);
374
+ --type-stat: var(--fw-stat) var(--fs-stat) / var(--lh-stat) var(--face-stat);
375
+ /* shadows of paper laid on paper: mixed from the onlight ink, so re-derived where a palette is set */
376
+ --shadow-paper: 0 var(--sp-1) var(--sp-2) color-mix(in srgb, var(--ink-onlight) 16%, transparent); /* a stamp lying on paper: follows its teeth (drop-shadow) */
377
+ --shadow-lift: 0 var(--sp-0) var(--sp-1) color-mix(in srgb, var(--ink-onlight) 35%, transparent); /* a small disc lifted over a picture */
378
+ --shadow-fold: calc(-1 * var(--sp-0)) calc(-1 * var(--sp-0)) var(--sp-1) color-mix(in srgb, var(--ink-onlight) 8%, transparent); /* a note's folded corner */
305
379
  --on-surface: color-mix(in srgb, var(--ink-ondark) calc(var(--surface-dark) * 100%), var(--ink-onlight));
306
380
  --on-surface-muted: color-mix(in srgb, var(--ink-ondark-muted) calc(var(--surface-dark) * 100%), var(--ink-onlight-muted));
307
381
  --action-ink: color-mix(in srgb, var(--ink-ondark) calc(var(--action-dark) * 100%), var(--ink-onlight));
@@ -322,7 +396,7 @@
322
396
  surface turns everything on it to ondark with no rule per piece. A piece with no fill that stands in for a surface (a stat
323
397
  tile) takes its ground's side: `--dark: inherit`. inkGrounds() measures it; docs/reference.md "Ink follows its ground" */
324
398
  :where(body, [data-palette], [data-mode], .card, .tile, .modal__drawer, .postcard__front, .postcard__back, .sticky, .post-paper,
325
- .float-bar__pill, .snackbar__body, .delta, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .map__pin, .sheet--paper > .sheet__body,
399
+ .float-bar__pill, .snackbar__body, .delta, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .map__pin, .sheet--paper > :is(.sheet__body, .card__foot),
326
400
  .postcard__band, .track__step, .toggle, .chip, .card__face, .card--bands > :is(.card__head, .card__foot), .card--bands > .card__face > :is(.card__head, .card__foot)) {
327
401
  --card-ink: color-mix(in srgb, var(--ink-ondark) calc(var(--dark) * 100%), var(--ink-onlight));
328
402
  --card-muted: color-mix(in srgb, var(--ink-ondark-muted) calc(var(--dark) * 100%), var(--ink-onlight-muted));
@@ -335,8 +409,8 @@
335
409
  }
336
410
  :where(body, [data-palette], [data-mode]) { --dark: var(--surface-dark); }
337
411
 
338
- :root { --safe-top: calc(env(safe-area-inset-top, 0px) + var(--vv-top, 0px)); /* + the phone's pan while a keyboard is up (cardds.js) */ --safe-bottom: env(safe-area-inset-bottom, 0px); }
339
- * { --_safe-b-open: max(0px, var(--safe-bottom) - var(--kb, 0px)); }
412
+ :root { --safe-top: calc(env(safe-area-inset-top, 0rem) + var(--vv-top, 0rem)); /* + the phone's pan while a keyboard is up (cardds.js) */ --safe-bottom: env(safe-area-inset-bottom, 0rem); }
413
+ * { --_safe-b-open: max(0rem, var(--safe-bottom) - var(--kb, 0rem)); }
340
414
 
341
415
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
342
416
  @property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; } /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
@@ -352,7 +426,7 @@
352
426
 
353
427
  /* ---- css/palette.css — color settings — the file you tune ---- */
354
428
  /* ============================================================
355
- cardds/palette.css — COLOR SETTINGS. The only file you tune.
429
+ cardds/palette.css — the palettes cardds ships. A project tunes its own theme.css (theme.template.css), never this file.
356
430
  Apply with <html data-palette="clay"> (or <body>).
357
431
  No attribute = wireframe grayscale from tokens.css.
358
432
  Each palette overrides role tokens only — components never change.
@@ -568,6 +642,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
568
642
 
569
643
  /* icon from icons.svg sprite: <svg class="icon"><use href="icons.svg#bell"/></svg> */
570
644
  .icon { width: var(--icon-size, var(--icon)); height: var(--icon-size, var(--icon)); flex: none; }
645
+ /* 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
646
+ is the icon as it is. No fill: a drawing's own fill="none" is an attribute any declared fill would beat */
647
+ .icon {
648
+ display: var(--icon-display, block); vertical-align: var(--icon-align, baseline); margin: var(--icon-margin, 0);
649
+ padding: var(--icon-pad, 0); border-radius: var(--icon-radius, 0); background: var(--icon-bg, none); color: var(--icon-ink, inherit);
650
+ rotate: var(--icon-turn, none); translate: var(--icon-shift, none); transition: var(--icon-motion, all 0s); animation: var(--icon-spin, none);
651
+ }
571
652
  .icon--sm { width: var(--icon-sm-size, var(--icon-sm)); height: var(--icon-sm-size, var(--icon-sm)); }
572
653
  .icon--xs { width: var(--icon-xs-size, var(--icon-xs)); height: var(--icon-xs-size, var(--icon-xs)); } /* inside captions, chips, links */
573
654
  .icon--lg { width: var(--icon-lg-size, var(--icon-lg)); height: var(--icon-lg-size, var(--icon-lg)); } /* empty states */
@@ -616,12 +697,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
616
697
  position: relative; /* the box a .card--centre or a .screen__centre floats in (card.css) */
617
698
  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 */
618
699
  }
619
- /* min-height: 0 — a flex item's floor is its content, and the stack's content
620
- is its own full-height sheets, so without it the stack grew to the whole
621
- screen and its open sheet ran past the bottom edge by the bars' height,
622
- unreachable by scrolling (timebank home, 2026-09-13). The stack IS the room
623
- left under the bars; every sheet fills exactly that. */
624
- .screen--fill > .sheet-stack { flex: 1; min-height: 0; }
625
700
 
626
701
  /* ---- typography levels ---- */
627
702
  /* --fs-self: each level publishes its own size, for a consumer that scales a level without knowing which */
@@ -631,7 +706,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
631
706
  .t-title { font: var(--type-title); margin: 0; --fs-self: var(--fs-title); }
632
707
  .t-body { font: var(--type-body); margin: 0; }
633
708
  .t-label { font: var(--type-label); }
634
- .t-caption { font: var(--type-caption); }
709
+ .t-caption { font: var(--type-caption); color: var(--t-caption-ink, inherit); }
635
710
  .t-stat { font: var(--type-stat); letter-spacing: var(--track-display); margin: 0; font-variant-numeric: tabular-nums; }
636
711
  .t-stat--mono { font-family: var(--font-mono); letter-spacing: 0; } /* clocks, ETAs, codes */
637
712
  .t-stat--xl { font-size: var(--fs-hero); } /* the one number on a sheet */
@@ -648,9 +723,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
648
723
  --_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 */
649
724
  background-color: var(--mark-bg, var(--_bg));
650
725
  background-image:
651
- linear-gradient(100deg, var(--_pool), transparent 0.55em), /* ink pools where the pen lands */
652
- linear-gradient(280deg, var(--_pool), transparent 0.4em), /* and a little where it lifts */
653
- linear-gradient(181deg, transparent 18%, var(--_pool) 52%, transparent 88%); /* the nib presses harder along its middle */
726
+ linear-gradient(calc(90deg + var(--tilt-base) * 10), var(--_pool), transparent 0.55em), /* ink pools where the pen lands */
727
+ linear-gradient(calc(270deg + var(--tilt-base) * 10), var(--_pool), transparent 0.4em), /* and a little where it lifts */
728
+ linear-gradient(calc(180deg + var(--tilt-base)), transparent 18%, var(--_pool) 52%, transparent 88%); /* the nib presses harder along its middle */
654
729
  color: var(--mark-ink, inherit);
655
730
  padding-inline: var(--mark-pad-x, 0.3em);
656
731
  border-radius: var(--mark-radius, var(--_shape));
@@ -664,7 +739,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
664
739
  /* 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.
665
740
  The 1px is the one the platform needs to keep it in the accessibility tree, not a length of the design. */
666
741
  .screen :is(input, textarea, [contenteditable="true"]), .screen .t-select { -webkit-user-select: text; user-select: text; -webkit-touch-callout: default; }
667
- .t-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
742
+ .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; }
668
743
 
669
744
  /* ---- page: words straight on the base, no card around them — a
670
745
  heading, a paragraph, maybe a .cta-pack. Was .section until 2026-09-16
@@ -683,8 +758,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
683
758
  /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
684
759
  h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
685
760
  .page > .card__head > .t-h1 { font-size: var(--fs-h2); }
686
- .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 */
687
- .page > .ph { width: 100%; height: var(--page-media-h, 14rem); }
688
761
  .page .cta-pack {
689
762
  display: flex;
690
763
  align-items: start; /* buttons keep their own height, never the row's */
@@ -709,12 +782,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
709
782
  /* ---- the turn: one flip mechanic for everything with two faces (a Postcard, a Card's `back`).
710
783
  The faces share one grid cell and turn on Y at --motion-flip; where motion is off they swap at once.
711
784
  The owner sets the box's size and the faces' look; .is-flipped on .flip shows the back. */
712
- .flip { perspective: 60rem; display: grid; }
785
+ .flip { perspective: calc(var(--tap-base) * 20); display: grid; }
713
786
  .flip__face {
714
787
  grid-area: 1 / 1;
715
788
  backface-visibility: hidden;
716
789
  -webkit-backface-visibility: hidden;
717
- transition: transform var(--motion-flip) ease-in-out;
790
+ transition: transform var(--motion-flip) var(--ease-in-out);
718
791
  }
719
792
  .flip__face--back { transform: rotateY(180deg); }
720
793
  .flip.is-flipped > .flip__face--front { transform: rotateY(-180deg); }
@@ -723,6 +796,131 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
723
796
  .flip__face { transition: none; }
724
797
  }
725
798
 
799
+ /* ==== 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) ==== */
800
+ /* 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:
801
+ a .step cancels it to touch the bar (actions.css) */
802
+ .screen:has(> .appbar) { --header-gap: var(--sp-2); padding-top: calc(var(--header-h) + var(--header-gap)); }
803
+ .screen.screen--fill:has(> .appbar) { padding-top: var(--safe-top); --header-gap: 0rem; }
804
+ /* 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) */
805
+ .screen:not(.screen--fill):has(> .action-bar:last-child, > .screen__centre) { display: flex; flex-direction: column; min-height: 100dvh; }
806
+ /* the centred group: the place of a .card--centre (card.css), for a small group of things */
807
+ .screen--fill > .screen__centre {
808
+ position: absolute;
809
+ top: var(--screen-centre);
810
+ translate: 0 -50%;
811
+ /* the cap: as a .card--centre's (card.css, which says why) */
812
+ --_lead: 0rem;
813
+ max-height: calc(2 * min(var(--screen-centre) - var(--_lead) - var(--screen-gap), 100% - var(--screen-centre) - var(--bar-reserve)));
814
+ overflow-y: auto;
815
+ overscroll-behavior: contain;
816
+ }
817
+ /* ---- centred block: the same place, for one thing to read that is not a
818
+ lone card — an empty state, a card with its quiet second button and the
819
+ pager under it. The block runs edge to
820
+ edge and keeps the screen's inset as padding, and
821
+ its children stack at the screen's own rhythm (Lh, 2026-09-11):
822
+ <div class="screen screen--fill"><header class="topbar">…</header>
823
+ <div class="screen__centre"><article class="card">…</article><button class="btn btn--quiet btn--block">…</button></div>
824
+ </div> */
825
+ .screen__centre {
826
+ inset-inline: 0;
827
+ padding-inline: var(--screen-pad);
828
+ display: grid;
829
+ gap: var(--screen-gap);
830
+ align-content: start;
831
+ }
832
+ .screen__centre > * { min-width: 0; }
833
+
834
+ /* ---- the same group on a plain .screen: centred when short, as long as it
835
+ likes when not (Lh, 2026-09-12) ----
836
+ The member sees one content area — whatever is between the top bar and the
837
+ bar — and reads it as a whole page: a short card sits at its middle, a long
838
+ one simply makes the page longer, runs under the bar (a sticky last row,
839
+ actions.css) and scrolls up like any page — nothing is capped, nothing
840
+ shrinks, no scroll box inside a box.
841
+ So on a .screen that is NOT --fill the group is in flow: the column is at
842
+ least one phone tall (step.css), the group takes the room after the top bar and
843
+ centres its children in it. The screen's own inset stays the inset.
844
+ <div class="screen"><header class="topbar">…</header>
845
+ <div class="screen__centre"><article class="card">…</article><div class="pager__at">…</div></div>
846
+ </div><div class="action-bar">…</div> */
847
+ .screen:not(.screen--fill) > .screen__centre { flex: 1; padding-inline: 0; align-content: center; }
848
+
849
+ .screen__centre:has(> :is(.stamp, .stamp-place)) { justify-items: center; text-align: center; }
850
+
851
+ /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
852
+ /* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
853
+ <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>
854
+ The heading and the far-right thing share the first row; a caption sits on
855
+ a row below (order 1), whatever the DOM order — so an author writes the
856
+ words in reading order. */
857
+ .card__head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
858
+ .card__head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
859
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
860
+ .sheet-stack__base > .screen__centre { padding-inline: 0; }
861
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
862
+ /* 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
863
+ street pattern never shows through the pin's body */
864
+ .map__pin > .icon { fill: var(--surface); }
865
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
866
+ /* the reading block: a .page (base.css) — the same words-on-the-base
867
+ block as anywhere else (it was .step__card, and a .card before 2026-09-16;
868
+ Lh: a step reads like a page, not a card in a page — so it IS the page's
869
+ block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
870
+ between things, the tokens .card publishes for the head and .bleed) and
871
+ gets the flex rule: it hugs its content, and when the column runs out (a
872
+ short phone, large text) it shrinks and scrolls inside itself instead of
873
+ pushing the bar or the sheet */
874
+ .step > .page, .step > .pages > .page {
875
+ gap: var(--card-gap);
876
+ padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
877
+ color: var(--on-surface); /* align-content: start is .page's own (base.css) — not restated here */
878
+ }
879
+ /* a lone .page carries the margin a walk's .pages does not (its own .page children do, below) —
880
+ the screen's gap, moved in here from above the step; the inset, taken back from the bleed.
881
+ It ends with the bottom layer's room, as a walk's page does: it scrolls only when the layer would cover its words
882
+ (timebank onboard-04, 2026-09-27 — its last Link lay under the bar; Lh: same rule as the walk) */
883
+ .step > .page { margin: var(--_lead, 0) var(--screen-pad) 0; padding-bottom: var(--bottom-h, 0rem); }
884
+ /* --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
885
+ 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. */
886
+ .step > .page--bleed { margin-inline: 0; padding-inline: var(--screen-pad); padding-block-start: var(--sp-2); overflow-x: hidden; }
887
+ .step > .pages > .page {
888
+ padding-inline: var(--screen-pad); /* the words keep the inset INSIDE it */
889
+ 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 */
890
+ 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) */
891
+ }
892
+ /* 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
893
+ the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
894
+ .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
895
+ .step > .empty-state { flex: none; }
896
+ .sheet__body > .empty-state:only-child { flex: none; }
897
+ /* ---- where it sits in what actions.css draws (docs/reference.md rule 15) ---- */
898
+ /* ---- top bar: back + screen title ---- */
899
+ .topbar .t-h1 {
900
+ text-transform: uppercase;
901
+ flex: 1;
902
+ min-width: 0;
903
+ overflow: hidden;
904
+ text-overflow: ellipsis;
905
+ white-space: nowrap;
906
+ padding-block: 0.15em; /* Thai marks survive the clip — see card.css, the head rule */
907
+ margin-block: -0.15em;
908
+ }
909
+ /* ---- where it sits in what media.css draws (docs/reference.md rule 15) ---- */
910
+ .postcard__band .icon-btn[aria-pressed="true"] .icon { fill: currentColor; }
911
+
912
+ /* ---- where it sits in what card.css draws: a walk's page (docs/reference.md rule 15) ---- */
913
+ .pages > .page { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
914
+
915
+ /* ---- where it sits in what step.css draws: the reading block (docs/reference.md rule 15) ---- */
916
+ .step:has(> .sheet--peek) > .page { padding-bottom: var(--sheet-peek); }
917
+ .step > .page {
918
+ flex: 0 1 auto;
919
+ min-height: 0;
920
+ overflow-y: auto;
921
+ overscroll-behavior: contain;
922
+ }
923
+
726
924
  /* ---- css/card.css — card boundary + list variants ---- */
727
925
  /* ============================================================
728
926
  cardds/card.css — the card boundary + list variants
@@ -755,10 +953,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
755
953
  card once `sent` and a `Card` with `leave` read the same two knobs; the time is --motion-sheet. `.card--leaver` is a card whose app may ask it
756
954
  to go (it holds the transition, so it comes back as it went); `.card--away` is one that has been asked. A knob of the time would read dead:
757
955
  the knob walk switches motion off */
758
- :is(.card--leaver, .post-desk__card) { transition: translate var(--motion-sheet) ease, rotate var(--motion-sheet) ease; }
759
- :is(.card--away, .post-desk--sent .post-desk__card) { --_y: calc(var(--leave-rise, 160%) * -1); rotate: var(--leave-turn, -5deg); }
956
+ .card--leaver { transition: translate var(--motion-sheet) var(--ease), rotate var(--motion-sheet) var(--ease); }
957
+ .card--away { --_y: calc(var(--leave-rise, 160%) * -1); rotate: var(--leave-turn, calc(var(--tilt-base) * -5)); }
760
958
  .card--away { translate: 0 var(--_y); }
761
- @media (prefers-reduced-motion: reduce) { :is(.card--leaver, .post-desk__card) { transition: none; } }
959
+ @media (prefers-reduced-motion: reduce) { .card--leaver { transition: none; } }
762
960
 
763
961
  /* each tone is a fill and a side; its words and glyphs follow from the side (tokens.css) */
764
962
  .card--2 { --card-bg: var(--card-2-bg); --dark: var(--card-2-dark); }
@@ -775,18 +973,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
775
973
  multi-line title can opt back out with .card__head--top. */
776
974
  .card__head {
777
975
  display: flex;
778
- flex-wrap: wrap; /* the caption takes a line of its own — no wrapper div */
976
+ flex-wrap: var(--card-head-wrap, wrap); /* the caption takes a line of its own — no wrapper div */
779
977
  align-items: center;
780
978
  justify-content: space-between;
781
979
  column-gap: var(--card-head-col-gap, var(--sp-3));
782
980
  row-gap: var(--card-head-row-gap, var(--sp-0));
981
+ min-height: var(--card-head-min-h, auto);
783
982
  }
784
- /* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
785
- <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>
786
- The heading and the far-right thing share the first row; a caption sits on
787
- a row below (order 1), whatever the DOM order — so an author writes the
788
- words in reading order. */
789
- .card__head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
790
983
  .card__head--top { align-items: flex-start; }
791
984
 
792
985
  .card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
@@ -819,15 +1012,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
819
1012
  <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><span class="stamp">…</span></div> */
820
1013
  .card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
821
1014
  .card__head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
822
- .card__head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
823
- .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
824
1015
 
825
1016
  /* a JourneyBadge at the edge of the card's own first head (the date box in the top-left or top-right corner) keeps its corner concentric
826
1017
  with the card's: the card's corner (--r-card) less the room the padding leaves round it. journey.css reads it behind the theme's --tile-badge-radius,
827
1018
  so a theme that sets that knob still wins. */
828
1019
  .card > .card__head:first-child > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); }
829
1020
 
830
- /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are white paper bands edge to
1021
+ /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are paper bands edge to
831
1022
  edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
832
1023
  clips, so the bands keep its corners. A two-faced card (below) carries the look on each face. */
833
1024
  :is(.card--bands, .card--bands > .card__face) {
@@ -864,13 +1055,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
864
1055
  position: relative;
865
1056
  }
866
1057
  .card__face > * { min-width: 0; }
867
- .card__middle { display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, 18.75rem); overflow: hidden; }
868
- .card__turn { position: absolute; inset: 0; width: 1px; height: 1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
1058
+ .card__middle { display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, calc(var(--tap-base) * 25 / 4)); overflow: hidden; }
1059
+ .card__turn { position: absolute; inset: 0; width: var(--px); height: var(--px); padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
869
1060
  .card__face:has(> .card__turn:focus-visible) { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
870
- /* 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 */
871
- .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; }
872
- .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.5; }
873
- .card__head > .chip-row > .chip > .icon { display: inline-block; vertical-align: -0.15em; margin-inline-end: var(--chip-sm-gap, var(--sp-1)); }
1061
+ .card__head > .chip-row > .chip > .icon { --icon-display: inline-block; --icon-align: -0.15em; --icon-margin: 0 var(--chip-sm-gap, var(--sp-1)) 0 0; }
874
1062
 
875
1063
  /* ---- bleed: a scrolling track that runs to the container's edges ----
876
1064
  For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
@@ -891,15 +1079,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
891
1079
  nothing jumps. Chromium/Safari paint it through the ::-webkit-scrollbar
892
1080
  parts; Firefox gets its thin bar in the same ink. */
893
1081
  .bleed {
894
- margin-inline: calc(-1 * var(--card-pad, 0px));
895
- padding-inline: var(--card-pad, 0px);
1082
+ margin-inline: calc(-1 * var(--card-pad, 0rem));
1083
+ padding-inline: var(--card-pad, 0rem);
896
1084
  padding-block-end: var(--sp-2);
897
- scroll-padding-inline: var(--card-pad, 0px);
1085
+ scroll-padding-inline: var(--card-pad, 0rem);
898
1086
  flex-wrap: nowrap;
899
1087
  overflow-x: auto;
900
1088
  }
901
1089
  .bleed::-webkit-scrollbar { height: var(--scroll-w); }
902
- .bleed::-webkit-scrollbar-track { background: transparent; margin-inline: var(--card-pad, 0px); }
1090
+ .bleed::-webkit-scrollbar-track { background: transparent; margin-inline: var(--card-pad, 0rem); }
903
1091
  .bleed::-webkit-scrollbar-thumb { background: transparent; }
904
1092
  .bleed:is(:hover, .is-scrolling)::-webkit-scrollbar-thumb { background: currentColor; }
905
1093
  @supports not selector(::-webkit-scrollbar) {
@@ -913,12 +1101,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
913
1101
  /* foot: the card's action buttons on one line. One button takes the width,
914
1102
  up to four share it in even parts — so the gaps read even too — and from
915
1103
  three on they drop their side padding so the labels keep the room. */
916
- .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; }
917
- .card__foot > .btn { flex: 1 1 0; min-width: 0; }
918
- /* …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
919
- a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
920
- .card__foot > .btn--form { flex: 0 1 auto; }
921
- .card__foot:has(> :nth-child(3)) > .btn { padding-inline: var(--sp-2); }
1104
+ .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; color: var(--card-foot-ink, inherit); }
1105
+ .card__foot:has(> :nth-child(3)) > .btn { --btn-pad-x: var(--sp-2); }
922
1106
  /* a small link under the buttons (CardFoot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
923
1107
  whole short row of its own, centred — it never shares the buttons' line */
924
1108
  .card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
@@ -927,15 +1111,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
927
1111
  /* media placeholder for wireframes — currentColor so it survives any palette */
928
1112
  .ph {
929
1113
  background:
930
- linear-gradient(to top right, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px)),
931
- linear-gradient(to top left, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px));
1114
+ linear-gradient(to top right, transparent calc(50% - var(--px)), currentColor calc(50% - var(--px)), currentColor calc(50% + var(--px)), transparent calc(50% + var(--px))),
1115
+ linear-gradient(to top left, transparent calc(50% - var(--px)), currentColor calc(50% - var(--px)), currentColor calc(50% + var(--px)), transparent calc(50% + var(--px)));
932
1116
  border: var(--border-w) solid currentColor;
933
1117
  border-radius: var(--r-media);
934
1118
  opacity: var(--off);
935
- min-height: 10rem;
1119
+ min-height: calc(var(--tap-base) * 10 / 3);
936
1120
  }
937
- .ph--sm { min-height: 7.5rem; }
938
- .ph--md { min-height: 9rem; }
1121
+ .ph--sm { min-height: calc(var(--tap-base) * 5 / 2); }
1122
+ .ph--md { min-height: calc(var(--tap-base) * 3); }
939
1123
  /* ---- centred card: one thing to read, at the human centre of a fill screen ----
940
1124
  (.screen__centre below is the same placement for a small GROUP of things)
941
1125
  Where the centre of a phone IS, to the person holding it: the bottom fifth is
@@ -948,8 +1132,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
948
1132
  The card hugs its words and keeps the screen's inset. It is capped so that it
949
1133
  never reaches the title bar above it nor the bar's room below it — past that it
950
1134
  scrolls inside — from the same tokens those bars reserve their room with. */
951
- .card--centre,
952
- .screen--fill > .screen__centre {
1135
+ .card--centre {
953
1136
  position: absolute;
954
1137
  top: var(--screen-centre);
955
1138
  translate: 0 -50%;
@@ -966,38 +1149,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
966
1149
  :is(.topbar, .appbar) ~ :is(.card--centre, .screen__centre) { --_lead: var(--header-h); }
967
1150
  .card--centre { inset-inline: var(--screen-pad); }
968
1151
 
969
- /* ---- centred block: the same place, for one thing to read that is not a
970
- lone card — an empty state, a card with its quiet second button and the
971
- pager under it. The block runs edge to
972
- edge and keeps the screen's inset as padding, and
973
- its children stack at the screen's own rhythm (Lh, 2026-09-11):
974
- <div class="screen screen--fill"><header class="topbar">…</header>
975
- <div class="screen__centre"><article class="card">…</article><button class="btn btn--quiet btn--block">…</button></div>
976
- </div> */
977
- .screen__centre {
978
- inset-inline: 0;
979
- padding-inline: var(--screen-pad);
980
- display: grid;
981
- gap: var(--screen-gap);
982
- align-content: start;
983
- }
984
- .screen__centre > * { min-width: 0; }
985
-
986
- /* ---- the same group on a plain .screen: centred when short, as long as it
987
- likes when not (Lh, 2026-09-12) ----
988
- The member sees one content area — whatever is between the top bar and the
989
- bar — and reads it as a whole page: a short card sits at its middle, a long
990
- one simply makes the page longer, runs under the bar (a sticky last row,
991
- actions.css) and scrolls up like any page — nothing is capped, nothing
992
- shrinks, no scroll box inside a box.
993
- So on a .screen that is NOT --fill the group is in flow: the column is at
994
- least one phone tall (step.css), the group takes the room after the top bar and
995
- centres its children in it. The screen's own inset stays the inset.
996
- <div class="screen"><header class="topbar">…</header>
997
- <div class="screen__centre"><article class="card">…</article><div class="pager__at">…</div></div>
998
- </div><div class="action-bar">…</div> */
999
- .screen:not(.screen--fill) > .screen__centre { flex: 1; padding-inline: 0; align-content: center; }
1000
-
1001
1152
  /* ---- the card that changes slides: out one side, the next in from the other ----
1002
1153
  A .screen__centre group that walks pages (data-slide="next" | "back" on the
1003
1154
  root while a step changes) turns its card like a page: the old card slides
@@ -1026,7 +1177,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1026
1177
  user-select: none; /* a walk is dragged, not read-and-selected — without this, a finger crossing the words highlights them instead of turning the page (timebank #59) */
1027
1178
  touch-action: pan-y; /* leaves vertical scroll to the browser; horizontal is the app's own drag, never a native pan */
1028
1179
  }
1029
- .pages > :is(.card, .page) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
1180
+ .pages > .card { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
1030
1181
  .pages > [inert] { visibility: hidden; }
1031
1182
  /* pages of WORDS (Lh, 2026-09-20): the same walk with .page pages, on the base — in a .step the
1032
1183
  .pages is the reading block (step.css), the round pager in the bar turns it, the same slideTo().
@@ -1038,7 +1189,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1038
1189
  ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
1039
1190
  ::view-transition-old(screen-out), ::view-transition-new(screen-in) { /* screen-*: slideScreen — one whole screen to the next (Lh 2026-09-25) */
1040
1191
  animation-duration: var(--motion-slide);
1041
- animation-timing-function: ease-out;
1192
+ animation-timing-function: var(--ease-out);
1042
1193
  mix-blend-mode: normal;
1043
1194
  }
1044
1195
  :root[data-slide="next"]::view-transition-old(centre-card-out), :root[data-slide="next"]::view-transition-old(screen-out) { animation-name: centre-card-out-left; }
@@ -1053,9 +1204,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1053
1204
  ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
1054
1205
  ::view-transition-old(screen-out), ::view-transition-new(screen-in) { animation: none; }
1055
1206
  }
1056
- /* the page dots under the card sit centred under it — the indicator belongs
1057
- with the page that changes, never in the bar (Lh, 2026-09-12) */
1058
- .screen__centre > .pager__at { justify-self: center; }
1059
1207
 
1060
1208
  /* compact card inside a card (nested note, promo) */
1061
1209
  .card--sm { padding: var(--card-sm-pad, var(--sp-4)); --card-gap: var(--sp-2); border-radius: var(--card-sm-radius, var(--r-card)); }
@@ -1064,7 +1212,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1064
1212
  stack.css; a stacked sheet is a plain .card, the material */
1065
1213
 
1066
1214
  /* trailing icon cluster in a card header */
1067
- .icon-row { display: flex; align-items: center; gap: var(--icon-row-gap, var(--sp-4)); opacity: 0.85; }
1215
+ .icon-row { display: flex; align-items: center; gap: var(--icon-row-gap, var(--sp-4)); opacity: var(--fade-near); }
1068
1216
 
1069
1217
  /* ---- card list: rows, each row its own boundary ---- */
1070
1218
  .card-list { display: grid; gap: var(--card-list-gap, var(--sp-3)); }
@@ -1100,7 +1248,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1100
1248
  .card__remove {
1101
1249
  --_size: var(--card-remove-size, calc(var(--tap-base) / 2));
1102
1250
  position: relative;
1103
- z-index: 1;
1251
+ z-index: var(--z-over);
1104
1252
  flex: none;
1105
1253
  align-self: flex-start;
1106
1254
  display: grid;
@@ -1118,7 +1266,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1118
1266
  cursor: pointer;
1119
1267
  }
1120
1268
  .card__remove::after { content: ""; position: absolute; inset: 50% auto auto 50%; translate: -50% -50%; width: max(var(--tap-floor), var(--_size)); aspect-ratio: 1; border-radius: 50%; }
1121
- .card__remove > .icon { width: 62%; height: 62%; }
1269
+ .card__remove > .icon { --icon-size: 62%; }
1122
1270
  .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
1123
1271
 
1124
1272
  /* leading thumb/icon in a row */
@@ -1161,7 +1309,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1161
1309
  .feed-card__rest > :first-child { margin-top: var(--card-gap); }
1162
1310
  .feed-card--small .feed-card__rest { overflow: clip; visibility: hidden; }
1163
1311
  @media (prefers-reduced-motion: no-preference) {
1164
- .feed-card__more { transition: grid-template-rows var(--motion-sheet) ease; }
1312
+ .feed-card__more { transition: grid-template-rows var(--motion-sheet) var(--ease); }
1165
1313
  .feed-card--small .feed-card__rest { transition: visibility 0s var(--motion-sheet); }
1166
1314
  }
1167
1315
 
@@ -1172,7 +1320,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1172
1320
 
1173
1321
  .delta {
1174
1322
  font: var(--delta-font, var(--type-caption));
1175
- font-weight: 700;
1323
+ font-weight: var(--fw-bold);
1176
1324
  padding: var(--delta-pad-y, var(--sp-1)) var(--delta-pad-x, var(--sp-2));
1177
1325
  border-radius: var(--delta-radius, var(--r-chip));
1178
1326
  background: var(--delta-bg, var(--card-3-bg));
@@ -1199,7 +1347,286 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1199
1347
  scroll-snap-align: start; scroll-snap-stop: always;
1200
1348
  display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--card-gap); align-content: start;
1201
1349
  }
1202
- .panes > .pager__at { justify-self: center; }
1350
+
1351
+ /* ==== the card's variants another family draws: Cover and the fold (media), Band (numbers) (#62: a block is written in its own file) ==== */
1352
+
1353
+ /* ---- cover: the image IS the card; text over a scrim ----
1354
+ <article class="card card--cover"><div class="ph" role="img"></div>
1355
+ <div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
1356
+ <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
1357
+ <div class="row">…meta…</div></article> */
1358
+ .card--cover {
1359
+ --card-bg: var(--card-3-bg);
1360
+ --dark: var(--card-3-dark);
1361
+ --accent: var(--card-ink);
1362
+ --accent-dark: calc(1 - var(--dark));
1363
+ min-height: var(--_cover-min-h);
1364
+ align-content: space-between;
1365
+ overflow: hidden;
1366
+ isolation: isolate;
1367
+ }
1368
+ .card--cover > img,
1369
+ .card--cover > .ph {
1370
+ position: absolute;
1371
+ inset: 0;
1372
+ width: 100%;
1373
+ height: 100%;
1374
+ object-fit: cover;
1375
+ z-index: var(--z-under-2);
1376
+ border: 0;
1377
+ border-radius: 0;
1378
+ min-height: 0;
1379
+ }
1380
+ /* scrim: dark at both ends so the meta stays readable */
1381
+ .card--cover::before {
1382
+ content: "";
1383
+ position: absolute;
1384
+ inset: 0;
1385
+ z-index: var(--z-under);
1386
+ background: linear-gradient(to bottom, var(--scrim), transparent 40% 60%, var(--scrim));
1387
+ }
1388
+
1389
+ /* ---- fold: a sticky-note corner on any card (decorative) ----
1390
+ No border. The card's outline follows its border box, so it ran straight
1391
+ across the squared bottom-right corner and cut through the fold's diagonal —
1392
+ the corner read as a notch with a line through it rather than a lifted flap.
1393
+ The COLOUR goes transparent rather than the border going to 0, so the box
1394
+ keeps its geometry and a fold card still measures the same as the cards
1395
+ beside it. */
1396
+ .card--fold {
1397
+ border-color: transparent;
1398
+ border-bottom-right-radius: 0;
1399
+ background-image: linear-gradient(to top left,
1400
+ var(--card-bg) 0 50%, /* the card's own background, not --surface (the page's) — a mismatch here drew a seam at the flap's edge */
1401
+ color-mix(in srgb, currentColor 18%, var(--card-bg)) 50% 100%);
1402
+ background-repeat: no-repeat;
1403
+ background-position: bottom right;
1404
+ background-size: var(--sp-8) var(--sp-8);
1405
+ background-origin: border-box; /* the flap sits on the true corner, not inside the (transparent) border */
1406
+ /* the corner is CUT: the triangle under the flap shows what is behind the card. Painted in the card's own colour it
1407
+ read as the square corner of a second card underneath (Lh 2026-09-23). The polygon reaches --sp-12 past the other
1408
+ edges so a coloured palette's shadow is not clipped there. */
1409
+ clip-path: polygon(calc(-1 * var(--sp-12)) calc(-1 * var(--sp-12)), calc(100% + var(--sp-12)) calc(-1 * var(--sp-12)), calc(100% + var(--sp-12)) calc(100% - var(--sp-8) - var(--sp-12)),
1410
+ calc(100% - var(--sp-8) - var(--sp-12)) calc(100% + var(--sp-12)), calc(-1 * var(--sp-12)) calc(100% + var(--sp-12)));
1411
+ }
1412
+
1413
+ /* ---- band: data card; numbers.css draws its parts and the band stack ---- */
1414
+ .card--band {
1415
+ grid-template-columns: 1fr auto;
1416
+ grid-template-areas: "label num" "foot num";
1417
+ align-items: end;
1418
+ gap: var(--sp-2) var(--sp-4);
1419
+ min-height: var(--card-band-min-h, var(--tile-h));
1420
+ }
1421
+
1422
+ /* ---- where it sits in what base.css draws (docs/reference.md rule 15) ---- */
1423
+ .page > :is(img, .ph) { max-height: var(--page-media-h, calc(var(--tap-base) * 14 / 3)); 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 */
1424
+ .page > .ph { width: 100%; height: var(--page-media-h, calc(var(--tap-base) * 14 / 3)); }
1425
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
1426
+ .sheet-stack > .card {
1427
+ grid-area: 1 / 1; /* every card shares the stack's cell; later ones offset down */
1428
+ border-bottom-left-radius: 0;
1429
+ border-bottom-right-radius: 0;
1430
+ border-bottom: 0;
1431
+ align-content: start;
1432
+ align-self: start; /* own natural height (padding + head) by default */
1433
+ z-index: var(--level, var(--z-over)); /* explicit paint order, 1 = back of the stack */
1434
+ }
1435
+ /* THE FRONT SHEET is the last .card, not the last child: a --closed stack
1436
+ may carry a .sheet-stack__base after its sheets (below). Written once here
1437
+ as "a card no card follows" so every rule about the front sheet agrees. */
1438
+ .sheet-stack > .card:not(:has(~ .card)) {
1439
+ align-self: stretch; /* the front card fills the remaining stack height */
1440
+ }
1441
+ .sheet-stack > .card:nth-child(2) { margin-top: var(--stack-step); }
1442
+ .sheet-stack > .card:nth-child(3) { margin-top: calc(2 * var(--stack-step)); }
1443
+ .sheet-stack > .card:nth-child(4) { margin-top: calc(3 * var(--stack-step)); }
1444
+ .sheet-stack > .card:nth-child(5) { margin-top: calc(4 * var(--stack-step)); }
1445
+ /* stack headers: uniform — [back?] title left, icons right, one height,
1446
+ same gap to content on every card */
1447
+ .sheet-stack > .card > .card__head {
1448
+ align-items: center;
1449
+ justify-content: flex-start;
1450
+ min-height: var(--tap);
1451
+ }
1452
+ .sheet-stack--display > .card > .card__head { align-items: start; min-height: 0; }
1453
+ /* ============================================================
1454
+ .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
1455
+
1456
+ A sheet (a .card — the material, sheet.css's single sheet is the same
1457
+ stuff) is rigid: it can move in, move out, or be covered — it can
1458
+ never change shape. So every sheet here is the SAME full height,
1459
+ always, set once and never touched again by anything. A "peek" is
1460
+ not a short sheet; it's a full sheet the next one happens to cover,
1461
+ showing only the strip above it, exactly like a sheet of paper part
1462
+ way out of a real stack.
1463
+
1464
+ Because of that, the whole animation is one property: translate.
1465
+ Nothing resizes, nothing re-stacks — z-index is the plain resting
1466
+ --level order and is never touched, since the order already says
1467
+ what has to happen. Cards after the tapped one sit IN FRONT of it,
1468
+ so they're what's covering it, so they're what must leave. Cards
1469
+ before it are already BEHIND it, so they need do nothing at all
1470
+ except let themselves be covered as it rises over them.
1471
+
1472
+ .is-open — the tapped card, moved up to the top of the stack. Its
1473
+ own resting offset is margin-top (below); opening just
1474
+ translates that offset back off, landing it flush at
1475
+ the stack's top with its full height exactly filling
1476
+ to the bottom.
1477
+ .is-before — an earlier card, nudged one step down so it slips
1478
+ under the next card's resting strip and is covered
1479
+ there. Each carries its own --stagger (set in JS: 0
1480
+ for the one nearest the tapped card, counting up
1481
+ further back) so they tuck away, and later un-tuck,
1482
+ one at a time.
1483
+ .is-after — a later card, moved out below, together, no stagger.
1484
+ ============================================================ */
1485
+ .sheet-stack--tap > .card {
1486
+ height: 100%; /* every card identical and full length, always — the one
1487
+ size in this whole component, never toggled, never transitioned. At a
1488
+ resting margin-top of n steps it simply overhangs the stack's bottom
1489
+ by n steps (clipped by .screen--fill), so translating it up by exactly
1490
+ that much lands it filling the stack precisely. */
1491
+ overflow: clip;
1492
+ transition: translate var(--motion-sheet) var(--ease); /* the only animated property here */
1493
+ transition-delay: calc(var(--stagger, 0) * var(--motion-stagger)); /* 0 for everything
1494
+ except a staggered is-before card: the card renders --stagger as it tucks or un-tucks */
1495
+ }
1496
+ .sheet-stack--tap > .card.is-open {
1497
+ /* up by its own resting offset — pure movement, nothing resizes */
1498
+ translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step) + var(--_pull, 0rem));
1499
+ cursor: default;
1500
+ overflow-y: auto;
1501
+ overflow-x: clip;
1502
+ overscroll-behavior: contain;
1503
+ }
1504
+ .sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
1505
+ .sheet-stack--tap > .card > .card__head { touch-action: none; }
1506
+ .sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
1507
+ padding-top: calc(var(--card-pad) + var(--_refresh, 0rem));
1508
+ transition: translate var(--motion-sheet) var(--ease), padding-top var(--motion-sheet) var(--ease);
1509
+ }
1510
+ /* the front sheet — the one open on arrival — never travels up (it is not
1511
+ tappable), so its resting offset would leave its last n steps below the
1512
+ screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
1513
+ alone is cut to the room it actually has, and it scrolls like an opened
1514
+ sheet does. Every other sheet keeps the one full height. */
1515
+ .sheet-stack--tap:not(.sheet-stack--closed) > .card:not(:has(~ .card)) {
1516
+ height: calc(100% - var(--slot, 0) * var(--stack-step));
1517
+ overflow-y: auto;
1518
+ overflow-x: clip;
1519
+ overscroll-behavior: contain;
1520
+ }
1521
+ /* a scrolling sheet shows no scrollbar (a desktop with a mouse would paint one
1522
+ down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself
1523
+ (the hiding rule is base.css's, shared with every other scrolling track) */
1524
+ .sheet-stack--tap > .card.is-before { translate: 0 var(--stack-step); }
1525
+ .sheet-stack--tap > .card.is-after { translate: 0 100vh; }
1526
+ .sheet-stack--closed > .card { translate: 0 calc(var(--_headroom) - var(--_lift, 0rem)); }
1527
+ .sheet-stack--closed > .card.is-dragging { transition: none; }
1528
+ .sheet-stack--tap:not(.sheet-stack--closed) > .card.is-dragging:not(.is-open) {
1529
+ translate: 0 calc(-1 * var(--_lift, 0rem));
1530
+ transition: none;
1531
+ }
1532
+ .sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
1533
+ .sheet-stack--closed > .card.is-after { translate: 0 100vh; }
1534
+ /* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
1535
+ line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
1536
+ :is([data-palette], [data-finish]) .sheet-stack__base .card { box-shadow: none; }
1537
+ /* at the screen's top the open sheet's top corners are SQUARE (Lh 2026-10-08): round, they left two small pockets of the
1538
+ bar's ground at the glass. They square as the sheet rises and round again as it folds, in the slide's own time. Paint
1539
+ only, the sheet's size is untouched. A pull on the head keeps them square until the finger lets go. */
1540
+ .sheet-stack--flood > .card { transition: translate var(--motion-sheet) var(--ease), border-radius var(--motion-sheet) var(--ease); }
1541
+ .sheet-stack--flood > .card.is-open { border-top-left-radius: 0; border-top-right-radius: 0; }
1542
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head {
1543
+ position: relative;
1544
+ padding-left: 0;
1545
+ transition: padding-left var(--motion-sheet) var(--ease);
1546
+ }
1547
+ /* ---- a stacked sheet with a SheetBody has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
1548
+ pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a CardFoot, the CTA) at the bottom.
1549
+ The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no SheetBody
1550
+ keeps the old way: the whole open card scrolls. ---- */
1551
+ .sheet-stack--tap > .card:has(> .sheet__body) { display: flex; flex-direction: column; gap: var(--card-gap); }
1552
+ .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
1553
+ /* a paper stack's foot is paper to the card's edges, and it covers the tinted flex gap above it so the
1554
+ button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
1555
+ where it was — the padding gives back what the bleed takes. */
1556
+ .sheet-stack > .card.sheet--paper > .card__foot {
1557
+ background: var(--action-bg);
1558
+ color: var(--card-ink);
1559
+ margin-top: calc(-1 * var(--card-gap));
1560
+ margin-inline: calc(-1 * var(--card-pad));
1561
+ margin-bottom: calc(-1 * var(--card-pad));
1562
+ padding-top: var(--card-gap);
1563
+ padding-inline: var(--card-pad);
1564
+ padding-bottom: var(--card-pad);
1565
+ }
1566
+ .stack-sheet > .card__head { flex-wrap: nowrap; }
1567
+ /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
1568
+ tappable"): a SMALL DISC — its ring kept, 32px instead of 44, the chevron in it at 20 — so the head gives it 32px + a gap
1569
+ (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
1570
+ .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back:not(.sheet-stack__back--end)) { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
1571
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) var(--ease); }
1572
+ .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) { padding-right: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
1573
+ /* a stacked sheet is a control until it is open — tapped, never read into: no text selection (a drag, a double tap, a long press),
1574
+ no grey flash, no iOS callout; its head stays one when open. Only the OPEN sheet's body is text to select (Lh 2026-09-23) */
1575
+ .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
1576
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
1577
+ .sheet > .card__foot {
1578
+ position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: var(--z-over); /* the sheet's border box: the same height the sheet's own % reads */
1579
+ align-items: end;
1580
+ align-content: end;
1581
+ padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
1582
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
1583
+ pointer-events: none;
1584
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
1585
+ transition: translate var(--motion-sheet) var(--ease), visibility 0s;
1586
+ }
1587
+ .sheet.is-dragging > .card__foot { transition: none; }
1588
+ /* at peek and away the foot is NOT held on the box's edge: it stands on the sheet's own bottom edge (no counter-slide), so it
1589
+ LEAVES AND ARRIVES WITH ITS SHEET — solid the whole way, hidden only once the slide is over. It used to stay in place and
1590
+ fade: the body slid through the strip behind a half-transparent button (timebank, Lh 2026-10-01: "ตัวหนังสือลอดใต้ปุ่ม"). */
1591
+ :is(.sheet--peek, .sheet--away) > .card__foot { translate: 0 0; visibility: hidden; transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--ease-linear) var(--motion-sheet); }
1592
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
1593
+ /* a WALK (Pages of Page) — Lh 2026-09-23, third call: no top; the picture and the words are ONE run that scrolls up together
1594
+ (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
1595
+ padding above AND below — so the words begin near the middle and, when a big text size runs them long, scroll up clear.
1596
+ The walk shows no scrollbar. */
1597
+ .step > .pages { touch-action: pan-y; } /* pan-y: the walk scrolls itself up and down; a sideways swipe is the Walkthrough's (it turns the page) */ /* the scrolling box is the whole content: from the step's top edge, edge to edge — no margin, the default (Lh 2026-09-23) */
1598
+ .step > .pages > .page > :is(img, .ph):first-child {
1599
+ width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
1600
+ max-width: none;
1601
+ margin-inline: calc(-1 * var(--screen-pad));
1602
+ height: calc(var(--_lead, 0rem) + var(--page-media-h, calc(var(--tap-base) * 14 / 3)));
1603
+ max-height: none;
1604
+ border-radius: 0;
1605
+ border-inline: 0;
1606
+ border-top: 0;
1607
+ object-fit: cover;
1608
+ }
1609
+ /* ---- where it sits in what numbers.css draws (docs/reference.md rule 15) ---- */
1610
+ .band-stack > .card--band { border: 0; border-radius: 0; box-shadow: none; }
1611
+ .band-stack > .card--band + .card--band { border-top: var(--border-w) solid var(--hairline); }
1612
+ /* ---- where it sits in what sticky.css draws (docs/reference.md rule 15) ---- */
1613
+ .sticky__head > .ph { flex: 1 1 0; min-width: 0; } /* a picture in the head takes the note's width */
1614
+ .sticky__head:has(> .sticky-date) > .ph { flex: none; width: var(--sticky-pic-size, calc(var(--tap) * 1.5)); min-height: 0; aspect-ratio: 1; } /* beside a date the picture is a small square at the start */
1615
+
1616
+ /* ---- where it sits in what media.css draws: a postcard's picture (docs/reference.md rule 15) ---- */
1617
+ .postcard__front .postcard__turn > .ph { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border: 0; border-radius: 0; min-height: 0; }
1618
+
1619
+ /* ---- where it sits in what stack.css draws: a sheet pulled to refresh holds still (docs/reference.md rule 15) ---- */
1620
+ .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
1621
+
1622
+ /* ---- where it sits in what step.css draws: the reading block, a walk of pages (docs/reference.md rule 15) ---- */
1623
+ .step:has(> .sheet--peek) > .pages { padding-bottom: var(--sheet-peek); }
1624
+ .step > .pages {
1625
+ flex: 0 1 auto;
1626
+ min-height: 0;
1627
+ overflow-y: auto;
1628
+ overscroll-behavior: contain;
1629
+ }
1203
1630
 
1204
1631
  /* ---- css/stack.css — sheet stack: .sheet-stack — the menu, sheets stacked, heads peek, tap opens ---- */
1205
1632
  /* ============================================================
@@ -1257,22 +1684,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1257
1684
  Lh, 2026-09-11). --_n: how many sheets the stack holds, read from the DOM
1258
1685
  by :has below so the closed geometry needs no script and no per-stack
1259
1686
  declaration. */
1260
- .sheet-stack { display: grid; grid-template-rows: minmax(0, 1fr); --stack-step: 5.5rem; margin-inline: calc(-1 * var(--screen-pad)); --_n: 1; }
1687
+ .sheet-stack { display: grid; grid-template-rows: minmax(0, 1fr); --stack-step: calc(var(--tap-base) * 11 / 6); margin-inline: calc(-1 * var(--screen-pad)); --_n: 1; }
1261
1688
  .sheet-stack > .card {
1262
- grid-area: 1 / 1; /* every card shares the stack's cell; later ones offset down */
1263
- border-bottom-left-radius: 0;
1264
- border-bottom-right-radius: 0;
1265
- border-bottom: 0;
1266
- align-content: start;
1267
- align-self: start; /* own natural height (padding + head) by default */
1268
1689
  --card-gap: var(--sp-5); /* head→content gap = the strip's 20px rhythm */
1269
- z-index: var(--level, 1); /* explicit paint order, 1 = back of the stack */
1270
- }
1271
- /* THE FRONT SHEET is the last .card, not the last child: a --closed stack
1272
- may carry a .sheet-stack__base after its sheets (below). Written once here
1273
- as "a card no card follows" so every rule about the front sheet agrees. */
1274
- .sheet-stack > .card:not(:has(~ .card)) {
1275
- align-self: stretch; /* the front card fills the remaining stack height */
1276
1690
  }
1277
1691
  .sheet-stack:has(> .card:nth-child(2)) { --_n: 2; }
1278
1692
  .sheet-stack:has(> .card:nth-child(3)) { --_n: 3; }
@@ -1283,119 +1697,53 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1283
1697
  script at all. --slot is that place in steps; the tap animation reads it
1284
1698
  to know how far up to travel. Five is the practical peek depth. */
1285
1699
  .sheet-stack > .card:nth-child(1) { --slot: 0; --level: 1; }
1286
- .sheet-stack > .card:nth-child(2) { --slot: 1; --level: 2; margin-top: var(--stack-step); }
1287
- .sheet-stack > .card:nth-child(3) { --slot: 2; --level: 3; margin-top: calc(2 * var(--stack-step)); }
1288
- .sheet-stack > .card:nth-child(4) { --slot: 3; --level: 4; margin-top: calc(3 * var(--stack-step)); }
1289
- .sheet-stack > .card:nth-child(5) { --slot: 4; --level: 5; margin-top: calc(4 * var(--stack-step)); }
1700
+ .sheet-stack > .card:nth-child(2) { --slot: 1; --level: 2; }
1701
+ .sheet-stack > .card:nth-child(3) { --slot: 2; --level: 3; }
1702
+ .sheet-stack > .card:nth-child(4) { --slot: 3; --level: 4; }
1703
+ .sheet-stack > .card:nth-child(5) { --slot: 4; --level: 5; }
1290
1704
 
1291
- /* stack headers: uniform — [back?] title left, icons right, one height,
1292
- same gap to content on every card */
1293
- .sheet-stack > .card > .card__head {
1294
- align-items: center;
1295
- justify-content: flex-start;
1296
- min-height: var(--tap);
1297
- }
1298
1705
  /* NOT a skeleton head: a peek is one line at its own size, and the display
1299
1706
  stack deliberately runs a second line under the next card. */
1300
1707
  .sheet-stack > .card > .card__head :is(h1, h2, h3) {
1301
1708
  font-size: var(--fs-h1);
1302
- line-height: 1;
1303
- letter-spacing: -0.01em;
1709
+ line-height: var(--lh-solid);
1710
+ letter-spacing: var(--track-h2);
1304
1711
  margin-right: auto; /* pins the icon cluster right */
1305
1712
  display: block;
1306
1713
  overflow-wrap: normal;
1307
1714
  }
1308
-
1309
- /* display stack: the giant title IS the peek — line one shows, line two is
1310
- clipped by the next card (recents / library lists) */
1311
- .sheet-stack--display { --stack-step: 6.5rem; }
1312
- .sheet-stack--display > .card > .card__head { align-items: start; min-height: 0; }
1313
- .sheet-stack--display > .card > .card__head :is(h1, h2, h3) {
1314
- font: var(--type-display);
1315
- letter-spacing: var(--track-display);
1316
- /* the FIRST line sits on the centre of the peek strip (--stack-step tall,
1317
- measured from the card's top edge); line two runs under the next card */
1318
- margin-top: calc((var(--stack-step) - 1lh) / 2 - var(--sp-5));
1319
- }
1320
- /* fanned stack: a toggle opens the stack wider so every peek shows more */
1321
- .sheet-stack--fanned { --stack-step: 9.5rem; }
1322
-
1323
- /* a peeked card in a plain stack hides its own body. A --tap stack does
1324
- NOT: there, every card is full height and full content always, and a
1325
- peek is simply the strip of it the next card doesn't cover — the way a
1326
- card in a real deck is never a shorter card, just a mostly hidden one. */
1327
- .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.card__head) { display: none; }
1328
-
1329
- /* ============================================================
1330
- .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
1331
-
1332
- A sheet (a .card — the material, sheet.css's single sheet is the same
1333
- stuff) is rigid: it can move in, move out, or be covered — it can
1334
- never change shape. So every sheet here is the SAME full height,
1335
- always, set once and never touched again by anything. A "peek" is
1336
- not a short sheet; it's a full sheet the next one happens to cover,
1337
- showing only the strip above it, exactly like a sheet of paper part
1338
- way out of a real stack.
1339
-
1340
- Because of that, the whole animation is one property: translate.
1341
- Nothing resizes, nothing re-stacks — z-index is the plain resting
1342
- --level order and is never touched, since the order already says
1343
- what has to happen. Cards after the tapped one sit IN FRONT of it,
1344
- so they're what's covering it, so they're what must leave. Cards
1345
- before it are already BEHIND it, so they need do nothing at all
1346
- except let themselves be covered as it rises over them.
1347
-
1348
- .is-open — the tapped card, moved up to the top of the stack. Its
1349
- own resting offset is margin-top (below); opening just
1350
- translates that offset back off, landing it flush at
1351
- the stack's top with its full height exactly filling
1352
- to the bottom.
1353
- .is-before — an earlier card, nudged one step down so it slips
1354
- under the next card's resting strip and is covered
1355
- there. Each carries its own --stagger (set in JS: 0
1356
- for the one nearest the tapped card, counting up
1357
- further back) so they tuck away, and later un-tuck,
1358
- one at a time.
1359
- .is-after — a later card, moved out below, together, no stagger.
1360
- ============================================================ */
1361
- .sheet-stack--tap > .card {
1362
- height: 100%; /* every card identical and full length, always — the one
1363
- size in this whole component, never toggled, never transitioned. At a
1364
- resting margin-top of n steps it simply overhangs the stack's bottom
1365
- by n steps (clipped by .screen--fill), so translating it up by exactly
1366
- that much lands it filling the stack precisely. */
1367
- overflow: clip;
1368
- transition: translate var(--motion-sheet) ease; /* the only animated property here */
1369
- transition-delay: calc(var(--stagger, 0) * 70ms); /* 0 for everything
1370
- except a staggered is-before card: the card renders --stagger as it tucks or un-tucks */
1371
- --_back-size: var(--stack-back-size, calc(var(--tap-base) * 2 / 3)); /* read once, shared by the back button and the head's padding below */
1372
- }
1373
- .sheet-stack--tap > .card.is-open {
1374
- /* up by its own resting offset — pure movement, nothing resizes */
1375
- translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step) + var(--_pull, 0px));
1376
- cursor: default;
1377
- overflow-y: auto;
1378
- overflow-x: clip;
1379
- overscroll-behavior: contain;
1715
+
1716
+ /* display stack: the giant title IS the peek — line one shows, line two is
1717
+ clipped by the next card (recents / library lists) */
1718
+ .sheet-stack--display { --stack-step: calc(var(--tap-base) * 13 / 6); }
1719
+ .sheet-stack--display > .card > .card__head :is(h1, h2, h3) {
1720
+ font: var(--type-display);
1721
+ letter-spacing: var(--track-display);
1722
+ /* the FIRST line sits on the centre of the peek strip (--stack-step tall,
1723
+ measured from the card's top edge); line two runs under the next card */
1724
+ margin-top: calc((var(--stack-step) - 1lh) / 2 - var(--sp-5));
1380
1725
  }
1381
- .sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
1382
- .sheet-stack--tap > .card > .card__head { touch-action: none; }
1383
- /* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private.
1384
- A paper body adds its own --sp-6 on top of this (the rule below): the stack's gap under the head is the
1385
- card's colour, so without the white inset the first row sits on that seam. */
1386
- .sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0px); transition: padding-top var(--motion-sheet) ease; }
1387
- .sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
1388
- padding-top: calc(var(--card-pad) + var(--_refresh, 0px));
1389
- transition: translate var(--motion-sheet) ease, padding-top var(--motion-sheet) ease;
1726
+ /* fanned stack: a toggle opens the stack wider so every peek shows more */
1727
+ .sheet-stack--fanned { --stack-step: calc(var(--tap-base) * 19 / 6); }
1728
+
1729
+ /* a peeked card in a plain stack hides its own body. A --tap stack does
1730
+ NOT: there, every card is full height and full content always, and a
1731
+ peek is simply the strip of it the next card doesn't cover — the way a
1732
+ card in a real deck is never a shorter card, just a mostly hidden one. */
1733
+ .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.card__head) { display: none; }
1734
+
1735
+ /* ---- .sheet-stack--tap — tap a peeked sheet open; a back button returns it. How its sheets move (one property: translate) is
1736
+ told over their rules, where they sit, in card.css. ---- */
1737
+ .sheet-stack--tap > .card {
1738
+ --_back-size: var(--stack-back-size, calc(var(--tap-base) * 2 / 3)); /* read once, shared by the back button and the head's padding below */
1390
1739
  }
1391
- .sheet-stack > .card > .sheet__body.is-refreshing,
1392
- .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
1740
+
1393
1741
  /* the GRIP (Lh 2026-10-05): four small dots at the middle of every sheet's top edge — a sign that this card
1394
1742
  slides up and down. A mark, not a control: nothing to press, nothing added to the sheet (the card's own background, in the
1395
1743
  room above its head). The card renders `.stack-card` when it is a sheet of a tap stack, so the dots are its own.
1396
1744
  At rest they are the card's own ink, faint. Only while THAT sheet reloads (`.is-loading`, from its `refreshing` prop,
1397
- and only while it is open) they dissolve white → black, so a light card and a dark one each get a moment the dots stand out.
1398
- Nothing travels. --_grip is the mix, 0 white → 1 black, private. */
1745
+ and only while it is open) they dissolve from the ondark ink to the onlight ink, so a light card and a dark one each get a moment the dots stand out.
1746
+ Nothing travels. --_grip is the mix, 0 the ondark ink → 1 the onlight ink, private. */
1399
1747
  @property --_grip { syntax: "<number>"; inherits: false; initial-value: 0; }
1400
1748
  .stack-card {
1401
1749
  --_grip-dot: var(--sp-1);
@@ -1411,8 +1759,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1411
1759
  calc(50% + 0.5 * var(--_grip-step)) var(--_grip-y), calc(50% + 1.5 * var(--_grip-step)) var(--_grip-y);
1412
1760
  }
1413
1761
  .stack-card.is-open.is-loading {
1414
- --_grip-ink: color-mix(in srgb, black calc(var(--_grip) * 100%), white);
1415
- animation: stack-grip-dissolve 0.4s ease-in-out infinite alternate;
1762
+ --_grip-ink: color-mix(in srgb, var(--ink-onlight) calc(var(--_grip) * 100%), var(--ink-ondark));
1763
+ animation: stack-grip-dissolve var(--motion-pulse) var(--ease-in-out) infinite alternate;
1416
1764
  }
1417
1765
  @keyframes stack-grip-dissolve { from { --_grip: 0; } to { --_grip: 1; } }
1418
1766
  /* the grip as ONE LINE (`mark="line"`, Lh 2026-10-06): the single Sheet's line, at the same place and the same length. It reads the
@@ -1432,22 +1780,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1432
1780
  @media (prefers-reduced-motion: reduce) {
1433
1781
  .stack-card.is-open.is-loading { animation: none; --_grip-ink: currentColor; }
1434
1782
  }
1435
- /* the front sheet — the one open on arrival — never travels up (it is not
1436
- tappable), so its resting offset would leave its last n steps below the
1437
- screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
1438
- alone is cut to the room it actually has, and it scrolls like an opened
1439
- sheet does. Every other sheet keeps the one full height. */
1440
- .sheet-stack--tap:not(.sheet-stack--closed) > .card:not(:has(~ .card)) {
1441
- height: calc(100% - var(--slot, 0) * var(--stack-step));
1442
- overflow-y: auto;
1443
- overflow-x: clip;
1444
- overscroll-behavior: contain;
1445
- }
1446
- /* a scrolling sheet shows no scrollbar (a desktop with a mouse would paint one
1447
- down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself
1448
- (the hiding rule is base.css's, shared with every other scrolling track) */
1449
- .sheet-stack--tap > .card.is-before { translate: 0 var(--stack-step); }
1450
- .sheet-stack--tap > .card.is-after { translate: 0 100vh; }
1451
1783
 
1452
1784
  /* ============================================================
1453
1785
  .sheet-stack--closed — nothing open on arrival. Every sheet, the front
@@ -1464,14 +1796,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1464
1796
  lands flush at the top. is-before tucks one step below its resting place.
1465
1797
  ============================================================ */
1466
1798
  .sheet-stack--closed { --_headroom: calc(100% - var(--_n) * var(--stack-step)); }
1467
- .sheet-stack--closed > .card { translate: 0 calc(var(--_headroom) - var(--_lift, 0px)); }
1468
- .sheet-stack--closed > .card.is-dragging { transition: none; }
1469
- .sheet-stack--tap:not(.sheet-stack--closed) > .card.is-dragging:not(.is-open) {
1470
- translate: 0 calc(-1 * var(--_lift, 0px));
1471
- transition: none;
1472
- }
1473
- .sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
1474
- .sheet-stack--closed > .card.is-after { translate: 0 100vh; }
1475
1799
 
1476
1800
  /* .sheet-stack__base — the base's content, in the headroom of a closed stack:
1477
1801
  behind every sheet (level 0), the stack's inset put back (the stack bleeds),
@@ -1481,9 +1805,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1481
1805
  .sheet-stack > .sheet-stack__base {
1482
1806
  grid-area: 1 / 1;
1483
1807
  align-self: start;
1484
- z-index: 0;
1485
- --_under: 0px;
1486
- height: calc(var(--_headroom, 0px) + var(--_under));
1808
+ z-index: var(--z-flat);
1809
+ --_under: 0rem;
1810
+ height: calc(var(--_headroom, 0rem) + var(--_under));
1487
1811
  padding-inline: var(--screen-pad);
1488
1812
  --_gap: var(--card-gap, var(--sp-5));
1489
1813
  display: grid;
@@ -1500,7 +1824,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1500
1824
  /* a base that is ONE Centre is one thing handed over (a note, an empty state): it stands in the MIDDLE of the room above the peeks
1501
1825
  (Lh 2026-10-06). The base already keeps the screen's inset, so the centre block gives its own back. */
1502
1826
  .sheet-stack__base:has(> .screen__centre:only-child) { align-content: center; }
1503
- .sheet-stack__base > .screen__centre { padding-inline: 0; }
1504
1827
  /* loose things rest against the peeks, so the room under the last one is the room between two of them (Lh 2026-10-09: a card's
1505
1828
  bottom edge lay on the first sheet's top edge). A lone Page reads from the top and a lone Centre stands in the middle: neither rests there. */
1506
1829
  .sheet-stack__base:not(:has(> .page:only-child, > .screen__centre:only-child)) {
@@ -1523,9 +1846,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1523
1846
  overscroll-behavior: contain;
1524
1847
  touch-action: pan-y;
1525
1848
  }
1526
- /* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
1527
- line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
1528
- :is([data-palette], [data-finish]) .sheet-stack__base .card { box-shadow: none; }
1529
1849
 
1530
1850
  /* ============================================================
1531
1851
  .sheet-stack--flood — a closed stack whose open sheet FLOODS the screen: it rises over the top bar and fills the whole screen
@@ -1547,38 +1867,17 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1547
1867
  ============================================================ */
1548
1868
  .screen--fill:has(> .sheet-stack--flood) { --_bar-gap: var(--sp-2); }
1549
1869
  .screen--fill:has(> .sheet-stack--flood):has(> .topbar:first-child) { --_bar-gap: var(--screen-gap); }
1550
- .screen--fill:has(> .sheet-stack--flood) > :is(.appbar, .topbar):first-child {
1551
- position: absolute;
1552
- inset: var(--safe-top) 0 auto;
1553
- max-width: none;
1554
- margin: 0;
1555
- z-index: 0; /* under every sheet (--level 1-5); the base clears it by margin */
1556
- }
1557
1870
  .sheet-stack--flood { --_bar: calc(var(--top-h, var(--header-h)) + var(--_bar-gap)); }
1558
1871
  .sheet-stack--flood > .sheet-stack__base {
1559
1872
  margin-top: var(--_bar);
1560
- height: calc(var(--_headroom, 0px) - var(--_bar) + var(--_under));
1873
+ height: calc(var(--_headroom, 0rem) - var(--_bar) + var(--_under));
1561
1874
  }
1562
- .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 {
1563
- visibility: hidden;
1564
- transition: visibility 0s linear var(--motion-sheet); /* gone once the sheet has covered it; back at once on a fold (the rule above has no delay) */
1565
- }
1566
- /* at the screen's top the open sheet's top corners are SQUARE (Lh 2026-10-08): round, they left two small pockets of the
1567
- bar's ground at the glass. They square as the sheet rises and round again as it folds, in the slide's own time. Paint
1568
- only, the sheet's size is untouched. A pull on the head keeps them square until the finger lets go. */
1569
- .sheet-stack--flood > .card { transition: translate var(--motion-sheet) ease, border-radius var(--motion-sheet) ease; }
1570
- .sheet-stack--flood > .card.is-open { border-top-left-radius: 0; border-top-right-radius: 0; }
1571
1875
 
1572
1876
  /* back button: lives in a slot the header reserves once open, and DISSOLVES
1573
1877
  into it — an opacity fade in place, never a slide in from the left (the
1574
1878
  slide read as a second thing moving while the sheet itself moved; Lh,
1575
1879
  2026-09-11). Only the sheet travels; the button appears where it is. */
1576
1880
  /* a sheet's OWN head and back: the card is the stack's child or, with one host wrapper, the wrapper's. A card standing in the sheet's body wears its own head */
1577
- :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head {
1578
- position: relative;
1579
- padding-left: 0;
1580
- transition: padding-left var(--motion-sheet) ease;
1581
- }
1582
1881
  :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head > .sheet-stack__back {
1583
1882
  position: absolute;
1584
1883
  left: 0;
@@ -1586,7 +1885,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1586
1885
  translate: 0 -50%;
1587
1886
  opacity: 0;
1588
1887
  visibility: hidden;
1589
- transition: opacity var(--motion-sheet) ease, visibility var(--motion-sheet);
1888
+ transition: opacity var(--motion-sheet) var(--ease), visibility var(--motion-sheet);
1590
1889
  /* SMALL (see "the back button, SMALL" below) */
1591
1890
  width: var(--_back-size);
1592
1891
  height: var(--_back-size);
@@ -1599,63 +1898,16 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1599
1898
  visibility: visible;
1600
1899
  }
1601
1900
 
1602
- /* ---- a stacked sheet with a SheetBody has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
1603
- pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a CardFoot, the CTA) at the bottom.
1604
- The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no SheetBody
1605
- keeps the old way: the whole open card scrolls. ---- */
1606
- .sheet-stack--tap > .card:has(> .sheet__body) { display: flex; flex-direction: column; gap: var(--card-gap); }
1607
- .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
1608
1901
  .sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
1609
1902
  .sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
1610
- /* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
1611
- cover the corners, and a white body showed there beside the head. Clear, the card's own colour
1612
- shows in that pocket. Open, the body is white, and the first row sits --sp-6 down, the same white
1613
- inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
1614
- would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
1615
- into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
1616
- sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
1617
- at the moment the sheet stops. Rising, the body is white at once. */
1618
- /* the head's band is as slim as it can be (Lh 2026-10-06: 20 over · 48 head · 20 under read as a heavy bar): the white starts
1619
- --sp-3 under the head, not a whole --card-gap (--sp-2 at first; Lh added 4 the same day). The body is pulled up by the difference — the card's gap is left alone, the
1620
- stack's step and the body's own rows read it. */
1621
- .sheet-stack > .card.sheet--paper > .sheet__body {
1622
- margin-top: calc(var(--sp-3) - var(--card-gap));
1623
- padding-top: calc(var(--sp-6) + var(--_refresh, 0px));
1624
- transition: padding-top var(--motion-sheet) ease, background-color 0s linear;
1625
- }
1626
- /* a pull follows the finger at once, on paper too: the rule above is as strong as the stack's own `is-refreshing` one and comes later */
1627
- .sheet-stack > .card.sheet--paper > .sheet__body.is-refreshing { transition: none; }
1628
- .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
1629
- background-color: transparent;
1630
- transition:
1631
- padding-top var(--motion-sheet) ease,
1632
- background-color var(--motion-quick) ease calc(var(--stagger, 0) * 70ms + var(--motion-sheet) - var(--motion-quick));
1633
- }
1634
- @media (prefers-reduced-motion: reduce) {
1635
- .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
1636
- transition: padding-top var(--motion-sheet) ease, background-color 0s linear;
1637
- }
1638
- }
1639
- /* a paper stack's foot is white to the card's edges, and it covers the tinted flex gap above it so the
1640
- button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
1641
- where it was — the padding gives back what the bleed takes. */
1642
- .sheet-stack > .card.sheet--paper > .card__foot {
1643
- background: var(--color-paper, #fff);
1644
- color: var(--color-ink, #141414);
1645
- margin-top: calc(-1 * var(--card-gap));
1646
- margin-inline: calc(-1 * var(--card-pad));
1647
- margin-bottom: calc(-1 * var(--card-pad));
1648
- padding-top: var(--card-gap);
1649
- padding-inline: var(--card-pad);
1650
- padding-bottom: var(--card-pad);
1651
- }
1903
+ /* a paper stack's foot is paper (card.css paints it): its ink reads the action's side */
1904
+ .sheet-stack > .card.sheet--paper > .card__foot { --dark: var(--action-dark); }
1652
1905
 
1653
1906
  /* ---- StackSheet's tabs (Lh 2026-09-23): the head's end is a row of small icon buttons, the sheet's pages — the chosen one
1654
1907
  filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
1655
1908
  .stack-sheet__tabs { display: flex; gap: var(--stack-sheet-tabs-gap, var(--sp-2)); flex: none; } /* room between the tabs: with none, the chosen tab's disc touched its neighbour's (Lh 2026-09-24) */
1656
1909
  /* the head is ONE line, always (Lh 2026-09-24): the tabs keep their size, the TITLE gives — it never wraps, it runs sideways
1657
1910
  (StackSheet measures --_over, the width it overflows by, and sets .is-long), one way: it waits, runs to the end, waits, and starts again from the first letter — like a song title */
1658
- .stack-sheet > .card__head { flex-wrap: nowrap; }
1659
1911
  .stack-sheet__title {
1660
1912
  flex: 1; min-width: 0; overflow-x: clip; overflow-y: visible; white-space: nowrap; overflow-wrap: normal; /* clip SIDEWAYS only:
1661
1913
  hidden clipped the Thai tone marks above a line-height-1 line (เพื่อน lost its ไม้เอก — Lh 2026-09-24) */
@@ -1664,7 +1916,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1664
1916
  }
1665
1917
  .stack-sheet__run { display: inline-block; }
1666
1918
  .stack-sheet__title.is-long .stack-sheet__run {
1667
- animation: stack-sheet-run var(--stack-sheet-run-time, var(--_run, 6s)) ease-in-out infinite; /* ONE way: start → end, then back to the start at once (Lh 2026-09-24) */ /* --_run: from the overflow, 30px a second + the pauses */
1919
+ animation: stack-sheet-run var(--stack-sheet-run-time, var(--_run, var(--motion-run))) var(--ease-in-out) infinite; /* ONE way: start → end, then back to the start at once (Lh 2026-09-24) */ /* --_run: from the overflow, 30px a second + the pauses */
1668
1920
  }
1669
1921
  @keyframes stack-sheet-run { 0%, 20% { translate: 0; } 85%, 100% { translate: calc(-1 * var(--_over)); } }
1670
1922
  @media (prefers-reduced-motion: reduce) {
@@ -1677,11 +1929,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1677
1929
  /* the disc fades with the sheet: in as it rises, out as it folds, and across when another tab is pressed.
1678
1930
  scale stays on the press (actions.css) so the give under a finger stays quick */
1679
1931
  transition:
1680
- scale var(--motion-press) ease-out,
1681
- background-color var(--motion-sheet) ease,
1682
- color var(--motion-sheet) ease,
1683
- border-color var(--motion-sheet) ease,
1684
- box-shadow var(--motion-sheet) ease;
1932
+ scale var(--motion-press) var(--ease-out),
1933
+ background-color var(--motion-sheet) var(--ease),
1934
+ color var(--motion-sheet) var(--ease),
1935
+ border-color var(--motion-sheet) var(--ease),
1936
+ box-shadow var(--motion-sheet) var(--ease);
1685
1937
  }
1686
1938
  .stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
1687
1939
  position: absolute;
@@ -1694,7 +1946,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1694
1946
  padding: 0 var(--stack-sheet-count-pad-x, 0.25em) var(--stack-sheet-count-lift, 0.1em); /* "99+" keeps its room at every text size;
1695
1947
  the room UNDER the number lifts it onto the circle's middle: a digit has no descender, so a centred line box sets it low (Lh 2026-10-01) */
1696
1948
  font: var(--type-caption);
1697
- line-height: 1;
1949
+ line-height: var(--lh-solid);
1698
1950
  pointer-events: none;
1699
1951
  }
1700
1952
  /* a tab not chosen is the icon alone — no ring, no ground (Lh 2026-09-23: "the circle around is not pretty"); the chosen one turns
@@ -1710,26 +1962,25 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1710
1962
  margin around it reaches into the card's padding — and an open head (back · title · three tabs) keeps one line (Lh 2026-09-23) */
1711
1963
  .card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
1712
1964
 
1713
- /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
1714
- tappable"): a SMALL DISC — its ring kept, 32px instead of 44, the chevron in it at 20 — so the head gives it 32px + a gap
1715
- (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
1716
- .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back:not(.sheet-stack__back--end)) { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
1717
1965
  /* AT THE END (Lh 2026-09-29, the home): after the tabs, chevron down — the open sheet folds down. The head's end gives it its room
1718
1966
  while open; the last tab then sits on the inset (no reach into the padding) */
1719
1967
  .sheet-stack--tap .sheet-stack__back--end { left: auto; right: 0; }
1720
- :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) ease; }
1721
- .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) { padding-right: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
1722
1968
  .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
1723
- .sheet-stack--tap .sheet-stack__back > .icon { width: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); height: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); flex: none; } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
1969
+ .sheet-stack--tap .sheet-stack__back > .icon { --icon-size: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
1724
1970
  .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--_back-size) - var(--tap-sm)) / 2); }
1725
1971
 
1726
1972
  /* the open sheet's title folds it (SheetStack.tsx): it says so to a pointer */
1727
1973
  .sheet-stack--tap > .card.is-open > .card__head > :is(h1, h2, h3) { cursor: pointer; }
1728
- /* a stacked sheet is a control until it is open — tapped, never read into: no text selection (a drag, a double tap, a long press),
1729
- no grey flash, no iOS callout; its head stays one when open. Only the OPEN sheet's body is text to select (Lh 2026-09-23) */
1730
- .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
1731
1974
  .sheet-stack--tap > .card.is-open > :not(.card__head) { -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); } /* the open sheet reads as the rest of the screen: not selectable (Lh 2026-09-25; it was text) */
1732
1975
 
1976
+ /* ---- where it sits in what base.css draws (docs/reference.md rule 15) ---- */
1977
+ /* min-height: 0 — a flex item's floor is its content, and the stack's content
1978
+ is its own full-height sheets, so without it the stack grew to the whole
1979
+ screen and its open sheet ran past the bottom edge by the bars' height,
1980
+ unreachable by scrolling (timebank home, 2026-09-13). The stack IS the room
1981
+ left under the bars; every sheet fills exactly that. */
1982
+ .screen--fill > .sheet-stack { flex: 1; min-height: 0; }
1983
+
1733
1984
  /* ---- css/sheet.css — sheet single: .sheet — one card as a bottom sheet, peek / half / full ---- */
1734
1985
  /* ============================================================
1735
1986
  cardds/sheet.css — sheet single: one card as a bottom sheet.
@@ -1804,7 +2055,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1804
2055
  inset: 0;
1805
2056
  top: var(--_top, 0); /* a stage keeps its top bar's line free — see .sheet-stage */
1806
2057
  translate: 0 calc(var(--_y) + var(--_drag, 0rem)); /* --_drag: the Sheet component, only while the handle is held */
1807
- transition: translate var(--motion-sheet) ease;
2058
+ transition: translate var(--motion-sheet) var(--ease);
1808
2059
 
1809
2060
  /* handle · head · body: a column, the body takes what is left and scrolls
1810
2061
  (below) — so the handle and the head never move, whatever the content */
@@ -1837,7 +2088,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1837
2088
  that holds that way's WHOLE form, its submit on the sheet's foot, leading to the next page. The base
1838
2089
  carries no control. It is NOT for a form taken apart — one sheet per field, a "send" left on the base:
1839
2090
  a form lives whole in one sheet. */
1840
- .sheet--away { --_y: 100%; --_cover: var(--_box); visibility: hidden; transition: translate var(--motion-sheet) ease, visibility 0s linear var(--motion-sheet); }
2091
+ .sheet--away { --_y: 100%; --_cover: var(--_box); visibility: hidden; transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--ease-linear) var(--motion-sheet); }
1841
2092
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
1842
2093
 
1843
2094
  /* the body — everything under the head, the one thing in a sheet that
@@ -1882,23 +2133,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1882
2133
  One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
1883
2134
  /* …and the link row under the button, when there is one (its line + the gap over it) */
1884
2135
  .sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--sheet-foot-h, calc(var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)))) + var(--card-pad) + var(--_safe-b-open)); } /* --sheet-foot-h: the foot's rows as the Sheet measured them (a button grows with the text size) */
1885
- .sheet > .card__foot {
1886
- position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: 1; /* the sheet's border box: the same height the sheet's own % reads */
1887
- align-items: end;
1888
- align-content: end;
1889
- padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
1890
- background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
1891
- pointer-events: none;
1892
- translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
1893
- transition: translate var(--motion-sheet) ease, visibility 0s;
1894
- }
1895
2136
  .sheet > .card__foot > * { pointer-events: auto; }
1896
2137
  .sheet:has(> .card__foot) > .sheet__body { margin-bottom: calc(var(--_foot) - var(--card-pad)); } /* the body stops above the strip (the card's own padding is the rest) */
1897
- .sheet.is-dragging > .card__foot { transition: none; }
1898
- /* at peek and away the foot is NOT held on the box's edge: it stands on the sheet's own bottom edge (no counter-slide), so it
1899
- LEAVES AND ARRIVES WITH ITS SHEET — solid the whole way, hidden only once the slide is over. It used to stay in place and
1900
- fade: the body slid through the strip behind a half-transparent button (timebank, Lh 2026-10-01: "ตัวหนังสือลอดใต้ปุ่ม"). */
1901
- :is(.sheet--peek, .sheet--away) > .card__foot { translate: 0 0; visibility: hidden; transition: translate var(--motion-sheet) ease, visibility 0s linear var(--motion-sheet); }
1902
2138
  /* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
1903
2139
  box is never smaller than its padding — so the pinned parts must not give way to it: the body runs past
1904
2140
  the sheet's clipped edge instead, where nothing shows anyway. At peek and away no part of the body shows. */
@@ -1915,14 +2151,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1915
2151
  touch-action: none hands a touch drag to the Sheet component instead of to the scroller. */
1916
2152
  .sheet__handle {
1917
2153
  position: absolute;
1918
- z-index: 1;
2154
+ z-index: var(--z-over);
1919
2155
  top: calc((var(--card-pad) - var(--sp-1)) / 2); /* the dots' top: (pad − 4) / 2. The line starts there too */
1920
2156
  left: 50%;
1921
2157
  translate: -50% 0;
1922
2158
  border: 0;
1923
2159
  color: inherit; /* a <button> has its own UA colour; the mark is the card's ink */
1924
2160
  background: currentColor;
1925
- opacity: 0.3;
2161
+ opacity: var(--fade-dim);
1926
2162
  width: calc(var(--sp-base) * 18); /* 72: the line. The grab is the ::before, not this width */
1927
2163
  height: var(--_handle);
1928
2164
  border-radius: var(--sheet-handle-radius, var(--r-chip));
@@ -1931,22 +2167,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1931
2167
  touch-action: none;
1932
2168
  cursor: grab;
1933
2169
  }
1934
- /* paper: the head keeps the sheet's own colour, the content is white (Lh 2026-10-05).
2170
+ /* paper: the head keeps the sheet's own colour, the content is the theme's paper, --action-bg, the ground of every field (Lh 2026-10-05: white; Lh 2026-10-20: the theme's, never a fixed white).
1935
2171
  The body and the foot read as paper; the handle and the head stay on the card.
1936
2172
  The flex gap under the head is outside the body, so it stays the sheet's tint.
1937
- The first field would sit on that colour seam. The white inset is --sp-6 (24px at
2173
+ The first field would sit on that colour seam. The paper's inset is --sp-6 (24px at
1938
2174
  the default spacing), the body's own top padding, and only when the sheet is paper.
1939
2175
  A plain sheet keeps padding-top 0. A sheet in a stack wears the same class and the same
1940
- inset: the stack's own gap under the head is the card's colour, so the row still needs this white.
1941
- The paper is a surface of the plain card's side: on a dark sheet its words still turn to onlight. */
2176
+ inset: the stack's own gap under the head is the card's colour, so the row still needs this paper.
2177
+ The paper takes the side of the controls' paper (--action-dark): on a dark sheet its words still turn to onlight. */
1942
2178
  .sheet--paper > .sheet__body {
1943
- --card-bg: var(--color-paper, #fff);
1944
- --dark: var(--card-1-dark);
2179
+ --card-bg: var(--action-bg);
2180
+ --dark: var(--action-dark);
1945
2181
  background: var(--card-bg);
1946
2182
  color: var(--card-ink);
1947
2183
  padding-top: var(--sp-6);
1948
2184
  }
1949
- .sheet--paper > .card__foot { --card-bg: var(--color-paper, #fff); color: var(--color-ink, #141414); }
2185
+ .sheet--paper > .card__foot { --card-bg: var(--action-bg); --dark: var(--action-dark); --card-foot-ink: var(--card-ink); }
1950
2186
  .sheet__handle--dots {
1951
2187
  --_dot: var(--sp-1);
1952
2188
  --_step: calc(var(--_dot) * 2.5);
@@ -1963,15 +2199,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1963
2199
  background-position: 0 0, var(--_step) 0, calc(2 * var(--_step)) 0, calc(3 * var(--_step)) 0;
1964
2200
  }
1965
2201
  /* the app reloads what the sheet shows (`refreshing`, off unless set): the mark changes colour exactly as a stack sheet's grip
1966
- does — white → black and back, so a light sheet and a dark one each get a moment it stands out. --_grip and the keyframes are
2202
+ does — the ondark ink → the onlight ink and back, so a light sheet and a dark one each get a moment it stands out. --_grip and the keyframes are
1967
2203
  the stack's (stack.css): one sign for both. Nothing travels. */
1968
2204
  .sheet.is-loading > .sheet__handle {
1969
2205
  opacity: 1;
1970
- background-color: color-mix(in srgb, black calc(var(--_grip) * 100%), white);
1971
- animation: stack-grip-dissolve 0.4s ease-in-out infinite alternate;
2206
+ background-color: color-mix(in srgb, var(--ink-onlight) calc(var(--_grip) * 100%), var(--ink-ondark));
2207
+ animation: stack-grip-dissolve var(--motion-pulse) var(--ease-in-out) infinite alternate;
1972
2208
  }
1973
2209
  .sheet.is-loading > .sheet__handle--dots {
1974
- --_ink: color-mix(in srgb, black calc(var(--_grip) * 100%), white);
2210
+ --_ink: color-mix(in srgb, var(--ink-onlight) calc(var(--_grip) * 100%), var(--ink-ondark));
1975
2211
  background-color: transparent;
1976
2212
  }
1977
2213
  @media (prefers-reduced-motion: reduce) {
@@ -1995,18 +2231,18 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1995
2231
  - a label .row with its .check / .toggle may be the head when the sheet's
1996
2232
  whole ask is that switch: peek shows it (a row is --tap tall plus its own
1997
2233
  padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
1998
- .sheet > .card__head { min-height: var(--sheet-head-h); }
2234
+ .sheet > .card__head { --card-head-min-h: var(--sheet-head-h); }
1999
2235
  /* a sheet's title is ONE line, always (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน" wrapped to two lines at 390px) — the
2000
2236
  thing at its end keeps its place beside it; a caption still takes its own row. A title too long for the line RUNS sideways
2001
2237
  like a song title (Lh 2026-10-03, as the StackSheet's — it was cut with …, and the end of it could never be read): the Sheet
2002
2238
  measures --_over, the width it overflows by, and sets .is-long; one way — it waits, runs to its end, waits, and starts again
2003
2239
  from the first letter. The move is text-indent: the heading is the app's own element, there is no inner box to slide.
2004
2240
  Before the Sheet has measured, and where motion is off, it is cut with …. */
2005
- .sheet > .card__head:not(:has(> :is(.t-caption, .t-body))) { flex-wrap: nowrap; }
2241
+ .sheet > .card__head:not(:has(> :is(.t-caption, .t-body))) { --card-head-wrap: nowrap; }
2006
2242
  .sheet > .card__head > :is(h1, h2, h3):first-child, .sheet__title { white-space: nowrap; overflow-x: clip; overflow-y: visible; text-overflow: ellipsis; min-width: 0; } /* clip SIDEWAYS only: hidden cut the Thai tone marks over the line */
2007
2243
  .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long {
2008
2244
  text-overflow: clip;
2009
- animation: sheet-title-run var(--sheet-title-run-time, var(--_run, 6s)) ease-in-out infinite; /* --_run: from the overflow, 30px a second + the pauses */
2245
+ animation: sheet-title-run var(--sheet-title-run-time, var(--_run, var(--motion-run))) var(--ease-in-out) infinite; /* --_run: from the overflow, 30px a second + the pauses */
2010
2246
  }
2011
2247
  @keyframes sheet-title-run { 0%, 20% { text-indent: 0; } 85%, 100% { text-indent: calc(-1 * var(--_over)); } }
2012
2248
  @media (prefers-reduced-motion: reduce) {
@@ -2097,11 +2333,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2097
2333
  isolation: isolate; /* the ring and the hint stack inside the map only — a sheet over the map covers them */
2098
2334
  color: var(--map-ink, var(--on-surface));
2099
2335
  background:
2100
- repeating-linear-gradient(75deg, transparent 0 5.5rem, var(--hairline) 5.5rem 6.25rem),
2101
- repeating-linear-gradient(-18deg, transparent 0 8.75rem, var(--ink-tint) 8.75rem 9.25rem),
2336
+ repeating-linear-gradient(var(--street-1), transparent 0 calc(var(--tap-base) * 11 / 6), var(--hairline) calc(var(--tap-base) * 11 / 6) calc(var(--tap-base) * 25 / 12)),
2337
+ repeating-linear-gradient(var(--street-2), transparent 0 calc(var(--tap-base) * 35 / 12), var(--ink-tint) calc(var(--tap-base) * 35 / 12) calc(var(--tap-base) * 37 / 12)),
2102
2338
  var(--card-2-bg);
2103
2339
  }
2104
- .map > :first-child { position: absolute; inset: 0; z-index: 0; isolation: isolate; background: transparent; } /* its own stacking context: an engine's layered panes stay under the ring; an engine's own grey ground would hide the pattern while tiles load */
2340
+ .map > :first-child { position: absolute; inset: 0; z-index: var(--z-flat); isolation: isolate; background: transparent; } /* its own stacking context: an engine's layered panes stay under the ring; an engine's own grey ground would hide the pattern while tiles load */
2105
2341
  .map__ring {
2106
2342
  position: absolute;
2107
2343
  left: 50%;
@@ -2116,7 +2352,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2116
2352
  background: var(--map-ring-bg, var(--accent-soft));
2117
2353
  box-shadow: var(--map-ring-shadow, var(--shadow-float));
2118
2354
  pointer-events: none;
2119
- z-index: 1; /* above the engine's panes (Leaflet's tiles sit at 200, but inside their own stacking context) */
2355
+ z-index: var(--z-over); /* above the engine's panes (Leaflet's tiles sit at 200, but inside their own stacking context) */
2120
2356
  }
2121
2357
  .map__pin { /* mark="pin": an exact point — the pin's TIP is where the ring's centre sits, so the engine reads the element's bottom-centre */
2122
2358
  position: absolute;
@@ -2131,11 +2367,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2131
2367
  color: var(--map-pin-ink, var(--card-ink));
2132
2368
  filter: drop-shadow(var(--map-pin-drop, var(--shadow-float)));
2133
2369
  pointer-events: none;
2134
- z-index: 1;
2370
+ z-index: var(--z-over);
2135
2371
  }
2136
2372
  /* 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
2137
2373
  street pattern never shows through the pin's body */
2138
- .map__pin > .icon { width: 100%; height: 100%; translate: 0 9%; fill: var(--surface); }
2374
+ .map__pin > .icon { --icon-size: 100%; --icon-shift: 0 9%; }
2139
2375
  .map__credit { /* the tile provider's credit — required by OSM; a caption, not the engine's own control */
2140
2376
  position: absolute;
2141
2377
  right: var(--sp-2);
@@ -2146,7 +2382,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2146
2382
  color: var(--map-credit-ink, var(--on-surface-muted));
2147
2383
  font: var(--map-credit-font, var(--type-caption));
2148
2384
  pointer-events: none;
2149
- z-index: 1;
2385
+ z-index: var(--z-over);
2150
2386
  }
2151
2387
  .map__hint {
2152
2388
  position: absolute;
@@ -2164,7 +2400,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2164
2400
  text-align: center;
2165
2401
  box-shadow: var(--map-hint-shadow, var(--shadow-float));
2166
2402
  pointer-events: none;
2167
- z-index: 1;
2403
+ z-index: var(--z-over);
2168
2404
  }
2169
2405
 
2170
2406
  /* ---- the map IN FLOW (Lh 2026-10-17): `.map--inline` — a box in a card, or the whole of a sheet's body ----
@@ -2194,7 +2430,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2194
2430
  name; the picked one fills with accent. Above the ring: the ring takes no taps, the places do. */
2195
2431
  .map-pick {
2196
2432
  position: absolute;
2197
- z-index: 2;
2433
+ z-index: var(--z-over-2);
2198
2434
  width: var(--tap);
2199
2435
  height: var(--tap);
2200
2436
  translate: -50% -50%;
@@ -2225,7 +2461,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2225
2461
  background: var(--map-pick-name-bg, color-mix(in srgb, var(--surface) 85%, transparent));
2226
2462
  font: var(--map-pick-font, var(--type-caption));
2227
2463
  }
2228
- .map-pick:has(:checked) { z-index: 3; } /* the picked place's name lies over a neighbour's */
2464
+ .map-pick:has(:checked) { z-index: var(--z-over-3); } /* the picked place's name lies over a neighbour's */
2229
2465
  .map-pick:has(:checked) > :is(.map-pick__mark, .map-pick__name) { background: var(--map-pick-on-bg, var(--accent)); color: var(--map-pick-on-ink, var(--on-accent)); }
2230
2466
  .map-pick:has(:focus-visible) > .map-pick__mark { outline: var(--focus); outline-offset: var(--sp-0); }
2231
2467
  .map-pick:has(:disabled) { opacity: var(--off); cursor: default; }
@@ -2250,8 +2486,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2250
2486
  /* words on the scrim: white on a strip of the scrim over it. The scrim alone is half black, so over a white page it is #808080, where white
2251
2487
  is 3.9:1; doubled, #404040, 10:1. It read --card-3-ink, a brand's light ink, on a black scrim */
2252
2488
  background: var(--sheet-stage-foot-bg, var(--scrim));
2253
- color: var(--sheet-stage-foot-ink, #ffffff);
2254
- z-index: 2;
2489
+ color: var(--sheet-stage-foot-ink, var(--ink-ondark));
2490
+ z-index: var(--z-over-2);
2255
2491
  }
2256
2492
 
2257
2493
  /* dim stage: blur + darken whatever sits behind the sheet */
@@ -2261,9 +2497,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2261
2497
  position: absolute;
2262
2498
  inset: 0;
2263
2499
  background: var(--scrim);
2264
- z-index: 1;
2500
+ z-index: var(--z-over);
2265
2501
  }
2266
- .sheet-stage--dim .sheet { z-index: 2; }
2502
+ .sheet-stage--dim .sheet { z-index: var(--z-over-2); }
2267
2503
 
2268
2504
  /* ---- sheet steps: one task in steps inside one sheet (Lh 2026-09-24) ----
2269
2505
  <div class="sheet-steps"> › <ol class="sheet-steps__track"> of <li class="sheet-step sheet-step--done|now|next">. The done steps
@@ -2274,14 +2510,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2274
2510
  falls — no height is written. Nothing fades: done and next read at full ink. */
2275
2511
  .sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
2276
2512
  .sheet-steps { flex-shrink: 0; min-height: calc(100% + var(--kb, 0rem)); /* the keyboard's height more: the pile of next steps stays under the keyboard, it never rises with it */ display: flex; flex-direction: column; touch-action: pan-y; position: relative; }
2277
- .sheet-steps__holder { position: absolute; top: 0; left: 0; width: 1rem; height: 1rem; padding: 0; border: 0; opacity: 0; pointer-events: none; font-size: 1rem; } /* holds the cursor (and the phone's keyboard) while a step folds; font 1rem = the browser's own size, so iOS does not zoom to it */
2513
+ .sheet-steps__holder { position: absolute; top: 0; left: 0; width: var(--browser-text); height: var(--browser-text); padding: 0; border: 0; opacity: 0; pointer-events: none; font-size: var(--browser-text); } /* holds the cursor (and the phone's keyboard) while a step folds; font 1rem = the browser's own size, so iOS does not zoom to it */
2278
2514
  .sheet-steps__track { list-style: none; margin: 0; padding: 0 0 var(--sheet-steps-end, var(--sp-6)); flex: 1; /* room under the LAST step: scrolled to the end it never touches the foot's button (Lh 2026-09-28, #153) */ display: flex; flex-direction: column; }
2279
2515
 
2280
2516
  .sheet-step {
2281
2517
  position: relative; min-width: 0;
2282
2518
  display: grid; grid-template-columns: minmax(0, 1fr); /* the column is the step's width, never its words' — a long summary is cut with …, it never pushes the row past the sheet's edge (Lh 2026-09-28, #153) */ grid-template-rows: auto 0fr; align-content: start; /* mid-fold the rows sum to less than the box: the spare room stays BELOW, never spread into the title row (Lh: it looked like a spring) */
2283
2519
  flex: 0 0 auto;
2284
- transition: grid-template-rows var(--motion-sheet) ease, flex-grow var(--motion-sheet) ease;
2520
+ transition: grid-template-rows var(--motion-sheet) var(--ease), flex-grow var(--motion-sheet) var(--ease);
2285
2521
  --_row-h: var(--sheet-steps-row-h, calc(var(--tap-base) * 2 / 3)); /* the bar's own floor, read once (__head and __go below share it) */
2286
2522
  }
2287
2523
  /* the open step GROWS into the room left, so the steps not done yet wait in a pile at the bottom — and a move is an ACCORDION:
@@ -2289,7 +2525,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2289
2525
  with them, nothing jumps (Lh 2026-09-24) */
2290
2526
  .sheet-step--now { grid-template-rows: auto 1fr; flex-grow: 1; }
2291
2527
  .sheet-step + .sheet-step { margin-top: var(--sheet-steps-gap, var(--sp-1)); } /* the folded bars read as a pile, a little ground between them (Lh 2026-09-25) */
2292
- .sheet-step__head { /* every step's title line is a quiet BAR (a tint of the ink, not the brand — a brand bar read as a button, Lh 2026-09-25), the same height open or folded — opening shows the white room under it, never a taller bar (Lh 2026-09-25; was a thin rule on top) */
2528
+ .sheet-step__head { /* every step's title line is a quiet BAR (a tint of the ink, not the brand — a brand bar read as a button, Lh 2026-09-25), the same height open or folded — opening shows the card's room under it, never a taller bar (Lh 2026-09-25; was a thin rule on top) */
2293
2529
  display: flex; align-items: center; gap: var(--sp-3); min-height: var(--_row-h); min-width: 0; /* THIN: 32px, a summary line, not a button (Lh 2026-09-25) — the tap stays 44, see __go */
2294
2530
  padding-inline: var(--sheet-step-bar-pad-x, var(--sp-3)); border-radius: var(--sheet-step-bar-radius, var(--r-chip));
2295
2531
  background: var(--sheet-step-bar-bg, var(--ink-tint)); color: var(--sheet-step-bar-ink, inherit);
@@ -2297,11 +2533,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2297
2533
  border-top: var(--sheet-step-border-w, 0rem) solid var(--sheet-step-border, currentColor); /* a rule instead of the bar: set this, the bar's bg transparent, radius 0 */
2298
2534
  }
2299
2535
  .sheet-step__title { font: var(--sheet-step-title-font, var(--type-label)); margin: 0; flex: 0 1 auto; min-width: 0; outline: none; /* the label size, not a body title: a pile of rows must stay small (Lh 2026-09-24) */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2300
- .sheet-step__num { /* `numbered`: the step's number in a disc BEFORE its title (Lh 2026-09-25) — a white disc on the bar, the number in the ink */
2536
+ .sheet-step__num { /* `numbered`: the step's number in a disc BEFORE its title (Lh 2026-09-25) — a disc of the card's colour on the bar, the number in the ink */
2301
2537
  flex: none; display: grid; place-items: center; box-sizing: border-box; inline-size: var(--sheet-step-num-size, 1.408em); block-size: var(--sheet-step-num-size, 1.408em); /* em, not rem (Lh 2026-10-03, as the StackSheet's count): the disc is cut from ITS OWN number's size —
2302
2538
  at a large text size the number grew and a 24px disc did not, so the digit sat on its bottom edge; 1.408em of a 17px caption = the 24px it always was */
2303
2539
  padding-bottom: var(--sheet-step-num-lift, 0.1em); /* the room UNDER the number lifts it onto the disc's middle: a digit has no descender, so a centred line box sets it low */
2304
- border-radius: 50%; font: var(--sheet-step-num-font, var(--type-caption)); line-height: 1; font-variant-numeric: tabular-nums;
2540
+ border-radius: 50%; font: var(--sheet-step-num-font, var(--type-caption)); line-height: var(--lh-solid); font-variant-numeric: tabular-nums;
2305
2541
  background: var(--sheet-step-num-bg, var(--card-bg)); color: var(--sheet-step-num-ink, inherit);
2306
2542
  }
2307
2543
  .sheet-step__summary { color: var(--sheet-step-summary-ink, var(--card-muted)); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -2309,14 +2545,57 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2309
2545
  .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
2310
2546
  .sheet-step__inner { display: grid; gap: var(--card-gap); padding-block: var(--card-gap); padding-inline: var(--sheet-step-inner-pad-x, var(--sheet-step-bar-pad-x, var(--sp-3))); } /* the open step's content in the frame of its title's words (the bar's own inset), so it reads as that step's (Lh 2026-09-28, #153) */
2311
2547
  .sheet-step__inner > * { min-width: 0; }
2312
- .sheet-step__inner > .link { justify-self: center; } /* a link of its own row in a step (skip, "change number", "set PIN again"): centred under the step, as the skip (Lh 2026-09-25) */
2313
- .sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s linear var(--motion-sheet); }
2548
+ .sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s var(--ease-linear) var(--motion-sheet); }
2314
2549
  .sheet-step__go { position: absolute; inset: min(0rem, (var(--_row-h) - var(--tap-sm)) / 2) 0 auto; min-height: max(100%, var(--tap-sm)); /* a thin bar still takes a 44px tap */ margin: 0; cursor: pointer; }
2315
2550
  /* the last step's finish, when the app gives one: under the steps */
2316
2551
  .sheet-steps__finish { display: grid; padding-top: var(--card-gap); }
2317
2552
  @media (prefers-reduced-motion: reduce) { .sheet-step { transition: none; } }
2318
- /* the step waiting for its answer: its buttons take no taps (the app guards its own handler too) */
2319
- .sheet-step--busy .btn { pointer-events: none; opacity: 0.5; }
2553
+
2554
+ /* ==== the step bar answers a press, as PRESS in actions.css answers every other control (#62: a block is written in its own file) ==== */
2555
+ .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
2556
+
2557
+ /* ==== a body that holds only an EmptyState stands it at --empty-state-at of its room, as a Step does (step.css) (#62: a block is written in its own file) ==== */
2558
+ .sheet__body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
2559
+ .sheet__body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
2560
+ .sheet__body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
2561
+
2562
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
2563
+ /* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private.
2564
+ A paper body adds its own --sp-6 on top of this (the rule below): the stack's gap under the head is the
2565
+ card's colour, so without the paper's inset the first row sits on that seam. */
2566
+ .sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0rem); transition: padding-top var(--motion-sheet) var(--ease); }
2567
+ /* the head's band is as slim as it can be (Lh 2026-10-06: 20 over · 48 head · 20 under read as a heavy bar): the paper starts
2568
+ --sp-3 under the head, not a whole --card-gap (--sp-2 at first; Lh added 4 the same day). The body is pulled up by the difference — the card's gap is left alone, the
2569
+ stack's step and the body's own rows read it. */
2570
+ .sheet-stack > .card.sheet--paper > .sheet__body {
2571
+ margin-top: calc(var(--sp-3) - var(--card-gap));
2572
+ padding-top: calc(var(--sp-6) + var(--_refresh, 0rem));
2573
+ transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
2574
+ }
2575
+ /* a pull follows the finger at once, on paper too: the rule above is as strong as the stack's own `is-refreshing` one and comes later */
2576
+ .sheet-stack > .card.sheet--paper > .sheet__body.is-refreshing { transition: none; }
2577
+ /* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
2578
+ cover the corners, and a paper body showed there beside the head. Clear, the card's own colour
2579
+ shows in that pocket. Open, the body is paper, and the first row sits --sp-6 down, the same paper
2580
+ inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
2581
+ would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
2582
+ into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
2583
+ sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
2584
+ at the moment the sheet stops. Rising, the body is paper at once. */
2585
+ .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
2586
+ background-color: transparent;
2587
+ transition:
2588
+ padding-top var(--motion-sheet) var(--ease),
2589
+ background-color var(--motion-quick) var(--ease) calc(var(--stagger, 0) * var(--motion-stagger) + var(--motion-sheet) - var(--motion-quick));
2590
+ }
2591
+ @media (prefers-reduced-motion: reduce) { .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
2592
+ transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
2593
+ } }
2594
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
2595
+ .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
2596
+
2597
+ /* ---- where it sits in what stack.css draws: a body pulled to refresh holds still (docs/reference.md rule 15) ---- */
2598
+ .sheet-stack > .card > .sheet__body.is-refreshing { transition: none; }
2320
2599
 
2321
2600
  /* ---- css/step.css — .step: one screen, one task — card, optional sheet, bar outside ---- */
2322
2601
  /* ============================================================
@@ -2386,135 +2665,29 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2386
2665
  own screens are composed (the guide); the framework does not police it — a component never changes
2387
2666
  because another one is there. */
2388
2667
 
2389
- /* the reading block: a .page (base.css) — the same words-on-the-base
2390
- block as anywhere else (it was .step__card, and a .card before 2026-09-16;
2391
- Lh: a step reads like a page, not a card in a page — so it IS the page's
2392
- block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
2393
- between things, the tokens .card publishes for the head and .bleed) and
2394
- gets the flex rule: it hugs its content, and when the column runs out (a
2395
- short phone, large text) it shrinks and scrolls inside itself instead of
2396
- pushing the bar or the sheet */
2668
+ /* the reading block (base.css writes where it sits and why): a card's inner rhythm, set here */
2397
2669
  .step > .page, .step > .pages > .page {
2398
2670
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
2399
2671
  --card-gap: var(--sp-4);
2400
- gap: var(--card-gap);
2401
- padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
2402
- color: var(--on-surface); /* align-content: start is .page's own (base.css) — not restated here */
2403
2672
  }
2404
2673
  /* the reading block is the .page — or the .pages that holds several of them, a walk of words
2405
2674
  (card.css). Behind a PEEKED sheet the words keep the strip's room under them, so whatever is down there
2406
2675
  (the buttons that call the other sheets) scrolls clear of it — CSS alone: the box declares --sheet-peek. */
2407
- .step:has(> .sheet--peek) > :is(.page, .pages) { padding-bottom: var(--sheet-peek); }
2408
- .step > :is(.page, .pages) {
2409
- flex: 0 1 auto;
2410
- min-height: 0;
2411
- overflow-y: auto;
2412
- overscroll-behavior: contain;
2413
- }
2414
- /* a lone .page carries the margin a walk's .pages does not (its own .page children do, below) —
2415
- the screen's gap, moved in here from above the step; the inset, taken back from the bleed.
2416
- It ends with the bottom layer's room, as a walk's page does: it scrolls only when the layer would cover its words
2417
- (timebank onboard-04, 2026-09-27 — its last Link lay under the bar; Lh: same rule as the walk) */
2418
- .step > .page { margin: var(--_lead, 0) var(--screen-pad) 0; padding-bottom: var(--bottom-h, 0rem); }
2419
- /* --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
2420
- 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. */
2421
- .step > .page--bleed { margin-inline: 0; padding-inline: var(--screen-pad); padding-block-start: var(--sp-2); overflow-x: hidden; }
2422
2676
 
2423
- /* a WALK (Pages of Page) — Lh 2026-09-23, third call: no top; the picture and the words are ONE run that scrolls up together
2424
- (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
2425
- padding above AND below — so the words begin near the middle and, when a big text size runs them long, scroll up clear.
2426
- The walk shows no scrollbar. */
2427
- .step > .pages { touch-action: pan-y; } /* pan-y: the walk scrolls itself up and down; a sideways swipe is the Walkthrough's (it turns the page) */ /* the scrolling box is the whole content: from the step's top edge, edge to edge — no margin, the default (Lh 2026-09-23) */
2428
- .step > .pages > .page {
2429
- padding-inline: var(--screen-pad); /* the words keep the inset INSIDE it */
2430
- 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 */
2431
- 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) */
2432
- }
2433
2677
 
2434
2678
  /* a SCROLLING (not fill) screen becomes a column at least one phone tall when it holds a bottom or a centre:
2435
2679
  an .action-bar as its last child (Lh 2026-09-24, 0.7.0: the bar is the column's last row — at the foot when the words are short,
2436
- sticky to the viewport's foot while they scroll; actions.css) or a .screen__centre (Lh 2026-09-12: the group is in flow, takes the
2437
- room after the top bar and centres its children in it; a long one makes the page longer and runs under the bar; card.css) */
2438
- .screen:not(.screen--fill):has(> .action-bar:last-child, > .screen__centre) { display: flex; flex-direction: column; min-height: 100dvh; }
2439
-
2440
- /* ============================================================
2441
- TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
2442
- 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
2443
- under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
2444
- (On a scrolling screen the bar is a sticky last row instead, actions.css.)
2445
- ============================================================ */
2446
- .screen--fill > .action-bar {
2447
- position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
2448
- max-width: none;
2449
- margin: 0;
2450
- /* 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
2451
- running under it (Lh 2026-09-25) — the bar's own top padding */
2452
- padding-top: var(--action-bar-fill-pad-top, var(--sp-3));
2453
- /* its GROUND (Lh 2026-09-24, timebank card 141 — was "no ground of its own"): words that run under the layer mid-scroll
2454
- showed through the dots, the controls and the buttons. The layer lies on the screen's own surface now; a project sets
2455
- another (or `transparent`, the old see-through) with the knob */
2456
- background: var(--action-bar-bg, var(--surface));
2457
- }
2458
- /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
2459
- .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
2460
- /* 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
2461
- reserves nothing for it; the breathing under it stays the one it had (--sp-2), not the screen's gap */
2462
- .screen--fill > .appbar {
2463
- position: relative; inset: auto; top: auto;
2464
- flex: none;
2465
- max-width: none;
2466
- margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge; its own padding is the inset */
2467
- margin-bottom: calc(var(--sp-2) - var(--screen-gap));
2468
- }
2469
- .screen.screen--fill:has(> .appbar) { padding-top: var(--safe-top); --header-gap: 0rem; }
2470
- /* …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
2471
- them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
2472
- .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
2473
- .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
2474
- /* a TRACK as the screen's TOP (Lh 2026-10-03): the walk's progress stands where a bar would, a row of the column, in the screen's
2475
- inset — and it never touches what comes up under it: the step (and so a sheet at full) starts --track-top-gap below it. That
2476
- room is the track's own, not the screen's gap; --track-top-lead is the air over it, under the status bar. Both = the screen's side inset (16): the pill
2477
- stands the same distance from everything around it (Lh 2026-10-03: 8 over was too little, 24 under too much). */
2478
- .screen--fill > .track:first-child {
2479
- flex: none;
2480
- margin-top: var(--track-top-lead, var(--sp-4));
2481
- margin-bottom: calc(var(--track-top-gap, var(--sp-4)) - var(--screen-gap));
2482
- }
2483
- /* …and over a STAGE (a map behind a sheet, Lh 2026-10-03): the stage is the whole screen, so the track is a LAYER on it — the map
2484
- shows around the pill — and the stage keeps the track's line free itself: a sheet at full stops --track-top-gap under the
2485
- track, as in a step. --top-h is the track's own height, measured by Screen (a numbered track grows with the text). */
2486
- .screen--fill > .track:first-child:has(+ .sheet-stage) {
2487
- position: absolute; z-index: 1;
2488
- inset: calc(var(--safe-top) + var(--track-top-lead, var(--sp-4))) var(--screen-pad) auto;
2489
- margin: 0;
2490
- }
2680
+ sticky to the viewport's foot while they scroll; actions.css) or a .screen__centre (Lh 2026-09-12: the group is in flow, takes the
2681
+ room after the top bar and centres its children in it; a long one makes the page longer and runs under the bar; card.css) */
2682
+
2491
2683
  .screen--fill > .track:first-child + .sheet-stage {
2492
2684
  --_top: calc(var(--track-top-lead, var(--sp-4)) + var(--top-h, var(--header-h)) + var(--track-top-gap, var(--sp-4)));
2493
2685
  }
2494
- /* 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
2495
- the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
2496
- .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
2497
- .step > .pages > .page > :is(img, .ph):first-child {
2498
- width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
2499
- max-width: none;
2500
- margin-inline: calc(-1 * var(--screen-pad));
2501
- height: calc(var(--_lead, 0rem) + var(--page-media-h, 14rem));
2502
- max-height: none;
2503
- border-radius: 0;
2504
- border-inline: 0;
2505
- border-top: 0;
2506
- object-fit: cover;
2507
- }
2508
2686
  /* an EMPTY STATE is a component in the content box too (Lh 2026-09-23 — it no longer floats in a Centre over the screen): alone in a
2509
2687
  Step it stands at the human centre of the content, 40% down (two parts of room above it, three below). --empty-state-at is the share of the
2510
2688
  free room that stands above it (a theme's 0.2 puts it a fifth down); a sheet's body holds an only EmptyState by the same share, but defaults to 0: at the top, as it always stood */
2511
2689
  .step:has(> .empty-state)::before { content: ""; flex: calc(var(--empty-state-at, 0.4) * 5) 1 0; }
2512
2690
  .step:has(> .empty-state)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0.4) * 5) 1 0; }
2513
- .step > .empty-state { flex: none; }
2514
- .sheet__body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
2515
- .sheet__body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
2516
- .sheet__body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
2517
- .sheet__body > .empty-state:only-child { flex: none; }
2518
2691
 
2519
2692
  /* ---- css/forms.css — outlined fields, add-row ---- */
2520
2693
  /* ============================================================
@@ -2542,7 +2715,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2542
2715
  .field select,
2543
2716
  .field textarea,
2544
2717
  .field--rows {
2545
- font: var(--field-font, 400 var(--fs-lg) / 1.4 var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does. .field--rows too, so 1lh is the textarea's line for its icon */
2718
+ font: var(--field-font, var(--fw-regular) var(--fs-lg) / var(--lh-field) var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does. .field--rows too, so 1lh is the textarea's line for its icon */
2546
2719
  }
2547
2720
  .field input,
2548
2721
  .field select,
@@ -2556,17 +2729,16 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2556
2729
  caret-color: currentColor;
2557
2730
  }
2558
2731
  .field input::placeholder,
2559
- .field textarea::placeholder { color: inherit; opacity: 0.55; font-size: calc(1em * var(--_ph-fit, 1)); } /* --_ph-fit: Field's measure — a placeholder longer than the box shrinks to show whole (Lh 2026-09-25) */
2732
+ .field textarea::placeholder { color: inherit; opacity: var(--fade-hint); font-size: calc(1em * var(--_ph-fit, 1)); } /* --_ph-fit: Field's measure — a placeholder longer than the box shrinks to show whole (Lh 2026-09-25) */
2560
2733
  /* several lines in the same frame (Field rows): the textarea is as tall as its `rows` × line, the frame grows round it, the icon and the action stay at the top (Lh 2026-10-19) */
2561
2734
  .field--rows { align-items: flex-start; padding-block: var(--field-rows-pad-y, var(--sp-3)); }
2562
2735
  .field textarea { flex: 1; resize: none; display: block; }
2563
- .field--rows > .icon { margin-top: calc((1lh - var(--icon-sm)) / 2); } /* centred on the first line */
2736
+ .field--rows > .icon { --icon-margin: calc((1lh - var(--icon-sm)) / 2) 0 0; } /* centred on the first line */
2564
2737
  /* the field the cursor is in: its own border goes full ink, in every palette (the coloured ones fade it to 25% at rest) */
2565
2738
  .field:has(:focus, [data-cursor]) { border-color: var(--field-active-border, currentColor); } /* [data-cursor]: SheetSteps holds the phone's keyboard for this field */
2566
2739
 
2567
2740
  /* field with its action inside: date pick, send, go — the button sits flush right */
2568
2741
  .field--action { padding-right: var(--sp-1); min-height: var(--field-action-min-h, var(--tap-lg)); }
2569
- .field--action > .icon-btn { margin-left: auto; }
2570
2742
 
2571
2743
  /* colored palettes drop the visible outline elsewhere — fields keep one */
2572
2744
  [data-palette] .field:not(:has(:focus)) { border-color: color-mix(in srgb, currentColor 25%, transparent); }
@@ -2591,25 +2763,18 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2591
2763
  display: grid;
2592
2764
  place-items: center;
2593
2765
  font-size: var(--add-row-plus-font-size, var(--fs-md));
2594
- line-height: 1;
2766
+ line-height: var(--lh-solid);
2595
2767
  }
2596
- .add-row__plus .icon { width: var(--icon-xs); height: var(--icon-xs); }
2768
+ .add-row__plus .icon { --icon-size: var(--icon-xs); }
2597
2769
 
2598
2770
  /* ---- a wrong value, and the line that says so (Lh 2026-09-24) ----
2599
2771
  --signal-error is the one colour the wireframe has; it never speaks alone — the line carries an icon and words.
2600
2772
  <p class="form-note form-note--error" role="alert"><svg class="icon icon--xs">…</svg>Not a phone number</p>
2601
2773
  <p class="form-note form-note--busy" role="status"><svg …/>Sending the code…</p> (the ring turns) */
2602
2774
  .field.field--error, [data-palette] .field.field--error { border-color: var(--field-error-border, var(--signal-error)); }
2603
- .pin.pin--error .pin__cell, .pin.pin--error .pin__cell:focus { border-color: var(--pin-error-border, var(--signal-error)); color: var(--pin-error-ink, var(--signal-error)); } /* ALL the cells, edge and digit — the one the cursor waits in too (Lh 2026-09-24); the wrong digits stay in red until the member types again */
2604
- /* a code that does not match (Pin match): the row shakes once, sideways, then the Pin empties itself for a new try (Lh 2026-09-25) */
2605
- @media (prefers-reduced-motion: no-preference) {
2606
- .pin--shake { animation: pin-shake var(--pin-shake-time, 0.4s) ease-in-out; }
2607
- }
2608
- @keyframes pin-shake { 0%, 100% { translate: 0; } 20%, 60% { translate: calc(-1 * var(--pin-shake-x, var(--sp-2))); } 40%, 80% { translate: var(--pin-shake-x, var(--sp-2)); } }
2609
2775
  .form-note { display: flex; align-items: center; justify-content: center; gap: var(--sp-2); margin: 0; font: var(--form-note-font, var(--type-caption)); color: var(--form-note-ink, var(--card-muted)); text-align: center; }
2610
- .form-note > .icon { flex: none; }
2611
2776
  .form-note--error { color: var(--form-note-error-ink, var(--signal-error)); }
2612
- @media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { animation: turn 0.9s linear infinite; } } /* `turn`: tokens.css */
2777
+ @media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { --icon-spin: turn var(--motion-spin) var(--ease-linear) infinite; } } /* `turn`: tokens.css */
2613
2778
 
2614
2779
  /* ---- css/journey.css — route, tile badge, note row ---- */
2615
2780
  /* ============================================================
@@ -2636,7 +2801,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2636
2801
  .route__time { grid-row: 3; align-self: start; }
2637
2802
 
2638
2803
  .route__code { font: var(--route-code-font, var(--type-h2)); letter-spacing: var(--track-h2); display: flex; align-items: center; gap: var(--route-code-gap, var(--sp-3)); }
2639
- .route__code::after, .route__point--end .route__code::before { content: ""; flex: 1; border-top: var(--rule-w) dotted currentColor; opacity: 0.65; }
2804
+ .route__code::after, .route__point--end .route__code::before { content: ""; flex: 1; border-top: var(--rule-w) dotted currentColor; opacity: var(--fade-mild); }
2640
2805
  .route__point--end .route__code::after { content: none; }
2641
2806
  .route__time { font: var(--route-time-font, var(--type-body)); font-variant-numeric: tabular-nums; }
2642
2807
  .route__place { font: var(--route-place-font, var(--type-caption)); color: var(--route-place-ink, var(--card-muted)); }
@@ -2657,11 +2822,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2657
2822
  /* the label is the badge's own ink, smaller: a badge is a small fill of any colour (a palette paints it yellow), and only its own ink is
2658
2823
  sure to hold 4.5:1 on it; --dim faded it to 2.4:1 */
2659
2824
  .tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); color: var(--tile-badge-label-ink, inherit); }
2660
- .tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
2825
+ .tile-badge__value { font: var(--fw-heavy) var(--fs-lg) / var(--lh-tight) var(--font-display); font-variant-numeric: tabular-nums; }
2661
2826
 
2662
2827
  /* plain: no fill — a date box beside a title (Jun / 18) */
2663
2828
  .tile-badge--plain { background: transparent; --dark: inherit; color: inherit; padding: 0; min-width: var(--tile-badge-plain-min-w, var(--tap-sm)); }
2664
- .tile-badge--plain .tile-badge__value { font: 800 var(--fs-h1)/1 var(--font-display); }
2829
+ .tile-badge--plain .tile-badge__value { font: var(--fw-heavy) var(--fs-h1) / var(--lh-solid) var(--font-display); }
2665
2830
 
2666
2831
  /* stretch: a date in a head (Lh 2026-10-19) — as tall as the title block beside it and no taller (the head stretches it; size
2667
2832
  containment keeps its own words from adding height). Its height picks the form: short (a one-line title) = two lines, the day name
@@ -2677,7 +2842,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2677
2842
  column-gap: var(--sp-1);
2678
2843
  row-gap: 0;
2679
2844
  padding-block: var(--sp-3);
2680
- line-height: var(--tile-badge-line, 0.95);
2845
+ line-height: var(--tile-badge-line, var(--lh-badge));
2681
2846
  }
2682
2847
  .tile-badge--stretch > * { line-height: inherit; }
2683
2848
  .tile-badge--stretch > :first-child { grid-column: 1 / -1; }
@@ -2726,17 +2891,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2726
2891
  min-height: var(--header-h);
2727
2892
  }
2728
2893
 
2729
- /* ---- top bar: back + screen title ---- */
2730
- .topbar .t-h1 {
2731
- text-transform: uppercase;
2732
- flex: 1;
2733
- min-width: 0;
2734
- overflow: hidden;
2735
- text-overflow: ellipsis;
2736
- white-space: nowrap;
2737
- padding-block: 0.15em; /* Thai marks survive the clip — see card.css, the head rule */
2738
- margin-block: -0.15em;
2739
- }
2740
2894
  .topbar__actions { margin-left: auto; display: flex; gap: var(--sp-2); }
2741
2895
  /* at most TWO trailing controls (Lh 2026-09-22): the title must keep its line; a third and
2742
2896
  beyond is not drawn — more moves belong in a Menu behind one button, or on the base */
@@ -2761,7 +2915,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2761
2915
  /* fixed, so it covers the top of the screen it sits in — reserve its height
2762
2916
  there, from the same header-h token. The breathing under it is published:
2763
2917
  a .step cancels it to touch the bar. */
2764
- .screen:has(> .appbar) { --header-gap: var(--sp-2); padding-top: calc(var(--header-h) + var(--header-gap)); }
2765
2918
  .appbar__brand { flex: 1; display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
2766
2919
  .appbar__text { display: grid; min-width: 0; flex: 1; } /* pushes trailing button to the right edge */
2767
2920
  /* 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 */
@@ -2771,7 +2924,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2771
2924
  margin: 0; text-align: start;
2772
2925
  border-radius: var(--appbar-who-radius, var(--r-chip));
2773
2926
  }
2774
- .appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.35; } /* clipped lines: room for Thai marks */
2927
+ .appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: var(--lh-caption); } /* clipped lines: room for Thai marks */
2775
2928
  .appbar__title { font: var(--type-label); }
2776
2929
  .appbar__sub { font: var(--type-caption); color: var(--on-surface-muted); }
2777
2930
  /* the bar's trailing controls (one or two: a labelled switch chip, a wallet button — home,
@@ -2786,10 +2939,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2786
2939
  border: var(--border-w) solid var(--icon-btn-border, var(--outline)); /* see .btn — same reason */
2787
2940
  background: var(--icon-btn-bg, var(--action-bg));
2788
2941
  color: var(--icon-btn-ink, var(--action-ink));
2942
+ box-shadow: var(--icon-btn-shadow, none);
2789
2943
  display: grid;
2790
2944
  place-items: center;
2791
2945
  font-size: var(--fs-lg);
2792
- line-height: 1;
2946
+ line-height: var(--lh-solid);
2793
2947
  }
2794
2948
  .icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
2795
2949
  .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); }
@@ -2854,11 +3008,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2854
3008
  /* quiet keeps a faint outline so it still reads as a button on colored palettes;
2855
3009
  it is ink on whatever it sits on — inherit, or on the dark slot (card--3) it
2856
3010
  would take the light-card action ink and vanish */
2857
- .btn--quiet { background: transparent; color: inherit; border-color: color-mix(in srgb, currentColor 35%, transparent); }
3011
+ .btn--quiet { background: transparent; color: inherit; border-color: var(--btn-quiet-border, color-mix(in srgb, currentColor 35%, transparent)); }
2858
3012
  .btn--block { width: 100%; }
2859
3013
  .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) */
2860
3014
  /* xl: the one giant call to action in a tinted card ("Taxi!") */
2861
- .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); }
3015
+ .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); }
2862
3016
  /* disabled keeps the slot — a control that vanishes moves everything else,
2863
3017
  and a pager whose "previous" disappeared on page one would shift the bar
2864
3018
  under the thumb on every page */
@@ -2883,9 +3037,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2883
3037
  padding: var(--chip-pad-y, var(--sp-2)) var(--chip-pad-x, var(--sp-3));
2884
3038
  font: var(--type-label);
2885
3039
  font-size: var(--chip-font-size, var(--fs-sm));
2886
- min-height: var(--chip-h);
3040
+ min-height: var(--chip-min-h, var(--chip-h));
2887
3041
  text-decoration: none;
2888
- justify-self: start; /* as a grid child it hugs its text, never stretches */
3042
+ justify-content: var(--chip-justify, normal);
3043
+ justify-self: var(--chip-self, start); /* as a grid child it hugs its text, never stretches */
2889
3044
  }
2890
3045
  .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.
2891
3046
  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
@@ -2902,8 +3057,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2902
3057
  /* muted: an untouched default — the words wash out, the edge stays; emphasis is the difference from the neighbours (docs/reference.md 13) */
2903
3058
  .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 */
2904
3059
 
2905
- .chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
2906
- .chip-row.bleed { flex-wrap: nowrap; } /* actions.css loads after card.css — the strip stays one line */
3060
+ .chip-row { display: flex; gap: var(--sp-2); }
3061
+ .chip-row:not(.bleed) { flex-wrap: wrap; } /* a strip (.bleed, card.css) stays one line */
2907
3062
  .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 */
2908
3063
 
2909
3064
  /* ---- dropdown: the standard for every select — a pill trigger + .menu popover ----
@@ -2932,10 +3087,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2932
3087
  /* muted: the value is still the default (docs/reference.md 13) */
2933
3088
  .dropdown--muted .dropdown__btn { color: var(--dropdown-muted-ink, var(--action-muted)); }
2934
3089
  .dropdown__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2935
- .dropdown__btn > .icon { transition: rotate var(--motion-quick) ease; }
2936
- .dropdown:has(.menu:popover-open) .dropdown__btn > .icon { rotate: 180deg; }
2937
- .dropdown:has(.menu.\:popover-open) .dropdown__btn > .icon { rotate: 180deg; } /* the polyfill's open (layover.css) */
2938
- .dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards (= .menu--end, layover.css) */
3090
+ .dropdown__btn > .icon { --icon-motion: rotate var(--motion-quick) var(--ease); }
3091
+ .dropdown:has(.menu:popover-open) .dropdown__btn > .icon { --icon-turn: 180deg; }
3092
+ .dropdown:has(.menu.\:popover-open) .dropdown__btn > .icon { --icon-turn: 180deg; } /* the polyfill's open (layover.css) */
2939
3093
  .filter-row { display: flex; gap: var(--sp-2); }
2940
3094
  .filter-row > .dropdown { flex: 1; min-width: 0; }
2941
3095
 
@@ -3081,7 +3235,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3081
3235
  cursor: default;
3082
3236
  }
3083
3237
  .action-note__words { display: block; } /* ONE run of words: an icon inside sits in the line, the spaces around it kept */
3084
- .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) */
3238
+ .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) */
3085
3239
 
3086
3240
  /* PRESS (Lh 2026-09-25; every tappable thing, 2026-09-27): every CONTROL answers a tap, INFORMATION never does (no pointer, no
3087
3241
  press). Two answers, by size: GIVE — a control the size of a finger shrinks under it (--press-scale) and springs back; DIM —
@@ -3097,34 +3251,27 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3097
3251
  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) {
3098
3252
  cursor: pointer;
3099
3253
  touch-action: manipulation;
3100
- transition: scale var(--motion-press) ease-out;
3254
+ transition: scale var(--motion-press) var(--ease-out);
3101
3255
  }
3102
3256
  :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) {
3103
3257
  cursor: pointer;
3104
3258
  touch-action: manipulation;
3105
- transition: opacity var(--motion-press) ease-out;
3259
+ transition: opacity var(--motion-press) var(--ease-out);
3106
3260
  }
3107
3261
  /* 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) */
3108
3262
  :where(.chip--pick, button.chip, .tile--pick, button.tile, .day, .segment__item, .calendar__day, .float-bar__item, .picker__item, .check,
3109
3263
  .icon-btn[role="tab"], .avatar-pick__item > .avatar, .map-pick > .map-pick__mark, .map-pick > .map-pick__name) {
3110
3264
  transition-property: scale, background-color, color, border-color, opacity;
3111
3265
  transition-duration: var(--motion-press);
3112
- transition-timing-function: ease-out;
3266
+ transition-timing-function: var(--ease-out);
3113
3267
  }
3114
3268
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
3115
3269
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
3116
3270
  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); }
3117
3271
  :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); }
3118
- .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
3119
- /* 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
3120
- notes do not show through one another); the ring is the button's own, round the whole pile */
3121
- .sticky-pile__press { cursor: pointer; touch-action: manipulation; }
3122
- .sticky-pile__notes:has(> .sticky-pile__press) { transition: opacity var(--motion-press) ease-out; }
3123
- .sticky-pile__notes:has(> .sticky-pile__press:active) { opacity: var(--press-dim); }
3124
- .sticky-pile__press:focus-visible { outline: var(--focus); outline-offset: 0; }
3125
3272
  /* 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:
3126
3273
  it darkens a touch instead (--press-shade) */
3127
- :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; }
3274
+ :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); }
3128
3275
  :where(.sheet-stack--tap > .card:not(.is-open), .postcard__turn, .post-letter__sheet:not(:has(.icon-btn:active))):active { filter: brightness(var(--press-shade)); }
3129
3276
  /* 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) */
3130
3277
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
@@ -3144,6 +3291,90 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3144
3291
  .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 */
3145
3292
  @media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
3146
3293
 
3294
+ /* ==== the chip a choice draws: picked (choice.css drives it with its input), and --sign (#62: a block is written in its own file) ==== */
3295
+ .chip--pick { position: relative; } /* the input covers the whole chip, on a row as in a grid */
3296
+ .chip--pick:has(:checked) { background: var(--accent); --dark: var(--accent-dark); color: var(--card-ink); border-color: transparent; }
3297
+ /* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
3298
+ .chip--sign { padding-right: var(--sp-2); }
3299
+ .chip--sign::after {
3300
+ content: "";
3301
+ width: var(--icon-xs);
3302
+ height: var(--icon-xs);
3303
+ flex: none;
3304
+ background: currentColor;
3305
+ 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;
3306
+ -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;
3307
+ }
3308
+ .chip--sign:has(:checked)::after { rotate: 45deg; }
3309
+ @media (prefers-reduced-motion: no-preference) { .chip--sign::after { transition: rotate var(--motion-slide); } }
3310
+
3311
+ /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
3312
+ /* 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 */
3313
+ .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; }
3314
+ .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: var(--lh-body); }
3315
+ .card__foot > .btn { flex: 1 1 0; min-width: 0; }
3316
+ /* …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
3317
+ a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
3318
+ .card__foot > .btn--form { flex: 0 1 auto; }
3319
+ /* the page dots under the card sit centred under it — the indicator belongs
3320
+ with the page that changes, never in the bar (Lh, 2026-09-12) */
3321
+ .screen__centre > .pager__at { justify-self: center; }
3322
+ .panes > .pager__at { justify-self: center; }
3323
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
3324
+ .screen--fill:has(> .sheet-stack--flood) > :is(.appbar, .topbar):first-child {
3325
+ position: absolute;
3326
+ inset: var(--safe-top) 0 auto;
3327
+ max-width: none;
3328
+ margin: 0;
3329
+ z-index: var(--z-flat); /* under every sheet (--level 1-5); the base clears it by margin */
3330
+ }
3331
+ .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 {
3332
+ visibility: hidden;
3333
+ 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) */
3334
+ }
3335
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
3336
+ /* the step waiting for its answer: its buttons take no taps (the app guards its own handler too) */
3337
+ .sheet-step--busy .btn { pointer-events: none; opacity: var(--fade-half); }
3338
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
3339
+ /* ============================================================
3340
+ TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
3341
+ 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
3342
+ under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
3343
+ (On a scrolling screen the bar is a sticky last row instead, actions.css.)
3344
+ ============================================================ */
3345
+ .screen--fill > .action-bar {
3346
+ position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
3347
+ max-width: none;
3348
+ margin: 0;
3349
+ /* 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
3350
+ running under it (Lh 2026-09-25) — the bar's own top padding */
3351
+ padding-top: var(--action-bar-fill-pad-top, var(--sp-3));
3352
+ /* its GROUND (Lh 2026-09-24, timebank card 141 — was "no ground of its own"): words that run under the layer mid-scroll
3353
+ showed through the dots, the controls and the buttons. The layer lies on the screen's own surface now; a project sets
3354
+ another (or `transparent`, the old see-through) with the knob */
3355
+ background: var(--action-bar-bg, var(--surface));
3356
+ }
3357
+ /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
3358
+ .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
3359
+ /* 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
3360
+ reserves nothing for it; the breathing under it stays the one it had (--sp-2), not the screen's gap */
3361
+ .screen--fill > .appbar {
3362
+ position: relative; inset: auto; top: auto;
3363
+ flex: none;
3364
+ max-width: none;
3365
+ margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge; its own padding is the inset */
3366
+ margin-bottom: calc(var(--sp-2) - var(--screen-gap));
3367
+ }
3368
+ /* …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
3369
+ them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
3370
+ .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
3371
+ /* ---- where it sits in what forms.css draws (docs/reference.md rule 15) ---- */
3372
+ .field--action > .icon-btn { margin-left: auto; }
3373
+ /* ---- where it sits in what sticky.css draws (docs/reference.md rule 15) ---- */
3374
+ .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; }
3375
+ /* ---- where it sits in what layover.css draws (docs/reference.md rule 15) ---- */
3376
+ .modal__drawer > .btn-row { justify-self: stretch; margin-top: var(--modal-drawer-answers-gap, var(--sp-5)); white-space: nowrap; }
3377
+
3147
3378
  /* ---- css/numbers.css — ring, track, band, dot grid, picker, badge, bars ---- */
3148
3379
  /* ============================================================
3149
3380
  cardds/numbers.css — big numbers inside cards:
@@ -3175,13 +3406,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3175
3406
  border-radius: 50%;
3176
3407
  background: conic-gradient(var(--accent) calc(var(--value) * 1%), var(--ink-tint) 0);
3177
3408
  /* masks only read alpha: currentColor = "opaque" without naming a color */
3178
- mask: radial-gradient(farthest-side, transparent calc(100% - var(--_w) - 1px), currentColor calc(100% - var(--_w)));
3179
- -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--_w) - 1px), currentColor calc(100% - var(--_w)));
3409
+ mask: radial-gradient(farthest-side, transparent calc(100% - var(--_w) - var(--px)), currentColor calc(100% - var(--_w)));
3410
+ -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--_w) - var(--px)), currentColor calc(100% - var(--_w)));
3180
3411
  }
3181
3412
  .ring__value { font: var(--ring-value-font, var(--type-stat)); letter-spacing: var(--track-display); }
3182
3413
  .ring__label { font: var(--ring-label-font, var(--type-caption)); color: var(--ring-label-ink, var(--card-muted)); }
3183
3414
  .ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
3184
- .ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
3415
+ .ring--sm .ring__value { font: var(--fw-heavy) var(--fs-2xl) / var(--lh-solid) var(--font-display); }
3185
3416
  .ring--xs { --_size: var(--tap-sm); --_w: var(--dot-xs); } /* inline counter (step 3 of 5) */
3186
3417
  .ring--xs .ring__value { font: var(--type-caption); }
3187
3418
 
@@ -3222,44 +3453,31 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3222
3453
  }
3223
3454
  .track__step:first-child::before { display: none; }
3224
3455
  .track__step.is-done::before { background: var(--accent); }
3225
- .track__step > .icon,
3226
- .track__step > .track__dot {
3227
- width: var(--_step);
3228
- height: var(--_step);
3456
+ .track__step > .icon { --icon-size: var(--_step); --icon-pad: calc(var(--_step) * 7 / 32); --icon-radius: 50%; --icon-bg: var(--ink-rail); --icon-ink: var(--card-ink); }
3457
+ .track__step > .track__dot { /* a number in the round (TrackStep num) */
3229
3458
  flex: none;
3230
- padding: calc(var(--_step) * 7 / 32); /* the glyph = 18 in the 32 round */
3231
3459
  border-radius: 50%;
3232
- background: var(--ink-rail);
3460
+ background: var(--ink-tint); /* a number is words: on the 30% rail it fell under 4.5:1 on a mid-dark track; the 12% tint is what every tone holds its ink on */
3233
3461
  color: var(--card-ink);
3234
- }
3235
- .track__step > .track__dot { background: var(--ink-tint); } /* a number is words: on the 30% rail it fell under 4.5:1 on a mid-dark track; the 12% tint is what every tone holds its ink on */
3236
- .track__step > .track__dot { /* a number in the round (TrackStep num) */
3237
3462
  display: grid; place-items: center; box-sizing: border-box;
3238
- font: var(--track-dot-font, var(--type-caption)); line-height: 1; font-variant-numeric: tabular-nums;
3463
+ font: var(--track-dot-font, var(--type-caption)); line-height: var(--lh-solid); font-variant-numeric: tabular-nums;
3239
3464
  width: var(--track-dot-size, 1.877em); height: var(--track-dot-size, 1.877em); /* em, not rem (Lh 2026-10-03, as the SheetSteps' number and the StackSheet's count): the round is cut from ITS OWN
3240
3465
  number's size — 1.877em of a 17px caption = the 32px of an icon's round; at a larger text it grows with its number, and the pill with it */
3241
3466
  padding: 0 0 var(--track-dot-lift, 0.1em); /* the room UNDER the number lifts it onto the round's middle: a digit has no descender */
3242
3467
  }
3243
- .track__step.is-done > .icon,
3468
+ .track__step.is-done > .icon { --icon-bg: var(--accent); --icon-ink: var(--on-accent); }
3244
3469
  .track__step.is-done > .track__dot { background: var(--accent); color: var(--on-accent); }
3245
3470
 
3246
3471
  /* ---- band: data card — 2-line label · giant number · footnote ----
3247
3472
  <article class="card card--band"><span class="band__label">Hours<br>banked</span><span class="band__num">18</span><span class="band__foot">this week</span></article> */
3248
- .card--band {
3249
- grid-template-columns: 1fr auto;
3250
- grid-template-areas: "label num" "foot num";
3251
- align-items: end;
3252
- gap: var(--sp-2) var(--sp-4);
3253
- min-height: var(--card-band-min-h, var(--tile-h));
3254
- }
3255
- .band__label { grid-area: label; align-self: start; font: var(--band-label-font, var(--type-label)); font-size: var(--band-label-font-size, var(--fs-body)); line-height: 1.2; }
3473
+ .band__label { grid-area: label; align-self: start; font: var(--band-label-font, var(--type-label)); font-size: var(--band-label-font-size, var(--fs-body)); line-height: var(--lh-label); }
3256
3474
  .band__num {
3257
3475
  grid-area: num;
3258
- font: 800 var(--fs-giant) / 0.9 var(--font-display);
3476
+ font: var(--fw-heavy) var(--fs-giant) / var(--lh-band) var(--font-display);
3259
3477
  letter-spacing: var(--track-display);
3260
3478
  font-variant-numeric: tabular-nums;
3261
3479
  }
3262
- .band__num small { font-size: 0.5em; font-weight: 700; margin-right: 0.1em; } /* unit or % */
3480
+ .band__num small { font-size: 0.5em; font-weight: var(--fw-bold); margin-right: 0.1em; } /* unit or % */
3263
3481
  .band__foot { grid-area: foot; font: var(--band-foot-font, var(--type-caption)); color: var(--band-foot-ink, var(--card-muted)); }
3264
3482
  /* band stack: ONE card boundary; bands are frameless inside it, a hairline
3265
3483
  between each. Bands may still carry slot tints (card--2 / card--3). */
@@ -3269,8 +3487,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3269
3487
  border-radius: var(--band-stack-radius, var(--r-card));
3270
3488
  overflow: hidden;
3271
3489
  }
3272
- .band-stack > .card--band { border: 0; border-radius: 0; box-shadow: none; }
3273
- .band-stack > .card--band + .card--band { border-top: var(--border-w) solid var(--hairline); }
3274
3490
 
3275
3491
  /* ---- dotgrid: progress over time as dots — --total, --done, --cols ----
3276
3492
  one element, no children:
@@ -3304,7 +3520,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3304
3520
  );
3305
3521
  }
3306
3522
  /* no round(): show the grid without the done fill rather than a broken shape */
3307
- @supports not (width: round(up, 1px, 1px)) {
3523
+ @supports not (width: round(up, 0.0625rem, 0.0625rem)) {
3308
3524
  .dotgrid::before { display: none; }
3309
3525
  }
3310
3526
  /* ---- picker: horizontal snap picker, centre value big ----
@@ -3349,7 +3565,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3349
3565
  .picker__unit { font: var(--picker-unit-font, var(--type-caption)); color: var(--picker-unit-ink, var(--card-muted)); }
3350
3566
  /* the scrolled-to item lights up by position — no JS */
3351
3567
  @supports (animation-timeline: view(x)) {
3352
- .picker__item { animation: picker-focus linear both; animation-timeline: view(x); }
3568
+ .picker__item { animation: picker-focus var(--ease-linear) both; animation-timeline: view(x); }
3353
3569
  @keyframes picker-focus {
3354
3570
  0%, 100% { color: var(--_rest); }
3355
3571
  45%, 55% { color: var(--card-ink); }
@@ -3375,7 +3591,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3375
3591
 
3376
3592
  /* ---- bars: three or four bars with a count on top — the glance chart ----
3377
3593
  <div class="bars"><div class="bar" style="--v: 16"><span>+16</span></div>… --max on the parent */
3378
- .bars { --max: 20; --_h: 5rem; display: flex; align-items: end; gap: var(--bars-gap, var(--sp-2)); height: calc(var(--_h) + var(--sp-8)); }
3594
+ .bars { --max: 20; --_h: calc(var(--tap-base) * 5 / 3); display: flex; align-items: end; gap: var(--bars-gap, var(--sp-2)); height: calc(var(--_h) + var(--sp-8)); }
3379
3595
  .bar {
3380
3596
  flex: 1;
3381
3597
  min-width: 0;
@@ -3385,7 +3601,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3385
3601
  gap: var(--bar-gap, var(--sp-1));
3386
3602
  height: 100%;
3387
3603
  font: var(--bar-font, var(--type-caption));
3388
- font-weight: 700;
3604
+ font-weight: var(--fw-bold);
3389
3605
  }
3390
3606
  .bar::after {
3391
3607
  content: "";
@@ -3401,8 +3617,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3401
3617
  <span class="spinner" role="status"><svg class="icon">…loader-circle…</svg>Loading…</span>
3402
3618
  The ring turns; words beside it are optional and take the caption's look. Still when motion is reduced. */
3403
3619
  .spinner { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); vertical-align: middle; font: var(--type-caption); color: var(--spinner-ink, currentColor); }
3404
- .spinner > .icon { flex: none; }
3405
- @media (prefers-reduced-motion: no-preference) { .spinner > .icon { animation: turn 0.9s linear infinite; } } /* `turn`: tokens.css */
3620
+ @media (prefers-reduced-motion: no-preference) { .spinner > .icon { --icon-spin: turn var(--motion-spin) var(--ease-linear) infinite; } } /* `turn`: tokens.css */
3406
3621
 
3407
3622
  /* ---- progress: one straight line, the done share solid, the rest faint (Lh 2026-09-22) — a voice note's
3408
3623
  timeline, a download, a step count as a line. <div class="progress" role="progressbar" style="--progress-at: 0.4"></div>
@@ -3420,6 +3635,27 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3420
3635
  background-repeat: no-repeat;
3421
3636
  }
3422
3637
 
3638
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
3639
+ /* a TRACK as the screen's TOP (Lh 2026-10-03): the walk's progress stands where a bar would, a row of the column, in the screen's
3640
+ inset — and it never touches what comes up under it: the step (and so a sheet at full) starts --track-top-gap below it. That
3641
+ room is the track's own, not the screen's gap; --track-top-lead is the air over it, under the status bar. Both = the screen's side inset (16): the pill
3642
+ stands the same distance from everything around it (Lh 2026-10-03: 8 over was too little, 24 under too much). */
3643
+ .screen--fill > .track:first-child {
3644
+ flex: none;
3645
+ margin-top: var(--track-top-lead, var(--sp-4));
3646
+ margin-bottom: calc(var(--track-top-gap, var(--sp-4)) - var(--screen-gap));
3647
+ }
3648
+ /* …and over a STAGE (a map behind a sheet, Lh 2026-10-03): the stage is the whole screen, so the track is a LAYER on it — the map
3649
+ shows around the pill — and the stage keeps the track's line free itself: a sheet at full stops --track-top-gap under the
3650
+ track, as in a step. --top-h is the track's own height, measured by Screen (a numbered track grows with the text). */
3651
+ .screen--fill > .track:first-child:has(+ .sheet-stage) {
3652
+ position: absolute; z-index: var(--z-over);
3653
+ inset: calc(var(--safe-top) + var(--track-top-lead, var(--sp-4))) var(--screen-pad) auto;
3654
+ margin: 0;
3655
+ }
3656
+ /* ---- where it sits in what media.css draws (docs/reference.md rule 15) ---- */
3657
+ .postcard__voice > .progress { justify-self: stretch; pointer-events: none; }
3658
+
3423
3659
  /* ---- css/people.css — avatar, avatar stack, chooser row ---- */
3424
3660
  /* ============================================================
3425
3661
  cardds/people.css — avatars: single, stacked, chooser row.
@@ -3444,12 +3680,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3444
3680
  font: var(--avatar-font, var(--type-label));
3445
3681
  font-size: calc(var(--_size) * 0.3);
3446
3682
  text-transform: uppercase;
3447
- letter-spacing: 0.02em;
3683
+ letter-spacing: var(--track-loose);
3448
3684
  position: relative;
3449
3685
  text-decoration: none; /* .avatar works as <a> too */
3450
3686
  }
3451
3687
  .avatar > img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
3452
- .avatar > .icon { width: 50%; height: 50%; }
3688
+ .avatar > .icon { --icon-size: 50%; }
3453
3689
  .avatar--sm { --_size: var(--avatar-sm); }
3454
3690
  .avatar--lg { --_size: var(--avatar-lg); }
3455
3691
  .avatar--xl { --_size: var(--avatar-xl); } /* sheet head icon ring */
@@ -3465,7 +3701,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3465
3701
  inset: calc(-0.55 * var(--_size));
3466
3702
  border-radius: 50%;
3467
3703
  background: repeating-radial-gradient(circle, transparent 0 calc(var(--_size) * 0.5), currentColor calc(var(--_size) * 0.5) calc(var(--_size) * 0.5 + var(--border-w)), transparent calc(var(--_size) * 0.5 + var(--border-w)) calc(var(--_size) * 0.68));
3468
- opacity: 0.25;
3704
+ opacity: var(--fade-faint);
3469
3705
  pointer-events: none;
3470
3706
  }
3471
3707
 
@@ -3474,11 +3710,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3474
3710
  .avatar-stack { display: inline-flex; align-items: center; }
3475
3711
  .avatar-stack > .avatar { box-shadow: 0 0 0 var(--sp-1) var(--card-bg, var(--surface)); }
3476
3712
  .avatar-stack > .avatar + .avatar { margin-left: calc(-1 * var(--sp-3)); }
3477
- /* a stamp in the stack is the stamp's small size (--stamp-sm-size: one rule in stamp.css, 48 × 60), lapped like the heads. Measured 2026-10-11 in a Row's
3478
- lead with a title, a meta line and an end button: at 375 the meta line breaks in two, accepted by Lh (a lap of 16 would hold it; the lap stays 8)
3479
- <span class="avatar-stack"><span class="stamp stamp--bare">…</span><span class="stamp stamp--bare">…</span><span class="avatar-stack__more">+9</span></span> */
3480
- .avatar-stack > .stamp { flex: none; margin-inline: 0; }
3481
- .avatar-stack > .stamp + .stamp { margin-left: calc(-1 * var(--sp-2)); }
3482
3713
  /* more: the count that trails the stack — plain words on the ground, title size so it reads at arm's length (the accent "+7" avatar is the small, framed way) */
3483
3714
  .avatar-stack__more { margin-left: var(--avatar-stack-more-gap, var(--sp-3)); font: var(--avatar-stack-more-font, var(--type-title)); color: var(--avatar-stack-more-ink, inherit); white-space: nowrap; }
3484
3715
 
@@ -3558,7 +3789,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3558
3789
  --dark: var(--action-dark); /* the paper's side: its knob at rest is the paper's muted ink, on the accent the accent's */
3559
3790
  color: var(--card-ink);
3560
3791
  position: relative;
3561
- transition: background-color var(--motion-press) ease-out, border-color var(--motion-press) ease-out;
3792
+ transition: background-color var(--motion-press) var(--ease-out), border-color var(--motion-press) var(--ease-out);
3562
3793
  }
3563
3794
  .toggle::after {
3564
3795
  content: "";
@@ -3569,7 +3800,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3569
3800
  height: calc(var(--_h) - 2 * var(--_pad) - 2 * var(--border-w));
3570
3801
  border-radius: 50%;
3571
3802
  background: var(--card-muted); /* a glyph: a shade of its side, never an opacity (0.45 of the ink was 2.4:1 on the paper) */
3572
- transition: translate var(--motion-quick) ease;
3803
+ transition: translate var(--motion-quick) var(--ease);
3573
3804
  }
3574
3805
  .toggle:checked { background: var(--accent); border-color: transparent; }
3575
3806
  .toggle:checked::after { translate: calc(var(--_w) - var(--_h)) 0; background: var(--on-accent); }
@@ -3581,34 +3812,18 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3581
3812
  <div class="chip-row"><label class="chip chip--pick chip--sign"><input type="checkbox">Nature</label>…</div> */
3582
3813
  .chip-grid {
3583
3814
  display: grid;
3584
- grid-template-columns: repeat(auto-fill, minmax(min(5.5rem, 100%), 1fr));
3815
+ grid-template-columns: repeat(auto-fill, minmax(min(calc(var(--tap-base) * 11 / 6), 100%), 1fr));
3585
3816
  gap: var(--chip-grid-gap, var(--sp-2));
3586
3817
  }
3587
3818
  /* .chip hugs its text as a loose grid child (justify-self: start); inside a
3588
3819
  chip grid the whole point is equal, full-width pick targets, so stretch.
3589
3820
  Matters most in scripts without word spaces (Thai): min-content is the
3590
3821
  whole string, tracks get wide, and hugging chips leave a ragged grid. */
3591
- .chip-grid > .chip { justify-self: stretch; justify-content: center; min-height: var(--btn-h); padding-inline: var(--sp-2); }
3592
- .chip--pick { position: relative; } /* the input covers the whole chip, on a row as in a grid */
3822
+ .chip-grid > .chip { --chip-self: stretch; --chip-justify: center; --chip-min-h: var(--btn-h); --chip-pad-x: var(--sp-2); }
3593
3823
  /* the hidden input, the same shape wherever a native radio/checkbox drives a picked look:
3594
3824
  a chip, a mood dot, a tile (media.css), a sticky's tone swatch (sticky.css), a place on a map (sheet.css), a card that is a label round a radio
3595
3825
  (a pickable row card: the app gives the picked one its `tone`) — the native input lies over its whole box, unseen */
3596
3826
  :is(.chip--pick, .mood__opt, .tile--pick, .sticky-pick__swatch, .map-pick) > input, label.card > input[type="radio"] { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
3597
- :is(.chip--pick, .tile--pick):has(:checked) { background: var(--accent); --dark: var(--accent-dark); color: var(--card-ink); border-color: transparent; }
3598
- /* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
3599
- .chip--sign { padding-right: var(--sp-2); }
3600
- .chip--sign::after {
3601
- content: "";
3602
- width: var(--icon-xs);
3603
- height: var(--icon-xs);
3604
- flex: none;
3605
- background: currentColor;
3606
- 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;
3607
- -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;
3608
- }
3609
- .chip--sign:has(:checked)::after { rotate: 45deg; }
3610
- @media (prefers-reduced-motion: no-preference) { .chip--sign::after { transition: rotate var(--motion-slide); } }
3611
-
3612
3827
  /* ---- day strip: a week, one day selected, today a light tone ----
3613
3828
  <div class="day-strip" role="tablist"><button class="day" role="tab" aria-selected="true"><span class="day__name">Thu</span><span class="day__num">16</span></button>…</div> */
3614
3829
  .day-strip { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--day-strip-gap, var(--sp-1)); }
@@ -3631,7 +3846,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3631
3846
  color: var(--day-rest-ink, var(--card-muted)); /* an unpicked day at rest: the muted words of the ground it stands on (an opacity faded it under AA) */
3632
3847
  }
3633
3848
  .day__name { font: var(--day-name-font, var(--type-caption)); }
3634
- .day__num { font: 700 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
3849
+ .day__num { font: var(--fw-bold) var(--fs-lg) / var(--lh-tight) var(--font-display); font-variant-numeric: tabular-nums; }
3635
3850
  /* today: a light tone, no louder than a rest; the selected pill below wins when it is both (it comes after and weighs more) */
3636
3851
  .day--today { background: var(--day-today-bg, var(--ink-tint)); color: var(--day-today-ink, var(--card-ink)); }
3637
3852
  .day[aria-selected="true"] { background: var(--day-selected-bg, var(--accent)); color: var(--day-selected-ink, var(--on-accent)); }
@@ -3650,7 +3865,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3650
3865
  display: grid;
3651
3866
  place-items: center;
3652
3867
  border-radius: 50%;
3653
- font: 500 var(--fs-title)/1 var(--font-ui);
3868
+ font: var(--fw-medium) var(--fs-title) / var(--lh-solid) var(--font-ui);
3654
3869
  font-variant-numeric: tabular-nums;
3655
3870
  grid-column-start: var(--start, auto); /* --start on the 1st: weekday column 1–7 */
3656
3871
  }
@@ -3658,8 +3873,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3658
3873
  strong mark adds a ring of the accent. A stronger fill sat at the middle, where neither the ink nor --on-accent holds 4.5:1 */
3659
3874
  .calendar__day--mark { background: var(--calendar-day-mark-bg, color-mix(in srgb, var(--accent) 12%, transparent)); }
3660
3875
  .calendar__day--mark2 { background: var(--calendar-day-mark2-bg, color-mix(in srgb, var(--accent) 12%, transparent)); color: var(--calendar-day-mark2-ink, inherit); box-shadow: var(--calendar-day-mark2-shadow, inset 0 0 0 var(--border-w) var(--accent)); }
3661
- .calendar__day[aria-selected="true"] { background: var(--calendar-day-selected-bg, var(--accent)); color: var(--calendar-day-selected-ink, var(--on-accent)); font-weight: 700; }
3662
- .calendar__day:disabled { opacity: 0.3; }
3876
+ .calendar__day[aria-selected="true"] { background: var(--calendar-day-selected-bg, var(--accent)); color: var(--calendar-day-selected-ink, var(--on-accent)); font-weight: var(--fw-bold); }
3877
+ .calendar__day:disabled { opacity: var(--fade-dim); }
3663
3878
  .calendar__day[aria-current="date"] { text-decoration: underline; text-underline-offset: 0.2em; }
3664
3879
 
3665
3880
  /* ---- calendar weeks: a run of week rows that scrolls week by week, `show` rows in sight; the head sticks at the top of the box
@@ -3672,12 +3887,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3672
3887
  overflow-y: auto;
3673
3888
  overscroll-behavior: contain;
3674
3889
  scroll-snap-type: y mandatory;
3675
- scroll-padding-block-start: var(--_weeks-lead, 0px);
3890
+ scroll-padding-block-start: var(--_weeks-lead, 0rem);
3676
3891
  overflow-anchor: none; /* a pick rerenders the rows; the browser must not shift the scroll to keep an anchor */
3677
3892
  scrollbar-width: none;
3678
3893
  }
3679
3894
  .calendar--weeks::-webkit-scrollbar { display: none; }
3680
- .calendar__weeks-head { position: sticky; inset-block-start: 0; z-index: 1; display: grid; gap: var(--calendar-head-gap, var(--sp-2)); background: var(--calendar-weeks-head-bg, var(--card-bg)); }
3895
+ .calendar__weeks-head { position: sticky; inset-block-start: 0; z-index: var(--z-over); display: grid; gap: var(--calendar-head-gap, var(--sp-2)); background: var(--calendar-weeks-head-bg, var(--card-bg)); }
3681
3896
  .calendar__label { font: var(--calendar-label-font, var(--type-label)); color: var(--calendar-label-ink, inherit); justify-self: start; }
3682
3897
  button.calendar__label { appearance: none; border: 0; padding: 0; background: none; cursor: pointer; border-radius: var(--sp-1); }
3683
3898
  button.calendar__label:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
@@ -3702,14 +3917,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3702
3917
  height: var(--mood-dot-size, var(--icon-xs));
3703
3918
  border-radius: 50%;
3704
3919
  background: var(--_mood, currentColor);
3705
- transition: scale 0.2s ease, box-shadow 0.2s ease;
3920
+ transition: scale var(--motion-quick) var(--ease), box-shadow var(--motion-quick) var(--ease);
3706
3921
  }
3707
3922
  .mood__opt:nth-child(1) { --_mood: var(--neg); }
3708
3923
  .mood__opt:nth-child(2) { --_mood: color-mix(in srgb, var(--neg) 50%, var(--dot-b)); }
3709
3924
  .mood__opt:nth-child(3) { --_mood: var(--dot-b); }
3710
3925
  .mood__opt:nth-child(4) { --_mood: color-mix(in srgb, var(--pos) 50%, var(--dot-b)); }
3711
3926
  .mood__opt:nth-child(5) { --_mood: var(--pos); }
3712
- .mood__opt:has(:checked) > .mood__dot { scale: 1.7; box-shadow: 0 0 0 var(--sp-2) color-mix(in srgb, var(--_mood, currentColor) 25%, transparent); }
3927
+ .mood__opt:has(:checked) > .mood__dot { scale: var(--scale-pick); box-shadow: 0 0 0 var(--sp-2) color-mix(in srgb, var(--_mood, currentColor) 25%, transparent); }
3713
3928
  .mood__opt:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-0); border-radius: 50%; }
3714
3929
 
3715
3930
  /* ---- pin: big code entry, one digit per pill ----
@@ -3749,7 +3964,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3749
3964
  background-position: center;
3750
3965
  background-repeat: no-repeat;
3751
3966
  }
3752
- .pin--secret .pin__cell.is-fresh:not(:placeholder-shown) { animation: pin-flash var(--pin-flash-time, 0.5s) steps(1); }
3967
+ .pin--secret .pin__cell.is-fresh:not(:placeholder-shown) { animation: pin-flash var(--pin-flash-time, var(--motion-flash)) var(--ease-step); }
3753
3968
  @keyframes pin-flash { 0% { -webkit-text-fill-color: currentColor; background-size: 0 0; } }
3754
3969
  .pin--secret .pin__cell::selection { background: transparent; } /* its digit is selected when the cell takes the cursor: nothing to show of it */
3755
3970
  .pin__cell:focus { outline: none; }
@@ -3760,12 +3975,12 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3760
3975
  .pin--asleep .pin__cell:focus:placeholder-shown { background-image: none; animation: none; }
3761
3976
  .pin__cell:is(:focus, [data-cursor]):placeholder-shown {
3762
3977
  background-image: linear-gradient(currentColor, currentColor);
3763
- background-size: var(--pin-caret-w, 2px) var(--pin-caret-h, 1em);
3978
+ background-size: var(--pin-caret-w, var(--hairline-w)) var(--pin-caret-h, 1em);
3764
3979
  background-position: center;
3765
3980
  background-repeat: no-repeat;
3766
3981
  }
3767
3982
  @media (prefers-reduced-motion: no-preference) {
3768
- .pin__cell:is(:focus, [data-cursor]):placeholder-shown { animation: pin-caret 1.1s steps(1) infinite; }
3983
+ .pin__cell:is(:focus, [data-cursor]):placeholder-shown { animation: pin-caret var(--motion-blink) var(--ease-step) infinite; }
3769
3984
  @keyframes pin-caret { 50% { background-size: 0 0; } }
3770
3985
  }
3771
3986
 
@@ -3777,13 +3992,13 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3777
3992
  .composer { display: grid; gap: var(--composer-gap, var(--sp-3)); }
3778
3993
  .composer__input {
3779
3994
  width: 100%;
3780
- min-height: var(--composer-input-min-h, 11rem);
3995
+ min-height: var(--composer-input-min-h, calc(var(--tap-base) * 11 / 3));
3781
3996
  resize: none;
3782
3997
  border: 0;
3783
3998
  padding: 0;
3784
3999
  background: transparent;
3785
4000
  color: inherit;
3786
- font: var(--composer-input-font, 700 var(--fs-2xl)/1.25 var(--font-display));
4001
+ font: var(--composer-input-font, var(--fw-bold) var(--fs-2xl) / var(--lh-h2) var(--font-display));
3787
4002
  letter-spacing: var(--track-h2);
3788
4003
  outline: none;
3789
4004
  }
@@ -3870,6 +4085,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3870
4085
  flex: none;
3871
4086
  }
3872
4087
 
4088
+ /* ==== the pin's wrong code: red cells and one shake (#62: a block is written in its own file) ==== */
4089
+ .pin.pin--error .pin__cell, .pin.pin--error .pin__cell:focus { border-color: var(--pin-error-border, var(--signal-error)); color: var(--pin-error-ink, var(--signal-error)); } /* ALL the cells, edge and digit — the one the cursor waits in too (Lh 2026-09-24); the wrong digits stay in red until the member types again */
4090
+ /* a code that does not match (Pin match): the row shakes once, sideways, then the Pin empties itself for a new try (Lh 2026-09-25) */
4091
+ @media (prefers-reduced-motion: no-preference) {
4092
+ .pin--shake { animation: pin-shake var(--pin-shake-time, var(--motion-shake)) var(--ease-in-out); }
4093
+ }
4094
+ @keyframes pin-shake { 0%, 100% { translate: 0; } 20%, 60% { translate: calc(-1 * var(--pin-shake-x, var(--sp-2))); } 40%, 80% { translate: var(--pin-shake-x, var(--sp-2)); } }
4095
+
3873
4096
  /* ---- css/lists.css — row, rows, key-value, kv grid, timeline, legend, link ---- */
3874
4097
  /* ============================================================
3875
4098
  cardds/lists.css — frameless rows and pairs INSIDE a card:
@@ -3907,15 +4130,15 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3907
4130
  .row__meta { font: var(--row-meta-font, var(--type-caption)); color: var(--row-meta-ink, var(--card-muted)); display: flex; align-items: center; gap: var(--row-meta-gap, var(--sp-1)); }
3908
4131
  .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
3909
4132
  .row--lg { min-height: var(--row-lg-min-h, var(--fab)); } /* menu rows: a taller target, round arrow at the end; .rows gives them more air */
3910
- .row--lg .row__title { font: 700 var(--fs-lg)/1.2 var(--font-display); }
4133
+ .row--lg .row__title { font: var(--fw-bold) var(--fs-lg) / var(--lh-h1) var(--font-display); }
3911
4134
  /* a row that EXPANDS (Lh 2026-09-22): the row is a <details>' summary, what it shows comes out under it —
3912
4135
  who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
3913
4136
  <details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
3914
4137
  .row-expand { display: block; }
3915
4138
  .row-expand > summary { list-style: none; }
3916
4139
  .row-expand > summary::-webkit-details-marker { display: none; }
3917
- .row-expand > summary > .icon:last-child { transition: rotate var(--motion-slide); }
3918
- .row-expand[open] > summary > .icon:last-child { rotate: 180deg; }
4140
+ .row-expand > summary > .icon:last-child { --icon-motion: rotate var(--motion-slide); }
4141
+ .row-expand[open] > summary > .icon:last-child { --icon-turn: 180deg; }
3919
4142
  .row-expand__more { padding-top: var(--row-expand-gap, var(--sp-3)); }
3920
4143
  /* a finished item in a checklist (a to-do list, an onboarding "complete your introduction" list):
3921
4144
  the words go muted, a filled tick sits at the end, and the row is no longer a tap target —
@@ -3964,7 +4187,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3964
4187
  .kv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(var(--tile-min), 100%), 1fr)); gap: var(--kv-grid-gap, var(--sp-4)); }
3965
4188
  .kv-grid > .kv-cell + .kv-cell { border-left: var(--border-w) solid var(--hairline); padding-left: var(--sp-4); }
3966
4189
  .kv-cell { display: grid; gap: var(--kv-cell-gap, var(--sp-0)); align-content: start; min-width: 0; }
3967
- .kv-cell > .icon { margin-bottom: var(--sp-1); }
4190
+ .kv-cell > .icon { --icon-margin: 0 0 var(--sp-1); }
3968
4191
  .kv-cell__val { font: var(--kv-cell-val-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
3969
4192
  .kv-cell__key { font: var(--kv-cell-key-font, var(--type-caption)); color: var(--kv-cell-key-ink, var(--card-muted)); }
3970
4193
 
@@ -4009,7 +4232,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4009
4232
  }
4010
4233
  .bubble__meta { display: flex; align-items: center; gap: var(--bubble-meta-gap, var(--sp-1)); font: var(--bubble-meta-font, var(--type-caption)); color: var(--bubble-meta-ink, var(--card-muted)); }
4011
4234
  .bubble--right > .bubble__meta { justify-self: end; }
4012
- .bubble__meta > .icon { color: var(--bubble-meta-icon-ink, var(--accent-ink)); }
4235
+ .bubble__meta > .icon { --icon-ink: var(--bubble-meta-icon-ink, var(--accent-ink)); }
4013
4236
 
4014
4237
  /* ---- timeline: a hairline with date chips, entries to the right ----
4015
4238
  <ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
@@ -4080,12 +4303,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4080
4303
  .bullets { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--bullets-gap, var(--sp-3)); font: var(--bullets-font, var(--type-body)); }
4081
4304
  .bullets__item { display: flex; align-items: start; gap: var(--bullets-pic-gap, var(--sp-3)); min-width: 0; }
4082
4305
  .bullets__pic { flex: none; inline-size: var(--bullets-pic-size, 1lh); block-size: var(--bullets-pic-size, 1lh); min-height: 0; object-fit: contain; border-radius: var(--bullets-pic-radius, 0rem); display: grid; place-items: center; }
4083
- .bullets__pic.ph { border-radius: var(--bullets-pic-radius, 0rem); }
4084
4306
  .bullets__text { flex: 1; min-width: 0; }
4085
4307
 
4308
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
4309
+ .sheet-step__inner > .link { justify-self: center; } /* a link of its own row in a step (skip, "change number", "set PIN again"): centred under the step, as the skip (Lh 2026-09-25) */
4310
+
4086
4311
  /* ---- css/sticky.css — sticky note, its field, tone chooser, thread of posts, pile ---- */
4087
4312
  /* cardds — sticky notes: the paper note, the field written on it, the tone chooser, notes stuck on a page, a thread of posts, the answer note.
4088
- A note is its own paper: it brings its colour and its ink, whatever card or palette it lies on. */
4313
+ A note is pastel paper: its tone is one of the theme's nine pastels (--pastel-*, tokens.css), its ink the onlight set, whatever card it lies on. */
4089
4314
 
4090
4315
  /* ---- sticky: a paper note — a flat pastel SQUARE of one size, the bottom-right corner folded over ----
4091
4316
  ONE SIZE (Lh 2026-10-10): every note is --sticky-size square (168), whatever it says; words it cannot hold are written one size smaller
@@ -4125,28 +4350,28 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4125
4350
  .sticky::before, .sticky::after {
4126
4351
  content: "";
4127
4352
  position: absolute;
4128
- z-index: -1;
4353
+ z-index: var(--z-under);
4129
4354
  pointer-events: none;
4130
4355
  border-radius: var(--sticky-radius, 0rem);
4131
4356
  background-image: linear-gradient(-45deg, transparent calc(var(--_fold) * 0.7071), var(--_ink) 0); /* the folded corner, cut */
4132
4357
  }
4133
- .sticky::after { --_ink: var(--sticky-edge-bg, rgb(0 0 0 / 0.14)); inset: calc(-1 * var(--border-w)); }
4358
+ .sticky::after { --_ink: var(--sticky-edge-bg, color-mix(in srgb, var(--ink-onlight) 14%, transparent)); inset: calc(-1 * var(--border-w)); }
4134
4359
  .sticky::before {
4135
- --_ink: var(--sticky-shadow-bg, rgb(0 0 0 / 0.28));
4360
+ --_ink: var(--sticky-shadow-bg, color-mix(in srgb, var(--ink-onlight) 28%, transparent));
4136
4361
  inset: var(--sp-1) var(--sp-0) calc(-1 * var(--sp-0)) var(--sp-0);
4137
4362
  filter: blur(var(--sticky-shadow-blur, var(--sp-0)));
4138
4363
  }
4139
4364
  .sticky--flat::before, .sticky--flat::after { background: var(--_ink); }
4140
4365
  /* the tones — the names in src/sticky/tones.ts. A swatch of the chooser wears the same class. */
4141
- .sticky--cotton { background-color: var(--sticky-cotton-bg, #f3efe2); }
4142
- .sticky--blush { background-color: var(--sticky-blush-bg, #f6d5da); }
4143
- .sticky--tea-leaf { background-color: var(--sticky-tea-leaf-bg, #d5e8c5); }
4144
- .sticky--custard { background-color: var(--sticky-custard-bg, #f6e9a0); }
4145
- .sticky--salmon { background-color: var(--sticky-salmon-bg, #f3c7b3); }
4146
- .sticky--iceberg { background-color: var(--sticky-iceberg-bg, #cfe0ee); }
4147
- .sticky--cocoa { background-color: var(--sticky-cocoa-bg, #dccbbd); }
4148
- .sticky--apricot { background-color: var(--sticky-apricot-bg, #f6d49a); }
4149
- .sticky--water { background-color: var(--sticky-water-bg, #bcd6f3); }
4366
+ .sticky--cotton { background-color: var(--sticky-cotton-bg, var(--pastel-cotton)); }
4367
+ .sticky--blush { background-color: var(--sticky-blush-bg, var(--pastel-blush)); }
4368
+ .sticky--tea-leaf { background-color: var(--sticky-tea-leaf-bg, var(--pastel-tea-leaf)); }
4369
+ .sticky--custard { background-color: var(--sticky-custard-bg, var(--pastel-custard)); }
4370
+ .sticky--salmon { background-color: var(--sticky-salmon-bg, var(--pastel-salmon)); }
4371
+ .sticky--iceberg { background-color: var(--sticky-iceberg-bg, var(--pastel-iceberg)); }
4372
+ .sticky--cocoa { background-color: var(--sticky-cocoa-bg, var(--pastel-cocoa)); }
4373
+ .sticky--apricot { background-color: var(--sticky-apricot-bg, var(--pastel-apricot)); }
4374
+ .sticky--water { background-color: var(--sticky-water-bg, var(--pastel-water)); }
4150
4375
 
4151
4376
  .sticky__paper {
4152
4377
  position: relative;
@@ -4163,12 +4388,10 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4163
4388
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--_fold)), calc(100% - var(--_fold)) 100%, 0 100%);
4164
4389
  }
4165
4390
  .sticky__head { flex: none; display: flex; align-items: center; gap: var(--sticky-head-gap, var(--sp-2)); font: var(--sticky-head-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); }
4166
- .sticky__head > .ph { flex: 1 1 0; min-width: 0; } /* a picture in the head takes the note's width */
4167
4391
  .sticky__head:has(> .sticky-date) { align-items: start; }
4168
- .sticky__head:has(> .sticky-date) > .ph { flex: none; width: var(--sticky-pic-size, calc(var(--tap) * 1.5)); min-height: 0; aspect-ratio: 1; } /* beside a date the picture is a small square at the start */
4169
4392
  .sticky__body { flex: 1 1 auto; min-width: 0; min-height: 0; overflow: clip; /* words past the note's room are cut, the foot keeps its place */ font: var(--sticky-font, var(--type-title)); overflow-wrap: anywhere; }
4170
4393
  .sticky--small .sticky__body { font: var(--sticky-small-font, var(--type-label)); } /* the note measured its words and they did not fit at its size (src/sticky/fit.ts) */
4171
- .sticky__paper > :not(.sticky__body) > .chip { border-color: transparent; background-color: var(--sticky-chip-bg, rgb(255 255 255 / 0.85)); --dark: inherit; color: var(--sticky-ink, var(--card-ink)); } /* a chip on a note is a label ON the paper (whose note it is): a pale ground nearly opaque (it may lie over words), no edge, the note's ink */
4394
+ .sticky__paper > :not(.sticky__body) > .chip { --chip-border: transparent; --chip-bg: var(--sticky-chip-bg, color-mix(in srgb, var(--stamp-paper) 85%, transparent)); --dark: inherit; --chip-ink: var(--sticky-ink, var(--card-ink)); } /* a chip on a note is a label ON the paper (whose note it is): a pale ground nearly opaque (it may lie over words), no edge, the note's ink */
4172
4395
  .sticky__foot { flex: none; font: var(--sticky-foot-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); padding-inline-end: var(--_fold); } /* the line stops before the fold */
4173
4396
  /* the flap: a square in the corner — the paper's cut takes its outer half, what is left is the triangle lying on the note */
4174
4397
  .sticky__fold {
@@ -4179,7 +4402,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4179
4402
  height: var(--_fold);
4180
4403
  border-top-left-radius: var(--sp-1);
4181
4404
  background-color: var(--sticky-fold-bg, color-mix(in srgb, currentColor 14%, transparent)); /* a darker shade of whatever the paper is */
4182
- box-shadow: calc(-1 * var(--sp-0)) calc(-1 * var(--sp-0)) var(--sp-1) rgb(0 0 0 / 0.08);
4405
+ box-shadow: var(--shadow-fold);
4183
4406
  }
4184
4407
  /* things stuck ON the note (a stamp, placed by hand): a layer over the whole paper. A tap between them still reaches the words under it.
4185
4408
  A stamp on a note is the stamp's small size whatever the note's (48 × 60, --stamp-sm-size: stamp.css; `StampOn main` is the main size), and may lie over the words. */
@@ -4191,8 +4414,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4191
4414
  /* a note that stands alone in a page's column or in a centre stands in its middle */
4192
4415
  .page > .sticky, .screen__centre > .sticky { margin-inline: auto; }
4193
4416
  .sticky--tilt-left, .sticky--tilt-right { margin-block: var(--sp-4); } /* the turned corners and the shadow need room: a box that scrolls (a centre) would cut them */
4194
- .sticky--tilt-left { rotate: calc(-1 * var(--sticky-tilt, 5deg)); }
4195
- .sticky--tilt-right { rotate: var(--sticky-tilt, 5deg); }
4417
+ .sticky--tilt-left { rotate: calc(-1 * var(--sticky-tilt, calc(var(--tilt-base) * 5))); }
4418
+ .sticky--tilt-right { rotate: var(--sticky-tilt, calc(var(--tilt-base) * 5)); }
4196
4419
 
4197
4420
  /* ---- sticky date: the day, big, the month under it — at the end of the note's head (its top-right corner) ----
4198
4421
  <div class="sticky__head">…<time class="sticky-date" datetime="2026-02-14"><span class="sticky-date__day">14</span><span class="sticky-date__month">ก.พ.</span></time></div> */
@@ -4234,7 +4457,6 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4234
4457
  /* a name label in the foot (a Chip, `sm`) stands at the START, what follows it (the time) at the end, on one line: a long name is cut
4235
4458
  with an ellipsis past its share of the row and never wraps; the time is never cut (a long one may run a little onto the fold) */
4236
4459
  .sticky__paper > .sticky__foot:has(> .chip) { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); white-space: nowrap; text-align: end; }
4237
- .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; }
4238
4460
 
4239
4461
  /* ---- sticky field: the words written ON a note — a bare textarea in the note's type and ink, filling it ----
4240
4462
  <div class="sticky__body"><textarea class="sticky-field" aria-label="Note" placeholder="Add text to this note"></textarea></div> */
@@ -4329,12 +4551,12 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4329
4551
  .sticky-thread--lapped > .sticky-post > .sticky-post__who { display: none; }
4330
4552
  .sticky-thread--lapped > .sticky-post > .sticky-post__notes { grid-area: 1 / 1; }
4331
4553
  .sticky-thread--lapped > .sticky-post > .sticky-post__head { grid-area: 2 / 1; }
4332
- .sticky-thread--lapped > .sticky-post > .sticky-post__head > .sticky-post__name { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } /* as .t-sr: heard, not seen */
4554
+ .sticky-thread--lapped > .sticky-post > .sticky-post__head > .sticky-post__name { position: absolute; width: var(--px); height: var(--px); margin: calc(-1 * var(--px)); overflow: hidden; clip-path: inset(50%); white-space: nowrap; } /* as .t-sr: heard, not seen */
4333
4555
  .sticky-thread--lapped > .sticky-post > .sticky-post__head > .sticky-post__time { margin-inline: 0; }
4334
4556
  .sticky-thread--lapped > .sticky-post > .sticky-post__head:not(:has(> .sticky-post__time)) { display: none; }
4335
4557
  /* each note leans a little, one way then the other; a post counts among posts only, so a heading before the first changes no note's lean */
4336
- .sticky-thread--lapped > .sticky-post > .sticky-post__notes > .sticky { rotate: calc(-1 * var(--sticky-thread-tilt, 2deg)); }
4337
- .sticky-thread--lapped > .sticky-post:nth-child(even of .sticky-post) > .sticky-post__notes > .sticky { rotate: var(--sticky-thread-tilt, 2deg); }
4558
+ .sticky-thread--lapped > .sticky-post > .sticky-post__notes > .sticky { rotate: calc(-1 * var(--sticky-thread-tilt, calc(var(--tilt-base) * 2))); }
4559
+ .sticky-thread--lapped > .sticky-post:nth-child(even of .sticky-post) > .sticky-post__notes > .sticky { rotate: var(--sticky-thread-tilt, calc(var(--tilt-base) * 2)); }
4338
4560
  /* under an answer note the thread ends with the room that note stands in: the last turn scrolls up clear of it */
4339
4561
  .screen:has(> .sticky-answer) .sticky-thread--lapped { padding-block-end: calc(var(--sticky-size, calc(var(--sp-12) * 3.5)) * var(--sticky-answer-show, 0.75) + var(--sp-6)); }
4340
4562
 
@@ -4367,11 +4589,12 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4367
4589
  .sticky-board .sticky__paper > .sticky__foot { position: absolute; inset: auto var(--sticky-board-foot-inset, var(--sp-3)) var(--sticky-board-foot-inset, var(--sp-3)); text-align: end; }
4368
4590
  /* how far each is turned: FOUR unlike angles in turn (Lh 2026-10-11). At most three notes meet, so four keep the lean from ever looking the
4369
4591
  same: two in a row, a note lies over the one two before it and those lean opposite ways; three in a row, over the one three before it,
4370
- never the same angle. (A run of seven left one column at 0.75° and −0.5°: its edges read as one straight line and the lean was lost.) */
4371
- .sticky-board > :nth-child(4n + 1) { rotate: var(--sticky-board-turn-1, -3deg); }
4372
- .sticky-board > :nth-child(4n + 2) { rotate: var(--sticky-board-turn-2, 1.5deg); }
4373
- .sticky-board > :nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
4374
- .sticky-board > :nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
4592
+ never the same angle. (A run of seven left one column at 0.75° and −0.5°: its edges read as one straight line and the lean was lost.)
4593
+ A pile (below) turns its notes by the same four, by their place among its children. */
4594
+ .sticky-board > :nth-child(4n + 1), .sticky-pile__notes > .sticky:nth-child(4n + 1) { rotate: var(--sticky-board-turn-1, calc(var(--tilt-base) * -3)); }
4595
+ .sticky-board > :nth-child(4n + 2), .sticky-pile__notes > .sticky:nth-child(4n + 2) { rotate: var(--sticky-board-turn-2, calc(var(--tilt-base) * 1.5)); }
4596
+ .sticky-board > :nth-child(4n + 3), .sticky-pile__notes > .sticky:nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, calc(var(--tilt-base) * 2.75)); }
4597
+ .sticky-board > :nth-child(4n), .sticky-pile__notes > .sticky:nth-child(4n) { rotate: var(--sticky-board-turn-4, calc(var(--tilt-base) * -1.75)); }
4375
4598
 
4376
4599
  /* ---- sticky pile: one to six notes lying one over another — the newest on top (Lh 2026-10-11) ----
4377
4600
  <div class="sticky-pile sticky-pile--tap" style="--_n: 3">
@@ -4402,17 +4625,13 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4402
4625
  grid-template: var(--_size) / var(--_size);
4403
4626
  padding: var(--_pad) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-x)) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-y)) var(--_pad);
4404
4627
  }
4405
- .sticky-pile__notes > .sticky { --_depth: 0; grid-area: 1 / 1; z-index: calc(7 - var(--_depth)); translate: calc(var(--_depth) * var(--_step-x)) calc(var(--_depth) * var(--_step-y)); }
4628
+ .sticky-pile__notes > .sticky { --_depth: 0; grid-area: 1 / 1; z-index: calc(var(--z-pile) - var(--_depth)); translate: calc(var(--_depth) * var(--_step-x)) calc(var(--_depth) * var(--_step-y)); }
4406
4629
  .sticky-pile__notes > .sticky:nth-child(2) { --_depth: 1; }
4407
4630
  .sticky-pile__notes > .sticky:nth-child(3) { --_depth: 2; }
4408
4631
  .sticky-pile__notes > .sticky:nth-child(4) { --_depth: 3; }
4409
4632
  .sticky-pile__notes > .sticky:nth-child(5) { --_depth: 4; }
4410
4633
  .sticky-pile__notes > .sticky:nth-child(6) { --_depth: 5; }
4411
4634
  .sticky-pile__notes > .sticky:nth-child(n + 2) .sticky__foot { display: none; }
4412
- .sticky-pile__notes > .sticky:nth-child(4n + 1) { rotate: var(--sticky-board-turn-1, -3deg); }
4413
- .sticky-pile__notes > .sticky:nth-child(4n + 2) { rotate: var(--sticky-board-turn-2, 1.5deg); }
4414
- .sticky-pile__notes > .sticky:nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
4415
- .sticky-pile__notes > .sticky:nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
4416
4635
  .sticky-pile__press { position: absolute; inset: 0; margin: 0; padding: 0; border: 0; appearance: none; background: none; -webkit-tap-highlight-color: transparent; }
4417
4636
  .sticky-pile--tap .sticky-pile__notes > .sticky { pointer-events: none; }
4418
4637
 
@@ -4438,16 +4657,16 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4438
4657
  pointer-events: none;
4439
4658
  }
4440
4659
  .screen--fill > .sticky-answer { position: absolute; }
4441
- .sticky-answer--writing { z-index: calc(var(--z-top) + 1); }
4442
- .sticky-answer__scrim { position: absolute; inset: 0; background: var(--sticky-answer-scrim-bg, color-mix(in srgb, var(--surface) 82%, transparent)); opacity: 0; visibility: hidden; transition: opacity var(--motion-sheet) ease, visibility 0s var(--motion-sheet); }
4660
+ .sticky-answer--writing { z-index: calc(var(--z-top) + var(--z-over)); }
4661
+ .sticky-answer__scrim { position: absolute; inset: 0; background: var(--sticky-answer-scrim-bg, color-mix(in srgb, var(--surface) 82%, transparent)); opacity: 0; visibility: hidden; transition: opacity var(--motion-sheet) var(--ease), visibility 0s var(--motion-sheet); }
4443
4662
  .sticky-answer--writing > .sticky-answer__scrim { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: 0s; }
4444
4663
  .sticky-answer__note {
4445
4664
  position: absolute;
4446
- top: calc(100% - var(--_size) * var(--sticky-answer-show, 0.75) - max(var(--safe-bottom), var(--kb, 0px)));
4665
+ top: calc(100% - var(--_size) * var(--sticky-answer-show, 0.75) - max(var(--safe-bottom), var(--kb, 0rem)));
4447
4666
  right: var(--sticky-answer-end, var(--screen-pad));
4448
- rotate: var(--sticky-answer-tilt, -3deg);
4667
+ rotate: var(--sticky-answer-tilt, calc(var(--tilt-base) * -3));
4449
4668
  pointer-events: auto;
4450
- transition: top var(--motion-sheet) ease, right var(--motion-sheet) ease, rotate var(--motion-sheet) ease;
4669
+ transition: top var(--motion-sheet) var(--ease), right var(--motion-sheet) var(--ease), rotate var(--motion-sheet) var(--ease);
4451
4670
  }
4452
4671
  .sticky-answer--writing > .sticky-answer__note { top: calc(var(--safe-top) + var(--header-h) + var(--sticky-answer-top, var(--sp-5))); right: calc(50% - var(--_size) / 2); rotate: 0deg; }
4453
4672
  .sticky-answer__under {
@@ -4455,7 +4674,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4455
4674
  top: calc(var(--safe-top) + var(--header-h) + var(--sticky-answer-top, var(--sp-5)) + var(--_size) + var(--sticky-answer-gap, var(--sp-4)));
4456
4675
  display: grid; gap: var(--sticky-answer-gap, var(--sp-4));
4457
4676
  opacity: 0; visibility: hidden;
4458
- transition: opacity var(--motion-sheet-rest) ease, visibility 0s var(--motion-sheet-rest);
4677
+ transition: opacity var(--motion-sheet-rest) var(--ease), visibility 0s var(--motion-sheet-rest);
4459
4678
  }
4460
4679
  .sticky-answer--writing > .sticky-answer__under { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: var(--motion-sheet-rest), 0s; }
4461
4680
  .sticky-answer__tools { display: flex; align-items: center; justify-content: space-between; gap: var(--sticky-answer-tools-gap, var(--sp-3)); }
@@ -4479,7 +4698,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4479
4698
  border-block-end: var(--rule-w) dotted currentColor;
4480
4699
  font: inherit;
4481
4700
  }
4482
- .sticky-heading__title { min-width: 0; max-width: 100%; font: var(--sticky-heading-title-font, var(--type-label)); line-height: 1.35; /* Thai marks stack taller than the label's 1.2, and overflow clips them */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
4701
+ .sticky-heading__title { min-width: 0; max-width: 100%; font: var(--sticky-heading-title-font, var(--type-label)); line-height: var(--lh-caption); /* Thai marks stack taller than the label's 1.2, and overflow clips them */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
4483
4702
  .sticky-heading__caption { font: var(--sticky-heading-caption-font, var(--type-caption)); color: var(--sticky-heading-caption-ink, var(--card-muted)); white-space: nowrap; } /* the muted words of what it stands on: a note, or the screen */
4484
4703
  /* on a note: across the top of its paper, and the note's words start under it */
4485
4704
  .sticky__stuck > .sticky-heading { position: absolute; inset: var(--sp-2) var(--sp-4) auto; }
@@ -4488,9 +4707,17 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4488
4707
  .sticky-board .sticky:has(> .sticky__stuck > .sticky-heading) .sticky__body { flex: none; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
4489
4708
  /* in a thread: across its top on the screen's ground, staying there while the notes run under it; the room before the first note is the turned corner's, as the thread's own top */
4490
4709
  /* the ground is drawn a little wider than the heading each way: a turned note's corner and its shadow stand outside the thread's width */
4491
- .sticky-thread > .sticky-heading { position: sticky; inset-block-start: 0; z-index: 2; padding-block-start: var(--sp-2); background: var(--surface); color: var(--on-surface); box-shadow: calc(-1 * var(--sp-3)) 0 0 var(--surface), var(--sp-3) 0 0 var(--surface); }
4710
+ .sticky-thread > .sticky-heading { position: sticky; inset-block-start: 0; z-index: var(--z-over-2); padding-block-start: var(--sp-2); background: var(--surface); color: var(--on-surface); box-shadow: calc(-1 * var(--sp-3)) 0 0 var(--surface), var(--sp-3) 0 0 var(--surface); }
4492
4711
  .sticky-thread--lapped > .sticky-heading { margin-block-end: var(--sp-4); }
4493
4712
 
4713
+ /* ==== the pile pressed whole, as PRESS in actions.css answers every other control (#62: a block is written in its own file) ==== */
4714
+ /* 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
4715
+ notes do not show through one another); the ring is the button's own, round the whole pile */
4716
+ .sticky-pile__press { cursor: pointer; touch-action: manipulation; }
4717
+ .sticky-pile__notes:has(> .sticky-pile__press) { transition: opacity var(--motion-press) var(--ease-out); }
4718
+ .sticky-pile__notes:has(> .sticky-pile__press:active) { opacity: var(--press-dim); }
4719
+ .sticky-pile__press:focus-visible { outline: var(--focus); outline-offset: 0; }
4720
+
4494
4721
  /* ---- css/stamp.css — stamp, empty place, album page, a stamp stuck on a surface ---- */
4495
4722
  /* cardds — stamps: the paper stamp, an empty place for one, the album page, a stamp stuck on a surface.
4496
4723
  A stamp is its own paper, like a sticky note: it brings its ground and its ink, whatever card or palette it lies on.
@@ -4527,42 +4754,44 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4527
4754
  position: relative;
4528
4755
  display: block;
4529
4756
  container-type: inline-size;
4530
- color: var(--stamp-ink, #141414);
4531
- filter: drop-shadow(var(--stamp-drop, 0 var(--sp-1) var(--sp-2) rgb(0 0 0 / 0.16))); /* drop-shadow follows the teeth; a box-shadow would draw the rectangle */
4757
+ color: var(--stamp-ink, var(--ink-onlight));
4758
+ filter: drop-shadow(var(--stamp-drop, var(--shadow-paper))); /* drop-shadow follows the teeth; a box-shadow would draw the rectangle */
4759
+ rotate: var(--stamp-turn, none);
4760
+ animation: var(--stamp-arrive, none);
4532
4761
  }
4533
4762
  .stamp__paper {
4534
4763
  --_pitch: calc(100cqw / 12); /* one tooth: 12 across, 15 down — the holes land whole on every edge. Fewer and larger than a real stamp's: a small stamp's teeth must still read (Lh 2026-10-07; it was 20 × 25) */
4535
4764
  --_hole: 2.6cqw;
4536
4765
  position: absolute;
4537
4766
  inset: 0;
4538
- background-color: var(--stamp-paper-bg, #fffdf8);
4767
+ background-color: var(--stamp-paper-bg, var(--stamp-paper));
4539
4768
  -webkit-mask: linear-gradient(#000 0 0) center / calc(100% - var(--_pitch)) calc(100% - var(--_pitch)) no-repeat,
4540
4769
  radial-gradient(circle, transparent var(--_hole), #000 calc(var(--_hole) + 0.4cqw)) calc(var(--_pitch) / -2) calc(var(--_pitch) / -2) / var(--_pitch) var(--_pitch);
4541
4770
  mask: linear-gradient(#000 0 0) center / calc(100% - var(--_pitch)) calc(100% - var(--_pitch)) no-repeat,
4542
4771
  radial-gradient(circle, transparent var(--_hole), #000 calc(var(--_hole) + 0.4cqw)) calc(var(--_pitch) / -2) calc(var(--_pitch) / -2) / var(--_pitch) var(--_pitch);
4543
4772
  }
4544
- .stamp__pic { position: absolute; inset: var(--stamp-frame, 8cqw); overflow: hidden; } /* the white frame round the picture; the ground is the tone class's */
4773
+ .stamp__pic { position: absolute; inset: var(--stamp-frame, 8cqw); overflow: hidden; } /* the paper's frame round the picture; the ground is the tone class's */
4545
4774
 
4546
4775
  /* the pictures, placed by COUNT: where, how wide, how far turned — the layout is the stamp's look, never the app's.
4547
4776
  A share of the picture area; the foot stays clear for the words. */
4548
4777
  .stamp__art { position: absolute; left: var(--_x); top: var(--_y); width: var(--_w); aspect-ratio: 1; translate: -50% -50%; rotate: var(--_r); }
4549
4778
  .stamp__art > * { display: block; width: 100%; height: 100%; }
4550
4779
  .stamp--1 .stamp__art { --_x: 50%; --_y: 40%; --_w: 72%; --_r: 0deg; }
4551
- .stamp--2 .stamp__art:nth-child(1) { --_x: 37%; --_y: 27%; --_w: 58%; --_r: -6deg; }
4552
- .stamp--2 .stamp__art:nth-child(2) { --_x: 63%; --_y: 56%; --_w: 54%; --_r: 8deg; }
4553
- .stamp--3 .stamp__art:nth-child(1) { --_x: 30%; --_y: 21%; --_w: 50%; --_r: -8deg; }
4554
- .stamp--3 .stamp__art:nth-child(2) { --_x: 71%; --_y: 31%; --_w: 46%; --_r: 7deg; }
4555
- .stamp--3 .stamp__art:nth-child(3) { --_x: 47%; --_y: 58%; --_w: 54%; --_r: -2deg; }
4556
- .stamp--4 .stamp__art:nth-child(1) { --_x: 29%; --_y: 19%; --_w: 44%; --_r: -8deg; }
4557
- .stamp--4 .stamp__art:nth-child(2) { --_x: 71%; --_y: 24%; --_w: 42%; --_r: 7deg; }
4558
- .stamp--4 .stamp__art:nth-child(3) { --_x: 31%; --_y: 57%; --_w: 42%; --_r: 5deg; }
4559
- .stamp--4 .stamp__art:nth-child(4) { --_x: 70%; --_y: 60%; --_w: 44%; --_r: -6deg; }
4780
+ .stamp--2 .stamp__art:nth-child(1) { --_x: 37%; --_y: 27%; --_w: 58%; --_r: calc(var(--tilt-base) * -6); }
4781
+ .stamp--2 .stamp__art:nth-child(2) { --_x: 63%; --_y: 56%; --_w: 54%; --_r: calc(var(--tilt-base) * 8); }
4782
+ .stamp--3 .stamp__art:nth-child(1) { --_x: 30%; --_y: 21%; --_w: 50%; --_r: calc(var(--tilt-base) * -8); }
4783
+ .stamp--3 .stamp__art:nth-child(2) { --_x: 71%; --_y: 31%; --_w: 46%; --_r: calc(var(--tilt-base) * 7); }
4784
+ .stamp--3 .stamp__art:nth-child(3) { --_x: 47%; --_y: 58%; --_w: 54%; --_r: calc(var(--tilt-base) * -2); }
4785
+ .stamp--4 .stamp__art:nth-child(1) { --_x: 29%; --_y: 19%; --_w: 44%; --_r: calc(var(--tilt-base) * -8); }
4786
+ .stamp--4 .stamp__art:nth-child(2) { --_x: 71%; --_y: 24%; --_w: 42%; --_r: calc(var(--tilt-base) * 7); }
4787
+ .stamp--4 .stamp__art:nth-child(3) { --_x: 31%; --_y: 57%; --_w: 42%; --_r: calc(var(--tilt-base) * 5); }
4788
+ .stamp--4 .stamp__art:nth-child(4) { --_x: 70%; --_y: 60%; --_w: 44%; --_r: calc(var(--tilt-base) * -6); }
4560
4789
 
4561
4790
  /* the words on the picture's foot: a small line, then the big name — one line each (clipped sideways only: Thai tone marks stand above the line box) */
4562
4791
  .stamp__cap { position: absolute; inset-inline: 5cqw; bottom: 3.5cqw; display: grid; gap: 0.4cqw; }
4563
4792
  .stamp__words, .stamp__name { display: block; min-width: 0; white-space: nowrap; overflow-x: clip; text-overflow: ellipsis; }
4564
- .stamp__words { --_words: 4.8cqw; font: 500 var(--_words) / 1.3 var(--font-ui); font-size: var(--stamp-words-font-size, var(--_words)); }
4565
- .stamp__name { --_name: 9.5cqw; font: 900 var(--_name) / 1.15 var(--font-display); font-size: var(--stamp-name-font-size, var(--_name)); }
4793
+ .stamp__words { --_words: 4.8cqw; font: var(--fw-medium) var(--_words) / var(--lh-title) var(--font-ui); font-size: var(--stamp-words-font-size, var(--_words)); }
4794
+ .stamp__name { --_name: 9.5cqw; font: var(--fw-black) var(--_name) / var(--lh-display) var(--font-display); font-size: var(--stamp-name-font-size, var(--_name)); }
4566
4795
  /* at the stamp's own size and under (and stuck on a note): the small line goes, the name grows */
4567
4796
  /* --bare: the pictures alone, no words — a stamp stuck on something (a note, a postcard). The words belong to the album.
4568
4797
  The pictures take the foot's room: each sits a little lower, so the group stands in the middle of the frame. */
@@ -4576,14 +4805,13 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4576
4805
 
4577
4806
  /* --tilt: turned a little on its middle — the one stamp a start screen hands over */
4578
4807
  .stamp--tilt-left, .stamp--tilt-right { margin-block: var(--sp-4); } /* the turned corners and the shadow need room: a centre scrolls and would cut them */
4579
- .stamp--tilt-left { rotate: calc(-1 * var(--stamp-tilt, 5deg)); }
4580
- .stamp--tilt-right { rotate: var(--stamp-tilt, 5deg); }
4808
+ .stamp--tilt-left { rotate: calc(-1 * var(--stamp-tilt, calc(var(--tilt-base) * 5))); }
4809
+ .stamp--tilt-right { rotate: var(--stamp-tilt, calc(var(--tilt-base) * 5)); }
4581
4810
 
4582
4811
  /* --tap: the whole stamp is the button (`onClick`); the press and the ring are the PRESS list's (actions.css) */
4583
4812
  .stamp--tap { appearance: none; border: 0; padding: 0; background: none; font: inherit; text-align: start; -webkit-tap-highlight-color: transparent; }
4584
4813
 
4585
4814
  /* a stamp — or the empty place for one — handed over in a Centre: its greeting centred under it. The stamp is its own size here as everywhere */
4586
- .screen__centre:has(> :is(.stamp, .stamp-place)) { justify-items: center; text-align: center; }
4587
4815
 
4588
4816
  /* ---- stamp place: an empty place for a stamp — an album's gap, "make yours" ----
4589
4817
  <span class="stamp-place"><svg class="stamp-place__edge" aria-hidden="true"><rect/></svg>สร้าง</span> · <button class="stamp-place">…</button> · <span class="stamp-place stamp-place--faint">…</span>
@@ -4677,7 +4905,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4677
4905
  background-color: var(--stamp-cell-on-bg, var(--accent));
4678
4906
  color: var(--stamp-cell-on-ink, var(--on-accent));
4679
4907
  }
4680
- .stamp-cell__on > .icon { width: 70%; height: 70%; }
4908
+ .stamp-cell__on > .icon { --icon-size: 70%; }
4681
4909
  /* --bare: no ground, no padding, no round — the stamps lie on what holds the book, three across as on the ground. The name is the Stamp's own; free places still draw.
4682
4910
  <div class="stamp-book stamp-book--bare"> */
4683
4911
  .stamp-book--bare { padding: 0; border-radius: 0; background: none; }
@@ -4721,24 +4949,24 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4721
4949
  After the rule above, at the same weight: `main` is the one place a stuck stamp reads --stamp-size. */
4722
4950
  .stamp-on--main > .stamp { width: var(--stamp-size, calc(var(--tap) * 2)); }
4723
4951
  .stamp-on--move { will-change: transform; } /* its own layer: Safari left the stamp's shadow behind as a trail along the drag when it was painted with the note (seen on the simulator, 2026-10-07) */
4724
- .stamp-on--move { cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; transition: scale var(--motion-press) ease-out; }
4952
+ .stamp-on--move { cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; transition: scale var(--motion-press) var(--ease-out); }
4725
4953
  .stamp-on--move:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
4726
- .stamp-on.is-held { cursor: grabbing; z-index: 1; scale: 1.06; } /* lifted off the paper while the finger holds it */
4954
+ .stamp-on.is-held { cursor: grabbing; z-index: var(--z-over); scale: var(--scale-lift); } /* lifted off the paper while the finger holds it */
4727
4955
  @media (prefers-reduced-motion: reduce) { .stamp-on.is-held { scale: 1; } }
4728
4956
 
4729
4957
  /* ---- the cross that takes a stamp off (`StampOn onRemove`): a small dark disc with an x on the stamp's top right corner ----
4730
4958
  <div class="stamp-on stamp-on--move">…<button class="stamp-on__remove" aria-label="Remove"><span class="stamp-on__remove-disc"><svg class="icon">x</svg></span></button></div>
4731
4959
  The button is the touch target — at least --tap-floor (44) square, invisible, centred on the corner — and the disc inside it is what is seen
4732
4960
  (24). It is NOT turned with the stamp: a hand turns a stamp, a button stays square to the screen, so the cross counter-turns by the stamp's own turn. Like the
4733
- stamp it brings its ground and its ink (a dark disc, a pale x and a pale ring: seen on any paper and over any picture). */
4961
+ stamp it takes the theme's two inks, the onlight ink as its disc and the ondark ink as its x and ring: seen on any paper and over any picture. */
4734
4962
  .stamp-on__remove-disc {
4735
4963
  display: grid;
4736
4964
  place-items: center;
4737
4965
  width: var(--stamp-on-remove-size, var(--sp-6));
4738
4966
  aspect-ratio: 1;
4739
4967
  border-radius: 50%;
4740
- background-color: var(--stamp-on-remove-bg, #141414);
4741
- box-shadow: 0 0 0 var(--sp-0) currentColor, 0 var(--sp-0) var(--sp-1) rgb(0 0 0 / 0.35);
4968
+ background-color: var(--stamp-on-remove-bg, var(--ink-onlight));
4969
+ box-shadow: 0 0 0 var(--sp-0) currentColor, var(--shadow-lift);
4742
4970
  }
4743
4971
  .stamp-on__remove {
4744
4972
  --_hit: max(var(--tap-floor), var(--stamp-on-remove-size, var(--sp-6)));
@@ -4755,20 +4983,31 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4755
4983
  border: 0;
4756
4984
  border-radius: 50%;
4757
4985
  background: none;
4758
- color: var(--stamp-on-remove-ink, #ffffff);
4986
+ color: var(--stamp-on-remove-ink, var(--ink-ondark));
4759
4987
  font: inherit;
4760
4988
  -webkit-tap-highlight-color: transparent;
4761
4989
  }
4762
- .stamp-on__remove-disc > .icon { width: 60%; height: 60%; }
4763
- @media (prefers-reduced-motion: no-preference) { .stamp-on__remove { animation: stamp-on-remove-in var(--motion-press) ease-out; } }
4764
- @keyframes stamp-on-remove-in { from { opacity: 0; scale: 0.6; } }
4990
+ .stamp-on__remove-disc > .icon { --icon-size: 60%; }
4991
+ @media (prefers-reduced-motion: no-preference) { .stamp-on__remove { animation: stamp-on-remove-in var(--motion-press) var(--ease-out); } }
4992
+ @keyframes stamp-on-remove-in { from { opacity: 0; scale: var(--scale-arrive); } }
4765
4993
 
4766
4994
  /* --arrive: the stamp sticks ITSELF (StampOn `arrive`; a Stamp's `arrive` too, where it already stands) — struck onto its place (`strike`: tokens.css).
4767
4995
  The component reads the animation's name to tell the app it has landed. `scale` and `opacity` leave a --tilt's `rotate` alone. Knob: --stamp-on-arrive-time */
4768
4996
  @media (prefers-reduced-motion: no-preference) {
4769
- .stamp-on--arrive, .stamp--arrive { animation: strike var(--stamp-on-arrive-time, var(--motion-sheet)) ease-in both; }
4997
+ .stamp-on--arrive, .stamp--arrive { animation: strike var(--stamp-on-arrive-time, var(--motion-sheet)) var(--ease-in) both; }
4770
4998
  }
4771
4999
 
5000
+ /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
5001
+ .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
5002
+ /* ---- where it sits in what people.css draws (docs/reference.md rule 15) ---- */
5003
+ /* a stamp in the stack is the stamp's small size (--stamp-sm-size: one rule in stamp.css, 48 × 60), lapped like the heads. Measured 2026-10-11 in a Row's
5004
+ lead with a title, a meta line and an end button: at 375 the meta line breaks in two, accepted by Lh (a lap of 16 would hold it; the lap stays 8)
5005
+ <span class="avatar-stack"><span class="stamp stamp--bare">…</span><span class="stamp stamp--bare">…</span><span class="avatar-stack__more">+9</span></span> */
5006
+ .avatar-stack > .stamp { flex: none; margin-inline: 0; }
5007
+ .avatar-stack > .stamp + .stamp { margin-left: calc(-1 * var(--sp-2)); }
5008
+ /* ---- where it sits in what post.css draws (docs/reference.md rule 15) ---- */
5009
+ .post-corner__stamp > :is(.stamp, .stamp-place) { grid-area: 1 / 1; width: 100%; max-width: none; } /* the corner is the postcard's own size (72 × 90), outside the stamp's two sizes, by the owner's decision (2026-10-11); its box sizes the stamp, which fills it */
5010
+
4772
5011
  /* ---- css/post.css — the post: postmark, a postcard read (letter), written (desk), kept (box) ---- */
4773
5012
  /* cardds — the post: a postcard read (post-letter), written (post-desk) and kept (post-box), the postmark, the card stock.
4774
5013
  Not the `Postcard` of media.css (a member's cover card that flips) — nothing here flips, and no class is shared with it.
@@ -4791,7 +5030,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4791
5030
  .postmark {
4792
5031
  --_base: calc(var(--tap) * 1.75);
4793
5032
  position: relative;
4794
- z-index: 1;
5033
+ z-index: var(--z-over);
4795
5034
  flex: none;
4796
5035
  display: grid;
4797
5036
  place-content: center;
@@ -4806,7 +5045,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4806
5045
  color: var(--postmark-ink, color-mix(in srgb, currentColor 72%, transparent));
4807
5046
  font: var(--postmark-font, var(--type-label));
4808
5047
  text-align: center;
4809
- rotate: var(--postmark-tilt, -10deg);
5048
+ rotate: var(--postmark-tilt, calc(var(--tilt-base) * -10));
4810
5049
  }
4811
5050
  .postmark::after { /* the cancel lines that run on over the stamp */
4812
5051
  content: "";
@@ -4831,9 +5070,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4831
5070
  display: grid;
4832
5071
  width: var(--post-corner-stamp-w, var(--_w));
4833
5072
  aspect-ratio: 4 / 5;
4834
- rotate: var(--post-corner-stamp-tilt, 2deg);
5073
+ rotate: var(--post-corner-stamp-tilt, calc(var(--tilt-base) * 2));
4835
5074
  }
4836
- .post-corner__stamp > :is(.stamp, .stamp-place) { grid-area: 1 / 1; width: 100%; max-width: none; } /* the corner is the postcard's own size (72 × 90), outside the stamp's two sizes, by the owner's decision (2026-10-11); its box sizes the stamp, which fills it */
4837
5075
 
4838
5076
  /* ============================================================
4839
5077
  post-letter — READING one that arrived. The written side stood upright: the address half over the message half.
@@ -4874,15 +5112,15 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4874
5112
  min-height: 0;
4875
5113
  overflow: clip;
4876
5114
  visibility: hidden;
4877
- rotate: calc(-1 * var(--post-letter-tilt, 2deg));
5115
+ rotate: calc(-1 * var(--post-letter-tilt, calc(var(--tilt-base) * 2)));
4878
5116
  }
4879
- .post-letter__sheet--back { rotate: var(--post-letter-tilt, 2deg); } /* turned over, the lean is the other way */
5117
+ .post-letter__sheet--back { rotate: var(--post-letter-tilt, calc(var(--tilt-base) * 2)); } /* turned over, the lean is the other way */
4880
5118
  .post-letter--back > .post-letter__sheet--back,
4881
5119
  .post-letter--picture > .post-letter__sheet--picture { visibility: visible; }
4882
5120
  .post-letter:not(:has(> .post-letter__journey)) { --_room: 0rem; } /* no journey line: its room goes back to the sheet */
4883
5121
  .post-letter__journey { grid-area: 2 / 1; margin: var(--sp-3) 0 0; text-align: center; font: var(--type-caption); color: var(--post-letter-journey-ink, var(--on-surface-muted)); }
4884
5122
 
4885
- /* the picture side: the print with its white margin, the place printed under it */
5123
+ /* the picture side: the print with its margin of paper, the place printed under it */
4886
5124
  .post-letter__sheet--picture {
4887
5125
  display: grid;
4888
5126
  grid-template-rows: minmax(0, 1fr) auto;
@@ -4901,7 +5139,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4901
5139
  .post-letter__sheet--back { display: flex; flex-direction: column; gap: var(--post-letter-gap, var(--sp-4)); padding: var(--post-letter-pad, var(--sp-5)); }
4902
5140
  .post-letter__head { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: var(--sp-2); padding-block-end: var(--sp-4); border-block-end: var(--rule-w) solid var(--_rule); }
4903
5141
  .post-letter__to { display: grid; gap: var(--sp-2); min-width: 0; align-self: stretch; align-content: space-between; }
4904
- .post-letter__kicker { font: var(--type-caption); color: var(--_muted); letter-spacing: 0.08em; }
5142
+ .post-letter__kicker { font: var(--type-caption); color: var(--_muted); letter-spacing: var(--track-caps); }
4905
5143
  .post-letter__lines { display: grid; gap: var(--sp-1); }
4906
5144
  .post-letter__line { display: block; padding-block: var(--sp-1); border-block-end: var(--border-w) solid var(--_rule); font: var(--type-caption); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
4907
5145
  .post-letter__line--name { font: var(--type-title); }
@@ -4914,7 +5152,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4914
5152
  gap: var(--sp-3);
4915
5153
  font: var(--post-letter-hand-font, var(--type-h2));
4916
5154
  font-size: var(--post-letter-hand-font-size, var(--fs-lg));
4917
- line-height: var(--post-letter-hand-leading, 1.6);
5155
+ line-height: var(--post-letter-hand-leading, var(--lh-hand));
4918
5156
  }
4919
5157
  .post-letter__message > p { margin: 0; }
4920
5158
  .post-letter__sign { align-self: flex-end; }
@@ -4926,7 +5164,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4926
5164
  inset-block-start: calc(var(--_peek) / 2);
4927
5165
  inset-inline-start: var(--sp-2);
4928
5166
  translate: 0 -50%;
4929
- z-index: 2;
5167
+ z-index: var(--z-over-2);
4930
5168
  display: inline-flex;
4931
5169
  align-items: center;
4932
5170
  gap: var(--sp-2);
@@ -4987,8 +5225,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4987
5225
  background: transparent;
4988
5226
  color: inherit;
4989
5227
  font: var(--post-desk-hand-font, var(--type-h2));
4990
- line-height: calc(var(--fs-body) * var(--post-desk-leading, 1.7));
4991
- --_line: calc(var(--fs-body) * var(--post-desk-leading, 1.7));
5228
+ line-height: calc(var(--fs-body) * var(--post-desk-leading, var(--lh-ruled)));
5229
+ --_line: calc(var(--fs-body) * var(--post-desk-leading, var(--lh-ruled)));
4992
5230
  /* the ruled lines: one under every line of writing */
4993
5231
  background-image: linear-gradient(to bottom, transparent calc(100% - var(--border-w)), var(--_rule) 0);
4994
5232
  background-size: 100% var(--_line);
@@ -5005,10 +5243,10 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5005
5243
  /* the stamp box: empty it is a place of dots to press; pressed, the chosen stamp lies in it */
5006
5244
  .post-desk__slot { border: 0; background: transparent; color: inherit; }
5007
5245
  @media (prefers-reduced-motion: no-preference) {
5008
- .post-desk__slot > .stamp { animation: post-desk-stick var(--motion-quick) ease-out both; }
5009
- .post-desk .postmark { animation: strike var(--stamp-on-arrive-time, var(--motion-sheet)) ease-in both; } /* struck as a StampOn is (`strike`: tokens.css), in the same time */
5246
+ .post-desk__slot > .stamp { --stamp-arrive: post-desk-stick var(--motion-quick) var(--ease-out) both; }
5247
+ .post-desk .postmark { animation: strike var(--stamp-on-arrive-time, var(--motion-sheet)) var(--ease-in) both; } /* struck as a StampOn is (`strike`: tokens.css), in the same time */
5010
5248
  }
5011
- @keyframes post-desk-stick { from { translate: 0 calc(-1 * var(--sp-8)); rotate: 14deg; opacity: 0; } }
5249
+ @keyframes post-desk-stick { from { translate: 0 calc(-1 * var(--sp-8)); rotate: calc(var(--tilt-base) * 14); opacity: 0; } }
5012
5250
  /* the other half's showing edge brings it across: a tap-wide strip out to the screen's edge */
5013
5251
  .post-desk__reach { position: absolute; inset-block: 0; width: var(--tap); border: 0; background: transparent; }
5014
5252
  .post-desk__reach--address { inset-inline-end: calc(-1 * var(--screen-pad)); }
@@ -5038,7 +5276,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5038
5276
  border-block-end: var(--rule-w) solid transparent;
5039
5277
  background: transparent;
5040
5278
  }
5041
- .post-desk__pick > .stamp { rotate: var(--post-corner-stamp-tilt, 2deg); } /* in the tray a stamp keeps its words */
5279
+ .post-desk__pick > .stamp { --stamp-turn: var(--post-corner-stamp-tilt, calc(var(--tilt-base) * 2)); } /* in the tray a stamp keeps its words */
5042
5280
  .post-desk__pick[aria-pressed="true"] { border-block-end-color: var(--on-surface); }
5043
5281
  .post-desk__note { margin: 0; font: var(--type-caption); color: var(--post-desk-note-ink, var(--on-surface-muted)); min-height: 1.35em; }
5044
5282
  /* what stands where the card was, once it has left */
@@ -5063,7 +5301,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5063
5301
  margin-block-end: calc(var(--_e) - var(--_h));
5064
5302
  display: grid;
5065
5303
  grid-template-rows: var(--post-box-edge, var(--_edge)) minmax(0, 1fr);
5066
- transition: translate var(--motion-sheet) ease, rotate var(--motion-sheet) ease;
5304
+ transition: translate var(--motion-sheet) var(--ease), rotate var(--motion-sheet) var(--ease);
5067
5305
  }
5068
5306
  .post-box {
5069
5307
  --_edge: calc(var(--sp-base) * 20);
@@ -5078,8 +5316,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5078
5316
  }
5079
5317
  .post-box:has(> .post-box__card.is-open:not(:last-child)) { padding-block-end: var(--_part); }
5080
5318
  .post-box__card:last-child { margin-block-end: 0; }
5081
- .post-box__card:nth-child(3n + 1) { rotate: var(--post-box-lean, 0.6deg); }
5082
- .post-box__card:nth-child(3n + 2) { rotate: calc(var(--post-box-lean, 0.6deg) * -0.8); }
5319
+ .post-box__card:nth-child(3n + 1) { rotate: var(--post-box-lean, calc(var(--tilt-base) * 0.6)); }
5320
+ .post-box__card:nth-child(3n + 2) { rotate: calc(var(--post-box-lean, calc(var(--tilt-base) * 0.6)) * -0.8); }
5083
5321
  .post-box__card.is-open { rotate: none; }
5084
5322
  .post-box__card.is-open ~ .post-box__card { translate: 0 var(--_part); }
5085
5323
  .post-box__edge {
@@ -5107,56 +5345,25 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5107
5345
  .post-box__card:not(.is-open) .postmark { box-sizing: border-box; place-content: start center; padding-block-start: calc(var(--_size) * 0.18); }
5108
5346
  @media (prefers-reduced-motion: reduce) { .post-box__card { transition: none; } }
5109
5347
 
5348
+ /* ---- the desk's card leaves as a card with `leave` does (card.css LEAVE): the same two knobs, the same time ---- */
5349
+ .post-desk__card { transition: translate var(--motion-sheet) var(--ease), rotate var(--motion-sheet) var(--ease); }
5350
+ .post-desk--sent .post-desk__card { --_y: calc(var(--leave-rise, 160%) * -1); rotate: var(--leave-turn, calc(var(--tilt-base) * -5)); }
5351
+ @media (prefers-reduced-motion: reduce) { .post-desk__card { transition: none; } }
5352
+
5110
5353
  /* ---- css/media.css — cover card, quote, tile grid, fold, mosaic, wave ---- */
5111
5354
  /* ============================================================
5112
5355
  cardds/media.css — media & display inside cards:
5113
- cover card · quote · tile grid · fold · waveform
5356
+ quote · tile grid · waveform (the cover card and the fold are card variants: card.css)
5114
5357
  ============================================================ */
5115
5358
 
5116
- /* ---- cover: the image IS the card; text over a scrim ----
5117
- <article class="card card--cover"><div class="ph" role="img"></div>
5118
- <div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
5119
- <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
5120
- <div class="row">…meta…</div></article> */
5121
- .card--cover {
5122
- --card-bg: var(--card-3-bg);
5123
- --dark: var(--card-3-dark);
5124
- --accent: var(--card-ink);
5125
- --accent-dark: calc(1 - var(--dark));
5126
- min-height: var(--_cover-min-h);
5127
- align-content: space-between;
5128
- overflow: hidden;
5129
- isolation: isolate;
5130
- }
5131
- .card--cover > img,
5132
- .card--cover > .ph {
5133
- position: absolute;
5134
- inset: 0;
5135
- width: 100%;
5136
- height: 100%;
5137
- object-fit: cover;
5138
- z-index: -2;
5139
- border: 0;
5140
- border-radius: 0;
5141
- min-height: 0;
5142
- }
5143
- /* scrim: dark at both ends so the meta stays readable */
5144
- .card--cover::before {
5145
- content: "";
5146
- position: absolute;
5147
- inset: 0;
5148
- z-index: -1;
5149
- background: linear-gradient(to bottom, var(--scrim), transparent 40% 60%, var(--scrim));
5150
- }
5151
-
5152
5359
  /* ---- quote: giant mark, big text, attribution ----
5153
5360
  <blockquote class="quote"><p class="quote__text">It's overcast…</p><footer class="quote__by">Wednesday 15</footer></blockquote> */
5154
5361
  .quote { margin: 0; display: grid; gap: var(--quote-gap, var(--sp-3)); }
5155
5362
  .quote::before {
5156
5363
  content: "\201C";
5157
- font: 900 calc(var(--fs-h1) * 3)/0.55 var(--font-display);
5364
+ font: var(--fw-black) calc(var(--fs-h1) * 3) / var(--lh-mark) var(--font-display);
5158
5365
  height: var(--sp-10);
5159
- opacity: 0.9;
5366
+ opacity: var(--fade-most);
5160
5367
  }
5161
5368
  .quote__text { margin: 0; font: var(--quote-text-font, var(--type-h1)); letter-spacing: var(--track-h1); text-wrap: balance; }
5162
5369
  .quote--sm .quote__text { font: var(--type-h2); letter-spacing: var(--track-h2); }
@@ -5170,7 +5377,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5170
5377
  grid-template-columns: repeat(auto-fill, minmax(min(var(--tile-min), 100%), 1fr));
5171
5378
  gap: var(--tile-grid-gap, var(--sp-3));
5172
5379
  }
5173
- .tile-grid--3 { --tile-min: 6rem; } /* three across on a phone (attachments, upload kinds) */
5380
+ .tile-grid--3 { --tile-min: calc(var(--tap-base) * 2); } /* three across on a phone (attachments, upload kinds) */
5174
5381
  .tile {
5175
5382
  display: grid;
5176
5383
  align-content: space-between;
@@ -5217,42 +5424,18 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5217
5424
  .tile--5 { background: var(--tile-5-bg, var(--card-5-bg)); --dark: var(--card-5-dark); color: var(--tile-5-ink, var(--card-ink)); border-color: transparent; }
5218
5425
  .tile--3 { background: var(--tile-3-bg, var(--card-3-bg)); --dark: var(--card-3-dark); color: var(--tile-3-ink, var(--card-ink)); border-color: transparent; }
5219
5426
 
5220
- /* ---- fold: a sticky-note corner on any card (decorative) ----
5221
- No border. The card's outline follows its border box, so it ran straight
5222
- across the squared bottom-right corner and cut through the fold's diagonal —
5223
- the corner read as a notch with a line through it rather than a lifted flap.
5224
- The COLOUR goes transparent rather than the border going to 0, so the box
5225
- keeps its geometry and a fold card still measures the same as the cards
5226
- beside it. */
5227
- .card--fold {
5228
- border-color: transparent;
5229
- border-bottom-right-radius: 0;
5230
- background-image: linear-gradient(to top left,
5231
- var(--card-bg) 0 50%, /* the card's own background, not --surface (the page's) — a mismatch here drew a seam at the flap's edge */
5232
- color-mix(in srgb, currentColor 18%, var(--card-bg)) 50% 100%);
5233
- background-repeat: no-repeat;
5234
- background-position: bottom right;
5235
- background-size: var(--sp-8) var(--sp-8);
5236
- background-origin: border-box; /* the flap sits on the true corner, not inside the (transparent) border */
5237
- /* the corner is CUT: the triangle under the flap shows what is behind the card. Painted in the card's own colour it
5238
- read as the square corner of a second card underneath (Lh 2026-09-23). The polygon reaches 3rem past the other
5239
- edges so a coloured palette's shadow is not clipped there. */
5240
- clip-path: polygon(-3rem -3rem, calc(100% + 3rem) -3rem, calc(100% + 3rem) calc(100% - var(--sp-8) - 3rem),
5241
- calc(100% - var(--sp-8) - 3rem) calc(100% + 3rem), -3rem calc(100% + 3rem));
5242
- }
5243
-
5244
5427
  /* ---- wave: audio waveform stand-in for voice rows ----
5245
5428
  <span class="wave"></span> — sits in a .row__body, fills the width */
5246
5429
  .wave {
5247
5430
  /* only WHOLE bars show (Lh 2026-09-23 — a clipped last bar read thinner): the columns are as many whole bar-widths as fit
5248
5431
  (auto-fill), spread edge to edge; bars that do not fit go to rows of no height, clipped away */
5249
5432
  display: grid;
5250
- grid-template-columns: repeat(auto-fit, var(--wave-bar-width, 0.125rem)); /* auto-FIT: columns no bar fills collapse, so a wide row still spreads its bars edge to edge */
5433
+ grid-template-columns: repeat(auto-fit, var(--wave-bar-width, var(--hairline-w))); /* auto-FIT: columns no bar fills collapse, so a wide row still spreads its bars edge to edge */
5251
5434
  grid-template-rows: 100%;
5252
5435
  grid-auto-rows: 0;
5253
5436
  justify-content: space-between;
5254
5437
  align-items: center;
5255
- column-gap: var(--wave-gap, 0.125rem);
5438
+ column-gap: var(--wave-gap, var(--hairline-w));
5256
5439
  height: var(--wave-h, var(--icon));
5257
5440
  width: 100%;
5258
5441
  overflow: hidden;
@@ -5278,7 +5461,6 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5278
5461
  }
5279
5462
  .mosaic > :first-child { grid-row: span 2; }
5280
5463
  .mosaic > * { min-height: 0; border-radius: 0; } /* opacity/border-width here were no-ops: .ph restates its own below, and nothing else in a mosaic carries either by default */
5281
- .mosaic > .ph { opacity: var(--off); border-width: var(--border-w); }
5282
5464
  .mosaic > img { width: 100%; height: 100%; object-fit: cover; }
5283
5465
 
5284
5466
  /* ---- postcard: a member's introduction in the shape of a cover card — the photo fills it,
@@ -5336,8 +5518,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5336
5518
  .postcard__turn:focus-visible { outline-offset: calc(-1 * var(--sp-1)); } /* the ring inside the card's edge */
5337
5519
  /* the front: the photo (or the wireframe box) fills it; the band reads on a scrim */
5338
5520
  .postcard__front { background: var(--postcard-front-bg, var(--card-3-bg)); --dark: var(--card-3-dark); color: var(--postcard-front-ink, var(--card-ink)); }
5339
- .postcard__front .postcard__turn > img,
5340
- .postcard__front .postcard__turn > .ph {
5521
+ .postcard__front .postcard__turn > img {
5341
5522
  position: absolute;
5342
5523
  inset: 0;
5343
5524
  width: 100%;
@@ -5351,7 +5532,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5351
5532
  content: "";
5352
5533
  position: absolute;
5353
5534
  inset: 0;
5354
- z-index: 1;
5535
+ z-index: var(--z-over);
5355
5536
  pointer-events: none;
5356
5537
  background: linear-gradient(to top, var(--scrim), transparent 45%);
5357
5538
  }
@@ -5367,7 +5548,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5367
5548
  Taps between its words fall through to the turn; its buttons take their own. */
5368
5549
  .postcard__band {
5369
5550
  position: relative;
5370
- z-index: 2;
5551
+ z-index: var(--z-over-2);
5371
5552
  display: flex;
5372
5553
  align-items: end;
5373
5554
  gap: var(--postcard-band-gap, var(--sp-3));
@@ -5377,12 +5558,13 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5377
5558
  .postcard__band > * { pointer-events: auto; }
5378
5559
  .postcard__words { display: grid; gap: var(--postcard-words-gap, var(--sp-1)); flex: 1; min-width: 0; pointer-events: none; }
5379
5560
  .postcard__front .postcard__band { --accent: var(--postcard-front-ink, var(--card-ink)); --accent-dark: calc(1 - var(--dark)); }
5380
- .postcard__band .icon-btn[aria-pressed="true"] .icon { fill: currentColor; }
5381
5561
  /* a voice note on the back: no words to read — its timeline (a Progress), its length and the control to hear it
5382
5562
  under them, at the middle over the turn, on the face's inset; the app plays and keeps the time */
5383
- .postcard__voice { position: absolute; inset: 0; z-index: 2; display: grid; align-content: center; justify-items: center; gap: var(--postcard-voice-gap, var(--sp-3)); padding: var(--postcard-face-pad, var(--card-pad-rest)); pointer-events: none; }
5563
+ .postcard__voice { position: absolute; inset: 0; z-index: var(--z-over-2); display: grid; align-content: center; justify-items: center; gap: var(--postcard-voice-gap, var(--sp-3)); padding: var(--postcard-face-pad, var(--card-pad-rest)); pointer-events: none; }
5384
5564
  .postcard__voice > * { pointer-events: auto; }
5385
- .postcard__voice > .progress { justify-self: stretch; pointer-events: none; }
5565
+
5566
+ /* ==== the tile a choice draws, picked (choice.css drives it with its input) (#62: a block is written in its own file) ==== */
5567
+ .tile--pick:has(:checked) { background: var(--accent); --dark: var(--accent-dark); color: var(--card-ink); border-color: transparent; }
5386
5568
 
5387
5569
  /* ---- css/layover.css — ask drawer, popover menu, banner, float bar, callout ---- */
5388
5570
  /* ============================================================
@@ -5418,14 +5600,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5418
5600
  sets open={false}).
5419
5601
  Reduced motion: the finished state, no beats; closing fires at once. */
5420
5602
  .modal {
5421
- --drawer-h: 14rem;
5603
+ --drawer-h: calc(var(--tap-base) * 14 / 3);
5422
5604
  position: absolute; /* at the stage's centre, and kept there as it grows: its middle is pinned at 50% and */
5423
5605
  top: 50%; /* translate -50% of its OWN height re-centres it on every height. NOT inset 0 + margin auto + */
5424
5606
  inset-inline: 0; /* align-self: center — Safari 18 ignores align-self on an absolute box in a block container, */
5425
5607
  margin-inline: auto; /* so the card stretched to the whole stage (iOS 18.6, Lh 2026-09-24); nor height: fit-content */
5426
5608
  translate: 0 -50%;
5427
5609
  width: calc(100% - 2 * var(--screen-pad));
5428
- z-index: 2; /* over the dim stage's scrim (z 1) */
5610
+ z-index: var(--z-over-2); /* over the dim stage's scrim (z 1) */
5429
5611
  max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) */
5430
5612
  padding: 0; /* the parts pad themselves */
5431
5613
  border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
@@ -5470,18 +5652,17 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5470
5652
  }
5471
5653
  /* the CONFIRM: a Modal with no ModalBody: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
5472
5654
  .modal:not(:has(> .modal__lift)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
5473
- .modal__drawer .t-caption { color: var(--modal-drawer-caption-ink, var(--card-muted)); }
5655
+ .modal__drawer .t-caption { --t-caption-ink: var(--modal-drawer-caption-ink, var(--card-muted)); }
5474
5656
  .modal__drawer > h2:focus { outline: none; } /* the question takes the cursor when the modal arrives (for a screen reader, the keyboard's way in) — it is not a control: no ring */
5475
5657
  /* the answers as WORD buttons (a BtnRow of Btns; Lh 2026-10-11): the row fills the ask with room over it and each button keeps its words on one
5476
5658
  line — left to its words' width the row squeezed two buttons into round blobs, their words on two lines. A quiet button is an outline only,
5477
5659
  so on the ask its edge is the ask's ink at full strength (light on the dark end, dark on --light). The BtnTrio of round buttons is not touched. */
5478
- .modal__drawer > .btn-row { justify-self: stretch; margin-top: var(--modal-drawer-answers-gap, var(--sp-5)); white-space: nowrap; }
5479
- .modal__drawer .btn--quiet { border-color: var(--modal-drawer-quiet-border, currentColor); }
5480
- .modal__drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
5481
- .modal__drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
5660
+ .modal__drawer .btn--quiet { --btn-quiet-border: var(--modal-drawer-quiet-border, currentColor); }
5661
+ .modal__drawer .icon-btn { --icon-btn-bg: transparent; --icon-btn-ink: currentColor; --icon-btn-border: var(--ink-rail); }
5662
+ .modal__drawer .icon-btn--invert { --icon-btn-size: var(--tap-lg); }
5482
5663
  @media (prefers-reduced-motion: no-preference) {
5483
- .modal { animation: modal-drop calc(2 * var(--motion-sheet)) ease both; }
5484
- .modal__drawer { animation: modal-open calc(2 * var(--motion-sheet)) ease both; }
5664
+ .modal { animation: modal-drop calc(2 * var(--motion-sheet)) var(--ease) both; }
5665
+ .modal__drawer { animation: modal-open calc(2 * var(--motion-sheet)) var(--ease) both; }
5485
5666
  @keyframes modal-drop {
5486
5667
  0% { translate: 0 calc(-50% - 100cqh); } /* above the stage */
5487
5668
  50%, 100% { translate: 0 -50%; } /* 1. at the centre — and it stays: beat 2 is the drawer's */
@@ -5496,8 +5677,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5496
5677
  .btn-trio__dots {
5497
5678
  width: var(--btn-trio-dots-w, var(--sp-5));
5498
5679
  height: var(--btn-trio-dots-h, var(--sp-1));
5499
- background: radial-gradient(circle, currentColor var(--sp-0), transparent calc(var(--sp-0) + 0.5px)) 0 0 / calc(var(--sp-5) / 2) 100% repeat-x;
5500
- opacity: 0.4;
5680
+ background: radial-gradient(circle, currentColor var(--sp-0), transparent calc(var(--sp-0) + var(--px) / 2)) 0 0 / calc(var(--sp-5) / 2) 100% repeat-x;
5681
+ opacity: var(--fade-soft);
5501
5682
  }
5502
5683
 
5503
5684
  /* ---- popover menu: a card anchored to its button, rows of icon + label ----
@@ -5520,7 +5701,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5520
5701
  border-radius: var(--menu-radius, var(--r-card));
5521
5702
  --menu-pad: var(--sp-2); /* published: a row's radius is concentric with the panel's */
5522
5703
  padding: var(--menu-pad);
5523
- min-width: 12rem;
5704
+ min-width: calc(var(--tap-base) * 4);
5524
5705
  align-content: start; /* rows keep their own height: WebKit stretches a popover grid's rows to the whole room under its anchor (iOS 26.5, 2026-10-08) */
5525
5706
  --_menu-max: min(60dvh, calc(100dvh - 2 * var(--sp-5)));
5526
5707
  max-height: var(--menu-max-h, var(--_menu-max)); /* a long list (twenty neighbours) scrolls inside the panel — it ran off the screen's foot, the last names out of reach (timebank, 2026-10-08) */
@@ -5554,7 +5735,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5554
5735
  .menu[popover] {
5555
5736
  position-area: block-end span-inline-end;
5556
5737
  margin-top: var(--sp-2);
5557
- min-width: max(12rem, anchor-size(width)); /* at least as wide as its trigger */
5738
+ min-width: max(calc(var(--tap-base) * 4), anchor-size(width)); /* at least as wide as its trigger */
5558
5739
  position-try-fallbacks: flip-block, flip-inline;
5559
5740
  }
5560
5741
  .menu--end[popover] { position-area: block-end span-inline-start; }
@@ -5587,11 +5768,11 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5587
5768
  .banner__summary {
5588
5769
  list-style: none;
5589
5770
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
5590
- padding-block: var(--banner-summary-pad-y, calc(var(--sp-base) * 1.5)); font: var(--banner-summary-font, var(--type-caption)); font-weight: 400;
5771
+ padding-block: var(--banner-summary-pad-y, calc(var(--sp-base) * 1.5)); font: var(--banner-summary-font, var(--type-caption)); font-weight: var(--fw-regular);
5591
5772
  }
5592
5773
  .banner__summary::-webkit-details-marker { display: none; }
5593
- .banner__summary .icon { transition: rotate var(--motion-quick) ease; }
5594
- .banner[open] .banner__summary .icon { rotate: 180deg; }
5774
+ .banner__summary .icon { --icon-motion: rotate var(--motion-quick) var(--ease); }
5775
+ .banner[open] .banner__summary .icon { --icon-turn: 180deg; }
5595
5776
  /* the band's words never run past it: one column that cannot be wider than the band (a grid track is as wide as its longest
5596
5777
  word otherwise), and a word with no break in it — a link — breaks where it must (mailpool, Lh 2026-10-01) */
5597
5778
  .banner__body { display: grid; grid-template-columns: minmax(0, 1fr); overflow-wrap: anywhere; gap: var(--banner-body-gap, var(--sp-1)); padding-bottom: var(--sp-4); }
@@ -5599,7 +5780,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5599
5780
  @supports (interpolate-size: allow-keywords) {
5600
5781
  @media (prefers-reduced-motion: no-preference) {
5601
5782
  .banner { interpolate-size: allow-keywords; }
5602
- .banner::details-content { block-size: 0; overflow: clip; transition: block-size var(--motion-quick) ease, content-visibility var(--motion-quick) allow-discrete; }
5783
+ .banner::details-content { block-size: 0; overflow: clip; transition: block-size var(--motion-quick) var(--ease), content-visibility var(--motion-quick) allow-discrete; }
5603
5784
  .banner[open]::details-content { block-size: auto; }
5604
5785
  }
5605
5786
  }
@@ -5658,7 +5839,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5658
5839
  /* aria-current="false" is the correct way to mark the OTHER tabs, so the
5659
5840
  bare attribute selector lights every one of them up */
5660
5841
  .float-bar__item[aria-current]:not([aria-current="false"]) { background: var(--accent); color: var(--on-accent); }
5661
- .float-bar > .icon-btn { width: var(--bar-h); height: var(--bar-h); box-shadow: var(--shadow-float); }
5842
+ .float-bar > .icon-btn { --icon-btn-size: var(--bar-h); --icon-btn-shadow: var(--shadow-float); }
5662
5843
 
5663
5844
  /* ---- callout: a small card with a tail — over a map, a media, a mascot ----
5664
5845
  <div class="callout" style="top: 30%; left: 40%"><span class="callout__title">Telok Ayer Park</span><span class="callout__sub">1.2 km</span></div> */
@@ -5667,7 +5848,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5667
5848
  display: grid;
5668
5849
  gap: var(--callout-gap, var(--sp-0));
5669
5850
  width: max-content;
5670
- max-width: 15rem;
5851
+ max-width: calc(var(--tap-base) * 5);
5671
5852
  padding: var(--callout-pad-y, var(--sp-3)) var(--callout-pad-x, var(--sp-4));
5672
5853
  border: var(--border-w) solid var(--callout-border, var(--action-border));
5673
5854
  border-radius: var(--callout-radius, var(--r-card));
@@ -5700,7 +5881,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5700
5881
  <p class="splash__text">…</p></div></section>
5701
5882
  The group's MIDDLE is on --screen-centre (40% down), the same line a centred card uses. The mark is the app's own content —
5702
5883
  a lottie box often carries empty room around its drawing — the app sets the knobs below, cardds does not guess it. */
5703
- .splash { position: relative; background: var(--splash-bg, var(--surface)); transition: opacity var(--motion-sheet) ease; }
5884
+ .splash { position: relative; background: var(--splash-bg, var(--surface)); transition: opacity var(--motion-sheet) var(--ease); }
5704
5885
  .splash--leaving { opacity: 0; }
5705
5886
  .splash__group {
5706
5887
  position: absolute; inset-inline: var(--screen-pad); top: var(--splash-at, var(--screen-centre)); translate: 0 -50%;
@@ -5722,13 +5903,13 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5722
5903
  position: fixed; inset-inline: 0; top: calc(var(--sp-3) + var(--safe-top));
5723
5904
  max-width: var(--screen-w); margin-inline: auto; padding-inline: var(--screen-pad, var(--sp-4));
5724
5905
  display: grid; justify-items: center;
5725
- z-index: calc(var(--z-top) + 1);
5906
+ z-index: calc(var(--z-top) + var(--z-over));
5726
5907
  pointer-events: none; visibility: hidden;
5727
5908
  translate: 0 calc(var(--_out) * (100% + var(--sp-8) + var(--safe-top)));
5728
- transition: translate var(--motion-sheet) ease, visibility 0s var(--motion-sheet);
5909
+ transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--motion-sheet);
5729
5910
  }
5730
5911
  .screen--fill > .snackbar, .sheet-stage > .snackbar { position: absolute; }
5731
- .snackbar--bottom { --_out: 1; top: auto; bottom: calc(var(--sp-3) + var(--bottom-h, 0px) + max(var(--safe-bottom), var(--kb, 0px))); }
5912
+ .snackbar--bottom { --_out: 1; top: auto; bottom: calc(var(--sp-3) + var(--bottom-h, 0rem) + max(var(--safe-bottom), var(--kb, 0rem))); }
5732
5913
  .snackbar--open { translate: 0; visibility: visible; transition-delay: 0s; }
5733
5914
  .snackbar__body {
5734
5915
  display: flex; align-items: center; gap: var(--snackbar-gap, var(--sp-3));
@@ -5743,12 +5924,15 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5743
5924
  font: inherit; text-align: start;
5744
5925
  pointer-events: auto;
5745
5926
  }
5746
- .snackbar__body > .icon { flex: none; color: var(--snackbar-icon-ink, var(--signal-ok)); }
5927
+ .snackbar__body > .icon { --icon-ink: var(--snackbar-icon-ink, var(--signal-ok)); }
5747
5928
  .snackbar__words { display: grid; min-width: 0; overflow-wrap: anywhere; }
5748
5929
  .snackbar__text { font: var(--snackbar-text-font, var(--type-body)); }
5749
5930
  .snackbar__caption { font: var(--snackbar-caption-font, var(--type-caption)); color: var(--snackbar-caption-ink, inherit); } /* on the signal's soft ground the full ink: a muted shade there fell under AA */
5750
5931
  @media (prefers-reduced-motion: reduce) { .snackbar { transition: none; } }
5751
5932
 
5933
+ /* ---- where it sits in what actions.css draws (docs/reference.md rule 15) ---- */
5934
+ .dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards (= .menu--end, layover.css) */
5935
+
5752
5936
  /* ---- css/finish.css — light, shadow, edges — only under data-finish="lit" ---- */
5753
5937
  /* ============================================================
5754
5938
  cardds/finish.css — FINISH: the finish of a card's paper. A style of its own (Lh 2026-10-02: the plain
@@ -5785,28 +5969,28 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5785
5969
  ============================================================ */
5786
5970
  [data-finish="lit"] {
5787
5971
  --_shade: var(--on-surface);
5788
- --finish-edge: inset 0 1px 0 rgb(255 255 255 / 0.55); /* the lit top edge */
5789
- --finish-ring: 0 0 0 1px color-mix(in srgb, var(--_shade) 7%, transparent); /* the hairline round a raised thing */
5972
+ --finish-edge: inset 0 0.0625rem 0 rgb(255 255 255 / 0.55); /* the lit top edge */
5973
+ --finish-ring: 0 0 0 0.0625rem color-mix(in srgb, var(--_shade) 7%, transparent); /* the hairline round a raised thing */
5790
5974
 
5791
5975
  /* the three shadows every component already reads — layered: one close and crisp, one far and soft */
5792
5976
  --shadow-card: var(--finish-edge), var(--finish-ring),
5793
- 0 1px 2px color-mix(in srgb, var(--_shade) 8%, transparent),
5977
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 8%, transparent),
5794
5978
  0 0.375rem 0.75rem -0.125rem color-mix(in srgb, var(--_shade) 8%, transparent),
5795
5979
  0 1.25rem 2.5rem -0.75rem color-mix(in srgb, var(--_shade) 14%, transparent);
5796
5980
  --shadow-float: var(--finish-edge), var(--finish-ring),
5797
- 0 1px 2px color-mix(in srgb, var(--_shade) 12%, transparent),
5981
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 12%, transparent),
5798
5982
  0 0.5rem 1.25rem -0.25rem color-mix(in srgb, var(--_shade) 22%, transparent);
5799
5983
  --shadow-sheet: var(--finish-edge), var(--finish-ring),
5800
- 0 -1px 2px color-mix(in srgb, var(--_shade) 6%, transparent),
5984
+ 0 -0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 6%, transparent),
5801
5985
  0 -0.75rem 2.5rem color-mix(in srgb, var(--_shade) 18%, transparent);
5802
5986
 
5803
5987
  /* a control: its own small lift, and the sheen that runs down its face */
5804
5988
  --finish-control: var(--finish-edge), var(--finish-ring),
5805
- 0 1px 2px color-mix(in srgb, var(--_shade) 12%, transparent),
5989
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 12%, transparent),
5806
5990
  0 0.25rem 0.5rem -0.125rem color-mix(in srgb, var(--_shade) 12%, transparent);
5807
- --finish-control-down: var(--finish-ring), inset 0 1px 2px color-mix(in srgb, var(--_shade) 18%, transparent);
5991
+ --finish-control-down: var(--finish-ring), inset 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 18%, transparent);
5808
5992
  --finish-sheen: linear-gradient(to bottom, rgb(255 255 255 / 0.16), rgb(255 255 255 / 0) 60%);
5809
- --finish-sunk: inset 0 1px 2px color-mix(in srgb, var(--_shade) 14%, transparent), 0 1px 0 rgb(255 255 255 / 0.5);
5993
+ --finish-sunk: inset 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 14%, transparent), 0 0.0625rem 0 rgb(255 255 255 / 0.5);
5810
5994
 
5811
5995
  --outline: color-mix(in srgb, currentColor 14%, transparent);
5812
5996
  }
@@ -5830,7 +6014,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5830
6014
  [data-finish="lit"] .btn--primary,
5831
6015
  [data-finish="lit"] .icon-btn--invert:not(.icon-btn-group > *) {
5832
6016
  box-shadow: var(--finish-edge),
5833
- 0 1px 2px color-mix(in srgb, var(--_shade) 20%, transparent),
6017
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 20%, transparent),
5834
6018
  0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
5835
6019
  }
5836
6020
  /* a peeked stack tab is the icon alone: no sheen and no shadow until its sheet is open (the fill fades with the sheet) */
@@ -5844,7 +6028,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5844
6028
  }
5845
6029
  [data-finish="lit"] .icon-btn-group { box-shadow: var(--finish-control); }
5846
6030
  [data-finish="lit"] .toggle { box-shadow: var(--finish-sunk); }
5847
- [data-finish="lit"] .toggle::after { box-shadow: 0 1px 2px color-mix(in srgb, var(--_shade) 35%, transparent); }
6031
+ [data-finish="lit"] .toggle::after { box-shadow: 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 35%, transparent); }
5848
6032
 
5849
6033
  /* ---- sunk: what the member types into ---- */
5850
6034
  [data-finish="lit"] :is(.field, .pin__cell) { box-shadow: var(--finish-sunk); }
@@ -5862,12 +6046,12 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5862
6046
  }
5863
6047
  [data-finish="print"] .card {
5864
6048
  border: var(--border-w) solid var(--_ink);
5865
- box-shadow: var(--print-off, 3px) var(--print-off, 3px) 0 0 color-mix(in srgb, var(--card-ink) 28%, transparent);
6049
+ box-shadow: var(--print-off, 0.1875rem) var(--print-off, 0.1875rem) 0 0 color-mix(in srgb, var(--card-ink) 28%, transparent);
5866
6050
  isolation: isolate; /* the grain's blend stays inside the card */
5867
6051
  }
5868
6052
  /* the grain: a ::after, not ::before — .card--cover's scrim is its ::before. Over everything in the card (z-index 1, the foot's own level), touches pass through it */
5869
6053
  [data-finish="print"] .card::after {
5870
- content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: 1;
6054
+ content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: var(--z-over);
5871
6055
  opacity: var(--print-grain, 0.32); mix-blend-mode: multiply;
5872
6056
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.3 0 0 0 0 0.25 0 0 0 0 0.2 0 0 0 0.9 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
5873
6057
  }
@@ -5879,10 +6063,11 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5879
6063
  }
5880
6064
  [data-finish="print"] :is(.btn, .chip) {
5881
6065
  border-color: var(--_ink);
5882
- box-shadow: 2px 2px 0 0 color-mix(in srgb, currentColor 35%, transparent);
6066
+ box-shadow: 0.125rem 0.125rem 0 0 color-mix(in srgb, currentColor 35%, transparent);
5883
6067
  }
5884
- [data-finish="print"] .btn--primary { box-shadow: 2px 2px 0 0 color-mix(in srgb, var(--card-ink) 60%, transparent); }
5885
- [data-finish="print"] :is(.btn, .chip):active { box-shadow: none; translate: 2px 2px; }
6068
+ [data-finish="print"] .chip { border-color: var(--chip-border, var(--_ink)); } /* a holder that clears a chip's edge (a note's chips) clears it in print too */
6069
+ [data-finish="print"] .btn--primary { box-shadow: 0.125rem 0.125rem 0 0 color-mix(in srgb, var(--card-ink) 60%, transparent); }
6070
+ [data-finish="print"] :is(.btn, .chip):active { box-shadow: none; translate: 0.125rem 0.125rem; }
5886
6071
  [data-finish="print"] .card__thumb { border-style: dashed; }
5887
6072
 
5888
6073
  /* ============================================================
@@ -5894,29 +6079,29 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5894
6079
  [data-finish="sticker"] { --outline: color-mix(in srgb, currentColor 18%, transparent); }
5895
6080
  [data-finish="sticker"] .card {
5896
6081
  box-shadow:
5897
- 0 0 0 var(--sticker-cut, 5px) #fff,
5898
- 0 calc(var(--sticker-cut, 5px) + 4px) 0 0 color-mix(in srgb, var(--on-surface) 16%, transparent),
5899
- 0 calc(var(--sticker-cut, 5px) + 12px) 20px -8px color-mix(in srgb, var(--on-surface) 22%, transparent);
6082
+ 0 0 0 var(--sticker-cut, 0.3125rem) #fff,
6083
+ 0 calc(var(--sticker-cut, 0.3125rem) + 0.25rem) 0 0 color-mix(in srgb, var(--on-surface) 16%, transparent),
6084
+ 0 calc(var(--sticker-cut, 0.3125rem) + 0.75rem) 1.25rem -0.5rem color-mix(in srgb, var(--on-surface) 22%, transparent);
5900
6085
  }
5901
6086
  [data-finish="sticker"] .card:not(.sheet, .modal, .card--away, .sheet-stack *, .card-list > *, .card-feed > *) { rotate: var(--sticker-tilt, 0.8deg); }
5902
6087
  [data-finish="sticker"] .card:not(.sheet, .modal, .card--away, .sheet-stack *, .card-list > *, .card-feed > *):nth-of-type(even) { rotate: calc(-1 * var(--sticker-tilt, 0.8deg)); }
5903
6088
  [data-finish="sticker"] .card-list { gap: var(--sp-5); }
5904
6089
  [data-finish="sticker"] .btn {
5905
- box-shadow: 0 3px 0 0 color-mix(in srgb, currentColor 30%, transparent);
6090
+ box-shadow: 0 0.1875rem 0 0 color-mix(in srgb, currentColor 30%, transparent);
5906
6091
  border-color: color-mix(in srgb, currentColor 25%, transparent);
5907
6092
  }
5908
- [data-finish="sticker"] .btn--primary { box-shadow: 0 3px 0 0 color-mix(in srgb, var(--btn-primary-bg, var(--accent)) 55%, var(--on-surface)); }
5909
- [data-finish="sticker"] .btn:active { box-shadow: none; translate: 0 3px; }
5910
- [data-finish="sticker"] .chip { box-shadow: 0 0 0 3px #fff; }
6093
+ [data-finish="sticker"] .btn--primary { box-shadow: 0 0.1875rem 0 0 color-mix(in srgb, var(--btn-primary-bg, var(--accent)) 55%, var(--on-surface)); }
6094
+ [data-finish="sticker"] .btn:active { box-shadow: none; translate: 0 0.1875rem; }
6095
+ [data-finish="sticker"] .chip { box-shadow: 0 0 0 0.1875rem #fff; }
5911
6096
 
5912
6097
  /* ============================================================
5913
6098
  ink — a poster / comic print: --ink-w of the surface's ink round every boundary and control, and a hard
5914
6099
  offset of --ink-off in the same ink. The two wireframe borders come back as that line.
5915
6100
  ============================================================ */
5916
6101
  [data-finish="ink"] { --card-border: var(--on-surface); --action-border: var(--on-surface); --outline: currentColor; }
5917
- [data-finish="ink"] .card { border: var(--ink-w, 2.5px) solid var(--on-surface); box-shadow: var(--ink-off, 5px) var(--ink-off, 5px) 0 0 var(--on-surface); }
5918
- [data-finish="ink"] :is(.btn, .chip, .icon-btn:not(.icon-btn-group > *)) { border: var(--ink-w, 2.5px) solid currentColor; box-shadow: 3px 3px 0 0 currentColor; }
5919
- [data-finish="ink"] .btn--primary { border-color: var(--on-surface); box-shadow: 3px 3px 0 0 var(--on-surface); }
5920
- [data-finish="ink"] .toggle { box-shadow: 2px 2px 0 0 var(--on-surface); }
5921
- [data-finish="ink"] .card__thumb { border-width: var(--ink-w, 2.5px); }
6102
+ [data-finish="ink"] .card { border: var(--ink-w, 0.15625rem) solid var(--on-surface); box-shadow: var(--ink-off, 0.3125rem) var(--ink-off, 0.3125rem) 0 0 var(--on-surface); }
6103
+ [data-finish="ink"] :is(.btn, .chip, .icon-btn:not(.icon-btn-group > *)) { border: var(--ink-w, 0.15625rem) solid currentColor; box-shadow: 0.1875rem 0.1875rem 0 0 currentColor; }
6104
+ [data-finish="ink"] .btn--primary { border-color: var(--on-surface); box-shadow: 0.1875rem 0.1875rem 0 0 var(--on-surface); }
6105
+ [data-finish="ink"] .toggle { box-shadow: 0.125rem 0.125rem 0 0 var(--on-surface); }
6106
+ [data-finish="ink"] .card__thumb { border-width: var(--ink-w, 0.15625rem); }
5922
6107