@usableapp/cardds 0.7.103 → 0.7.105

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 (66) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/css/actions.css +112 -37
  3. package/css/base.css +139 -15
  4. package/css/card.css +331 -86
  5. package/css/choice.css +25 -33
  6. package/css/finish.css +29 -28
  7. package/css/forms.css +6 -14
  8. package/css/journey.css +8 -6
  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 +2 -2
  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 +40 -216
  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 +101 -26
  23. package/dist/actions/Chip.d.ts +1 -1
  24. package/dist/actions/Chip.js +3 -2
  25. package/dist/actions/IconBtn.d.ts +12 -3
  26. package/dist/actions/IconBtn.js +9 -3
  27. package/dist/cardds.css +1094 -881
  28. package/dist/cards/Card.d.ts +8 -6
  29. package/dist/cards/Card.js +4 -4
  30. package/dist/cards/CardFeed.js +7 -9
  31. package/dist/cards/CardFoot.d.ts +7 -2
  32. package/dist/cards/CardFoot.js +4 -3
  33. package/dist/catalog.json +1 -1
  34. package/dist/journey/JourneyBadge.d.ts +1 -1
  35. package/dist/journey/JourneyBadge.js +1 -1
  36. package/dist/layover/Snackbar.js +2 -2
  37. package/dist/lists/Bubble.d.ts +1 -1
  38. package/dist/lists/Bubble.js +1 -1
  39. package/dist/lists/Row.js +5 -7
  40. package/dist/numbers/Picker.js +11 -18
  41. package/dist/people/Avatar.d.ts +1 -1
  42. package/dist/people/Avatar.js +3 -2
  43. package/dist/post/PostDesk.js +6 -9
  44. package/dist/post/PostLetter.js +4 -4
  45. package/dist/pressable.d.ts +9 -0
  46. package/dist/pressable.js +6 -0
  47. package/dist/room.d.ts +5 -0
  48. package/dist/room.js +9 -0
  49. package/dist/scaffold/Panes.js +8 -14
  50. package/dist/sheets/Sheet.d.ts +1 -1
  51. package/dist/sheets/Sheet.js +2 -3
  52. package/dist/sheets/SheetSteps.js +3 -3
  53. package/dist/sheets/StackSheet.d.ts +1 -1
  54. package/dist/sheets/sheetStepsPile.js +2 -3
  55. package/dist/sheets/stackCard.js +67 -115
  56. package/dist/state.d.ts +21 -0
  57. package/dist/state.js +28 -1
  58. package/dist/sticky/Sticky.js +3 -7
  59. package/dist/templates/Home.d.ts +1 -1
  60. package/dist/templates/Splash.js +2 -2
  61. package/dist/templates/Walkthrough.js +4 -5
  62. package/docs/reference.md +76 -12
  63. package/lib/catalog.mjs +23 -7
  64. package/lint/rules.mjs +7 -5
  65. package/lint/values.mjs +144 -0
  66. 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) */
@@ -88,6 +88,7 @@
88
88
  --tap: max(var(--tap-floor), var(--tap-base)); /* 48 primary touch target */
89
89
  --tap-sm: max(var(--tap-floor), calc(var(--tap-base) * 11 / 12)); /* 44 minimum touch target */
90
90
  --tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: send, field with action */
91
+ --tap-xl: max(var(--tap-floor), calc(var(--tap-base) * 17 / 12)); /* 68 the biggest round control: a foot of a few big round buttons (IconBtn xl) */
91
92
  --btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
92
93
  --btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
93
94
  --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
@@ -119,7 +120,10 @@
119
120
  --tile-h: calc(var(--tap-base) * 11 / 4); /* 132 tile / band height */
120
121
 
121
122
  /* ---- radius ---- */
122
- --r-chip: 999px; /* pills, chips, buttons — always fully round */
123
+ /* 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
124
+ sent away) turns by a multiple of this. A theme that wants everything straight sets it to 0deg */
125
+ --tilt-base: 1deg;
126
+ --r-chip: 62.4375rem; /* pills, chips, buttons — always fully round */
123
127
  --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
128
  --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
129
  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 +132,19 @@
128
132
  --r-control: calc(var(--r-base) * 0.875); /* 18.2 fields, thumbs, menu rows */
129
133
  --r-badge: calc(var(--r-base) * 0.625); /* 13 tile badge, bars */
130
134
 
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);
135
+ /* ---- borders / rules / elevation (rem, like every length) ---- */
136
+ /* one device-independent pixel in rem: what the platform needs to keep a hidden word in the accessibility tree, and the half-width
137
+ of a drawn line. The browser's own text size: a field under it makes iOS zoom in on focus */
138
+ --px: 0.0625rem;
139
+ --browser-text: 1rem;
140
+ /* the wireframe map's two street directions (.map) */
141
+ --street-1: 75deg; --street-2: -18deg;
142
+ --border-w: 0.09375rem;
143
+ --hairline-w: 0.125rem; /* the thinnest mark: a sheet's grab line (Lh 2026-10-06: 1px, then 2px the same day) */
144
+ --rule-w: 0.125rem; /* dotted connectors, picker rules */
145
+ --scroll-w: 0.0625rem; /* the scrollbar of a .bleed track: a hairline of ink */
146
+ --focus: 0.125rem solid currentColor; /* :focus-visible ring */
147
+ --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
148
  --shadow-float: 0 0.25rem 0.75rem rgb(0 0 0 / 0.14);
139
149
  --shadow-sheet: 0 -0.5rem 2rem rgb(0 0 0 / 0.16); /* sheets, lifted cards */
140
150
 
@@ -144,11 +154,44 @@
144
154
  --motion-quick: 0.2s; /* a small state flip that isn't a press: a chevron turning, a popover/banner opening */
145
155
  --motion-slide: 0.22s; /* a centred card sliding out and the next in (card.css) — quick, a page turn (Lh, 2026-09-12) */
146
156
  --motion-flip: 0.45s; /* a postcard turning over (media.css) — slow enough to read as a card in the hand */
157
+ --motion-spin: 0.9s; /* a busy ring's one turn (Spinner, a busy FormNote) */
158
+ --motion-blink: 1.1s; /* a code cell's caret, on and off once */
159
+ --motion-flash: 0.5s; /* a secret code's digit seen before its dot takes it */
160
+ --motion-shake: 0.4s; /* a wrong code shaking once */
161
+ --motion-pulse: 0.4s; /* a sheet's grip, ink to ink and back, while the app reloads */
162
+ --motion-run: 6s; /* a long title running sideways, until its own length is measured (--_run) */
163
+ --motion-stagger: 70ms; /* one stacked sheet after another as they tuck or untuck */
164
+ /* LAYERS (Lh 2026-10-20: no number a component writes for itself): the order of things INSIDE one piece, which keeps its own
165
+ stacking context. Under its ground, flat, over it, over that, the picked one on top */
166
+ --z-under-2: -2; --z-under: -1; --z-flat: 0; --z-over: 1; --z-over-2: 2; --z-over-3: 3;
167
+ --z-pile: 7; /* a sticky pile's top note: one over the six it can show, each note under it one lower (StickyPile) */
168
+ /* 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.
169
+ --ease-settle is the one a moved row or card lands with (Row, CardFeed: Web Animations read it) */
170
+ --ease: ease;
171
+ --ease-in: ease-in;
172
+ --ease-out: ease-out;
173
+ --ease-in-out: ease-in-out;
174
+ --ease-linear: linear;
175
+ --ease-settle: cubic-bezier(0.2, 0.8, 0.2, 1);
176
+ --ease-step: steps(1); /* on or off with nothing between: a code cell's caret blinking, a secret digit before its dot */
147
177
  --motion-press: 0.12s; /* a control giving under the finger and springing back (actions.css PRESS) */
148
178
  --press-scale: 0.96; /* how far a finger-sized control gives — a share of its size */
179
+ --scale-lift: 1.06; /* a stamp lifted off the paper while the finger holds it */
180
+ --scale-pick: 1.7; /* a mood's dot, the one picked */
181
+ --scale-arrive: 0.6; /* a stamp's remove button, the size it grows from */
149
182
  --press-dim: 0.6; /* how far a card-wide one (a row, a summary line, a card) fades under the finger */
150
183
  --press-shade: 0.93; /* how far something with a thing behind it (a peeked sheet, a postcard) darkens under the finger */
151
184
  --off: 0.4; /* a disabled control's fade */
185
+ /* FADES (Lh 2026-10-20: no opacity a component writes for itself): what each lets through. Nine uses had eight values; one name each
186
+ until the system settles on fewer */
187
+ --fade-faint: 0.25; /* a halo behind an avatar */
188
+ --fade-dim: 0.3; /* a sheet's grip at rest, a day that cannot be picked */
189
+ --fade-soft: 0.4; /* the dots between a trio's answers */
190
+ --fade-half: 0.5; /* a step's button while the step is busy */
191
+ --fade-hint: 0.55; /* a field's placeholder */
192
+ --fade-mild: 0.65; /* a route's line after its code */
193
+ --fade-near: 0.85; /* an icon row */
194
+ --fade-most: 0.9; /* a quote's mark */
152
195
 
153
196
  /* ---- z-index: the few layers that stack ABOVE ordinary content, low to high ---- */
154
197
  --z-bar: 30; /* .action-bar, .float-bar — above .sheet-stack sheets (--level 1-5) */
@@ -184,18 +227,31 @@
184
227
  --fs-hero: calc(var(--fs-base) * 2.5); /* 55 the one number on a sheet */
185
228
  --fs-giant: calc(var(--fs-base) * 2.9); /* 64 data bands */
186
229
 
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 */
230
+ /* TYPE (Lh 2026-10-20: every kind and every size has its tokens; a component writes no type value of its own).
231
+ 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. */
232
+ --fw-regular: 400; --fw-medium: 500; --fw-semibold: 600; --fw-strong: 650; --fw-bold: 700; --fw-heavy: 800; --fw-black: 900;
233
+ --lh-mark: 0.55; /* a quotation mark three lines tall, set into the first line */
234
+ --lh-band: 0.9; /* a data band's giant number */
235
+ --lh-badge: 0.95; /* a date box's two lines (JourneyBadge) */
236
+ --lh-solid: 1; /* a line as tall as its letters: a number in a disc, an icon's glyph, one big number */
237
+ --lh-tight: 1.1; /* a day's number, a tile's value */
238
+ --lh-field: 1.4; /* the words in a field */
239
+ --lh-hand: 1.6; /* a letter written by hand */
240
+ --lh-ruled: 1.7; /* a ruled sheet: the line under every line of writing */
241
+ --track-loose: 0.02em; /* initials in an avatar */
242
+ --track-caps: 0.08em; /* a kicker in capitals */
243
+ /* THE EIGHT KINDS: each has its face, weight, size (--fs-* above), leading and tracking, and its style --type-<kind> is made of them */
244
+ --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) */
245
+ --fw-h1: var(--fw-black); --lh-h1: 1.2; --track-h1: -0.015em;
246
+ --fw-h2: var(--fw-bold); --lh-h2: 1.25; --track-h2: -0.01em;
247
+ --fw-title: var(--fw-strong); --lh-title: 1.3; --track-title: normal;
248
+ --fw-body: var(--fw-regular); --lh-body: 1.5; --track-body: normal;
249
+ --fw-label: var(--fw-semibold); --lh-label: 1.2; --track-label: normal; /* chips, buttons */
250
+ --fw-caption: var(--fw-medium); --lh-caption: 1.35; --track-caption: normal;
251
+ --fw-stat: var(--fw-heavy); --lh-stat: 1.05; --track-stat: normal; /* big numbers */
252
+ --face-display: var(--font-display); --face-h1: var(--font-display); --face-h2: var(--font-display); --face-stat: var(--font-display);
253
+ --face-title: var(--font-ui); --face-body: var(--font-ui); --face-label: var(--font-ui); --face-caption: var(--font-ui);
254
+ /* --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
255
 
200
256
  /* ============================================================
201
257
  COLOR ROLES — wireframe defaults (grayscale).
@@ -278,6 +334,12 @@
278
334
  --ink-rail: color-mix(in srgb, currentColor 30%, transparent); /* todo steps, ghost edges */
279
335
  --accent-soft: color-mix(in srgb, var(--accent) 22%, transparent); /* calendar marks */
280
336
  --accent-mid: color-mix(in srgb, var(--accent) 50%, transparent);
337
+ /* 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
338
+ 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
339
+ grounds in every palette: their words are the onlight set. A theme retunes them here; a knob (--sticky-<tone>-bg) retunes one place */
340
+ --pastel-cotton: #f3efe2; --pastel-blush: #f6d5da; --pastel-tea-leaf: #d5e8c5; --pastel-custard: #f6e9a0; --pastel-salmon: #f3c7b3;
341
+ --pastel-iceberg: #cfe0ee; --pastel-cocoa: #dccbbd; --pastel-apricot: #f6d49a; --pastel-water: #bcd6f3;
342
+ --stamp-paper: #fffdf8;
281
343
  --scrim: color-mix(in srgb, #000 50%, transparent); /* dim stage, cover gradient — ALWAYS black, never the brand (Lh 2026-09-24) */
282
344
  }
283
345
 
@@ -302,6 +364,19 @@
302
364
  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
365
  onlight shade washed 14% into the surface (tone 2's formula) */
304
366
  :root, [data-palette], [data-mode] {
367
+ /* 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> */
368
+ --type-display: var(--fw-display) var(--fs-display) / var(--lh-display) var(--face-display);
369
+ --type-h1: var(--fw-h1) var(--fs-h1) / var(--lh-h1) var(--face-h1);
370
+ --type-h2: var(--fw-h2) var(--fs-h2) / var(--lh-h2) var(--face-h2);
371
+ --type-title: var(--fw-title) var(--fs-title) / var(--lh-title) var(--face-title);
372
+ --type-body: var(--fw-body) var(--fs-body) / var(--lh-body) var(--face-body);
373
+ --type-label: var(--fw-label) var(--fs-label) / var(--lh-label) var(--face-label);
374
+ --type-caption: var(--fw-caption) var(--fs-caption) / var(--lh-caption) var(--face-caption);
375
+ --type-stat: var(--fw-stat) var(--fs-stat) / var(--lh-stat) var(--face-stat);
376
+ /* shadows of paper laid on paper: mixed from the onlight ink, so re-derived where a palette is set */
377
+ --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) */
378
+ --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 */
379
+ --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
380
  --on-surface: color-mix(in srgb, var(--ink-ondark) calc(var(--surface-dark) * 100%), var(--ink-onlight));
306
381
  --on-surface-muted: color-mix(in srgb, var(--ink-ondark-muted) calc(var(--surface-dark) * 100%), var(--ink-onlight-muted));
307
382
  --action-ink: color-mix(in srgb, var(--ink-ondark) calc(var(--action-dark) * 100%), var(--ink-onlight));
@@ -322,7 +397,7 @@
322
397
  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
398
  tile) takes its ground's side: `--dark: inherit`. inkGrounds() measures it; docs/reference.md "Ink follows its ground" */
324
399
  :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,
400
+ .float-bar__pill, .snackbar__body, .delta, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .map__pin, .sheet--paper > :is(.sheet__body, .card__foot),
326
401
  .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
402
  --card-ink: color-mix(in srgb, var(--ink-ondark) calc(var(--dark) * 100%), var(--ink-onlight));
328
403
  --card-muted: color-mix(in srgb, var(--ink-ondark-muted) calc(var(--dark) * 100%), var(--ink-onlight-muted));
@@ -335,8 +410,8 @@
335
410
  }
336
411
  :where(body, [data-palette], [data-mode]) { --dark: var(--surface-dark); }
337
412
 
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)); }
413
+ :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); }
414
+ * { --_safe-b-open: max(0rem, var(--safe-bottom) - var(--kb, 0rem)); }
340
415
 
341
416
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
342
417
  @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 +427,7 @@
352
427
 
353
428
  /* ---- css/palette.css — color settings — the file you tune ---- */
354
429
  /* ============================================================
355
- cardds/palette.css — COLOR SETTINGS. The only file you tune.
430
+ cardds/palette.css — the palettes cardds ships. A project tunes its own theme.css (theme.template.css), never this file.
356
431
  Apply with <html data-palette="clay"> (or <body>).
357
432
  No attribute = wireframe grayscale from tokens.css.
358
433
  Each palette overrides role tokens only — components never change.
@@ -451,7 +526,7 @@
451
526
  [data-palette="orchard"] .field { background: var(--field-bg, #ffffff); color: var(--ink-onlight); }
452
527
  [data-palette="orchard"] .pin__cell { background: var(--pin-cell-bg, #ffffff); color: var(--ink-onlight); }
453
528
  /* the vivid ones, tried on the live parts (Lh 2026-09-25): primary = yellow · a user avatar = fuchsia · ring = sky · a switch on = sky · maple only on small dots (the count, dot a); the chosen stack tab stays moss */
454
- [data-palette="orchard"] .btn--primary { background: var(--btn-primary-bg, var(--highlight-2)); color: var(--btn-primary-ink, var(--ink-onlight)); }
529
+ [data-palette="orchard"] :is(.btn--primary, .icon-btn--primary) { background: var(--btn-primary-bg, var(--highlight-2)); color: var(--btn-primary-ink, var(--ink-onlight)); }
455
530
  [data-palette="orchard"] .avatar:not(.avatar--outline, .avatar--add, .avatar--on) { background: var(--avatar-bg, var(--highlight-4)); --dark: 1; }
456
531
  [data-palette="orchard"] .ring::before { background: conic-gradient(var(--highlight-3) calc(var(--value) * 1%), var(--ink-tint) 0); }
457
532
  [data-palette="orchard"] .toggle:checked { background: var(--highlight-3); --accent-dark: 0; } /* sky is a light ground: its knob is onlight */
@@ -568,6 +643,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
568
643
 
569
644
  /* icon from icons.svg sprite: <svg class="icon"><use href="icons.svg#bell"/></svg> */
570
645
  .icon { width: var(--icon-size, var(--icon)); height: var(--icon-size, var(--icon)); flex: none; }
646
+ /* 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
647
+ is the icon as it is. No fill: a drawing's own fill="none" is an attribute any declared fill would beat */
648
+ .icon {
649
+ display: var(--icon-display, block); vertical-align: var(--icon-align, baseline); margin: var(--icon-margin, 0);
650
+ padding: var(--icon-pad, 0); border-radius: var(--icon-radius, 0); background: var(--icon-bg, none); color: var(--icon-ink, inherit);
651
+ rotate: var(--icon-turn, none); translate: var(--icon-shift, none); transition: var(--icon-motion, all 0s); animation: var(--icon-spin, none);
652
+ }
571
653
  .icon--sm { width: var(--icon-sm-size, var(--icon-sm)); height: var(--icon-sm-size, var(--icon-sm)); }
572
654
  .icon--xs { width: var(--icon-xs-size, var(--icon-xs)); height: var(--icon-xs-size, var(--icon-xs)); } /* inside captions, chips, links */
573
655
  .icon--lg { width: var(--icon-lg-size, var(--icon-lg)); height: var(--icon-lg-size, var(--icon-lg)); } /* empty states */
@@ -616,12 +698,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
616
698
  position: relative; /* the box a .card--centre or a .screen__centre floats in (card.css) */
617
699
  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
700
  }
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
701
 
626
702
  /* ---- typography levels ---- */
627
703
  /* --fs-self: each level publishes its own size, for a consumer that scales a level without knowing which */
@@ -631,7 +707,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
631
707
  .t-title { font: var(--type-title); margin: 0; --fs-self: var(--fs-title); }
632
708
  .t-body { font: var(--type-body); margin: 0; }
633
709
  .t-label { font: var(--type-label); }
634
- .t-caption { font: var(--type-caption); }
710
+ .t-caption { font: var(--type-caption); color: var(--t-caption-ink, inherit); }
635
711
  .t-stat { font: var(--type-stat); letter-spacing: var(--track-display); margin: 0; font-variant-numeric: tabular-nums; }
636
712
  .t-stat--mono { font-family: var(--font-mono); letter-spacing: 0; } /* clocks, ETAs, codes */
637
713
  .t-stat--xl { font-size: var(--fs-hero); } /* the one number on a sheet */
@@ -648,9 +724,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
648
724
  --_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
725
  background-color: var(--mark-bg, var(--_bg));
650
726
  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 */
727
+ linear-gradient(calc(90deg + var(--tilt-base) * 10), var(--_pool), transparent 0.55em), /* ink pools where the pen lands */
728
+ linear-gradient(calc(270deg + var(--tilt-base) * 10), var(--_pool), transparent 0.4em), /* and a little where it lifts */
729
+ linear-gradient(calc(180deg + var(--tilt-base)), transparent 18%, var(--_pool) 52%, transparent 88%); /* the nib presses harder along its middle */
654
730
  color: var(--mark-ink, inherit);
655
731
  padding-inline: var(--mark-pad-x, 0.3em);
656
732
  border-radius: var(--mark-radius, var(--_shape));
@@ -664,7 +740,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
664
740
  /* 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
741
  The 1px is the one the platform needs to keep it in the accessibility tree, not a length of the design. */
666
742
  .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; }
743
+ .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
744
 
669
745
  /* ---- page: words straight on the base, no card around them — a
670
746
  heading, a paragraph, maybe a .cta-pack. Was .section until 2026-09-16
@@ -683,8 +759,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
683
759
  /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
684
760
  h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
685
761
  .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
762
  .page .cta-pack {
689
763
  display: flex;
690
764
  align-items: start; /* buttons keep their own height, never the row's */
@@ -709,12 +783,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
709
783
  /* ---- the turn: one flip mechanic for everything with two faces (a Postcard, a Card's `back`).
710
784
  The faces share one grid cell and turn on Y at --motion-flip; where motion is off they swap at once.
711
785
  The owner sets the box's size and the faces' look; .is-flipped on .flip shows the back. */
712
- .flip { perspective: 60rem; display: grid; }
786
+ .flip { perspective: calc(var(--tap-base) * 20); display: grid; }
713
787
  .flip__face {
714
788
  grid-area: 1 / 1;
715
789
  backface-visibility: hidden;
716
790
  -webkit-backface-visibility: hidden;
717
- transition: transform var(--motion-flip) ease-in-out;
791
+ transition: transform var(--motion-flip) var(--ease-in-out);
718
792
  }
719
793
  .flip__face--back { transform: rotateY(180deg); }
720
794
  .flip.is-flipped > .flip__face--front { transform: rotateY(-180deg); }
@@ -723,6 +797,131 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
723
797
  .flip__face { transition: none; }
724
798
  }
725
799
 
800
+ /* ==== 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) ==== */
801
+ /* 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:
802
+ a .step cancels it to touch the bar (actions.css) */
803
+ .screen:has(> .appbar) { --header-gap: var(--sp-2); padding-top: calc(var(--header-h) + var(--header-gap)); }
804
+ .screen.screen--fill:has(> .appbar) { padding-top: var(--safe-top); --header-gap: 0rem; }
805
+ /* 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) */
806
+ .screen:not(.screen--fill):has(> .action-bar:last-child, > .screen__centre) { display: flex; flex-direction: column; min-height: 100dvh; }
807
+ /* the centred group: the place of a .card--centre (card.css), for a small group of things */
808
+ .screen--fill > .screen__centre {
809
+ position: absolute;
810
+ top: var(--screen-centre);
811
+ translate: 0 -50%;
812
+ /* the cap: as a .card--centre's (card.css, which says why) */
813
+ --_lead: 0rem;
814
+ max-height: calc(2 * min(var(--screen-centre) - var(--_lead) - var(--screen-gap), 100% - var(--screen-centre) - var(--bar-reserve)));
815
+ overflow-y: auto;
816
+ overscroll-behavior: contain;
817
+ }
818
+ /* ---- centred block: the same place, for one thing to read that is not a
819
+ lone card — an empty state, a card with its quiet second button and the
820
+ pager under it. The block runs edge to
821
+ edge and keeps the screen's inset as padding, and
822
+ its children stack at the screen's own rhythm (Lh, 2026-09-11):
823
+ <div class="screen screen--fill"><header class="topbar">…</header>
824
+ <div class="screen__centre"><article class="card">…</article><button class="btn btn--quiet btn--block">…</button></div>
825
+ </div> */
826
+ .screen__centre {
827
+ inset-inline: 0;
828
+ padding-inline: var(--screen-pad);
829
+ display: grid;
830
+ gap: var(--screen-gap);
831
+ align-content: start;
832
+ }
833
+ .screen__centre > * { min-width: 0; }
834
+
835
+ /* ---- the same group on a plain .screen: centred when short, as long as it
836
+ likes when not (Lh, 2026-09-12) ----
837
+ The member sees one content area — whatever is between the top bar and the
838
+ bar — and reads it as a whole page: a short card sits at its middle, a long
839
+ one simply makes the page longer, runs under the bar (a sticky last row,
840
+ actions.css) and scrolls up like any page — nothing is capped, nothing
841
+ shrinks, no scroll box inside a box.
842
+ So on a .screen that is NOT --fill the group is in flow: the column is at
843
+ least one phone tall (step.css), the group takes the room after the top bar and
844
+ centres its children in it. The screen's own inset stays the inset.
845
+ <div class="screen"><header class="topbar">…</header>
846
+ <div class="screen__centre"><article class="card">…</article><div class="pager__at">…</div></div>
847
+ </div><div class="action-bar">…</div> */
848
+ .screen:not(.screen--fill) > .screen__centre { flex: 1; padding-inline: 0; align-content: center; }
849
+
850
+ .screen__centre:has(> :is(.stamp, .stamp-place)) { justify-items: center; text-align: center; }
851
+
852
+ /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
853
+ /* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
854
+ <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>
855
+ The heading and the far-right thing share the first row; a caption sits on
856
+ a row below (order 1), whatever the DOM order — so an author writes the
857
+ words in reading order. */
858
+ .card__head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
859
+ .card__head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
860
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
861
+ .sheet-stack__base > .screen__centre { padding-inline: 0; }
862
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
863
+ /* 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
864
+ street pattern never shows through the pin's body */
865
+ .map__pin > .icon { fill: var(--surface); }
866
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
867
+ /* the reading block: a .page (base.css) — the same words-on-the-base
868
+ block as anywhere else (it was .step__card, and a .card before 2026-09-16;
869
+ Lh: a step reads like a page, not a card in a page — so it IS the page's
870
+ block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
871
+ between things, the tokens .card publishes for the head and .bleed) and
872
+ gets the flex rule: it hugs its content, and when the column runs out (a
873
+ short phone, large text) it shrinks and scrolls inside itself instead of
874
+ pushing the bar or the sheet */
875
+ .step > .page, .step > .pages > .page {
876
+ gap: var(--card-gap);
877
+ padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
878
+ color: var(--on-surface); /* align-content: start is .page's own (base.css) — not restated here */
879
+ }
880
+ /* a lone .page carries the margin a walk's .pages does not (its own .page children do, below) —
881
+ the screen's gap, moved in here from above the step; the inset, taken back from the bleed.
882
+ It ends with the bottom layer's room, as a walk's page does: it scrolls only when the layer would cover its words
883
+ (timebank onboard-04, 2026-09-27 — its last Link lay under the bar; Lh: same rule as the walk) */
884
+ .step > .page { margin: var(--_lead, 0) var(--screen-pad) 0; padding-bottom: var(--bottom-h, 0rem); }
885
+ /* --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
886
+ 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. */
887
+ .step > .page--bleed { margin-inline: 0; padding-inline: var(--screen-pad); padding-block-start: var(--sp-2); overflow-x: hidden; }
888
+ .step > .pages > .page {
889
+ padding-inline: var(--screen-pad); /* the words keep the inset INSIDE it */
890
+ 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 */
891
+ 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) */
892
+ }
893
+ /* 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
894
+ the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
895
+ .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
896
+ .step > .empty-state { flex: none; }
897
+ .sheet__body > .empty-state:only-child { flex: none; }
898
+ /* ---- where it sits in what actions.css draws (docs/reference.md rule 15) ---- */
899
+ /* ---- top bar: back + screen title ---- */
900
+ .topbar .t-h1 {
901
+ text-transform: uppercase;
902
+ flex: 1;
903
+ min-width: 0;
904
+ overflow: hidden;
905
+ text-overflow: ellipsis;
906
+ white-space: nowrap;
907
+ padding-block: 0.15em; /* Thai marks survive the clip — see card.css, the head rule */
908
+ margin-block: -0.15em;
909
+ }
910
+ /* ---- where it sits in what media.css draws (docs/reference.md rule 15) ---- */
911
+ .postcard__band .icon-btn[aria-pressed="true"] .icon { fill: currentColor; }
912
+
913
+ /* ---- where it sits in what card.css draws: a walk's page (docs/reference.md rule 15) ---- */
914
+ .pages > .page { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
915
+
916
+ /* ---- where it sits in what step.css draws: the reading block (docs/reference.md rule 15) ---- */
917
+ .step:has(> .sheet--peek) > .page { padding-bottom: var(--sheet-peek); }
918
+ .step > .page {
919
+ flex: 0 1 auto;
920
+ min-height: 0;
921
+ overflow-y: auto;
922
+ overscroll-behavior: contain;
923
+ }
924
+
726
925
  /* ---- css/card.css — card boundary + list variants ---- */
727
926
  /* ============================================================
728
927
  cardds/card.css — the card boundary + list variants
@@ -755,10 +954,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
755
954
  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
955
  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
956
  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); }
957
+ .card--leaver { transition: translate var(--motion-sheet) var(--ease), rotate var(--motion-sheet) var(--ease); }
958
+ .card--away { --_y: calc(var(--leave-rise, 160%) * -1); rotate: var(--leave-turn, calc(var(--tilt-base) * -5)); }
760
959
  .card--away { translate: 0 var(--_y); }
761
- @media (prefers-reduced-motion: reduce) { :is(.card--leaver, .post-desk__card) { transition: none; } }
960
+ @media (prefers-reduced-motion: reduce) { .card--leaver { transition: none; } }
762
961
 
763
962
  /* each tone is a fill and a side; its words and glyphs follow from the side (tokens.css) */
764
963
  .card--2 { --card-bg: var(--card-2-bg); --dark: var(--card-2-dark); }
@@ -775,18 +974,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
775
974
  multi-line title can opt back out with .card__head--top. */
776
975
  .card__head {
777
976
  display: flex;
778
- flex-wrap: wrap; /* the caption takes a line of its own — no wrapper div */
977
+ flex-wrap: var(--card-head-wrap, wrap); /* the caption takes a line of its own — no wrapper div */
779
978
  align-items: center;
780
979
  justify-content: space-between;
781
980
  column-gap: var(--card-head-col-gap, var(--sp-3));
782
981
  row-gap: var(--card-head-row-gap, var(--sp-0));
982
+ min-height: var(--card-head-min-h, auto);
783
983
  }
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
984
  .card__head--top { align-items: flex-start; }
791
985
 
792
986
  .card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
@@ -806,12 +1000,23 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
806
1000
  .card__head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
807
1001
 
808
1002
  /* Skeleton head — the heading in a card's OWN head, and only there.
809
- The heading wraps freely: no line budget, no squeeze, no clamp (Lh
810
- 2026-09-16 — a height rule on words only distorts them; content that needs
811
- more rows takes more rows). The group that holds heading ·
1003
+ The heading wraps, no squeeze, up to three lines and cut there with an
1004
+ ellipsis (Lh 2026-10-21: "มันควรจะ max สามบรรทัด" — a request's title ran to
1005
+ four lines on a phone; reverses 2026-09-16's "no clamp"). The count is the knob
1006
+ --card-head-title-lines. The group that holds heading ·
812
1007
  caption is the head's own wrapping row (above) — no wrapper element, no
813
1008
  height, only the row gap. */
814
- .card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
1009
+ .card__head :is(h1, h2, h3) {
1010
+ overflow-wrap: anywhere;
1011
+ display: -webkit-box;
1012
+ -webkit-box-orient: vertical;
1013
+ -webkit-line-clamp: var(--card-head-title-lines, 3);
1014
+ line-clamp: var(--card-head-title-lines, 3);
1015
+ overflow: hidden;
1016
+ padding-block-start: var(--_mark-room); /* a stacked Thai mark rises over the first line's box: room inside the clip, given back outside */
1017
+ margin-block-start: calc(-1 * var(--_mark-room));
1018
+ --_mark-room: 0.2em;
1019
+ }
815
1020
 
816
1021
  /* a Stamp in a card's head (`Card end={<Stamp …/>}`): the words stand at the left, the stamp at the right BESIDE BOTH LINES — the heading and the
817
1022
  caption pair up beside it, not the stamp beside the heading alone with the caption left under it. The stamp is its own size (--stamp-size,
@@ -819,15 +1024,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
819
1024
  <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><span class="stamp">…</span></div> */
820
1025
  .card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
821
1026
  .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
1027
 
825
1028
  /* 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
1029
  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
1030
  so a theme that sets that knob still wins. */
828
1031
  .card > .card__head:first-child > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); }
829
1032
 
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
1033
+ /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are paper bands edge to
831
1034
  edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
832
1035
  clips, so the bands keep its corners. A two-faced card (below) carries the look on each face. */
833
1036
  :is(.card--bands, .card--bands > .card__face) {
@@ -848,12 +1051,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
848
1051
 
849
1052
  /* ---- two faces: the WHOLE card turns (`Card back`, Lh 2026-10-19). The article is the turning box (.flip, base.css) and only that;
850
1053
  each face is the card's paper — head · middle · foot — and both lie in one grid cell. The head and the foot are the same on both
851
- faces; the middle is one fixed height (--card-middle-h, the theme's call), so the card never changes height as it turns. The hidden
1054
+ faces; the card is as tall as its taller face and each face fills it, the middle taking what the head and the foot leave (2026-10-21,
1055
+ timebank: a fixed middle left a short front a tall empty band), so the card never changes height as it turns; --card-middle-h fixes
1056
+ the middle's height instead, the theme's call. The hidden
852
1057
  face is inert. Each face holds an unseen turn button: the keyboard's way to turn it; its ring is drawn on the face. */
853
1058
  .card--turns { background: none; border: 0; padding: 0; cursor: pointer; touch-action: manipulation; }
854
1059
  .card__face {
855
- display: grid;
856
- align-content: start;
1060
+ display: flex;
1061
+ flex-direction: column;
857
1062
  gap: var(--card-gap);
858
1063
  min-width: 0;
859
1064
  padding: var(--card-pad);
@@ -864,13 +1069,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
864
1069
  position: relative;
865
1070
  }
866
1071
  .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%); }
1072
+ .card__middle { flex: 1 0 auto; display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, auto); overflow: hidden; }
1073
+ .card__turn { position: absolute; inset: 0; width: var(--px); height: var(--px); padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
869
1074
  .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)); }
1075
+ .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
1076
 
875
1077
  /* ---- bleed: a scrolling track that runs to the container's edges ----
876
1078
  For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
@@ -891,15 +1093,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
891
1093
  nothing jumps. Chromium/Safari paint it through the ::-webkit-scrollbar
892
1094
  parts; Firefox gets its thin bar in the same ink. */
893
1095
  .bleed {
894
- margin-inline: calc(-1 * var(--card-pad, 0px));
895
- padding-inline: var(--card-pad, 0px);
1096
+ margin-inline: calc(-1 * var(--card-pad, 0rem));
1097
+ padding-inline: var(--card-pad, 0rem);
896
1098
  padding-block-end: var(--sp-2);
897
- scroll-padding-inline: var(--card-pad, 0px);
1099
+ scroll-padding-inline: var(--card-pad, 0rem);
898
1100
  flex-wrap: nowrap;
899
1101
  overflow-x: auto;
900
1102
  }
901
1103
  .bleed::-webkit-scrollbar { height: var(--scroll-w); }
902
- .bleed::-webkit-scrollbar-track { background: transparent; margin-inline: var(--card-pad, 0px); }
1104
+ .bleed::-webkit-scrollbar-track { background: transparent; margin-inline: var(--card-pad, 0rem); }
903
1105
  .bleed::-webkit-scrollbar-thumb { background: transparent; }
904
1106
  .bleed:is(:hover, .is-scrolling)::-webkit-scrollbar-thumb { background: currentColor; }
905
1107
  @supports not selector(::-webkit-scrollbar) {
@@ -913,29 +1115,28 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
913
1115
  /* foot: the card's action buttons on one line. One button takes the width,
914
1116
  up to four share it in even parts — so the gaps read even too — and from
915
1117
  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); }
1118
+ .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; color: var(--card-foot-ink, inherit); }
1119
+ .card__foot:has(> :nth-child(3)) > .btn { --btn-pad-x: var(--sp-2); }
922
1120
  /* 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
1121
  whole short row of its own, centred — it never shares the buttons' line */
924
1122
  .card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
925
1123
  .card__foot-link { flex: 1 0 100%; display: flex; justify-content: center; }
1124
+ /* round (CardFoot round): the foot holds a few round IconBtns, each its own size (they never share the line), as ONE group in the middle with
1125
+ a gap that does not follow the card's width. The gap is wider than the Btns' (a miss between two round targets): --card-foot-gap sets it */
1126
+ .card__foot--round { justify-content: center; gap: var(--card-foot-gap, var(--sp-6)); }
926
1127
 
927
1128
  /* media placeholder for wireframes — currentColor so it survives any palette */
928
1129
  .ph {
929
1130
  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));
1131
+ 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))),
1132
+ 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
1133
  border: var(--border-w) solid currentColor;
933
1134
  border-radius: var(--r-media);
934
1135
  opacity: var(--off);
935
- min-height: 10rem;
1136
+ min-height: calc(var(--tap-base) * 10 / 3);
936
1137
  }
937
- .ph--sm { min-height: 7.5rem; }
938
- .ph--md { min-height: 9rem; }
1138
+ .ph--sm { min-height: calc(var(--tap-base) * 5 / 2); }
1139
+ .ph--md { min-height: calc(var(--tap-base) * 3); }
939
1140
  /* ---- centred card: one thing to read, at the human centre of a fill screen ----
940
1141
  (.screen__centre below is the same placement for a small GROUP of things)
941
1142
  Where the centre of a phone IS, to the person holding it: the bottom fifth is
@@ -948,8 +1149,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
948
1149
  The card hugs its words and keeps the screen's inset. It is capped so that it
949
1150
  never reaches the title bar above it nor the bar's room below it — past that it
950
1151
  scrolls inside — from the same tokens those bars reserve their room with. */
951
- .card--centre,
952
- .screen--fill > .screen__centre {
1152
+ .card--centre {
953
1153
  position: absolute;
954
1154
  top: var(--screen-centre);
955
1155
  translate: 0 -50%;
@@ -966,38 +1166,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
966
1166
  :is(.topbar, .appbar) ~ :is(.card--centre, .screen__centre) { --_lead: var(--header-h); }
967
1167
  .card--centre { inset-inline: var(--screen-pad); }
968
1168
 
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
1169
  /* ---- the card that changes slides: out one side, the next in from the other ----
1002
1170
  A .screen__centre group that walks pages (data-slide="next" | "back" on the
1003
1171
  root while a step changes) turns its card like a page: the old card slides
@@ -1026,7 +1194,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1026
1194
  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
1195
  touch-action: pan-y; /* leaves vertical scroll to the browser; horizontal is the app's own drag, never a native pan */
1028
1196
  }
1029
- .pages > :is(.card, .page) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
1197
+ .pages > .card { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
1030
1198
  .pages > [inert] { visibility: hidden; }
1031
1199
  /* pages of WORDS (Lh, 2026-09-20): the same walk with .page pages, on the base — in a .step the
1032
1200
  .pages is the reading block (step.css), the round pager in the bar turns it, the same slideTo().
@@ -1038,7 +1206,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1038
1206
  ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
1039
1207
  ::view-transition-old(screen-out), ::view-transition-new(screen-in) { /* screen-*: slideScreen — one whole screen to the next (Lh 2026-09-25) */
1040
1208
  animation-duration: var(--motion-slide);
1041
- animation-timing-function: ease-out;
1209
+ animation-timing-function: var(--ease-out);
1042
1210
  mix-blend-mode: normal;
1043
1211
  }
1044
1212
  :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 +1221,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1053
1221
  ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
1054
1222
  ::view-transition-old(screen-out), ::view-transition-new(screen-in) { animation: none; }
1055
1223
  }
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
1224
 
1060
1225
  /* compact card inside a card (nested note, promo) */
1061
1226
  .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 +1229,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1064
1229
  stack.css; a stacked sheet is a plain .card, the material */
1065
1230
 
1066
1231
  /* 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; }
1232
+ .icon-row { display: flex; align-items: center; gap: var(--icon-row-gap, var(--sp-4)); opacity: var(--fade-near); }
1068
1233
 
1069
1234
  /* ---- card list: rows, each row its own boundary ---- */
1070
1235
  .card-list { display: grid; gap: var(--card-list-gap, var(--sp-3)); }
@@ -1100,7 +1265,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1100
1265
  .card__remove {
1101
1266
  --_size: var(--card-remove-size, calc(var(--tap-base) / 2));
1102
1267
  position: relative;
1103
- z-index: 1;
1268
+ z-index: var(--z-over);
1104
1269
  flex: none;
1105
1270
  align-self: flex-start;
1106
1271
  display: grid;
@@ -1118,7 +1283,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1118
1283
  cursor: pointer;
1119
1284
  }
1120
1285
  .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%; }
1286
+ .card__remove > .icon { --icon-size: 62%; }
1122
1287
  .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
1123
1288
 
1124
1289
  /* leading thumb/icon in a row */
@@ -1161,7 +1326,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1161
1326
  .feed-card__rest > :first-child { margin-top: var(--card-gap); }
1162
1327
  .feed-card--small .feed-card__rest { overflow: clip; visibility: hidden; }
1163
1328
  @media (prefers-reduced-motion: no-preference) {
1164
- .feed-card__more { transition: grid-template-rows var(--motion-sheet) ease; }
1329
+ .feed-card__more { transition: grid-template-rows var(--motion-sheet) var(--ease); }
1165
1330
  .feed-card--small .feed-card__rest { transition: visibility 0s var(--motion-sheet); }
1166
1331
  }
1167
1332
 
@@ -1172,7 +1337,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1172
1337
 
1173
1338
  .delta {
1174
1339
  font: var(--delta-font, var(--type-caption));
1175
- font-weight: 700;
1340
+ font-weight: var(--fw-bold);
1176
1341
  padding: var(--delta-pad-y, var(--sp-1)) var(--delta-pad-x, var(--sp-2));
1177
1342
  border-radius: var(--delta-radius, var(--r-chip));
1178
1343
  background: var(--delta-bg, var(--card-3-bg));
@@ -1199,7 +1364,286 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1199
1364
  scroll-snap-align: start; scroll-snap-stop: always;
1200
1365
  display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--card-gap); align-content: start;
1201
1366
  }
1202
- .panes > .pager__at { justify-self: center; }
1367
+
1368
+ /* ==== the card's variants another family draws: Cover and the fold (media), Band (numbers) (#62: a block is written in its own file) ==== */
1369
+
1370
+ /* ---- cover: the image IS the card; text over a scrim ----
1371
+ <article class="card card--cover"><div class="ph" role="img"></div>
1372
+ <div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
1373
+ <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
1374
+ <div class="row">…meta…</div></article> */
1375
+ .card--cover {
1376
+ --card-bg: var(--card-3-bg);
1377
+ --dark: var(--card-3-dark);
1378
+ --accent: var(--card-ink);
1379
+ --accent-dark: calc(1 - var(--dark));
1380
+ min-height: var(--_cover-min-h);
1381
+ align-content: space-between;
1382
+ overflow: hidden;
1383
+ isolation: isolate;
1384
+ }
1385
+ .card--cover > img,
1386
+ .card--cover > .ph {
1387
+ position: absolute;
1388
+ inset: 0;
1389
+ width: 100%;
1390
+ height: 100%;
1391
+ object-fit: cover;
1392
+ z-index: var(--z-under-2);
1393
+ border: 0;
1394
+ border-radius: 0;
1395
+ min-height: 0;
1396
+ }
1397
+ /* scrim: dark at both ends so the meta stays readable */
1398
+ .card--cover::before {
1399
+ content: "";
1400
+ position: absolute;
1401
+ inset: 0;
1402
+ z-index: var(--z-under);
1403
+ background: linear-gradient(to bottom, var(--scrim), transparent 40% 60%, var(--scrim));
1404
+ }
1405
+
1406
+ /* ---- fold: a sticky-note corner on any card (decorative) ----
1407
+ No border. The card's outline follows its border box, so it ran straight
1408
+ across the squared bottom-right corner and cut through the fold's diagonal —
1409
+ the corner read as a notch with a line through it rather than a lifted flap.
1410
+ The COLOUR goes transparent rather than the border going to 0, so the box
1411
+ keeps its geometry and a fold card still measures the same as the cards
1412
+ beside it. */
1413
+ .card--fold {
1414
+ border-color: transparent;
1415
+ border-bottom-right-radius: 0;
1416
+ background-image: linear-gradient(to top left,
1417
+ 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 */
1418
+ color-mix(in srgb, currentColor 18%, var(--card-bg)) 50% 100%);
1419
+ background-repeat: no-repeat;
1420
+ background-position: bottom right;
1421
+ background-size: var(--sp-8) var(--sp-8);
1422
+ background-origin: border-box; /* the flap sits on the true corner, not inside the (transparent) border */
1423
+ /* the corner is CUT: the triangle under the flap shows what is behind the card. Painted in the card's own colour it
1424
+ read as the square corner of a second card underneath (Lh 2026-09-23). The polygon reaches --sp-12 past the other
1425
+ edges so a coloured palette's shadow is not clipped there. */
1426
+ 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)),
1427
+ calc(100% - var(--sp-8) - var(--sp-12)) calc(100% + var(--sp-12)), calc(-1 * var(--sp-12)) calc(100% + var(--sp-12)));
1428
+ }
1429
+
1430
+ /* ---- band: data card; numbers.css draws its parts and the band stack ---- */
1431
+ .card--band {
1432
+ grid-template-columns: 1fr auto;
1433
+ grid-template-areas: "label num" "foot num";
1434
+ align-items: end;
1435
+ gap: var(--sp-2) var(--sp-4);
1436
+ min-height: var(--card-band-min-h, var(--tile-h));
1437
+ }
1438
+
1439
+ /* ---- where it sits in what base.css draws (docs/reference.md rule 15) ---- */
1440
+ .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 */
1441
+ .page > .ph { width: 100%; height: var(--page-media-h, calc(var(--tap-base) * 14 / 3)); }
1442
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
1443
+ .sheet-stack > .card {
1444
+ grid-area: 1 / 1; /* every card shares the stack's cell; later ones offset down */
1445
+ border-bottom-left-radius: 0;
1446
+ border-bottom-right-radius: 0;
1447
+ border-bottom: 0;
1448
+ align-content: start;
1449
+ align-self: start; /* own natural height (padding + head) by default */
1450
+ z-index: var(--level, var(--z-over)); /* explicit paint order, 1 = back of the stack */
1451
+ }
1452
+ /* THE FRONT SHEET is the last .card, not the last child: a --closed stack
1453
+ may carry a .sheet-stack__base after its sheets (below). Written once here
1454
+ as "a card no card follows" so every rule about the front sheet agrees. */
1455
+ .sheet-stack > .card:not(:has(~ .card)) {
1456
+ align-self: stretch; /* the front card fills the remaining stack height */
1457
+ }
1458
+ .sheet-stack > .card:nth-child(2) { margin-top: var(--stack-step); }
1459
+ .sheet-stack > .card:nth-child(3) { margin-top: calc(2 * var(--stack-step)); }
1460
+ .sheet-stack > .card:nth-child(4) { margin-top: calc(3 * var(--stack-step)); }
1461
+ .sheet-stack > .card:nth-child(5) { margin-top: calc(4 * var(--stack-step)); }
1462
+ /* stack headers: uniform — [back?] title left, icons right, one height,
1463
+ same gap to content on every card */
1464
+ .sheet-stack > .card > .card__head {
1465
+ align-items: center;
1466
+ justify-content: flex-start;
1467
+ min-height: var(--tap);
1468
+ }
1469
+ .sheet-stack--display > .card > .card__head { align-items: start; min-height: 0; }
1470
+ /* ============================================================
1471
+ .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
1472
+
1473
+ A sheet (a .card — the material, sheet.css's single sheet is the same
1474
+ stuff) is rigid: it can move in, move out, or be covered — it can
1475
+ never change shape. So every sheet here is the SAME full height,
1476
+ always, set once and never touched again by anything. A "peek" is
1477
+ not a short sheet; it's a full sheet the next one happens to cover,
1478
+ showing only the strip above it, exactly like a sheet of paper part
1479
+ way out of a real stack.
1480
+
1481
+ Because of that, the whole animation is one property: translate.
1482
+ Nothing resizes, nothing re-stacks — z-index is the plain resting
1483
+ --level order and is never touched, since the order already says
1484
+ what has to happen. Cards after the tapped one sit IN FRONT of it,
1485
+ so they're what's covering it, so they're what must leave. Cards
1486
+ before it are already BEHIND it, so they need do nothing at all
1487
+ except let themselves be covered as it rises over them.
1488
+
1489
+ .is-open — the tapped card, moved up to the top of the stack. Its
1490
+ own resting offset is margin-top (below); opening just
1491
+ translates that offset back off, landing it flush at
1492
+ the stack's top with its full height exactly filling
1493
+ to the bottom.
1494
+ .is-before — an earlier card, nudged one step down so it slips
1495
+ under the next card's resting strip and is covered
1496
+ there. Each carries its own --stagger (set in JS: 0
1497
+ for the one nearest the tapped card, counting up
1498
+ further back) so they tuck away, and later un-tuck,
1499
+ one at a time.
1500
+ .is-after — a later card, moved out below, together, no stagger.
1501
+ ============================================================ */
1502
+ .sheet-stack--tap > .card {
1503
+ height: 100%; /* every card identical and full length, always — the one
1504
+ size in this whole component, never toggled, never transitioned. At a
1505
+ resting margin-top of n steps it simply overhangs the stack's bottom
1506
+ by n steps (clipped by .screen--fill), so translating it up by exactly
1507
+ that much lands it filling the stack precisely. */
1508
+ overflow: clip;
1509
+ transition: translate var(--motion-sheet) var(--ease); /* the only animated property here */
1510
+ transition-delay: calc(var(--stagger, 0) * var(--motion-stagger)); /* 0 for everything
1511
+ except a staggered is-before card: the card renders --stagger as it tucks or un-tucks */
1512
+ }
1513
+ .sheet-stack--tap > .card.is-open {
1514
+ /* up by its own resting offset — pure movement, nothing resizes */
1515
+ translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step) + var(--_pull, 0rem));
1516
+ cursor: default;
1517
+ overflow-y: auto;
1518
+ overflow-x: clip;
1519
+ overscroll-behavior: contain;
1520
+ }
1521
+ .sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
1522
+ .sheet-stack--tap > .card > .card__head { touch-action: none; }
1523
+ .sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
1524
+ padding-top: calc(var(--card-pad) + var(--_refresh, 0rem));
1525
+ transition: translate var(--motion-sheet) var(--ease), padding-top var(--motion-sheet) var(--ease);
1526
+ }
1527
+ /* the front sheet — the one open on arrival — never travels up (it is not
1528
+ tappable), so its resting offset would leave its last n steps below the
1529
+ screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
1530
+ alone is cut to the room it actually has, and it scrolls like an opened
1531
+ sheet does. Every other sheet keeps the one full height. */
1532
+ .sheet-stack--tap:not(.sheet-stack--closed) > .card:not(:has(~ .card)) {
1533
+ height: calc(100% - var(--slot, 0) * var(--stack-step));
1534
+ overflow-y: auto;
1535
+ overflow-x: clip;
1536
+ overscroll-behavior: contain;
1537
+ }
1538
+ /* a scrolling sheet shows no scrollbar (a desktop with a mouse would paint one
1539
+ down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself
1540
+ (the hiding rule is base.css's, shared with every other scrolling track) */
1541
+ .sheet-stack--tap > .card.is-before { translate: 0 var(--stack-step); }
1542
+ .sheet-stack--tap > .card.is-after { translate: 0 100vh; }
1543
+ .sheet-stack--closed > .card { translate: 0 calc(var(--_headroom) - var(--_lift, 0rem)); }
1544
+ .sheet-stack--closed > .card.is-dragging { transition: none; }
1545
+ .sheet-stack--tap:not(.sheet-stack--closed) > .card.is-dragging:not(.is-open) {
1546
+ translate: 0 calc(-1 * var(--_lift, 0rem));
1547
+ transition: none;
1548
+ }
1549
+ .sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
1550
+ .sheet-stack--closed > .card.is-after { translate: 0 100vh; }
1551
+ /* 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
1552
+ line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
1553
+ :is([data-palette], [data-finish]) .sheet-stack__base .card { box-shadow: none; }
1554
+ /* 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
1555
+ 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
1556
+ only, the sheet's size is untouched. A pull on the head keeps them square until the finger lets go. */
1557
+ .sheet-stack--flood > .card { transition: translate var(--motion-sheet) var(--ease), border-radius var(--motion-sheet) var(--ease); }
1558
+ .sheet-stack--flood > .card.is-open { border-top-left-radius: 0; border-top-right-radius: 0; }
1559
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head {
1560
+ position: relative;
1561
+ padding-left: 0;
1562
+ transition: padding-left var(--motion-sheet) var(--ease);
1563
+ }
1564
+ /* ---- a stacked sheet with a SheetBody has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
1565
+ pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a CardFoot, the CTA) at the bottom.
1566
+ The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no SheetBody
1567
+ keeps the old way: the whole open card scrolls. ---- */
1568
+ .sheet-stack--tap > .card:has(> .sheet__body) { display: flex; flex-direction: column; gap: var(--card-gap); }
1569
+ .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
1570
+ /* a paper stack's foot is paper to the card's edges, and it covers the tinted flex gap above it so the
1571
+ button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
1572
+ where it was — the padding gives back what the bleed takes. */
1573
+ .sheet-stack > .card.sheet--paper > .card__foot {
1574
+ background: var(--action-bg);
1575
+ color: var(--card-ink);
1576
+ margin-top: calc(-1 * var(--card-gap));
1577
+ margin-inline: calc(-1 * var(--card-pad));
1578
+ margin-bottom: calc(-1 * var(--card-pad));
1579
+ padding-top: var(--card-gap);
1580
+ padding-inline: var(--card-pad);
1581
+ padding-bottom: var(--card-pad);
1582
+ }
1583
+ .stack-sheet > .card__head { flex-wrap: nowrap; }
1584
+ /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
1585
+ 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
1586
+ (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
1587
+ .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))); }
1588
+ :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); }
1589
+ .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))); }
1590
+ /* 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),
1591
+ 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) */
1592
+ .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
1593
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
1594
+ .sheet > .card__foot {
1595
+ 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 */
1596
+ align-items: end;
1597
+ align-content: end;
1598
+ padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
1599
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
1600
+ pointer-events: none;
1601
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
1602
+ transition: translate var(--motion-sheet) var(--ease), visibility 0s;
1603
+ }
1604
+ .sheet.is-dragging > .card__foot { transition: none; }
1605
+ /* 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
1606
+ LEAVES AND ARRIVES WITH ITS SHEET — solid the whole way, hidden only once the slide is over. It used to stay in place and
1607
+ fade: the body slid through the strip behind a half-transparent button (timebank, Lh 2026-10-01: "ตัวหนังสือลอดใต้ปุ่ม"). */
1608
+ :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); }
1609
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
1610
+ /* 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
1611
+ (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
1612
+ padding above AND below — so the words begin near the middle and, when a big text size runs them long, scroll up clear.
1613
+ The walk shows no scrollbar. */
1614
+ .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) */
1615
+ .step > .pages > .page > :is(img, .ph):first-child {
1616
+ width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
1617
+ max-width: none;
1618
+ margin-inline: calc(-1 * var(--screen-pad));
1619
+ height: calc(var(--_lead, 0rem) + var(--page-media-h, calc(var(--tap-base) * 14 / 3)));
1620
+ max-height: none;
1621
+ border-radius: 0;
1622
+ border-inline: 0;
1623
+ border-top: 0;
1624
+ object-fit: cover;
1625
+ }
1626
+ /* ---- where it sits in what numbers.css draws (docs/reference.md rule 15) ---- */
1627
+ .band-stack > .card--band { border: 0; border-radius: 0; box-shadow: none; }
1628
+ .band-stack > .card--band + .card--band { border-top: var(--border-w) solid var(--hairline); }
1629
+ /* ---- where it sits in what sticky.css draws (docs/reference.md rule 15) ---- */
1630
+ .sticky__head > .ph { flex: 1 1 0; min-width: 0; } /* a picture in the head takes the note's width */
1631
+ .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 */
1632
+
1633
+ /* ---- where it sits in what media.css draws: a postcard's picture (docs/reference.md rule 15) ---- */
1634
+ .postcard__front .postcard__turn > .ph { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border: 0; border-radius: 0; min-height: 0; }
1635
+
1636
+ /* ---- where it sits in what stack.css draws: a sheet pulled to refresh holds still (docs/reference.md rule 15) ---- */
1637
+ .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
1638
+
1639
+ /* ---- where it sits in what step.css draws: the reading block, a walk of pages (docs/reference.md rule 15) ---- */
1640
+ .step:has(> .sheet--peek) > .pages { padding-bottom: var(--sheet-peek); }
1641
+ .step > .pages {
1642
+ flex: 0 1 auto;
1643
+ min-height: 0;
1644
+ overflow-y: auto;
1645
+ overscroll-behavior: contain;
1646
+ }
1203
1647
 
1204
1648
  /* ---- css/stack.css — sheet stack: .sheet-stack — the menu, sheets stacked, heads peek, tap opens ---- */
1205
1649
  /* ============================================================
@@ -1257,22 +1701,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1257
1701
  Lh, 2026-09-11). --_n: how many sheets the stack holds, read from the DOM
1258
1702
  by :has below so the closed geometry needs no script and no per-stack
1259
1703
  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; }
1704
+ .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
1705
  .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
1706
  --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
1707
  }
1277
1708
  .sheet-stack:has(> .card:nth-child(2)) { --_n: 2; }
1278
1709
  .sheet-stack:has(> .card:nth-child(3)) { --_n: 3; }
@@ -1283,119 +1714,53 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1283
1714
  script at all. --slot is that place in steps; the tap animation reads it
1284
1715
  to know how far up to travel. Five is the practical peek depth. */
1285
1716
  .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)); }
1717
+ .sheet-stack > .card:nth-child(2) { --slot: 1; --level: 2; }
1718
+ .sheet-stack > .card:nth-child(3) { --slot: 2; --level: 3; }
1719
+ .sheet-stack > .card:nth-child(4) { --slot: 3; --level: 4; }
1720
+ .sheet-stack > .card:nth-child(5) { --slot: 4; --level: 5; }
1290
1721
 
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
1722
  /* NOT a skeleton head: a peek is one line at its own size, and the display
1299
1723
  stack deliberately runs a second line under the next card. */
1300
1724
  .sheet-stack > .card > .card__head :is(h1, h2, h3) {
1301
1725
  font-size: var(--fs-h1);
1302
- line-height: 1;
1303
- letter-spacing: -0.01em;
1726
+ line-height: var(--lh-solid);
1727
+ letter-spacing: var(--track-h2);
1304
1728
  margin-right: auto; /* pins the icon cluster right */
1305
1729
  display: block;
1306
- overflow-wrap: normal;
1307
- }
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 */
1730
+ overflow-wrap: normal;
1372
1731
  }
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;
1732
+
1733
+ /* display stack: the giant title IS the peek — line one shows, line two is
1734
+ clipped by the next card (recents / library lists) */
1735
+ .sheet-stack--display { --stack-step: calc(var(--tap-base) * 13 / 6); }
1736
+ .sheet-stack--display > .card > .card__head :is(h1, h2, h3) {
1737
+ font: var(--type-display);
1738
+ letter-spacing: var(--track-display);
1739
+ /* the FIRST line sits on the centre of the peek strip (--stack-step tall,
1740
+ measured from the card's top edge); line two runs under the next card */
1741
+ margin-top: calc((var(--stack-step) - 1lh) / 2 - var(--sp-5) - var(--_mark-room)); /* --_mark-room: card.css, the room for the marks over line one */
1380
1742
  }
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;
1743
+ /* fanned stack: a toggle opens the stack wider so every peek shows more */
1744
+ .sheet-stack--fanned { --stack-step: calc(var(--tap-base) * 19 / 6); }
1745
+
1746
+ /* a peeked card in a plain stack hides its own body. A --tap stack does
1747
+ NOT: there, every card is full height and full content always, and a
1748
+ peek is simply the strip of it the next card doesn't cover — the way a
1749
+ card in a real deck is never a shorter card, just a mostly hidden one. */
1750
+ .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.card__head) { display: none; }
1751
+
1752
+ /* ---- .sheet-stack--tap — tap a peeked sheet open; a back button returns it. How its sheets move (one property: translate) is
1753
+ told over their rules, where they sit, in card.css. ---- */
1754
+ .sheet-stack--tap > .card {
1755
+ --_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
1756
  }
1391
- .sheet-stack > .card > .sheet__body.is-refreshing,
1392
- .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
1757
+
1393
1758
  /* the GRIP (Lh 2026-10-05): four small dots at the middle of every sheet's top edge — a sign that this card
1394
1759
  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
1760
  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
1761
  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. */
1762
+ 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.
1763
+ Nothing travels. --_grip is the mix, 0 the ondark ink → 1 the onlight ink, private. */
1399
1764
  @property --_grip { syntax: "<number>"; inherits: false; initial-value: 0; }
1400
1765
  .stack-card {
1401
1766
  --_grip-dot: var(--sp-1);
@@ -1411,8 +1776,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1411
1776
  calc(50% + 0.5 * var(--_grip-step)) var(--_grip-y), calc(50% + 1.5 * var(--_grip-step)) var(--_grip-y);
1412
1777
  }
1413
1778
  .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;
1779
+ --_grip-ink: color-mix(in srgb, var(--ink-onlight) calc(var(--_grip) * 100%), var(--ink-ondark));
1780
+ animation: stack-grip-dissolve var(--motion-pulse) var(--ease-in-out) infinite alternate;
1416
1781
  }
1417
1782
  @keyframes stack-grip-dissolve { from { --_grip: 0; } to { --_grip: 1; } }
1418
1783
  /* 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 +1797,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1432
1797
  @media (prefers-reduced-motion: reduce) {
1433
1798
  .stack-card.is-open.is-loading { animation: none; --_grip-ink: currentColor; }
1434
1799
  }
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
1800
 
1452
1801
  /* ============================================================
1453
1802
  .sheet-stack--closed — nothing open on arrival. Every sheet, the front
@@ -1464,14 +1813,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1464
1813
  lands flush at the top. is-before tucks one step below its resting place.
1465
1814
  ============================================================ */
1466
1815
  .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
1816
 
1476
1817
  /* .sheet-stack__base — the base's content, in the headroom of a closed stack:
1477
1818
  behind every sheet (level 0), the stack's inset put back (the stack bleeds),
@@ -1481,9 +1822,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1481
1822
  .sheet-stack > .sheet-stack__base {
1482
1823
  grid-area: 1 / 1;
1483
1824
  align-self: start;
1484
- z-index: 0;
1485
- --_under: 0px;
1486
- height: calc(var(--_headroom, 0px) + var(--_under));
1825
+ z-index: var(--z-flat);
1826
+ --_under: 0rem;
1827
+ height: calc(var(--_headroom, 0rem) + var(--_under));
1487
1828
  padding-inline: var(--screen-pad);
1488
1829
  --_gap: var(--card-gap, var(--sp-5));
1489
1830
  display: grid;
@@ -1500,7 +1841,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1500
1841
  /* 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
1842
  (Lh 2026-10-06). The base already keeps the screen's inset, so the centre block gives its own back. */
1502
1843
  .sheet-stack__base:has(> .screen__centre:only-child) { align-content: center; }
1503
- .sheet-stack__base > .screen__centre { padding-inline: 0; }
1504
1844
  /* 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
1845
  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
1846
  .sheet-stack__base:not(:has(> .page:only-child, > .screen__centre:only-child)) {
@@ -1523,9 +1863,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1523
1863
  overscroll-behavior: contain;
1524
1864
  touch-action: pan-y;
1525
1865
  }
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
1866
 
1530
1867
  /* ============================================================
1531
1868
  .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 +1884,17 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1547
1884
  ============================================================ */
1548
1885
  .screen--fill:has(> .sheet-stack--flood) { --_bar-gap: var(--sp-2); }
1549
1886
  .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
1887
  .sheet-stack--flood { --_bar: calc(var(--top-h, var(--header-h)) + var(--_bar-gap)); }
1558
1888
  .sheet-stack--flood > .sheet-stack__base {
1559
1889
  margin-top: var(--_bar);
1560
- height: calc(var(--_headroom, 0px) - var(--_bar) + var(--_under));
1890
+ height: calc(var(--_headroom, 0rem) - var(--_bar) + var(--_under));
1561
1891
  }
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
1892
 
1572
1893
  /* back button: lives in a slot the header reserves once open, and DISSOLVES
1573
1894
  into it — an opacity fade in place, never a slide in from the left (the
1574
1895
  slide read as a second thing moving while the sheet itself moved; Lh,
1575
1896
  2026-09-11). Only the sheet travels; the button appears where it is. */
1576
1897
  /* 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
1898
  :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head > .sheet-stack__back {
1583
1899
  position: absolute;
1584
1900
  left: 0;
@@ -1586,7 +1902,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1586
1902
  translate: 0 -50%;
1587
1903
  opacity: 0;
1588
1904
  visibility: hidden;
1589
- transition: opacity var(--motion-sheet) ease, visibility var(--motion-sheet);
1905
+ transition: opacity var(--motion-sheet) var(--ease), visibility var(--motion-sheet);
1590
1906
  /* SMALL (see "the back button, SMALL" below) */
1591
1907
  width: var(--_back-size);
1592
1908
  height: var(--_back-size);
@@ -1599,63 +1915,16 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1599
1915
  visibility: visible;
1600
1916
  }
1601
1917
 
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
1918
  .sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
1609
1919
  .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
- }
1920
+ /* a paper stack's foot is paper (card.css paints it): its ink reads the action's side */
1921
+ .sheet-stack > .card.sheet--paper > .card__foot { --dark: var(--action-dark); }
1652
1922
 
1653
1923
  /* ---- 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
1924
  filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
1655
1925
  .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
1926
  /* 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
1927
  (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
1928
  .stack-sheet__title {
1660
1929
  flex: 1; min-width: 0; overflow-x: clip; overflow-y: visible; white-space: nowrap; overflow-wrap: normal; /* clip SIDEWAYS only:
1661
1930
  hidden clipped the Thai tone marks above a line-height-1 line (เพื่อน lost its ไม้เอก — Lh 2026-09-24) */
@@ -1664,7 +1933,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1664
1933
  }
1665
1934
  .stack-sheet__run { display: inline-block; }
1666
1935
  .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 */
1936
+ 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
1937
  }
1669
1938
  @keyframes stack-sheet-run { 0%, 20% { translate: 0; } 85%, 100% { translate: calc(-1 * var(--_over)); } }
1670
1939
  @media (prefers-reduced-motion: reduce) {
@@ -1677,11 +1946,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1677
1946
  /* the disc fades with the sheet: in as it rises, out as it folds, and across when another tab is pressed.
1678
1947
  scale stays on the press (actions.css) so the give under a finger stays quick */
1679
1948
  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;
1949
+ scale var(--motion-press) var(--ease-out),
1950
+ background-color var(--motion-sheet) var(--ease),
1951
+ color var(--motion-sheet) var(--ease),
1952
+ border-color var(--motion-sheet) var(--ease),
1953
+ box-shadow var(--motion-sheet) var(--ease);
1685
1954
  }
1686
1955
  .stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
1687
1956
  position: absolute;
@@ -1694,7 +1963,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1694
1963
  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
1964
  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
1965
  font: var(--type-caption);
1697
- line-height: 1;
1966
+ line-height: var(--lh-solid);
1698
1967
  pointer-events: none;
1699
1968
  }
1700
1969
  /* 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 +1979,25 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1710
1979
  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
1980
  .card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
1712
1981
 
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
1982
  /* 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
1983
  while open; the last tab then sits on the inset (no reach into the padding) */
1719
1984
  .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
1985
  .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) */
1986
+ .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
1987
  .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--_back-size) - var(--tap-sm)) / 2); }
1725
1988
 
1726
1989
  /* the open sheet's title folds it (SheetStack.tsx): it says so to a pointer */
1727
1990
  .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
1991
  .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
1992
 
1993
+ /* ---- where it sits in what base.css draws (docs/reference.md rule 15) ---- */
1994
+ /* min-height: 0 — a flex item's floor is its content, and the stack's content
1995
+ is its own full-height sheets, so without it the stack grew to the whole
1996
+ screen and its open sheet ran past the bottom edge by the bars' height,
1997
+ unreachable by scrolling (timebank home, 2026-09-13). The stack IS the room
1998
+ left under the bars; every sheet fills exactly that. */
1999
+ .screen--fill > .sheet-stack { flex: 1; min-height: 0; }
2000
+
1733
2001
  /* ---- css/sheet.css — sheet single: .sheet — one card as a bottom sheet, peek / half / full ---- */
1734
2002
  /* ============================================================
1735
2003
  cardds/sheet.css — sheet single: one card as a bottom sheet.
@@ -1804,7 +2072,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1804
2072
  inset: 0;
1805
2073
  top: var(--_top, 0); /* a stage keeps its top bar's line free — see .sheet-stage */
1806
2074
  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;
2075
+ transition: translate var(--motion-sheet) var(--ease);
1808
2076
 
1809
2077
  /* handle · head · body: a column, the body takes what is left and scrolls
1810
2078
  (below) — so the handle and the head never move, whatever the content */
@@ -1837,7 +2105,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1837
2105
  that holds that way's WHOLE form, its submit on the sheet's foot, leading to the next page. The base
1838
2106
  carries no control. It is NOT for a form taken apart — one sheet per field, a "send" left on the base:
1839
2107
  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); }
2108
+ .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
2109
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
1842
2110
 
1843
2111
  /* the body — everything under the head, the one thing in a sheet that
@@ -1882,23 +2150,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1882
2150
  One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
1883
2151
  /* …and the link row under the button, when there is one (its line + the gap over it) */
1884
2152
  .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
2153
  .sheet > .card__foot > * { pointer-events: auto; }
1896
2154
  .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
2155
  /* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
1903
2156
  box is never smaller than its padding — so the pinned parts must not give way to it: the body runs past
1904
2157
  the sheet's clipped edge instead, where nothing shows anyway. At peek and away no part of the body shows. */
@@ -1915,14 +2168,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1915
2168
  touch-action: none hands a touch drag to the Sheet component instead of to the scroller. */
1916
2169
  .sheet__handle {
1917
2170
  position: absolute;
1918
- z-index: 1;
2171
+ z-index: var(--z-over);
1919
2172
  top: calc((var(--card-pad) - var(--sp-1)) / 2); /* the dots' top: (pad − 4) / 2. The line starts there too */
1920
2173
  left: 50%;
1921
2174
  translate: -50% 0;
1922
2175
  border: 0;
1923
2176
  color: inherit; /* a <button> has its own UA colour; the mark is the card's ink */
1924
2177
  background: currentColor;
1925
- opacity: 0.3;
2178
+ opacity: var(--fade-dim);
1926
2179
  width: calc(var(--sp-base) * 18); /* 72: the line. The grab is the ::before, not this width */
1927
2180
  height: var(--_handle);
1928
2181
  border-radius: var(--sheet-handle-radius, var(--r-chip));
@@ -1931,22 +2184,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1931
2184
  touch-action: none;
1932
2185
  cursor: grab;
1933
2186
  }
1934
- /* paper: the head keeps the sheet's own colour, the content is white (Lh 2026-10-05).
2187
+ /* 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
2188
  The body and the foot read as paper; the handle and the head stay on the card.
1936
2189
  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
2190
+ The first field would sit on that colour seam. The paper's inset is --sp-6 (24px at
1938
2191
  the default spacing), the body's own top padding, and only when the sheet is paper.
1939
2192
  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. */
2193
+ inset: the stack's own gap under the head is the card's colour, so the row still needs this paper.
2194
+ The paper takes the side of the controls' paper (--action-dark): on a dark sheet its words still turn to onlight. */
1942
2195
  .sheet--paper > .sheet__body {
1943
- --card-bg: var(--color-paper, #fff);
1944
- --dark: var(--card-1-dark);
2196
+ --card-bg: var(--action-bg);
2197
+ --dark: var(--action-dark);
1945
2198
  background: var(--card-bg);
1946
2199
  color: var(--card-ink);
1947
2200
  padding-top: var(--sp-6);
1948
2201
  }
1949
- .sheet--paper > .card__foot { --card-bg: var(--color-paper, #fff); color: var(--color-ink, #141414); }
2202
+ .sheet--paper > .card__foot { --card-bg: var(--action-bg); --dark: var(--action-dark); --card-foot-ink: var(--card-ink); }
1950
2203
  .sheet__handle--dots {
1951
2204
  --_dot: var(--sp-1);
1952
2205
  --_step: calc(var(--_dot) * 2.5);
@@ -1963,15 +2216,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1963
2216
  background-position: 0 0, var(--_step) 0, calc(2 * var(--_step)) 0, calc(3 * var(--_step)) 0;
1964
2217
  }
1965
2218
  /* 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
2219
+ 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
2220
  the stack's (stack.css): one sign for both. Nothing travels. */
1968
2221
  .sheet.is-loading > .sheet__handle {
1969
2222
  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;
2223
+ background-color: color-mix(in srgb, var(--ink-onlight) calc(var(--_grip) * 100%), var(--ink-ondark));
2224
+ animation: stack-grip-dissolve var(--motion-pulse) var(--ease-in-out) infinite alternate;
1972
2225
  }
1973
2226
  .sheet.is-loading > .sheet__handle--dots {
1974
- --_ink: color-mix(in srgb, black calc(var(--_grip) * 100%), white);
2227
+ --_ink: color-mix(in srgb, var(--ink-onlight) calc(var(--_grip) * 100%), var(--ink-ondark));
1975
2228
  background-color: transparent;
1976
2229
  }
1977
2230
  @media (prefers-reduced-motion: reduce) {
@@ -1995,18 +2248,18 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1995
2248
  - a label .row with its .check / .toggle may be the head when the sheet's
1996
2249
  whole ask is that switch: peek shows it (a row is --tap tall plus its own
1997
2250
  padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
1998
- .sheet > .card__head { min-height: var(--sheet-head-h); }
2251
+ .sheet > .card__head { --card-head-min-h: var(--sheet-head-h); }
1999
2252
  /* a sheet's title is ONE line, always (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน" wrapped to two lines at 390px) — the
2000
2253
  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
2254
  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
2255
  measures --_over, the width it overflows by, and sets .is-long; one way — it waits, runs to its end, waits, and starts again
2003
2256
  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
2257
  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; }
2258
+ .sheet > .card__head:not(:has(> :is(.t-caption, .t-body))) { --card-head-wrap: nowrap; }
2006
2259
  .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
2260
  .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long {
2008
2261
  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 */
2262
+ 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
2263
  }
2011
2264
  @keyframes sheet-title-run { 0%, 20% { text-indent: 0; } 85%, 100% { text-indent: calc(-1 * var(--_over)); } }
2012
2265
  @media (prefers-reduced-motion: reduce) {
@@ -2097,11 +2350,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2097
2350
  isolation: isolate; /* the ring and the hint stack inside the map only — a sheet over the map covers them */
2098
2351
  color: var(--map-ink, var(--on-surface));
2099
2352
  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),
2353
+ 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)),
2354
+ 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
2355
  var(--card-2-bg);
2103
2356
  }
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 */
2357
+ .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
2358
  .map__ring {
2106
2359
  position: absolute;
2107
2360
  left: 50%;
@@ -2116,7 +2369,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2116
2369
  background: var(--map-ring-bg, var(--accent-soft));
2117
2370
  box-shadow: var(--map-ring-shadow, var(--shadow-float));
2118
2371
  pointer-events: none;
2119
- z-index: 1; /* above the engine's panes (Leaflet's tiles sit at 200, but inside their own stacking context) */
2372
+ z-index: var(--z-over); /* above the engine's panes (Leaflet's tiles sit at 200, but inside their own stacking context) */
2120
2373
  }
2121
2374
  .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
2375
  position: absolute;
@@ -2131,11 +2384,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2131
2384
  color: var(--map-pin-ink, var(--card-ink));
2132
2385
  filter: drop-shadow(var(--map-pin-drop, var(--shadow-float)));
2133
2386
  pointer-events: none;
2134
- z-index: 1;
2387
+ z-index: var(--z-over);
2135
2388
  }
2136
2389
  /* 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
2390
  street pattern never shows through the pin's body */
2138
- .map__pin > .icon { width: 100%; height: 100%; translate: 0 9%; fill: var(--surface); }
2391
+ .map__pin > .icon { --icon-size: 100%; --icon-shift: 0 9%; }
2139
2392
  .map__credit { /* the tile provider's credit — required by OSM; a caption, not the engine's own control */
2140
2393
  position: absolute;
2141
2394
  right: var(--sp-2);
@@ -2146,7 +2399,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2146
2399
  color: var(--map-credit-ink, var(--on-surface-muted));
2147
2400
  font: var(--map-credit-font, var(--type-caption));
2148
2401
  pointer-events: none;
2149
- z-index: 1;
2402
+ z-index: var(--z-over);
2150
2403
  }
2151
2404
  .map__hint {
2152
2405
  position: absolute;
@@ -2164,7 +2417,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2164
2417
  text-align: center;
2165
2418
  box-shadow: var(--map-hint-shadow, var(--shadow-float));
2166
2419
  pointer-events: none;
2167
- z-index: 1;
2420
+ z-index: var(--z-over);
2168
2421
  }
2169
2422
 
2170
2423
  /* ---- 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 +2447,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2194
2447
  name; the picked one fills with accent. Above the ring: the ring takes no taps, the places do. */
2195
2448
  .map-pick {
2196
2449
  position: absolute;
2197
- z-index: 2;
2450
+ z-index: var(--z-over-2);
2198
2451
  width: var(--tap);
2199
2452
  height: var(--tap);
2200
2453
  translate: -50% -50%;
@@ -2225,7 +2478,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2225
2478
  background: var(--map-pick-name-bg, color-mix(in srgb, var(--surface) 85%, transparent));
2226
2479
  font: var(--map-pick-font, var(--type-caption));
2227
2480
  }
2228
- .map-pick:has(:checked) { z-index: 3; } /* the picked place's name lies over a neighbour's */
2481
+ .map-pick:has(:checked) { z-index: var(--z-over-3); } /* the picked place's name lies over a neighbour's */
2229
2482
  .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
2483
  .map-pick:has(:focus-visible) > .map-pick__mark { outline: var(--focus); outline-offset: var(--sp-0); }
2231
2484
  .map-pick:has(:disabled) { opacity: var(--off); cursor: default; }
@@ -2250,8 +2503,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2250
2503
  /* 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
2504
  is 3.9:1; doubled, #404040, 10:1. It read --card-3-ink, a brand's light ink, on a black scrim */
2252
2505
  background: var(--sheet-stage-foot-bg, var(--scrim));
2253
- color: var(--sheet-stage-foot-ink, #ffffff);
2254
- z-index: 2;
2506
+ color: var(--sheet-stage-foot-ink, var(--ink-ondark));
2507
+ z-index: var(--z-over-2);
2255
2508
  }
2256
2509
 
2257
2510
  /* dim stage: blur + darken whatever sits behind the sheet */
@@ -2261,9 +2514,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2261
2514
  position: absolute;
2262
2515
  inset: 0;
2263
2516
  background: var(--scrim);
2264
- z-index: 1;
2517
+ z-index: var(--z-over);
2265
2518
  }
2266
- .sheet-stage--dim .sheet { z-index: 2; }
2519
+ .sheet-stage--dim .sheet { z-index: var(--z-over-2); }
2267
2520
 
2268
2521
  /* ---- sheet steps: one task in steps inside one sheet (Lh 2026-09-24) ----
2269
2522
  <div class="sheet-steps"> › <ol class="sheet-steps__track"> of <li class="sheet-step sheet-step--done|now|next">. The done steps
@@ -2274,14 +2527,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2274
2527
  falls — no height is written. Nothing fades: done and next read at full ink. */
2275
2528
  .sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
2276
2529
  .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 */
2530
+ .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
2531
  .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
2532
 
2280
2533
  .sheet-step {
2281
2534
  position: relative; min-width: 0;
2282
2535
  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
2536
  flex: 0 0 auto;
2284
- transition: grid-template-rows var(--motion-sheet) ease, flex-grow var(--motion-sheet) ease;
2537
+ transition: grid-template-rows var(--motion-sheet) var(--ease), flex-grow var(--motion-sheet) var(--ease);
2285
2538
  --_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
2539
  }
2287
2540
  /* 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 +2542,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2289
2542
  with them, nothing jumps (Lh 2026-09-24) */
2290
2543
  .sheet-step--now { grid-template-rows: auto 1fr; flex-grow: 1; }
2291
2544
  .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) */
2545
+ .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
2546
  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
2547
  padding-inline: var(--sheet-step-bar-pad-x, var(--sp-3)); border-radius: var(--sheet-step-bar-radius, var(--r-chip));
2295
2548
  background: var(--sheet-step-bar-bg, var(--ink-tint)); color: var(--sheet-step-bar-ink, inherit);
@@ -2297,11 +2550,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2297
2550
  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
2551
  }
2299
2552
  .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 */
2553
+ .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
2554
  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
2555
  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
2556
  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;
2557
+ border-radius: 50%; font: var(--sheet-step-num-font, var(--type-caption)); line-height: var(--lh-solid); font-variant-numeric: tabular-nums;
2305
2558
  background: var(--sheet-step-num-bg, var(--card-bg)); color: var(--sheet-step-num-ink, inherit);
2306
2559
  }
2307
2560
  .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 +2562,57 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2309
2562
  .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
2310
2563
  .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
2564
  .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); }
2565
+ .sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s var(--ease-linear) var(--motion-sheet); }
2314
2566
  .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
2567
  /* the last step's finish, when the app gives one: under the steps */
2316
2568
  .sheet-steps__finish { display: grid; padding-top: var(--card-gap); }
2317
2569
  @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; }
2570
+
2571
+ /* ==== the step bar answers a press, as PRESS in actions.css answers every other control (#62: a block is written in its own file) ==== */
2572
+ .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
2573
+
2574
+ /* ==== 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) ==== */
2575
+ .sheet__body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
2576
+ .sheet__body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
2577
+ .sheet__body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
2578
+
2579
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
2580
+ /* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private.
2581
+ A paper body adds its own --sp-6 on top of this (the rule below): the stack's gap under the head is the
2582
+ card's colour, so without the paper's inset the first row sits on that seam. */
2583
+ .sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0rem); transition: padding-top var(--motion-sheet) var(--ease); }
2584
+ /* 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
2585
+ --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
2586
+ stack's step and the body's own rows read it. */
2587
+ .sheet-stack > .card.sheet--paper > .sheet__body {
2588
+ margin-top: calc(var(--sp-3) - var(--card-gap));
2589
+ padding-top: calc(var(--sp-6) + var(--_refresh, 0rem));
2590
+ transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
2591
+ }
2592
+ /* 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 */
2593
+ .sheet-stack > .card.sheet--paper > .sheet__body.is-refreshing { transition: none; }
2594
+ /* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
2595
+ cover the corners, and a paper body showed there beside the head. Clear, the card's own colour
2596
+ shows in that pocket. Open, the body is paper, and the first row sits --sp-6 down, the same paper
2597
+ inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
2598
+ would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
2599
+ into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
2600
+ sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
2601
+ at the moment the sheet stops. Rising, the body is paper at once. */
2602
+ .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
2603
+ background-color: transparent;
2604
+ transition:
2605
+ padding-top var(--motion-sheet) var(--ease),
2606
+ background-color var(--motion-quick) var(--ease) calc(var(--stagger, 0) * var(--motion-stagger) + var(--motion-sheet) - var(--motion-quick));
2607
+ }
2608
+ @media (prefers-reduced-motion: reduce) { .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
2609
+ transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
2610
+ } }
2611
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
2612
+ .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
2613
+
2614
+ /* ---- where it sits in what stack.css draws: a body pulled to refresh holds still (docs/reference.md rule 15) ---- */
2615
+ .sheet-stack > .card > .sheet__body.is-refreshing { transition: none; }
2320
2616
 
2321
2617
  /* ---- css/step.css — .step: one screen, one task — card, optional sheet, bar outside ---- */
2322
2618
  /* ============================================================
@@ -2386,135 +2682,29 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2386
2682
  own screens are composed (the guide); the framework does not police it — a component never changes
2387
2683
  because another one is there. */
2388
2684
 
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 */
2685
+ /* the reading block (base.css writes where it sits and why): a card's inner rhythm, set here */
2397
2686
  .step > .page, .step > .pages > .page {
2398
2687
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
2399
2688
  --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
2689
  }
2404
2690
  /* the reading block is the .page — or the .pages that holds several of them, a walk of words
2405
2691
  (card.css). Behind a PEEKED sheet the words keep the strip's room under them, so whatever is down there
2406
2692
  (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
2693
 
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
2694
 
2434
2695
  /* a SCROLLING (not fill) screen becomes a column at least one phone tall when it holds a bottom or a centre:
2435
2696
  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
- }
2697
+ 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
2698
+ 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) */
2699
+
2491
2700
  .screen--fill > .track:first-child + .sheet-stage {
2492
2701
  --_top: calc(var(--track-top-lead, var(--sp-4)) + var(--top-h, var(--header-h)) + var(--track-top-gap, var(--sp-4)));
2493
2702
  }
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
2703
  /* 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
2704
  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
2705
  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
2706
  .step:has(> .empty-state)::before { content: ""; flex: calc(var(--empty-state-at, 0.4) * 5) 1 0; }
2512
2707
  .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
2708
 
2519
2709
  /* ---- css/forms.css — outlined fields, add-row ---- */
2520
2710
  /* ============================================================
@@ -2542,7 +2732,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2542
2732
  .field select,
2543
2733
  .field textarea,
2544
2734
  .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 */
2735
+ 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
2736
  }
2547
2737
  .field input,
2548
2738
  .field select,
@@ -2556,17 +2746,16 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2556
2746
  caret-color: currentColor;
2557
2747
  }
2558
2748
  .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) */
2749
+ .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
2750
  /* 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
2751
  .field--rows { align-items: flex-start; padding-block: var(--field-rows-pad-y, var(--sp-3)); }
2562
2752
  .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 */
2753
+ .field--rows > .icon { --icon-margin: calc((1lh - var(--icon-sm)) / 2) 0 0; } /* centred on the first line */
2564
2754
  /* 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
2755
  .field:has(:focus, [data-cursor]) { border-color: var(--field-active-border, currentColor); } /* [data-cursor]: SheetSteps holds the phone's keyboard for this field */
2566
2756
 
2567
2757
  /* field with its action inside: date pick, send, go — the button sits flush right */
2568
2758
  .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
2759
 
2571
2760
  /* colored palettes drop the visible outline elsewhere — fields keep one */
2572
2761
  [data-palette] .field:not(:has(:focus)) { border-color: color-mix(in srgb, currentColor 25%, transparent); }
@@ -2591,25 +2780,18 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2591
2780
  display: grid;
2592
2781
  place-items: center;
2593
2782
  font-size: var(--add-row-plus-font-size, var(--fs-md));
2594
- line-height: 1;
2783
+ line-height: var(--lh-solid);
2595
2784
  }
2596
- .add-row__plus .icon { width: var(--icon-xs); height: var(--icon-xs); }
2785
+ .add-row__plus .icon { --icon-size: var(--icon-xs); }
2597
2786
 
2598
2787
  /* ---- a wrong value, and the line that says so (Lh 2026-09-24) ----
2599
2788
  --signal-error is the one colour the wireframe has; it never speaks alone — the line carries an icon and words.
2600
2789
  <p class="form-note form-note--error" role="alert"><svg class="icon icon--xs">…</svg>Not a phone number</p>
2601
2790
  <p class="form-note form-note--busy" role="status"><svg …/>Sending the code…</p> (the ring turns) */
2602
2791
  .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
2792
  .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
2793
  .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 */
2794
+ @media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { --icon-spin: turn var(--motion-spin) var(--ease-linear) infinite; } } /* `turn`: tokens.css */
2613
2795
 
2614
2796
  /* ---- css/journey.css — route, tile badge, note row ---- */
2615
2797
  /* ============================================================
@@ -2636,7 +2818,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2636
2818
  .route__time { grid-row: 3; align-self: start; }
2637
2819
 
2638
2820
  .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; }
2821
+ .route__code::after, .route__point--end .route__code::before { content: ""; flex: 1; border-top: var(--rule-w) dotted currentColor; opacity: var(--fade-mild); }
2640
2822
  .route__point--end .route__code::after { content: none; }
2641
2823
  .route__time { font: var(--route-time-font, var(--type-body)); font-variant-numeric: tabular-nums; }
2642
2824
  .route__place { font: var(--route-place-font, var(--type-caption)); color: var(--route-place-ink, var(--card-muted)); }
@@ -2657,17 +2839,19 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2657
2839
  /* 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
2840
  sure to hold 4.5:1 on it; --dim faded it to 2.4:1 */
2659
2841
  .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; }
2842
+ .tile-badge__value { font: var(--fw-heavy) var(--fs-lg) / var(--lh-tight) var(--font-display); font-variant-numeric: tabular-nums; }
2661
2843
 
2662
2844
  /* plain: no fill — a date box beside a title (Jun / 18) */
2663
2845
  .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); }
2846
+ .tile-badge--plain .tile-badge__value { font: var(--fw-heavy) var(--fs-h1) / var(--lh-solid) var(--font-display); }
2665
2847
 
2666
2848
  /* 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
2849
  containment keeps its own words from adding height). Its height picks the form: short (a one-line title) = two lines, the day name
2668
- over the number and the month on one line; tall (a two-line title) = three lines, day · number · month. */
2850
+ over the number and the month on one line; tall (a two-line title) = three lines, day · number · month. It grows no taller than
2851
+ its three lines (2026-10-21, timebank: beside a four-line title it was a tall empty block); held there, it stands at the head's top. */
2669
2852
  .tile-badge--stretch {
2670
2853
  align-self: stretch;
2854
+ max-block-size: calc((2 * var(--fs-caption) + var(--fs-lg)) * var(--tile-badge-line, var(--lh-badge)) + 2 * var(--sp-3));
2671
2855
  container-type: size;
2672
2856
  inline-size: calc(var(--fs-lg) * 3.25);
2673
2857
  grid-template-columns: auto auto;
@@ -2677,11 +2861,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2677
2861
  column-gap: var(--sp-1);
2678
2862
  row-gap: 0;
2679
2863
  padding-block: var(--sp-3);
2680
- line-height: var(--tile-badge-line, 0.95);
2864
+ line-height: var(--tile-badge-line, var(--lh-badge));
2681
2865
  }
2682
2866
  .tile-badge--stretch > * { line-height: inherit; }
2683
2867
  .tile-badge--stretch > :first-child { grid-column: 1 / -1; }
2684
- @container (min-height: 5.5rem) {
2868
+ @container (min-height: 3.5rem) { /* just under the three lines' own height, the most the badge grows to */
2685
2869
  .tile-badge--stretch > * { grid-column: 1 / -1; }
2686
2870
  }
2687
2871
 
@@ -2726,17 +2910,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2726
2910
  min-height: var(--header-h);
2727
2911
  }
2728
2912
 
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
2913
  .topbar__actions { margin-left: auto; display: flex; gap: var(--sp-2); }
2741
2914
  /* at most TWO trailing controls (Lh 2026-09-22): the title must keep its line; a third and
2742
2915
  beyond is not drawn — more moves belong in a Menu behind one button, or on the base */
@@ -2761,7 +2934,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2761
2934
  /* fixed, so it covers the top of the screen it sits in — reserve its height
2762
2935
  there, from the same header-h token. The breathing under it is published:
2763
2936
  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
2937
  .appbar__brand { flex: 1; display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
2766
2938
  .appbar__text { display: grid; min-width: 0; flex: 1; } /* pushes trailing button to the right edge */
2767
2939
  /* 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 +2943,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2771
2943
  margin: 0; text-align: start;
2772
2944
  border-radius: var(--appbar-who-radius, var(--r-chip));
2773
2945
  }
2774
- .appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.35; } /* clipped lines: room for Thai marks */
2946
+ .appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: var(--lh-caption); } /* clipped lines: room for Thai marks */
2775
2947
  .appbar__title { font: var(--type-label); }
2776
2948
  .appbar__sub { font: var(--type-caption); color: var(--on-surface-muted); }
2777
2949
  /* the bar's trailing controls (one or two: a labelled switch chip, a wallet button — home,
@@ -2786,13 +2958,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2786
2958
  border: var(--border-w) solid var(--icon-btn-border, var(--outline)); /* see .btn — same reason */
2787
2959
  background: var(--icon-btn-bg, var(--action-bg));
2788
2960
  color: var(--icon-btn-ink, var(--action-ink));
2961
+ box-shadow: var(--icon-btn-shadow, none);
2789
2962
  display: grid;
2790
2963
  place-items: center;
2791
2964
  font-size: var(--fs-lg);
2792
- line-height: 1;
2965
+ line-height: var(--lh-solid);
2793
2966
  }
2794
2967
  .icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
2795
2968
  .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); }
2969
+ /* the bigger round sizes (`IconBtn size`): lg is the hero control's size (--tap-lg), xl the biggest (--tap-xl), both on --tap-base like every size.
2970
+ What stands inside is a share of the circle (`--_ib`, its side), so it grows with it: the ICON is --icon-btn-icon of the side (7/12, the share of the default 48px
2971
+ button's 28px icon), and a WORD (a string child) keeps to --icon-btn-word-max of the side (0.45: a word of four letters stays inside) — it still
2972
+ grows with the member's text size, to that ceiling and no further. The default and the sm button keep their icon tokens and their word as before. */
2973
+ .icon-btn--lg { --_ib: var(--icon-btn-lg-size, var(--tap-lg)); width: var(--_ib); height: var(--_ib); }
2974
+ .icon-btn--xl { --_ib: var(--icon-btn-xl-size, var(--tap-xl)); width: var(--_ib); height: var(--_ib); }
2975
+ :is(.icon-btn--lg, .icon-btn--xl) { font-size: min(var(--fs-lg), calc(var(--_ib) * var(--icon-btn-word-max, 0.45))); }
2976
+ :is(.icon-btn--lg, .icon-btn--xl) > .icon { --icon-size: calc(var(--_ib) * var(--icon-btn-icon, 7 / 12)); --icon-sm-size: var(--icon-size); --icon-lg-size: var(--icon-size); } /* the Icon's own knobs */
2796
2977
  /* ---- icon button group: up to four icon-only commands in ONE frame (Lh 2026-09-29) ----
2797
2978
  <div class="icon-btn-group" role="group"><button class="icon-btn" aria-label="…">…</button>…</div>
2798
2979
  One pill as tall as a button, the buttons share its width, a hairline between them; in a foot it takes the row. */
@@ -2850,15 +3031,16 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2850
3031
  min-height: var(--btn-h);
2851
3032
  text-decoration: none; /* .btn works as <a> too */
2852
3033
  }
2853
- .btn--primary { background: var(--btn-primary-bg, var(--accent)); color: var(--btn-primary-ink, var(--on-accent)); border-color: transparent; }
3034
+ /* primary: a Btn and a round IconBtn fill alike, from the same tokens (the palette's primary — orchard's yellow — retunes both) */
3035
+ :is(.btn--primary, .icon-btn--primary) { background: var(--btn-primary-bg, var(--accent)); color: var(--btn-primary-ink, var(--on-accent)); border-color: transparent; }
2854
3036
  /* quiet keeps a faint outline so it still reads as a button on colored palettes;
2855
3037
  it is ink on whatever it sits on — inherit, or on the dark slot (card--3) it
2856
3038
  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); }
3039
+ .btn--quiet { background: transparent; color: inherit; border-color: var(--btn-quiet-border, color-mix(in srgb, currentColor 35%, transparent)); }
2858
3040
  .btn--block { width: 100%; }
2859
3041
  .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
3042
  /* 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); }
3043
+ .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
3044
  /* disabled keeps the slot — a control that vanishes moves everything else,
2863
3045
  and a pager whose "previous" disappeared on page one would shift the bar
2864
3046
  under the thumb on every page */
@@ -2883,9 +3065,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2883
3065
  padding: var(--chip-pad-y, var(--sp-2)) var(--chip-pad-x, var(--sp-3));
2884
3066
  font: var(--type-label);
2885
3067
  font-size: var(--chip-font-size, var(--fs-sm));
2886
- min-height: var(--chip-h);
3068
+ min-height: var(--chip-min-h, var(--chip-h));
2887
3069
  text-decoration: none;
2888
- justify-self: start; /* as a grid child it hugs its text, never stretches */
3070
+ justify-content: var(--chip-justify, normal);
3071
+ justify-self: var(--chip-self, start); /* as a grid child it hugs its text, never stretches */
2889
3072
  }
2890
3073
  .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
3074
  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 +3085,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2902
3085
  /* muted: an untouched default — the words wash out, the edge stays; emphasis is the difference from the neighbours (docs/reference.md 13) */
2903
3086
  .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
3087
 
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 */
3088
+ .chip-row { display: flex; gap: var(--sp-2); }
3089
+ .chip-row:not(.bleed) { flex-wrap: wrap; } /* a strip (.bleed, card.css) stays one line */
2907
3090
  .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
3091
 
2909
3092
  /* ---- dropdown: the standard for every select — a pill trigger + .menu popover ----
@@ -2932,10 +3115,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2932
3115
  /* muted: the value is still the default (docs/reference.md 13) */
2933
3116
  .dropdown--muted .dropdown__btn { color: var(--dropdown-muted-ink, var(--action-muted)); }
2934
3117
  .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) */
3118
+ .dropdown__btn > .icon { --icon-motion: rotate var(--motion-quick) var(--ease); }
3119
+ .dropdown:has(.menu:popover-open) .dropdown__btn > .icon { --icon-turn: 180deg; }
3120
+ .dropdown:has(.menu.\:popover-open) .dropdown__btn > .icon { --icon-turn: 180deg; } /* the polyfill's open (layover.css) */
2939
3121
  .filter-row { display: flex; gap: var(--sp-2); }
2940
3122
  .filter-row > .dropdown { flex: 1; min-width: 0; }
2941
3123
 
@@ -3081,7 +3263,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3081
3263
  cursor: default;
3082
3264
  }
3083
3265
  .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) */
3266
+ .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
3267
 
3086
3268
  /* PRESS (Lh 2026-09-25; every tappable thing, 2026-09-27): every CONTROL answers a tap, INFORMATION never does (no pointer, no
3087
3269
  press). Two answers, by size: GIVE — a control the size of a finger shrinks under it (--press-scale) and springs back; DIM —
@@ -3097,34 +3279,27 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3097
3279
  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
3280
  cursor: pointer;
3099
3281
  touch-action: manipulation;
3100
- transition: scale var(--motion-press) ease-out;
3282
+ transition: scale var(--motion-press) var(--ease-out);
3101
3283
  }
3102
3284
  :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
3285
  cursor: pointer;
3104
3286
  touch-action: manipulation;
3105
- transition: opacity var(--motion-press) ease-out;
3287
+ transition: opacity var(--motion-press) var(--ease-out);
3106
3288
  }
3107
3289
  /* 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
3290
  :where(.chip--pick, button.chip, .tile--pick, button.tile, .day, .segment__item, .calendar__day, .float-bar__item, .picker__item, .check,
3109
3291
  .icon-btn[role="tab"], .avatar-pick__item > .avatar, .map-pick > .map-pick__mark, .map-pick > .map-pick__name) {
3110
3292
  transition-property: scale, background-color, color, border-color, opacity;
3111
3293
  transition-duration: var(--motion-press);
3112
- transition-timing-function: ease-out;
3294
+ transition-timing-function: var(--ease-out);
3113
3295
  }
3114
3296
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
3115
3297
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
3116
3298
  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
3299
  :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
3300
  /* 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
3301
  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; }
3302
+ :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
3303
  :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
3304
  /* 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
3305
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
@@ -3144,6 +3319,90 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3144
3319
  .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
3320
  @media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
3146
3321
 
3322
+ /* ==== the chip a choice draws: picked (choice.css drives it with its input), and --sign (#62: a block is written in its own file) ==== */
3323
+ .chip--pick { position: relative; } /* the input covers the whole chip, on a row as in a grid */
3324
+ .chip--pick:has(:checked) { background: var(--accent); --dark: var(--accent-dark); color: var(--card-ink); border-color: transparent; }
3325
+ /* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
3326
+ .chip--sign { padding-right: var(--sp-2); }
3327
+ .chip--sign::after {
3328
+ content: "";
3329
+ width: var(--icon-xs);
3330
+ height: var(--icon-xs);
3331
+ flex: none;
3332
+ background: currentColor;
3333
+ 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;
3334
+ -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;
3335
+ }
3336
+ .chip--sign:has(:checked)::after { rotate: 45deg; }
3337
+ @media (prefers-reduced-motion: no-preference) { .chip--sign::after { transition: rotate var(--motion-slide); } }
3338
+
3339
+ /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
3340
+ /* 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 */
3341
+ .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; }
3342
+ .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: var(--lh-body); }
3343
+ .card__foot > .btn { flex: 1 1 0; min-width: 0; }
3344
+ /* …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
3345
+ a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
3346
+ .card__foot > .btn--form { flex: 0 1 auto; }
3347
+ /* the page dots under the card sit centred under it — the indicator belongs
3348
+ with the page that changes, never in the bar (Lh, 2026-09-12) */
3349
+ .screen__centre > .pager__at { justify-self: center; }
3350
+ .panes > .pager__at { justify-self: center; }
3351
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
3352
+ .screen--fill:has(> .sheet-stack--flood) > :is(.appbar, .topbar):first-child {
3353
+ position: absolute;
3354
+ inset: var(--safe-top) 0 auto;
3355
+ max-width: none;
3356
+ margin: 0;
3357
+ z-index: var(--z-flat); /* under every sheet (--level 1-5); the base clears it by margin */
3358
+ }
3359
+ .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 {
3360
+ visibility: hidden;
3361
+ 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) */
3362
+ }
3363
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
3364
+ /* the step waiting for its answer: its buttons take no taps (the app guards its own handler too) */
3365
+ .sheet-step--busy .btn { pointer-events: none; opacity: var(--fade-half); }
3366
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
3367
+ /* ============================================================
3368
+ TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
3369
+ 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
3370
+ under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
3371
+ (On a scrolling screen the bar is a sticky last row instead, actions.css.)
3372
+ ============================================================ */
3373
+ .screen--fill > .action-bar {
3374
+ position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
3375
+ max-width: none;
3376
+ margin: 0;
3377
+ /* 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
3378
+ running under it (Lh 2026-09-25) — the bar's own top padding */
3379
+ padding-top: var(--action-bar-fill-pad-top, var(--sp-3));
3380
+ /* its GROUND (Lh 2026-09-24, timebank card 141 — was "no ground of its own"): words that run under the layer mid-scroll
3381
+ showed through the dots, the controls and the buttons. The layer lies on the screen's own surface now; a project sets
3382
+ another (or `transparent`, the old see-through) with the knob */
3383
+ background: var(--action-bar-bg, var(--surface));
3384
+ }
3385
+ /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
3386
+ .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
3387
+ /* 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
3388
+ reserves nothing for it; the breathing under it stays the one it had (--sp-2), not the screen's gap */
3389
+ .screen--fill > .appbar {
3390
+ position: relative; inset: auto; top: auto;
3391
+ flex: none;
3392
+ max-width: none;
3393
+ margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge; its own padding is the inset */
3394
+ margin-bottom: calc(var(--sp-2) - var(--screen-gap));
3395
+ }
3396
+ /* …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
3397
+ them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
3398
+ .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
3399
+ /* ---- where it sits in what forms.css draws (docs/reference.md rule 15) ---- */
3400
+ .field--action > .icon-btn { margin-left: auto; }
3401
+ /* ---- where it sits in what sticky.css draws (docs/reference.md rule 15) ---- */
3402
+ .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; }
3403
+ /* ---- where it sits in what layover.css draws (docs/reference.md rule 15) ---- */
3404
+ .modal__drawer > .btn-row { justify-self: stretch; margin-top: var(--modal-drawer-answers-gap, var(--sp-5)); white-space: nowrap; }
3405
+
3147
3406
  /* ---- css/numbers.css — ring, track, band, dot grid, picker, badge, bars ---- */
3148
3407
  /* ============================================================
3149
3408
  cardds/numbers.css — big numbers inside cards:
@@ -3175,13 +3434,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3175
3434
  border-radius: 50%;
3176
3435
  background: conic-gradient(var(--accent) calc(var(--value) * 1%), var(--ink-tint) 0);
3177
3436
  /* 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)));
3437
+ mask: radial-gradient(farthest-side, transparent calc(100% - var(--_w) - var(--px)), currentColor calc(100% - var(--_w)));
3438
+ -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--_w) - var(--px)), currentColor calc(100% - var(--_w)));
3180
3439
  }
3181
3440
  .ring__value { font: var(--ring-value-font, var(--type-stat)); letter-spacing: var(--track-display); }
3182
3441
  .ring__label { font: var(--ring-label-font, var(--type-caption)); color: var(--ring-label-ink, var(--card-muted)); }
3183
3442
  .ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
3184
- .ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
3443
+ .ring--sm .ring__value { font: var(--fw-heavy) var(--fs-2xl) / var(--lh-solid) var(--font-display); }
3185
3444
  .ring--xs { --_size: var(--tap-sm); --_w: var(--dot-xs); } /* inline counter (step 3 of 5) */
3186
3445
  .ring--xs .ring__value { font: var(--type-caption); }
3187
3446
 
@@ -3222,44 +3481,31 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3222
3481
  }
3223
3482
  .track__step:first-child::before { display: none; }
3224
3483
  .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);
3484
+ .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); }
3485
+ .track__step > .track__dot { /* a number in the round (TrackStep num) */
3229
3486
  flex: none;
3230
- padding: calc(var(--_step) * 7 / 32); /* the glyph = 18 in the 32 round */
3231
3487
  border-radius: 50%;
3232
- background: var(--ink-rail);
3488
+ 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
3489
  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
3490
  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;
3491
+ font: var(--track-dot-font, var(--type-caption)); line-height: var(--lh-solid); font-variant-numeric: tabular-nums;
3239
3492
  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
3493
  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
3494
  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
3495
  }
3243
- .track__step.is-done > .icon,
3496
+ .track__step.is-done > .icon { --icon-bg: var(--accent); --icon-ink: var(--on-accent); }
3244
3497
  .track__step.is-done > .track__dot { background: var(--accent); color: var(--on-accent); }
3245
3498
 
3246
3499
  /* ---- band: data card — 2-line label · giant number · footnote ----
3247
3500
  <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; }
3501
+ .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
3502
  .band__num {
3257
3503
  grid-area: num;
3258
- font: 800 var(--fs-giant) / 0.9 var(--font-display);
3504
+ font: var(--fw-heavy) var(--fs-giant) / var(--lh-band) var(--font-display);
3259
3505
  letter-spacing: var(--track-display);
3260
3506
  font-variant-numeric: tabular-nums;
3261
3507
  }
3262
- .band__num small { font-size: 0.5em; font-weight: 700; margin-right: 0.1em; } /* unit or % */
3508
+ .band__num small { font-size: 0.5em; font-weight: var(--fw-bold); margin-right: 0.1em; } /* unit or % */
3263
3509
  .band__foot { grid-area: foot; font: var(--band-foot-font, var(--type-caption)); color: var(--band-foot-ink, var(--card-muted)); }
3264
3510
  /* band stack: ONE card boundary; bands are frameless inside it, a hairline
3265
3511
  between each. Bands may still carry slot tints (card--2 / card--3). */
@@ -3269,8 +3515,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3269
3515
  border-radius: var(--band-stack-radius, var(--r-card));
3270
3516
  overflow: hidden;
3271
3517
  }
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
3518
 
3275
3519
  /* ---- dotgrid: progress over time as dots — --total, --done, --cols ----
3276
3520
  one element, no children:
@@ -3304,7 +3548,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3304
3548
  );
3305
3549
  }
3306
3550
  /* no round(): show the grid without the done fill rather than a broken shape */
3307
- @supports not (width: round(up, 1px, 1px)) {
3551
+ @supports not (width: round(up, 0.0625rem, 0.0625rem)) {
3308
3552
  .dotgrid::before { display: none; }
3309
3553
  }
3310
3554
  /* ---- picker: horizontal snap picker, centre value big ----
@@ -3349,7 +3593,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3349
3593
  .picker__unit { font: var(--picker-unit-font, var(--type-caption)); color: var(--picker-unit-ink, var(--card-muted)); }
3350
3594
  /* the scrolled-to item lights up by position — no JS */
3351
3595
  @supports (animation-timeline: view(x)) {
3352
- .picker__item { animation: picker-focus linear both; animation-timeline: view(x); }
3596
+ .picker__item { animation: picker-focus var(--ease-linear) both; animation-timeline: view(x); }
3353
3597
  @keyframes picker-focus {
3354
3598
  0%, 100% { color: var(--_rest); }
3355
3599
  45%, 55% { color: var(--card-ink); }
@@ -3375,7 +3619,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3375
3619
 
3376
3620
  /* ---- bars: three or four bars with a count on top — the glance chart ----
3377
3621
  <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)); }
3622
+ .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
3623
  .bar {
3380
3624
  flex: 1;
3381
3625
  min-width: 0;
@@ -3385,7 +3629,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3385
3629
  gap: var(--bar-gap, var(--sp-1));
3386
3630
  height: 100%;
3387
3631
  font: var(--bar-font, var(--type-caption));
3388
- font-weight: 700;
3632
+ font-weight: var(--fw-bold);
3389
3633
  }
3390
3634
  .bar::after {
3391
3635
  content: "";
@@ -3401,8 +3645,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3401
3645
  <span class="spinner" role="status"><svg class="icon">…loader-circle…</svg>Loading…</span>
3402
3646
  The ring turns; words beside it are optional and take the caption's look. Still when motion is reduced. */
3403
3647
  .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 */
3648
+ @media (prefers-reduced-motion: no-preference) { .spinner > .icon { --icon-spin: turn var(--motion-spin) var(--ease-linear) infinite; } } /* `turn`: tokens.css */
3406
3649
 
3407
3650
  /* ---- progress: one straight line, the done share solid, the rest faint (Lh 2026-09-22) — a voice note's
3408
3651
  timeline, a download, a step count as a line. <div class="progress" role="progressbar" style="--progress-at: 0.4"></div>
@@ -3420,6 +3663,27 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3420
3663
  background-repeat: no-repeat;
3421
3664
  }
3422
3665
 
3666
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
3667
+ /* 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
3668
+ 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
3669
+ 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
3670
+ stands the same distance from everything around it (Lh 2026-10-03: 8 over was too little, 24 under too much). */
3671
+ .screen--fill > .track:first-child {
3672
+ flex: none;
3673
+ margin-top: var(--track-top-lead, var(--sp-4));
3674
+ margin-bottom: calc(var(--track-top-gap, var(--sp-4)) - var(--screen-gap));
3675
+ }
3676
+ /* …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
3677
+ shows around the pill — and the stage keeps the track's line free itself: a sheet at full stops --track-top-gap under the
3678
+ track, as in a step. --top-h is the track's own height, measured by Screen (a numbered track grows with the text). */
3679
+ .screen--fill > .track:first-child:has(+ .sheet-stage) {
3680
+ position: absolute; z-index: var(--z-over);
3681
+ inset: calc(var(--safe-top) + var(--track-top-lead, var(--sp-4))) var(--screen-pad) auto;
3682
+ margin: 0;
3683
+ }
3684
+ /* ---- where it sits in what media.css draws (docs/reference.md rule 15) ---- */
3685
+ .postcard__voice > .progress { justify-self: stretch; pointer-events: none; }
3686
+
3423
3687
  /* ---- css/people.css — avatar, avatar stack, chooser row ---- */
3424
3688
  /* ============================================================
3425
3689
  cardds/people.css — avatars: single, stacked, chooser row.
@@ -3444,12 +3708,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3444
3708
  font: var(--avatar-font, var(--type-label));
3445
3709
  font-size: calc(var(--_size) * 0.3);
3446
3710
  text-transform: uppercase;
3447
- letter-spacing: 0.02em;
3711
+ letter-spacing: var(--track-loose);
3448
3712
  position: relative;
3449
3713
  text-decoration: none; /* .avatar works as <a> too */
3450
3714
  }
3451
3715
  .avatar > img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
3452
- .avatar > .icon { width: 50%; height: 50%; }
3716
+ .avatar > .icon { --icon-size: 50%; }
3453
3717
  .avatar--sm { --_size: var(--avatar-sm); }
3454
3718
  .avatar--lg { --_size: var(--avatar-lg); }
3455
3719
  .avatar--xl { --_size: var(--avatar-xl); } /* sheet head icon ring */
@@ -3465,7 +3729,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3465
3729
  inset: calc(-0.55 * var(--_size));
3466
3730
  border-radius: 50%;
3467
3731
  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;
3732
+ opacity: var(--fade-faint);
3469
3733
  pointer-events: none;
3470
3734
  }
3471
3735
 
@@ -3474,11 +3738,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3474
3738
  .avatar-stack { display: inline-flex; align-items: center; }
3475
3739
  .avatar-stack > .avatar { box-shadow: 0 0 0 var(--sp-1) var(--card-bg, var(--surface)); }
3476
3740
  .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
3741
  /* 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
3742
  .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
3743
 
@@ -3558,7 +3817,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3558
3817
  --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
3818
  color: var(--card-ink);
3560
3819
  position: relative;
3561
- transition: background-color var(--motion-press) ease-out, border-color var(--motion-press) ease-out;
3820
+ transition: background-color var(--motion-press) var(--ease-out), border-color var(--motion-press) var(--ease-out);
3562
3821
  }
3563
3822
  .toggle::after {
3564
3823
  content: "";
@@ -3569,7 +3828,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3569
3828
  height: calc(var(--_h) - 2 * var(--_pad) - 2 * var(--border-w));
3570
3829
  border-radius: 50%;
3571
3830
  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;
3831
+ transition: translate var(--motion-quick) var(--ease);
3573
3832
  }
3574
3833
  .toggle:checked { background: var(--accent); border-color: transparent; }
3575
3834
  .toggle:checked::after { translate: calc(var(--_w) - var(--_h)) 0; background: var(--on-accent); }
@@ -3581,34 +3840,18 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3581
3840
  <div class="chip-row"><label class="chip chip--pick chip--sign"><input type="checkbox">Nature</label>…</div> */
3582
3841
  .chip-grid {
3583
3842
  display: grid;
3584
- grid-template-columns: repeat(auto-fill, minmax(min(5.5rem, 100%), 1fr));
3843
+ grid-template-columns: repeat(auto-fill, minmax(min(calc(var(--tap-base) * 11 / 6), 100%), 1fr));
3585
3844
  gap: var(--chip-grid-gap, var(--sp-2));
3586
3845
  }
3587
3846
  /* .chip hugs its text as a loose grid child (justify-self: start); inside a
3588
3847
  chip grid the whole point is equal, full-width pick targets, so stretch.
3589
3848
  Matters most in scripts without word spaces (Thai): min-content is the
3590
3849
  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 */
3850
+ .chip-grid > .chip { --chip-self: stretch; --chip-justify: center; --chip-min-h: var(--btn-h); --chip-pad-x: var(--sp-2); }
3593
3851
  /* the hidden input, the same shape wherever a native radio/checkbox drives a picked look:
3594
3852
  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
3853
  (a pickable row card: the app gives the picked one its `tone`) — the native input lies over its whole box, unseen */
3596
3854
  :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
3855
  /* ---- day strip: a week, one day selected, today a light tone ----
3613
3856
  <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
3857
  .day-strip { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--day-strip-gap, var(--sp-1)); }
@@ -3631,7 +3874,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3631
3874
  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
3875
  }
3633
3876
  .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; }
3877
+ .day__num { font: var(--fw-bold) var(--fs-lg) / var(--lh-tight) var(--font-display); font-variant-numeric: tabular-nums; }
3635
3878
  /* 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
3879
  .day--today { background: var(--day-today-bg, var(--ink-tint)); color: var(--day-today-ink, var(--card-ink)); }
3637
3880
  .day[aria-selected="true"] { background: var(--day-selected-bg, var(--accent)); color: var(--day-selected-ink, var(--on-accent)); }
@@ -3650,7 +3893,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3650
3893
  display: grid;
3651
3894
  place-items: center;
3652
3895
  border-radius: 50%;
3653
- font: 500 var(--fs-title)/1 var(--font-ui);
3896
+ font: var(--fw-medium) var(--fs-title) / var(--lh-solid) var(--font-ui);
3654
3897
  font-variant-numeric: tabular-nums;
3655
3898
  grid-column-start: var(--start, auto); /* --start on the 1st: weekday column 1–7 */
3656
3899
  }
@@ -3658,8 +3901,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3658
3901
  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
3902
  .calendar__day--mark { background: var(--calendar-day-mark-bg, color-mix(in srgb, var(--accent) 12%, transparent)); }
3660
3903
  .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; }
3904
+ .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); }
3905
+ .calendar__day:disabled { opacity: var(--fade-dim); }
3663
3906
  .calendar__day[aria-current="date"] { text-decoration: underline; text-underline-offset: 0.2em; }
3664
3907
 
3665
3908
  /* ---- 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 +3915,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3672
3915
  overflow-y: auto;
3673
3916
  overscroll-behavior: contain;
3674
3917
  scroll-snap-type: y mandatory;
3675
- scroll-padding-block-start: var(--_weeks-lead, 0px);
3918
+ scroll-padding-block-start: var(--_weeks-lead, 0rem);
3676
3919
  overflow-anchor: none; /* a pick rerenders the rows; the browser must not shift the scroll to keep an anchor */
3677
3920
  scrollbar-width: none;
3678
3921
  }
3679
3922
  .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)); }
3923
+ .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
3924
  .calendar__label { font: var(--calendar-label-font, var(--type-label)); color: var(--calendar-label-ink, inherit); justify-self: start; }
3682
3925
  button.calendar__label { appearance: none; border: 0; padding: 0; background: none; cursor: pointer; border-radius: var(--sp-1); }
3683
3926
  button.calendar__label:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
@@ -3702,14 +3945,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3702
3945
  height: var(--mood-dot-size, var(--icon-xs));
3703
3946
  border-radius: 50%;
3704
3947
  background: var(--_mood, currentColor);
3705
- transition: scale 0.2s ease, box-shadow 0.2s ease;
3948
+ transition: scale var(--motion-quick) var(--ease), box-shadow var(--motion-quick) var(--ease);
3706
3949
  }
3707
3950
  .mood__opt:nth-child(1) { --_mood: var(--neg); }
3708
3951
  .mood__opt:nth-child(2) { --_mood: color-mix(in srgb, var(--neg) 50%, var(--dot-b)); }
3709
3952
  .mood__opt:nth-child(3) { --_mood: var(--dot-b); }
3710
3953
  .mood__opt:nth-child(4) { --_mood: color-mix(in srgb, var(--pos) 50%, var(--dot-b)); }
3711
3954
  .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); }
3955
+ .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
3956
  .mood__opt:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-0); border-radius: 50%; }
3714
3957
 
3715
3958
  /* ---- pin: big code entry, one digit per pill ----
@@ -3749,7 +3992,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3749
3992
  background-position: center;
3750
3993
  background-repeat: no-repeat;
3751
3994
  }
3752
- .pin--secret .pin__cell.is-fresh:not(:placeholder-shown) { animation: pin-flash var(--pin-flash-time, 0.5s) steps(1); }
3995
+ .pin--secret .pin__cell.is-fresh:not(:placeholder-shown) { animation: pin-flash var(--pin-flash-time, var(--motion-flash)) var(--ease-step); }
3753
3996
  @keyframes pin-flash { 0% { -webkit-text-fill-color: currentColor; background-size: 0 0; } }
3754
3997
  .pin--secret .pin__cell::selection { background: transparent; } /* its digit is selected when the cell takes the cursor: nothing to show of it */
3755
3998
  .pin__cell:focus { outline: none; }
@@ -3760,12 +4003,12 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3760
4003
  .pin--asleep .pin__cell:focus:placeholder-shown { background-image: none; animation: none; }
3761
4004
  .pin__cell:is(:focus, [data-cursor]):placeholder-shown {
3762
4005
  background-image: linear-gradient(currentColor, currentColor);
3763
- background-size: var(--pin-caret-w, 2px) var(--pin-caret-h, 1em);
4006
+ background-size: var(--pin-caret-w, var(--hairline-w)) var(--pin-caret-h, 1em);
3764
4007
  background-position: center;
3765
4008
  background-repeat: no-repeat;
3766
4009
  }
3767
4010
  @media (prefers-reduced-motion: no-preference) {
3768
- .pin__cell:is(:focus, [data-cursor]):placeholder-shown { animation: pin-caret 1.1s steps(1) infinite; }
4011
+ .pin__cell:is(:focus, [data-cursor]):placeholder-shown { animation: pin-caret var(--motion-blink) var(--ease-step) infinite; }
3769
4012
  @keyframes pin-caret { 50% { background-size: 0 0; } }
3770
4013
  }
3771
4014
 
@@ -3777,13 +4020,13 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3777
4020
  .composer { display: grid; gap: var(--composer-gap, var(--sp-3)); }
3778
4021
  .composer__input {
3779
4022
  width: 100%;
3780
- min-height: var(--composer-input-min-h, 11rem);
4023
+ min-height: var(--composer-input-min-h, calc(var(--tap-base) * 11 / 3));
3781
4024
  resize: none;
3782
4025
  border: 0;
3783
4026
  padding: 0;
3784
4027
  background: transparent;
3785
4028
  color: inherit;
3786
- font: var(--composer-input-font, 700 var(--fs-2xl)/1.25 var(--font-display));
4029
+ font: var(--composer-input-font, var(--fw-bold) var(--fs-2xl) / var(--lh-h2) var(--font-display));
3787
4030
  letter-spacing: var(--track-h2);
3788
4031
  outline: none;
3789
4032
  }
@@ -3870,6 +4113,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3870
4113
  flex: none;
3871
4114
  }
3872
4115
 
4116
+ /* ==== the pin's wrong code: red cells and one shake (#62: a block is written in its own file) ==== */
4117
+ .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 */
4118
+ /* 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) */
4119
+ @media (prefers-reduced-motion: no-preference) {
4120
+ .pin--shake { animation: pin-shake var(--pin-shake-time, var(--motion-shake)) var(--ease-in-out); }
4121
+ }
4122
+ @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)); } }
4123
+
3873
4124
  /* ---- css/lists.css — row, rows, key-value, kv grid, timeline, legend, link ---- */
3874
4125
  /* ============================================================
3875
4126
  cardds/lists.css — frameless rows and pairs INSIDE a card:
@@ -3907,15 +4158,15 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3907
4158
  .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
4159
  .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
3909
4160
  .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); }
4161
+ .row--lg .row__title { font: var(--fw-bold) var(--fs-lg) / var(--lh-h1) var(--font-display); }
3911
4162
  /* a row that EXPANDS (Lh 2026-09-22): the row is a <details>' summary, what it shows comes out under it —
3912
4163
  who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
3913
4164
  <details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
3914
4165
  .row-expand { display: block; }
3915
4166
  .row-expand > summary { list-style: none; }
3916
4167
  .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; }
4168
+ .row-expand > summary > .icon:last-child { --icon-motion: rotate var(--motion-slide); }
4169
+ .row-expand[open] > summary > .icon:last-child { --icon-turn: 180deg; }
3919
4170
  .row-expand__more { padding-top: var(--row-expand-gap, var(--sp-3)); }
3920
4171
  /* a finished item in a checklist (a to-do list, an onboarding "complete your introduction" list):
3921
4172
  the words go muted, a filled tick sits at the end, and the row is no longer a tap target —
@@ -3964,7 +4215,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
3964
4215
  .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
4216
  .kv-grid > .kv-cell + .kv-cell { border-left: var(--border-w) solid var(--hairline); padding-left: var(--sp-4); }
3966
4217
  .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); }
4218
+ .kv-cell > .icon { --icon-margin: 0 0 var(--sp-1); }
3968
4219
  .kv-cell__val { font: var(--kv-cell-val-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
3969
4220
  .kv-cell__key { font: var(--kv-cell-key-font, var(--type-caption)); color: var(--kv-cell-key-ink, var(--card-muted)); }
3970
4221
 
@@ -4009,7 +4260,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4009
4260
  }
4010
4261
  .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
4262
  .bubble--right > .bubble__meta { justify-self: end; }
4012
- .bubble__meta > .icon { color: var(--bubble-meta-icon-ink, var(--accent-ink)); }
4263
+ .bubble__meta > .icon { --icon-ink: var(--bubble-meta-icon-ink, var(--accent-ink)); }
4013
4264
 
4014
4265
  /* ---- timeline: a hairline with date chips, entries to the right ----
4015
4266
  <ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
@@ -4080,12 +4331,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4080
4331
  .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
4332
  .bullets__item { display: flex; align-items: start; gap: var(--bullets-pic-gap, var(--sp-3)); min-width: 0; }
4082
4333
  .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
4334
  .bullets__text { flex: 1; min-width: 0; }
4085
4335
 
4336
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
4337
+ .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) */
4338
+
4086
4339
  /* ---- css/sticky.css — sticky note, its field, tone chooser, thread of posts, pile ---- */
4087
4340
  /* 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. */
4341
+ 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
4342
 
4090
4343
  /* ---- sticky: a paper note — a flat pastel SQUARE of one size, the bottom-right corner folded over ----
4091
4344
  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 +4378,28 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4125
4378
  .sticky::before, .sticky::after {
4126
4379
  content: "";
4127
4380
  position: absolute;
4128
- z-index: -1;
4381
+ z-index: var(--z-under);
4129
4382
  pointer-events: none;
4130
4383
  border-radius: var(--sticky-radius, 0rem);
4131
4384
  background-image: linear-gradient(-45deg, transparent calc(var(--_fold) * 0.7071), var(--_ink) 0); /* the folded corner, cut */
4132
4385
  }
4133
- .sticky::after { --_ink: var(--sticky-edge-bg, rgb(0 0 0 / 0.14)); inset: calc(-1 * var(--border-w)); }
4386
+ .sticky::after { --_ink: var(--sticky-edge-bg, color-mix(in srgb, var(--ink-onlight) 14%, transparent)); inset: calc(-1 * var(--border-w)); }
4134
4387
  .sticky::before {
4135
- --_ink: var(--sticky-shadow-bg, rgb(0 0 0 / 0.28));
4388
+ --_ink: var(--sticky-shadow-bg, color-mix(in srgb, var(--ink-onlight) 28%, transparent));
4136
4389
  inset: var(--sp-1) var(--sp-0) calc(-1 * var(--sp-0)) var(--sp-0);
4137
4390
  filter: blur(var(--sticky-shadow-blur, var(--sp-0)));
4138
4391
  }
4139
4392
  .sticky--flat::before, .sticky--flat::after { background: var(--_ink); }
4140
4393
  /* 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); }
4394
+ .sticky--cotton { background-color: var(--sticky-cotton-bg, var(--pastel-cotton)); }
4395
+ .sticky--blush { background-color: var(--sticky-blush-bg, var(--pastel-blush)); }
4396
+ .sticky--tea-leaf { background-color: var(--sticky-tea-leaf-bg, var(--pastel-tea-leaf)); }
4397
+ .sticky--custard { background-color: var(--sticky-custard-bg, var(--pastel-custard)); }
4398
+ .sticky--salmon { background-color: var(--sticky-salmon-bg, var(--pastel-salmon)); }
4399
+ .sticky--iceberg { background-color: var(--sticky-iceberg-bg, var(--pastel-iceberg)); }
4400
+ .sticky--cocoa { background-color: var(--sticky-cocoa-bg, var(--pastel-cocoa)); }
4401
+ .sticky--apricot { background-color: var(--sticky-apricot-bg, var(--pastel-apricot)); }
4402
+ .sticky--water { background-color: var(--sticky-water-bg, var(--pastel-water)); }
4150
4403
 
4151
4404
  .sticky__paper {
4152
4405
  position: relative;
@@ -4163,12 +4416,10 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4163
4416
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--_fold)), calc(100% - var(--_fold)) 100%, 0 100%);
4164
4417
  }
4165
4418
  .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
4419
  .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
4420
  .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
4421
  .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 */
4422
+ .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
4423
  .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
4424
  /* 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
4425
  .sticky__fold {
@@ -4179,7 +4430,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4179
4430
  height: var(--_fold);
4180
4431
  border-top-left-radius: var(--sp-1);
4181
4432
  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);
4433
+ box-shadow: var(--shadow-fold);
4183
4434
  }
4184
4435
  /* 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
4436
  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 +4442,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4191
4442
  /* a note that stands alone in a page's column or in a centre stands in its middle */
4192
4443
  .page > .sticky, .screen__centre > .sticky { margin-inline: auto; }
4193
4444
  .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); }
4445
+ .sticky--tilt-left { rotate: calc(-1 * var(--sticky-tilt, calc(var(--tilt-base) * 5))); }
4446
+ .sticky--tilt-right { rotate: var(--sticky-tilt, calc(var(--tilt-base) * 5)); }
4196
4447
 
4197
4448
  /* ---- sticky date: the day, big, the month under it — at the end of the note's head (its top-right corner) ----
4198
4449
  <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 +4485,6 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4234
4485
  /* 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
4486
  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
4487
  .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
4488
 
4239
4489
  /* ---- sticky field: the words written ON a note — a bare textarea in the note's type and ink, filling it ----
4240
4490
  <div class="sticky__body"><textarea class="sticky-field" aria-label="Note" placeholder="Add text to this note"></textarea></div> */
@@ -4329,12 +4579,12 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4329
4579
  .sticky-thread--lapped > .sticky-post > .sticky-post__who { display: none; }
4330
4580
  .sticky-thread--lapped > .sticky-post > .sticky-post__notes { grid-area: 1 / 1; }
4331
4581
  .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 */
4582
+ .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
4583
  .sticky-thread--lapped > .sticky-post > .sticky-post__head > .sticky-post__time { margin-inline: 0; }
4334
4584
  .sticky-thread--lapped > .sticky-post > .sticky-post__head:not(:has(> .sticky-post__time)) { display: none; }
4335
4585
  /* 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); }
4586
+ .sticky-thread--lapped > .sticky-post > .sticky-post__notes > .sticky { rotate: calc(-1 * var(--sticky-thread-tilt, calc(var(--tilt-base) * 2))); }
4587
+ .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
4588
  /* under an answer note the thread ends with the room that note stands in: the last turn scrolls up clear of it */
4339
4589
  .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
4590
 
@@ -4367,11 +4617,12 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4367
4617
  .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
4618
  /* 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
4619
  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); }
4620
+ 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.)
4621
+ A pile (below) turns its notes by the same four, by their place among its children. */
4622
+ .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)); }
4623
+ .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)); }
4624
+ .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)); }
4625
+ .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
4626
 
4376
4627
  /* ---- sticky pile: one to six notes lying one over another — the newest on top (Lh 2026-10-11) ----
4377
4628
  <div class="sticky-pile sticky-pile--tap" style="--_n: 3">
@@ -4402,17 +4653,13 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4402
4653
  grid-template: var(--_size) / var(--_size);
4403
4654
  padding: var(--_pad) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-x)) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-y)) var(--_pad);
4404
4655
  }
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)); }
4656
+ .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
4657
  .sticky-pile__notes > .sticky:nth-child(2) { --_depth: 1; }
4407
4658
  .sticky-pile__notes > .sticky:nth-child(3) { --_depth: 2; }
4408
4659
  .sticky-pile__notes > .sticky:nth-child(4) { --_depth: 3; }
4409
4660
  .sticky-pile__notes > .sticky:nth-child(5) { --_depth: 4; }
4410
4661
  .sticky-pile__notes > .sticky:nth-child(6) { --_depth: 5; }
4411
4662
  .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
4663
  .sticky-pile__press { position: absolute; inset: 0; margin: 0; padding: 0; border: 0; appearance: none; background: none; -webkit-tap-highlight-color: transparent; }
4417
4664
  .sticky-pile--tap .sticky-pile__notes > .sticky { pointer-events: none; }
4418
4665
 
@@ -4438,16 +4685,16 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4438
4685
  pointer-events: none;
4439
4686
  }
4440
4687
  .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); }
4688
+ .sticky-answer--writing { z-index: calc(var(--z-top) + var(--z-over)); }
4689
+ .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
4690
  .sticky-answer--writing > .sticky-answer__scrim { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: 0s; }
4444
4691
  .sticky-answer__note {
4445
4692
  position: absolute;
4446
- top: calc(100% - var(--_size) * var(--sticky-answer-show, 0.75) - max(var(--safe-bottom), var(--kb, 0px)));
4693
+ top: calc(100% - var(--_size) * var(--sticky-answer-show, 0.75) - max(var(--safe-bottom), var(--kb, 0rem)));
4447
4694
  right: var(--sticky-answer-end, var(--screen-pad));
4448
- rotate: var(--sticky-answer-tilt, -3deg);
4695
+ rotate: var(--sticky-answer-tilt, calc(var(--tilt-base) * -3));
4449
4696
  pointer-events: auto;
4450
- transition: top var(--motion-sheet) ease, right var(--motion-sheet) ease, rotate var(--motion-sheet) ease;
4697
+ transition: top var(--motion-sheet) var(--ease), right var(--motion-sheet) var(--ease), rotate var(--motion-sheet) var(--ease);
4451
4698
  }
4452
4699
  .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
4700
  .sticky-answer__under {
@@ -4455,7 +4702,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4455
4702
  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
4703
  display: grid; gap: var(--sticky-answer-gap, var(--sp-4));
4457
4704
  opacity: 0; visibility: hidden;
4458
- transition: opacity var(--motion-sheet-rest) ease, visibility 0s var(--motion-sheet-rest);
4705
+ transition: opacity var(--motion-sheet-rest) var(--ease), visibility 0s var(--motion-sheet-rest);
4459
4706
  }
4460
4707
  .sticky-answer--writing > .sticky-answer__under { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: var(--motion-sheet-rest), 0s; }
4461
4708
  .sticky-answer__tools { display: flex; align-items: center; justify-content: space-between; gap: var(--sticky-answer-tools-gap, var(--sp-3)); }
@@ -4479,7 +4726,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4479
4726
  border-block-end: var(--rule-w) dotted currentColor;
4480
4727
  font: inherit;
4481
4728
  }
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; }
4729
+ .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
4730
  .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
4731
  /* on a note: across the top of its paper, and the note's words start under it */
4485
4732
  .sticky__stuck > .sticky-heading { position: absolute; inset: var(--sp-2) var(--sp-4) auto; }
@@ -4488,9 +4735,17 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4488
4735
  .sticky-board .sticky:has(> .sticky__stuck > .sticky-heading) .sticky__body { flex: none; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
4489
4736
  /* 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
4737
  /* 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); }
4738
+ .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
4739
  .sticky-thread--lapped > .sticky-heading { margin-block-end: var(--sp-4); }
4493
4740
 
4741
+ /* ==== the pile pressed whole, as PRESS in actions.css answers every other control (#62: a block is written in its own file) ==== */
4742
+ /* 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
4743
+ notes do not show through one another); the ring is the button's own, round the whole pile */
4744
+ .sticky-pile__press { cursor: pointer; touch-action: manipulation; }
4745
+ .sticky-pile__notes:has(> .sticky-pile__press) { transition: opacity var(--motion-press) var(--ease-out); }
4746
+ .sticky-pile__notes:has(> .sticky-pile__press:active) { opacity: var(--press-dim); }
4747
+ .sticky-pile__press:focus-visible { outline: var(--focus); outline-offset: 0; }
4748
+
4494
4749
  /* ---- css/stamp.css — stamp, empty place, album page, a stamp stuck on a surface ---- */
4495
4750
  /* cardds — stamps: the paper stamp, an empty place for one, the album page, a stamp stuck on a surface.
4496
4751
  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 +4782,44 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4527
4782
  position: relative;
4528
4783
  display: block;
4529
4784
  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 */
4785
+ color: var(--stamp-ink, var(--ink-onlight));
4786
+ filter: drop-shadow(var(--stamp-drop, var(--shadow-paper))); /* drop-shadow follows the teeth; a box-shadow would draw the rectangle */
4787
+ rotate: var(--stamp-turn, none);
4788
+ animation: var(--stamp-arrive, none);
4532
4789
  }
4533
4790
  .stamp__paper {
4534
4791
  --_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
4792
  --_hole: 2.6cqw;
4536
4793
  position: absolute;
4537
4794
  inset: 0;
4538
- background-color: var(--stamp-paper-bg, #fffdf8);
4795
+ background-color: var(--stamp-paper-bg, var(--stamp-paper));
4539
4796
  -webkit-mask: linear-gradient(#000 0 0) center / calc(100% - var(--_pitch)) calc(100% - var(--_pitch)) no-repeat,
4540
4797
  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
4798
  mask: linear-gradient(#000 0 0) center / calc(100% - var(--_pitch)) calc(100% - var(--_pitch)) no-repeat,
4542
4799
  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
4800
  }
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 */
4801
+ .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
4802
 
4546
4803
  /* the pictures, placed by COUNT: where, how wide, how far turned — the layout is the stamp's look, never the app's.
4547
4804
  A share of the picture area; the foot stays clear for the words. */
4548
4805
  .stamp__art { position: absolute; left: var(--_x); top: var(--_y); width: var(--_w); aspect-ratio: 1; translate: -50% -50%; rotate: var(--_r); }
4549
4806
  .stamp__art > * { display: block; width: 100%; height: 100%; }
4550
4807
  .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; }
4808
+ .stamp--2 .stamp__art:nth-child(1) { --_x: 37%; --_y: 27%; --_w: 58%; --_r: calc(var(--tilt-base) * -6); }
4809
+ .stamp--2 .stamp__art:nth-child(2) { --_x: 63%; --_y: 56%; --_w: 54%; --_r: calc(var(--tilt-base) * 8); }
4810
+ .stamp--3 .stamp__art:nth-child(1) { --_x: 30%; --_y: 21%; --_w: 50%; --_r: calc(var(--tilt-base) * -8); }
4811
+ .stamp--3 .stamp__art:nth-child(2) { --_x: 71%; --_y: 31%; --_w: 46%; --_r: calc(var(--tilt-base) * 7); }
4812
+ .stamp--3 .stamp__art:nth-child(3) { --_x: 47%; --_y: 58%; --_w: 54%; --_r: calc(var(--tilt-base) * -2); }
4813
+ .stamp--4 .stamp__art:nth-child(1) { --_x: 29%; --_y: 19%; --_w: 44%; --_r: calc(var(--tilt-base) * -8); }
4814
+ .stamp--4 .stamp__art:nth-child(2) { --_x: 71%; --_y: 24%; --_w: 42%; --_r: calc(var(--tilt-base) * 7); }
4815
+ .stamp--4 .stamp__art:nth-child(3) { --_x: 31%; --_y: 57%; --_w: 42%; --_r: calc(var(--tilt-base) * 5); }
4816
+ .stamp--4 .stamp__art:nth-child(4) { --_x: 70%; --_y: 60%; --_w: 44%; --_r: calc(var(--tilt-base) * -6); }
4560
4817
 
4561
4818
  /* 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
4819
  .stamp__cap { position: absolute; inset-inline: 5cqw; bottom: 3.5cqw; display: grid; gap: 0.4cqw; }
4563
4820
  .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)); }
4821
+ .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)); }
4822
+ .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
4823
  /* at the stamp's own size and under (and stuck on a note): the small line goes, the name grows */
4567
4824
  /* --bare: the pictures alone, no words — a stamp stuck on something (a note, a postcard). The words belong to the album.
4568
4825
  The pictures take the foot's room: each sits a little lower, so the group stands in the middle of the frame. */
@@ -4576,14 +4833,13 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4576
4833
 
4577
4834
  /* --tilt: turned a little on its middle — the one stamp a start screen hands over */
4578
4835
  .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); }
4836
+ .stamp--tilt-left { rotate: calc(-1 * var(--stamp-tilt, calc(var(--tilt-base) * 5))); }
4837
+ .stamp--tilt-right { rotate: var(--stamp-tilt, calc(var(--tilt-base) * 5)); }
4581
4838
 
4582
4839
  /* --tap: the whole stamp is the button (`onClick`); the press and the ring are the PRESS list's (actions.css) */
4583
4840
  .stamp--tap { appearance: none; border: 0; padding: 0; background: none; font: inherit; text-align: start; -webkit-tap-highlight-color: transparent; }
4584
4841
 
4585
4842
  /* 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
4843
 
4588
4844
  /* ---- stamp place: an empty place for a stamp — an album's gap, "make yours" ----
4589
4845
  <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 +4933,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4677
4933
  background-color: var(--stamp-cell-on-bg, var(--accent));
4678
4934
  color: var(--stamp-cell-on-ink, var(--on-accent));
4679
4935
  }
4680
- .stamp-cell__on > .icon { width: 70%; height: 70%; }
4936
+ .stamp-cell__on > .icon { --icon-size: 70%; }
4681
4937
  /* --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
4938
  <div class="stamp-book stamp-book--bare"> */
4683
4939
  .stamp-book--bare { padding: 0; border-radius: 0; background: none; }
@@ -4721,24 +4977,24 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4721
4977
  After the rule above, at the same weight: `main` is the one place a stuck stamp reads --stamp-size. */
4722
4978
  .stamp-on--main > .stamp { width: var(--stamp-size, calc(var(--tap) * 2)); }
4723
4979
  .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; }
4980
+ .stamp-on--move { cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; transition: scale var(--motion-press) var(--ease-out); }
4725
4981
  .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 */
4982
+ .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
4983
  @media (prefers-reduced-motion: reduce) { .stamp-on.is-held { scale: 1; } }
4728
4984
 
4729
4985
  /* ---- the cross that takes a stamp off (`StampOn onRemove`): a small dark disc with an x on the stamp's top right corner ----
4730
4986
  <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
4987
  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
4988
  (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). */
4989
+ 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
4990
  .stamp-on__remove-disc {
4735
4991
  display: grid;
4736
4992
  place-items: center;
4737
4993
  width: var(--stamp-on-remove-size, var(--sp-6));
4738
4994
  aspect-ratio: 1;
4739
4995
  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);
4996
+ background-color: var(--stamp-on-remove-bg, var(--ink-onlight));
4997
+ box-shadow: 0 0 0 var(--sp-0) currentColor, var(--shadow-lift);
4742
4998
  }
4743
4999
  .stamp-on__remove {
4744
5000
  --_hit: max(var(--tap-floor), var(--stamp-on-remove-size, var(--sp-6)));
@@ -4755,20 +5011,31 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4755
5011
  border: 0;
4756
5012
  border-radius: 50%;
4757
5013
  background: none;
4758
- color: var(--stamp-on-remove-ink, #ffffff);
5014
+ color: var(--stamp-on-remove-ink, var(--ink-ondark));
4759
5015
  font: inherit;
4760
5016
  -webkit-tap-highlight-color: transparent;
4761
5017
  }
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; } }
5018
+ .stamp-on__remove-disc > .icon { --icon-size: 60%; }
5019
+ @media (prefers-reduced-motion: no-preference) { .stamp-on__remove { animation: stamp-on-remove-in var(--motion-press) var(--ease-out); } }
5020
+ @keyframes stamp-on-remove-in { from { opacity: 0; scale: var(--scale-arrive); } }
4765
5021
 
4766
5022
  /* --arrive: the stamp sticks ITSELF (StampOn `arrive`; a Stamp's `arrive` too, where it already stands) — struck onto its place (`strike`: tokens.css).
4767
5023
  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
5024
  @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; }
5025
+ .stamp-on--arrive, .stamp--arrive { animation: strike var(--stamp-on-arrive-time, var(--motion-sheet)) var(--ease-in) both; }
4770
5026
  }
4771
5027
 
5028
+ /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
5029
+ .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
5030
+ /* ---- where it sits in what people.css draws (docs/reference.md rule 15) ---- */
5031
+ /* 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
5032
+ 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)
5033
+ <span class="avatar-stack"><span class="stamp stamp--bare">…</span><span class="stamp stamp--bare">…</span><span class="avatar-stack__more">+9</span></span> */
5034
+ .avatar-stack > .stamp { flex: none; margin-inline: 0; }
5035
+ .avatar-stack > .stamp + .stamp { margin-left: calc(-1 * var(--sp-2)); }
5036
+ /* ---- where it sits in what post.css draws (docs/reference.md rule 15) ---- */
5037
+ .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 */
5038
+
4772
5039
  /* ---- css/post.css — the post: postmark, a postcard read (letter), written (desk), kept (box) ---- */
4773
5040
  /* cardds — the post: a postcard read (post-letter), written (post-desk) and kept (post-box), the postmark, the card stock.
4774
5041
  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 +5058,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4791
5058
  .postmark {
4792
5059
  --_base: calc(var(--tap) * 1.75);
4793
5060
  position: relative;
4794
- z-index: 1;
5061
+ z-index: var(--z-over);
4795
5062
  flex: none;
4796
5063
  display: grid;
4797
5064
  place-content: center;
@@ -4806,7 +5073,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4806
5073
  color: var(--postmark-ink, color-mix(in srgb, currentColor 72%, transparent));
4807
5074
  font: var(--postmark-font, var(--type-label));
4808
5075
  text-align: center;
4809
- rotate: var(--postmark-tilt, -10deg);
5076
+ rotate: var(--postmark-tilt, calc(var(--tilt-base) * -10));
4810
5077
  }
4811
5078
  .postmark::after { /* the cancel lines that run on over the stamp */
4812
5079
  content: "";
@@ -4831,9 +5098,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4831
5098
  display: grid;
4832
5099
  width: var(--post-corner-stamp-w, var(--_w));
4833
5100
  aspect-ratio: 4 / 5;
4834
- rotate: var(--post-corner-stamp-tilt, 2deg);
5101
+ rotate: var(--post-corner-stamp-tilt, calc(var(--tilt-base) * 2));
4835
5102
  }
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
5103
 
4838
5104
  /* ============================================================
4839
5105
  post-letter — READING one that arrived. The written side stood upright: the address half over the message half.
@@ -4874,15 +5140,15 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4874
5140
  min-height: 0;
4875
5141
  overflow: clip;
4876
5142
  visibility: hidden;
4877
- rotate: calc(-1 * var(--post-letter-tilt, 2deg));
5143
+ rotate: calc(-1 * var(--post-letter-tilt, calc(var(--tilt-base) * 2)));
4878
5144
  }
4879
- .post-letter__sheet--back { rotate: var(--post-letter-tilt, 2deg); } /* turned over, the lean is the other way */
5145
+ .post-letter__sheet--back { rotate: var(--post-letter-tilt, calc(var(--tilt-base) * 2)); } /* turned over, the lean is the other way */
4880
5146
  .post-letter--back > .post-letter__sheet--back,
4881
5147
  .post-letter--picture > .post-letter__sheet--picture { visibility: visible; }
4882
5148
  .post-letter:not(:has(> .post-letter__journey)) { --_room: 0rem; } /* no journey line: its room goes back to the sheet */
4883
5149
  .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
5150
 
4885
- /* the picture side: the print with its white margin, the place printed under it */
5151
+ /* the picture side: the print with its margin of paper, the place printed under it */
4886
5152
  .post-letter__sheet--picture {
4887
5153
  display: grid;
4888
5154
  grid-template-rows: minmax(0, 1fr) auto;
@@ -4901,7 +5167,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4901
5167
  .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
5168
  .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
5169
  .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; }
5170
+ .post-letter__kicker { font: var(--type-caption); color: var(--_muted); letter-spacing: var(--track-caps); }
4905
5171
  .post-letter__lines { display: grid; gap: var(--sp-1); }
4906
5172
  .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
5173
  .post-letter__line--name { font: var(--type-title); }
@@ -4914,7 +5180,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4914
5180
  gap: var(--sp-3);
4915
5181
  font: var(--post-letter-hand-font, var(--type-h2));
4916
5182
  font-size: var(--post-letter-hand-font-size, var(--fs-lg));
4917
- line-height: var(--post-letter-hand-leading, 1.6);
5183
+ line-height: var(--post-letter-hand-leading, var(--lh-hand));
4918
5184
  }
4919
5185
  .post-letter__message > p { margin: 0; }
4920
5186
  .post-letter__sign { align-self: flex-end; }
@@ -4926,7 +5192,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4926
5192
  inset-block-start: calc(var(--_peek) / 2);
4927
5193
  inset-inline-start: var(--sp-2);
4928
5194
  translate: 0 -50%;
4929
- z-index: 2;
5195
+ z-index: var(--z-over-2);
4930
5196
  display: inline-flex;
4931
5197
  align-items: center;
4932
5198
  gap: var(--sp-2);
@@ -4987,8 +5253,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
4987
5253
  background: transparent;
4988
5254
  color: inherit;
4989
5255
  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));
5256
+ line-height: calc(var(--fs-body) * var(--post-desk-leading, var(--lh-ruled)));
5257
+ --_line: calc(var(--fs-body) * var(--post-desk-leading, var(--lh-ruled)));
4992
5258
  /* the ruled lines: one under every line of writing */
4993
5259
  background-image: linear-gradient(to bottom, transparent calc(100% - var(--border-w)), var(--_rule) 0);
4994
5260
  background-size: 100% var(--_line);
@@ -5005,10 +5271,10 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5005
5271
  /* the stamp box: empty it is a place of dots to press; pressed, the chosen stamp lies in it */
5006
5272
  .post-desk__slot { border: 0; background: transparent; color: inherit; }
5007
5273
  @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 */
5274
+ .post-desk__slot > .stamp { --stamp-arrive: post-desk-stick var(--motion-quick) var(--ease-out) both; }
5275
+ .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
5276
  }
5011
- @keyframes post-desk-stick { from { translate: 0 calc(-1 * var(--sp-8)); rotate: 14deg; opacity: 0; } }
5277
+ @keyframes post-desk-stick { from { translate: 0 calc(-1 * var(--sp-8)); rotate: calc(var(--tilt-base) * 14); opacity: 0; } }
5012
5278
  /* the other half's showing edge brings it across: a tap-wide strip out to the screen's edge */
5013
5279
  .post-desk__reach { position: absolute; inset-block: 0; width: var(--tap); border: 0; background: transparent; }
5014
5280
  .post-desk__reach--address { inset-inline-end: calc(-1 * var(--screen-pad)); }
@@ -5038,7 +5304,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5038
5304
  border-block-end: var(--rule-w) solid transparent;
5039
5305
  background: transparent;
5040
5306
  }
5041
- .post-desk__pick > .stamp { rotate: var(--post-corner-stamp-tilt, 2deg); } /* in the tray a stamp keeps its words */
5307
+ .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
5308
  .post-desk__pick[aria-pressed="true"] { border-block-end-color: var(--on-surface); }
5043
5309
  .post-desk__note { margin: 0; font: var(--type-caption); color: var(--post-desk-note-ink, var(--on-surface-muted)); min-height: 1.35em; }
5044
5310
  /* what stands where the card was, once it has left */
@@ -5063,7 +5329,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5063
5329
  margin-block-end: calc(var(--_e) - var(--_h));
5064
5330
  display: grid;
5065
5331
  grid-template-rows: var(--post-box-edge, var(--_edge)) minmax(0, 1fr);
5066
- transition: translate var(--motion-sheet) ease, rotate var(--motion-sheet) ease;
5332
+ transition: translate var(--motion-sheet) var(--ease), rotate var(--motion-sheet) var(--ease);
5067
5333
  }
5068
5334
  .post-box {
5069
5335
  --_edge: calc(var(--sp-base) * 20);
@@ -5078,8 +5344,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5078
5344
  }
5079
5345
  .post-box:has(> .post-box__card.is-open:not(:last-child)) { padding-block-end: var(--_part); }
5080
5346
  .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); }
5347
+ .post-box__card:nth-child(3n + 1) { rotate: var(--post-box-lean, calc(var(--tilt-base) * 0.6)); }
5348
+ .post-box__card:nth-child(3n + 2) { rotate: calc(var(--post-box-lean, calc(var(--tilt-base) * 0.6)) * -0.8); }
5083
5349
  .post-box__card.is-open { rotate: none; }
5084
5350
  .post-box__card.is-open ~ .post-box__card { translate: 0 var(--_part); }
5085
5351
  .post-box__edge {
@@ -5107,56 +5373,25 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5107
5373
  .post-box__card:not(.is-open) .postmark { box-sizing: border-box; place-content: start center; padding-block-start: calc(var(--_size) * 0.18); }
5108
5374
  @media (prefers-reduced-motion: reduce) { .post-box__card { transition: none; } }
5109
5375
 
5376
+ /* ---- the desk's card leaves as a card with `leave` does (card.css LEAVE): the same two knobs, the same time ---- */
5377
+ .post-desk__card { transition: translate var(--motion-sheet) var(--ease), rotate var(--motion-sheet) var(--ease); }
5378
+ .post-desk--sent .post-desk__card { --_y: calc(var(--leave-rise, 160%) * -1); rotate: var(--leave-turn, calc(var(--tilt-base) * -5)); }
5379
+ @media (prefers-reduced-motion: reduce) { .post-desk__card { transition: none; } }
5380
+
5110
5381
  /* ---- css/media.css — cover card, quote, tile grid, fold, mosaic, wave ---- */
5111
5382
  /* ============================================================
5112
5383
  cardds/media.css — media & display inside cards:
5113
- cover card · quote · tile grid · fold · waveform
5384
+ quote · tile grid · waveform (the cover card and the fold are card variants: card.css)
5114
5385
  ============================================================ */
5115
5386
 
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
5387
  /* ---- quote: giant mark, big text, attribution ----
5153
5388
  <blockquote class="quote"><p class="quote__text">It's overcast…</p><footer class="quote__by">Wednesday 15</footer></blockquote> */
5154
5389
  .quote { margin: 0; display: grid; gap: var(--quote-gap, var(--sp-3)); }
5155
5390
  .quote::before {
5156
5391
  content: "\201C";
5157
- font: 900 calc(var(--fs-h1) * 3)/0.55 var(--font-display);
5392
+ font: var(--fw-black) calc(var(--fs-h1) * 3) / var(--lh-mark) var(--font-display);
5158
5393
  height: var(--sp-10);
5159
- opacity: 0.9;
5394
+ opacity: var(--fade-most);
5160
5395
  }
5161
5396
  .quote__text { margin: 0; font: var(--quote-text-font, var(--type-h1)); letter-spacing: var(--track-h1); text-wrap: balance; }
5162
5397
  .quote--sm .quote__text { font: var(--type-h2); letter-spacing: var(--track-h2); }
@@ -5170,7 +5405,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5170
5405
  grid-template-columns: repeat(auto-fill, minmax(min(var(--tile-min), 100%), 1fr));
5171
5406
  gap: var(--tile-grid-gap, var(--sp-3));
5172
5407
  }
5173
- .tile-grid--3 { --tile-min: 6rem; } /* three across on a phone (attachments, upload kinds) */
5408
+ .tile-grid--3 { --tile-min: calc(var(--tap-base) * 2); } /* three across on a phone (attachments, upload kinds) */
5174
5409
  .tile {
5175
5410
  display: grid;
5176
5411
  align-content: space-between;
@@ -5217,42 +5452,18 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5217
5452
  .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
5453
  .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
5454
 
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
5455
  /* ---- wave: audio waveform stand-in for voice rows ----
5245
5456
  <span class="wave"></span> — sits in a .row__body, fills the width */
5246
5457
  .wave {
5247
5458
  /* 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
5459
  (auto-fill), spread edge to edge; bars that do not fit go to rows of no height, clipped away */
5249
5460
  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 */
5461
+ 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
5462
  grid-template-rows: 100%;
5252
5463
  grid-auto-rows: 0;
5253
5464
  justify-content: space-between;
5254
5465
  align-items: center;
5255
- column-gap: var(--wave-gap, 0.125rem);
5466
+ column-gap: var(--wave-gap, var(--hairline-w));
5256
5467
  height: var(--wave-h, var(--icon));
5257
5468
  width: 100%;
5258
5469
  overflow: hidden;
@@ -5278,7 +5489,6 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5278
5489
  }
5279
5490
  .mosaic > :first-child { grid-row: span 2; }
5280
5491
  .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
5492
  .mosaic > img { width: 100%; height: 100%; object-fit: cover; }
5283
5493
 
5284
5494
  /* ---- postcard: a member's introduction in the shape of a cover card — the photo fills it,
@@ -5336,8 +5546,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5336
5546
  .postcard__turn:focus-visible { outline-offset: calc(-1 * var(--sp-1)); } /* the ring inside the card's edge */
5337
5547
  /* the front: the photo (or the wireframe box) fills it; the band reads on a scrim */
5338
5548
  .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 {
5549
+ .postcard__front .postcard__turn > img {
5341
5550
  position: absolute;
5342
5551
  inset: 0;
5343
5552
  width: 100%;
@@ -5351,7 +5560,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5351
5560
  content: "";
5352
5561
  position: absolute;
5353
5562
  inset: 0;
5354
- z-index: 1;
5563
+ z-index: var(--z-over);
5355
5564
  pointer-events: none;
5356
5565
  background: linear-gradient(to top, var(--scrim), transparent 45%);
5357
5566
  }
@@ -5367,7 +5576,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5367
5576
  Taps between its words fall through to the turn; its buttons take their own. */
5368
5577
  .postcard__band {
5369
5578
  position: relative;
5370
- z-index: 2;
5579
+ z-index: var(--z-over-2);
5371
5580
  display: flex;
5372
5581
  align-items: end;
5373
5582
  gap: var(--postcard-band-gap, var(--sp-3));
@@ -5377,12 +5586,13 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5377
5586
  .postcard__band > * { pointer-events: auto; }
5378
5587
  .postcard__words { display: grid; gap: var(--postcard-words-gap, var(--sp-1)); flex: 1; min-width: 0; pointer-events: none; }
5379
5588
  .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
5589
  /* a voice note on the back: no words to read — its timeline (a Progress), its length and the control to hear it
5382
5590
  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; }
5591
+ .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
5592
  .postcard__voice > * { pointer-events: auto; }
5385
- .postcard__voice > .progress { justify-self: stretch; pointer-events: none; }
5593
+
5594
+ /* ==== the tile a choice draws, picked (choice.css drives it with its input) (#62: a block is written in its own file) ==== */
5595
+ .tile--pick:has(:checked) { background: var(--accent); --dark: var(--accent-dark); color: var(--card-ink); border-color: transparent; }
5386
5596
 
5387
5597
  /* ---- css/layover.css — ask drawer, popover menu, banner, float bar, callout ---- */
5388
5598
  /* ============================================================
@@ -5418,14 +5628,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5418
5628
  sets open={false}).
5419
5629
  Reduced motion: the finished state, no beats; closing fires at once. */
5420
5630
  .modal {
5421
- --drawer-h: 14rem;
5631
+ --drawer-h: calc(var(--tap-base) * 14 / 3);
5422
5632
  position: absolute; /* at the stage's centre, and kept there as it grows: its middle is pinned at 50% and */
5423
5633
  top: 50%; /* translate -50% of its OWN height re-centres it on every height. NOT inset 0 + margin auto + */
5424
5634
  inset-inline: 0; /* align-self: center — Safari 18 ignores align-self on an absolute box in a block container, */
5425
5635
  margin-inline: auto; /* so the card stretched to the whole stage (iOS 18.6, Lh 2026-09-24); nor height: fit-content */
5426
5636
  translate: 0 -50%;
5427
5637
  width: calc(100% - 2 * var(--screen-pad));
5428
- z-index: 2; /* over the dim stage's scrim (z 1) */
5638
+ z-index: var(--z-over-2); /* over the dim stage's scrim (z 1) */
5429
5639
  max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) */
5430
5640
  padding: 0; /* the parts pad themselves */
5431
5641
  border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
@@ -5470,18 +5680,17 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5470
5680
  }
5471
5681
  /* the CONFIRM: a Modal with no ModalBody: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
5472
5682
  .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)); }
5683
+ .modal__drawer .t-caption { --t-caption-ink: var(--modal-drawer-caption-ink, var(--card-muted)); }
5474
5684
  .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
5685
  /* 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
5686
  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
5687
  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); }
5688
+ .modal__drawer .btn--quiet { --btn-quiet-border: var(--modal-drawer-quiet-border, currentColor); }
5689
+ .modal__drawer .icon-btn { --icon-btn-bg: transparent; --icon-btn-ink: currentColor; --icon-btn-border: var(--ink-rail); }
5690
+ .modal__drawer .icon-btn--invert { --icon-btn-size: var(--tap-lg); }
5482
5691
  @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; }
5692
+ .modal { animation: modal-drop calc(2 * var(--motion-sheet)) var(--ease) both; }
5693
+ .modal__drawer { animation: modal-open calc(2 * var(--motion-sheet)) var(--ease) both; }
5485
5694
  @keyframes modal-drop {
5486
5695
  0% { translate: 0 calc(-50% - 100cqh); } /* above the stage */
5487
5696
  50%, 100% { translate: 0 -50%; } /* 1. at the centre — and it stays: beat 2 is the drawer's */
@@ -5496,8 +5705,8 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5496
5705
  .btn-trio__dots {
5497
5706
  width: var(--btn-trio-dots-w, var(--sp-5));
5498
5707
  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;
5708
+ 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;
5709
+ opacity: var(--fade-soft);
5501
5710
  }
5502
5711
 
5503
5712
  /* ---- popover menu: a card anchored to its button, rows of icon + label ----
@@ -5520,7 +5729,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5520
5729
  border-radius: var(--menu-radius, var(--r-card));
5521
5730
  --menu-pad: var(--sp-2); /* published: a row's radius is concentric with the panel's */
5522
5731
  padding: var(--menu-pad);
5523
- min-width: 12rem;
5732
+ min-width: calc(var(--tap-base) * 4);
5524
5733
  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
5734
  --_menu-max: min(60dvh, calc(100dvh - 2 * var(--sp-5)));
5526
5735
  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 +5763,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5554
5763
  .menu[popover] {
5555
5764
  position-area: block-end span-inline-end;
5556
5765
  margin-top: var(--sp-2);
5557
- min-width: max(12rem, anchor-size(width)); /* at least as wide as its trigger */
5766
+ min-width: max(calc(var(--tap-base) * 4), anchor-size(width)); /* at least as wide as its trigger */
5558
5767
  position-try-fallbacks: flip-block, flip-inline;
5559
5768
  }
5560
5769
  .menu--end[popover] { position-area: block-end span-inline-start; }
@@ -5587,11 +5796,11 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5587
5796
  .banner__summary {
5588
5797
  list-style: none;
5589
5798
  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;
5799
+ 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
5800
  }
5592
5801
  .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; }
5802
+ .banner__summary .icon { --icon-motion: rotate var(--motion-quick) var(--ease); }
5803
+ .banner[open] .banner__summary .icon { --icon-turn: 180deg; }
5595
5804
  /* 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
5805
  word otherwise), and a word with no break in it — a link — breaks where it must (mailpool, Lh 2026-10-01) */
5597
5806
  .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 +5808,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5599
5808
  @supports (interpolate-size: allow-keywords) {
5600
5809
  @media (prefers-reduced-motion: no-preference) {
5601
5810
  .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; }
5811
+ .banner::details-content { block-size: 0; overflow: clip; transition: block-size var(--motion-quick) var(--ease), content-visibility var(--motion-quick) allow-discrete; }
5603
5812
  .banner[open]::details-content { block-size: auto; }
5604
5813
  }
5605
5814
  }
@@ -5658,7 +5867,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5658
5867
  /* aria-current="false" is the correct way to mark the OTHER tabs, so the
5659
5868
  bare attribute selector lights every one of them up */
5660
5869
  .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); }
5870
+ .float-bar > .icon-btn { --icon-btn-size: var(--bar-h); --icon-btn-shadow: var(--shadow-float); }
5662
5871
 
5663
5872
  /* ---- callout: a small card with a tail — over a map, a media, a mascot ----
5664
5873
  <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 +5876,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5667
5876
  display: grid;
5668
5877
  gap: var(--callout-gap, var(--sp-0));
5669
5878
  width: max-content;
5670
- max-width: 15rem;
5879
+ max-width: calc(var(--tap-base) * 5);
5671
5880
  padding: var(--callout-pad-y, var(--sp-3)) var(--callout-pad-x, var(--sp-4));
5672
5881
  border: var(--border-w) solid var(--callout-border, var(--action-border));
5673
5882
  border-radius: var(--callout-radius, var(--r-card));
@@ -5700,7 +5909,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5700
5909
  <p class="splash__text">…</p></div></section>
5701
5910
  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
5911
  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; }
5912
+ .splash { position: relative; background: var(--splash-bg, var(--surface)); transition: opacity var(--motion-sheet) var(--ease); }
5704
5913
  .splash--leaving { opacity: 0; }
5705
5914
  .splash__group {
5706
5915
  position: absolute; inset-inline: var(--screen-pad); top: var(--splash-at, var(--screen-centre)); translate: 0 -50%;
@@ -5722,13 +5931,13 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5722
5931
  position: fixed; inset-inline: 0; top: calc(var(--sp-3) + var(--safe-top));
5723
5932
  max-width: var(--screen-w); margin-inline: auto; padding-inline: var(--screen-pad, var(--sp-4));
5724
5933
  display: grid; justify-items: center;
5725
- z-index: calc(var(--z-top) + 1);
5934
+ z-index: calc(var(--z-top) + var(--z-over));
5726
5935
  pointer-events: none; visibility: hidden;
5727
5936
  translate: 0 calc(var(--_out) * (100% + var(--sp-8) + var(--safe-top)));
5728
- transition: translate var(--motion-sheet) ease, visibility 0s var(--motion-sheet);
5937
+ transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--motion-sheet);
5729
5938
  }
5730
5939
  .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))); }
5940
+ .snackbar--bottom { --_out: 1; top: auto; bottom: calc(var(--sp-3) + var(--bottom-h, 0rem) + max(var(--safe-bottom), var(--kb, 0rem))); }
5732
5941
  .snackbar--open { translate: 0; visibility: visible; transition-delay: 0s; }
5733
5942
  .snackbar__body {
5734
5943
  display: flex; align-items: center; gap: var(--snackbar-gap, var(--sp-3));
@@ -5743,12 +5952,15 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5743
5952
  font: inherit; text-align: start;
5744
5953
  pointer-events: auto;
5745
5954
  }
5746
- .snackbar__body > .icon { flex: none; color: var(--snackbar-icon-ink, var(--signal-ok)); }
5955
+ .snackbar__body > .icon { --icon-ink: var(--snackbar-icon-ink, var(--signal-ok)); }
5747
5956
  .snackbar__words { display: grid; min-width: 0; overflow-wrap: anywhere; }
5748
5957
  .snackbar__text { font: var(--snackbar-text-font, var(--type-body)); }
5749
5958
  .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
5959
  @media (prefers-reduced-motion: reduce) { .snackbar { transition: none; } }
5751
5960
 
5961
+ /* ---- where it sits in what actions.css draws (docs/reference.md rule 15) ---- */
5962
+ .dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards (= .menu--end, layover.css) */
5963
+
5752
5964
  /* ---- css/finish.css — light, shadow, edges — only under data-finish="lit" ---- */
5753
5965
  /* ============================================================
5754
5966
  cardds/finish.css — FINISH: the finish of a card's paper. A style of its own (Lh 2026-10-02: the plain
@@ -5785,28 +5997,28 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5785
5997
  ============================================================ */
5786
5998
  [data-finish="lit"] {
5787
5999
  --_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 */
6000
+ --finish-edge: inset 0 0.0625rem 0 rgb(255 255 255 / 0.55); /* the lit top edge */
6001
+ --finish-ring: 0 0 0 0.0625rem color-mix(in srgb, var(--_shade) 7%, transparent); /* the hairline round a raised thing */
5790
6002
 
5791
6003
  /* the three shadows every component already reads — layered: one close and crisp, one far and soft */
5792
6004
  --shadow-card: var(--finish-edge), var(--finish-ring),
5793
- 0 1px 2px color-mix(in srgb, var(--_shade) 8%, transparent),
6005
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 8%, transparent),
5794
6006
  0 0.375rem 0.75rem -0.125rem color-mix(in srgb, var(--_shade) 8%, transparent),
5795
6007
  0 1.25rem 2.5rem -0.75rem color-mix(in srgb, var(--_shade) 14%, transparent);
5796
6008
  --shadow-float: var(--finish-edge), var(--finish-ring),
5797
- 0 1px 2px color-mix(in srgb, var(--_shade) 12%, transparent),
6009
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 12%, transparent),
5798
6010
  0 0.5rem 1.25rem -0.25rem color-mix(in srgb, var(--_shade) 22%, transparent);
5799
6011
  --shadow-sheet: var(--finish-edge), var(--finish-ring),
5800
- 0 -1px 2px color-mix(in srgb, var(--_shade) 6%, transparent),
6012
+ 0 -0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 6%, transparent),
5801
6013
  0 -0.75rem 2.5rem color-mix(in srgb, var(--_shade) 18%, transparent);
5802
6014
 
5803
6015
  /* a control: its own small lift, and the sheen that runs down its face */
5804
6016
  --finish-control: var(--finish-edge), var(--finish-ring),
5805
- 0 1px 2px color-mix(in srgb, var(--_shade) 12%, transparent),
6017
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 12%, transparent),
5806
6018
  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);
6019
+ --finish-control-down: var(--finish-ring), inset 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 18%, transparent);
5808
6020
  --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);
6021
+ --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
6022
 
5811
6023
  --outline: color-mix(in srgb, currentColor 14%, transparent);
5812
6024
  }
@@ -5828,9 +6040,9 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5828
6040
  }
5829
6041
  /* the main button throws its own colour */
5830
6042
  [data-finish="lit"] .btn--primary,
5831
- [data-finish="lit"] .icon-btn--invert:not(.icon-btn-group > *) {
6043
+ [data-finish="lit"] :is(.icon-btn--invert, .icon-btn--primary):not(.icon-btn-group > *) {
5832
6044
  box-shadow: var(--finish-edge),
5833
- 0 1px 2px color-mix(in srgb, var(--_shade) 20%, transparent),
6045
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 20%, transparent),
5834
6046
  0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
5835
6047
  }
5836
6048
  /* 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 +6056,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5844
6056
  }
5845
6057
  [data-finish="lit"] .icon-btn-group { box-shadow: var(--finish-control); }
5846
6058
  [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); }
6059
+ [data-finish="lit"] .toggle::after { box-shadow: 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 35%, transparent); }
5848
6060
 
5849
6061
  /* ---- sunk: what the member types into ---- */
5850
6062
  [data-finish="lit"] :is(.field, .pin__cell) { box-shadow: var(--finish-sunk); }
@@ -5862,12 +6074,12 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5862
6074
  }
5863
6075
  [data-finish="print"] .card {
5864
6076
  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);
6077
+ box-shadow: var(--print-off, 0.1875rem) var(--print-off, 0.1875rem) 0 0 color-mix(in srgb, var(--card-ink) 28%, transparent);
5866
6078
  isolation: isolate; /* the grain's blend stays inside the card */
5867
6079
  }
5868
6080
  /* 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
6081
  [data-finish="print"] .card::after {
5870
- content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: 1;
6082
+ content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: var(--z-over);
5871
6083
  opacity: var(--print-grain, 0.32); mix-blend-mode: multiply;
5872
6084
  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
6085
  }
@@ -5879,10 +6091,11 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5879
6091
  }
5880
6092
  [data-finish="print"] :is(.btn, .chip) {
5881
6093
  border-color: var(--_ink);
5882
- box-shadow: 2px 2px 0 0 color-mix(in srgb, currentColor 35%, transparent);
6094
+ box-shadow: 0.125rem 0.125rem 0 0 color-mix(in srgb, currentColor 35%, transparent);
5883
6095
  }
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; }
6096
+ [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 */
6097
+ [data-finish="print"] .btn--primary { box-shadow: 0.125rem 0.125rem 0 0 color-mix(in srgb, var(--card-ink) 60%, transparent); }
6098
+ [data-finish="print"] :is(.btn, .chip):active { box-shadow: none; translate: 0.125rem 0.125rem; }
5886
6099
  [data-finish="print"] .card__thumb { border-style: dashed; }
5887
6100
 
5888
6101
  /* ============================================================
@@ -5894,29 +6107,29 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
5894
6107
  [data-finish="sticker"] { --outline: color-mix(in srgb, currentColor 18%, transparent); }
5895
6108
  [data-finish="sticker"] .card {
5896
6109
  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);
6110
+ 0 0 0 var(--sticker-cut, 0.3125rem) #fff,
6111
+ 0 calc(var(--sticker-cut, 0.3125rem) + 0.25rem) 0 0 color-mix(in srgb, var(--on-surface) 16%, transparent),
6112
+ 0 calc(var(--sticker-cut, 0.3125rem) + 0.75rem) 1.25rem -0.5rem color-mix(in srgb, var(--on-surface) 22%, transparent);
5900
6113
  }
5901
6114
  [data-finish="sticker"] .card:not(.sheet, .modal, .card--away, .sheet-stack *, .card-list > *, .card-feed > *) { rotate: var(--sticker-tilt, 0.8deg); }
5902
6115
  [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
6116
  [data-finish="sticker"] .card-list { gap: var(--sp-5); }
5904
6117
  [data-finish="sticker"] .btn {
5905
- box-shadow: 0 3px 0 0 color-mix(in srgb, currentColor 30%, transparent);
6118
+ box-shadow: 0 0.1875rem 0 0 color-mix(in srgb, currentColor 30%, transparent);
5906
6119
  border-color: color-mix(in srgb, currentColor 25%, transparent);
5907
6120
  }
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; }
6121
+ [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)); }
6122
+ [data-finish="sticker"] .btn:active { box-shadow: none; translate: 0 0.1875rem; }
6123
+ [data-finish="sticker"] .chip { box-shadow: 0 0 0 0.1875rem #fff; }
5911
6124
 
5912
6125
  /* ============================================================
5913
6126
  ink — a poster / comic print: --ink-w of the surface's ink round every boundary and control, and a hard
5914
6127
  offset of --ink-off in the same ink. The two wireframe borders come back as that line.
5915
6128
  ============================================================ */
5916
6129
  [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); }
6130
+ [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); }
6131
+ [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; }
6132
+ [data-finish="ink"] .btn--primary { border-color: var(--on-surface); box-shadow: 0.1875rem 0.1875rem 0 0 var(--on-surface); }
6133
+ [data-finish="ink"] .toggle { box-shadow: 0.125rem 0.125rem 0 0 var(--on-surface); }
6134
+ [data-finish="ink"] .card__thumb { border-width: var(--ink-w, 0.15625rem); }
5922
6135