@usableapp/cardds 0.7.53 → 0.7.54

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.7.54 — 2026-10-06
6
+
7
+ ### Changed
8
+ - **A stacked sheet renders its own place.** `SheetStack` still takes `open` and `refreshing` as indexes: which sheet is open, and whether its data is in, stay yours. The card renders what those mean (`is-open`, `is-before`, `is-after`, `is-loading`, the grip, the pull). Nothing to change in a screen that already passes those two. Do not stamp those classes onto the card after it has rendered.
9
+ - **The open step's cursor mark comes from the field.** `Field`, `Pin` (its first cell), `Composer`, `FileBtn`, and `Slider` render `data-autofocus`, and `data-cursor` while the steps' holder is parked. A raw `<input>` written by hand in a step does not get the mark. Nothing to change when the step's first control is one of those.
10
+ - **`Slider` publishes `--slider-at` from the value it renders.** Nothing to change in a screen.
11
+
5
12
  ## 0.7.53 — 2026-10-05
6
13
 
7
14
  ### Fixed
package/README.md CHANGED
@@ -188,7 +188,7 @@ the card is the material, the sheet is what it is in the stack.
188
188
  sixth would render at slot 0, on top of the first.
189
189
  - **Every head needs its own `.sheet-stack__back`** if the stack is `--tap`. It stays
190
190
  hidden until that sheet is open, then slides into a reserved header slot.
191
- - **No ids, no wiring.** `SheetStack` holds which sheet is open (`open` + `onOpenChange`, or `defaultOpen`). A pull down in the open content asks `onRefresh(index)`. The stack does not fetch: the app reloads (that sheet alone, or more — its call) and says so with `refreshing` (that index, then `null` when the data is in) — the sheet's grip dots fade up and down meanwhile, and a pull there does not ask again.
191
+ - **No ids, no wiring.** `SheetStack` holds which sheet is open (`open` + `onOpenChange`, or `defaultOpen`). A pull down in the open content asks `onRefresh(index)`. The stack does not fetch: the app reloads (that sheet alone, or more — its call) and says so with `refreshing` (that index, then `null` when the data is in) — that sheet's grip dots dissolve white to black meanwhile, and a pull there does not ask again.
192
192
  - **`.card-stack` is gone.** That was this component's first name and the wrong
193
193
  one — the members are sheets and the stack is the menu, not a list of cards.
194
194
  Never bring it back, not even as an alias.
@@ -220,8 +220,9 @@ of `Screen`:
220
220
  - **Checked:** `validateScreen` (`scripts/validate.mjs`, run on every story and template by
221
221
  `tests/composition.spec.js`) fails a bar after `</Screen>` or a pinned bar, warns on a Sheet + ActionBar.
222
222
  - **Templates** are `Screen`s with the three parts filled (`Walkthrough` = top `lead` · content `Pages` · bottom
223
- `ActionBar`); the gallery draws those parts on a template's page. The gallery's nav groups the components by
224
- the part they fill: *top* · *content* · *bottom*.
223
+ `ActionBar`); the gallery draws those parts on a template's page. The gallery's nav groups a screen's
224
+ *top* and *bottom*, then each frame with the pieces that fill it: *card* (Card, its head, body and foot),
225
+ *sheet*, *stack*, *step*. A piece that also fits another frame says so on its page.
225
226
 
226
227
  ## Step markup
227
228
 
@@ -271,7 +272,10 @@ whose markup has rules, because the sheet's box is the step itself:
271
272
  **handle · head · body**: the handle, then a `.card__head` title row (that
272
273
  row is what `peek` shows — heading left, the contextual thing right), then
273
274
  `.sheet__body` with what the member acts on. The handle and the head are
274
- pinned; the body is the one thing that scrolls, to the card's own edges. A
275
+ pinned; the body is the one thing that scrolls, to the card's own edges. The
276
+ handle is a line, or four dots (`mark="dots"`): both sit in the card's top
277
+ padding, the same distance from the edge, so the title stays put. `paper` makes
278
+ the content white while the head keeps the sheet's colour (`tone`, as a card). A
275
279
  sheet with no head (the dim page sheet) is a body alone.
276
280
  - **The step follows its top bar directly, and touches it.** `full` means
277
281
  the sheet's top edge at the bar's bottom edge — nothing above it but the
@@ -532,9 +536,13 @@ Live: every screen in the gallery (`npm run dev`).
532
536
  get covered. Closing reverses it in two phases: the after-pack returns
533
537
  while the card drops into its slot, then the before-pack slides back up
534
538
  from behind. Slots, offsets and paint order are all CSS `:nth-child`, so
535
- a stack renders correctly with no script; `SheetStack` only sets the
536
- state classes. Don't reintroduce height, `display` or z-index switching
537
- here — every past bug in this animation came from exactly that.
539
+ a stack renders correctly with no script. The app owns which index is
540
+ open and whether that sheet's data is in. The card renders what those
541
+ mean (`is-open`, `is-before`, `is-after`, `is-loading`, the grip, the
542
+ pull) from the `stack` prop `SheetStack` passes it. Don't reintroduce
543
+ height, `display` or z-index switching here — every past bug in this
544
+ animation came from exactly that. Don't stamp those classes onto the
545
+ card after it has rendered.
538
546
  7. **Everything shrinks to 320px.** `.screen > *`, `.card` and `.card > *`
539
547
  carry `min-width: 0`, so no child's min-content (an input, a fixed row)
540
548
  can widen a card's grid track past its padding; topbar titles ellipsize;
@@ -629,7 +637,7 @@ That file is where a project's look lives. Bases and roles:
629
637
  |---|---|---|
630
638
  | room **between** things | `--sp-base` (0.25rem = 4) | every `--sp-N` = base × N → paddings, gaps, the screen's inset, the bar's reserve, the sheet's peek |
631
639
  | type | `--fs-base` (1.375rem = 22 — the body) | every `--fs-*`, `--badge`, `--icon-xs` |
632
- | corners | `--r-base` (1rem = 16) | every `--r-*`; `0` = square. `--r-chip` stays fully round |
640
+ | corners | `--r-base` (1.3rem = 20.8) | every `--r-*`; `0` = square. `--r-chip` stays fully round |
633
641
  | the size **of** things | `--tap-base` (3rem = 48) | `--tap*`, `--btn-h`, `--chip-h`, `--bar-h`, `--header-h`, `--fab`, `--icon*`, `--avatar*`, `--dot*`, `--ring*`, `--tile-*`. `--tap*` never under `--tap-floor` (44px) |
634
642
  | faces | `--font-ui`, `--font-display` | |
635
643
  | colour | `--color-surface`, `--color-ink`, `--color-paper`, `--color-brand`, `--color-on-brand` — no default: unset = the wireframe | every role: surface / words, the three card tones (paper · the brand washed 14% into the surface · the brand), the muted inks (the ink washed into their ground), accent, neutral actions, `--dot-a` |
package/css/sheet.css CHANGED
@@ -46,10 +46,10 @@
46
46
  ============================================================ */
47
47
 
48
48
  .sheet {
49
- /* the strip a peeked sheet shows: the card's own top padding, the handle,
50
- the gap, one title row (--tap — the head rule below makes every head at
51
- least that tall) and the gap under it, so the cut lands exactly where
52
- the first control starts and nothing of it leaks through. Published
49
+ /* the strip a peeked sheet shows: the card's own top padding (the handle
50
+ sits inside it), one title row (--tap — the head rule below makes every
51
+ head at least that tall) and the gap under it, so the cut lands exactly
52
+ where the first control starts and nothing of it leaks through. Published
53
53
  (and registered in tokens.css) so a sheet whose head is taller can
54
54
  retune it: .my-sheet { --sheet-peek: … } */
55
55
  --sheet-half: 0.5; /* a share of the sheet's own height */
@@ -90,7 +90,7 @@
90
90
  /* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
91
91
  have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
92
92
  inherits it. A sheet whose head is taller retunes it on the box. */
93
- :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h) + var(--safe-bottom)); } /* + the gesture bar: the peeked head stands above it */
93
+ :is(.step, .sheet-stage) { --_handle: var(--sp-0); --sheet-peek: calc(var(--card-pad-rest) + var(--sheet-head-h) + var(--card-gap-rest) + var(--safe-bottom)); } /* + the gesture bar: the peeked head stands above it. The handle sits IN the card's top padding, so the strip is that padding, the title row and the gap under it */
94
94
  /* the head's floor: a tap, or an h2 line with its Thai-mark padding when the type is bigger than that (a large text-size
95
95
  setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
96
96
  :root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
@@ -174,25 +174,55 @@
174
174
  .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
175
175
  .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover) + var(--_safe-b-open)); }
176
176
 
177
- /* grab handle — first child of the sheet. Drawn as a small bar, but it is
178
- the thumb's grab area for the whole strip above the title: a pseudo widens
179
- its hit box to --tap tall and wider than the bar without changing the
180
- picture. touch-action: none hands a touch drag to the Sheet component instead of to
181
- the scroller behind it. */
177
+ /* grab handle — first child of the sheet, but it does not take a row: it sits in the
178
+ card's top padding, the same air as a stack sheet's four dots (stack.css --_grip-y),
179
+ so a line and the dots leave the title in the same place (Lh 2026-10-05). The line
180
+ is the default; .sheet__handle--dots is the four dots. Either way it is the thumb's
181
+ grab: a pseudo widens its hit box to --tap tall and wider than the mark.
182
+ touch-action: none hands a touch drag to the Sheet component instead of to the scroller. */
182
183
  .sheet__handle {
183
- position: relative;
184
+ position: absolute;
185
+ z-index: 1;
186
+ top: calc((var(--card-pad) - var(--sp-1)) / 2); /* the dots' top: (pad − 4) / 2. The line starts there too */
187
+ left: 50%;
188
+ translate: -50% 0;
184
189
  border: 0;
185
- color: inherit; /* a <button> has its own UA colour; the bar is the card's ink */
190
+ color: inherit; /* a <button> has its own UA colour; the mark is the card's ink */
186
191
  background: currentColor;
187
192
  opacity: 0.3;
188
- width: var(--tap-sm);
193
+ width: calc(var(--sp-base) * 18); /* 72: the line. The grab is the ::before, not this width */
189
194
  height: var(--_handle);
190
195
  border-radius: var(--sheet-handle-radius, var(--r-chip));
191
- margin: 0 auto;
196
+ margin: 0;
192
197
  padding: 0;
193
198
  touch-action: none;
194
199
  cursor: grab;
195
200
  }
201
+ /* paper: the head keeps the sheet's own colour, the content is white (Lh 2026-10-05).
202
+ The body and the foot read as paper; the handle and the head stay on the card. */
203
+ .sheet--paper > .sheet__body {
204
+ --card-bg: var(--color-paper, #fff);
205
+ --card-ink: var(--color-ink);
206
+ --card-muted: var(--_muted-on-paper);
207
+ background: var(--card-bg);
208
+ color: var(--card-ink);
209
+ }
210
+ .sheet--paper > .card__foot { --card-bg: var(--color-paper, #fff); color: var(--color-ink); }
211
+ .sheet__handle--dots {
212
+ --_dot: var(--sp-1);
213
+ --_step: calc(var(--_dot) * 2.5);
214
+ --_ink: color-mix(in srgb, currentColor 35%, transparent);
215
+ --_img: radial-gradient(circle closest-side, var(--_ink) 85%, transparent);
216
+ width: calc(3 * var(--_step) + var(--_dot));
217
+ height: var(--_dot);
218
+ opacity: 1;
219
+ border-radius: 0;
220
+ background-color: transparent;
221
+ background-image: var(--_img), var(--_img), var(--_img), var(--_img);
222
+ background-repeat: no-repeat;
223
+ background-size: var(--_dot) var(--_dot);
224
+ background-position: 0 0, var(--_step) 0, calc(2 * var(--_step)) 0, calc(3 * var(--_step)) 0;
225
+ }
196
226
  .sheet__handle::before {
197
227
  content: "";
198
228
  position: absolute;
package/css/stack.css CHANGED
@@ -162,7 +162,7 @@
162
162
  overflow: clip;
163
163
  transition: translate var(--motion-sheet) ease; /* the only animated property here */
164
164
  transition-delay: calc(var(--stagger, 0) * 70ms); /* 0 for everything
165
- except a staggered is-before card, set in JS as it tucks or un-tucks */
165
+ except a staggered is-before card: the card renders --stagger as it tucks or un-tucks */
166
166
  --_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 */
167
167
  }
168
168
  .sheet-stack--tap > .card.is-open {
@@ -183,16 +183,18 @@
183
183
  }
184
184
  .sheet-stack > .card > .sheet__body.is-refreshing,
185
185
  .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
186
- /* the GRIP (Lh 2026-10-05): four small grey dots at the middle of every sheet's top edge — just enough of a sign that this card
186
+ /* the GRIP (Lh 2026-10-05): four small dots at the middle of every sheet's top edge — a sign that this card
187
187
  slides up and down. A mark, not a control: nothing to press, nothing added to the sheet (the card's own background, in the
188
- room above its head). While the app reloads the open sheet (.is-loading, from `refreshing`) the same four dots fade up and
189
- down until it clears — nothing travels, nobody knows how long a reload takes. --_grip is their strength, private. */
190
- @property --_grip { syntax: "<number>"; inherits: false; initial-value: 0.35; }
191
- .sheet-stack--tap > .card {
188
+ room above its head). The card renders `.stack-card` when it is a sheet of a tap stack, so the dots are its own.
189
+ At rest they are the card's own ink, faint. Only while THAT sheet reloads (`.is-loading`, from its `refreshing` prop,
190
+ 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.
191
+ Nothing travels. --_grip is the mix, 0 white → 1 black, private. */
192
+ @property --_grip { syntax: "<number>"; inherits: false; initial-value: 0; }
193
+ .stack-card {
192
194
  --_grip-dot: var(--sp-1);
193
195
  --_grip-step: calc(var(--sp-1) * 2.5);
194
196
  --_grip-y: calc((var(--card-pad) - var(--_grip-dot)) / 2);
195
- --_grip-ink: color-mix(in srgb, currentColor calc(var(--_grip) * 100%), transparent);
197
+ --_grip-ink: color-mix(in srgb, currentColor 35%, transparent);
196
198
  --_grip-img: radial-gradient(circle closest-side, var(--_grip-ink) 85%, transparent);
197
199
  background-image: var(--_grip-img), var(--_grip-img), var(--_grip-img), var(--_grip-img);
198
200
  background-repeat: no-repeat;
@@ -201,10 +203,13 @@
201
203
  calc(50% - 1.5 * var(--_grip-step)) var(--_grip-y), calc(50% - 0.5 * var(--_grip-step)) var(--_grip-y),
202
204
  calc(50% + 0.5 * var(--_grip-step)) var(--_grip-y), calc(50% + 1.5 * var(--_grip-step)) var(--_grip-y);
203
205
  }
204
- .sheet-stack--tap > .card.is-open.is-loading { animation: stack-grip-breath 0.8s ease-in-out infinite alternate; }
205
- @keyframes stack-grip-breath { from { --_grip: 0.2; } to { --_grip: 1; } }
206
+ .stack-card.is-open.is-loading {
207
+ --_grip-ink: color-mix(in srgb, black calc(var(--_grip) * 100%), white);
208
+ animation: stack-grip-dissolve 0.4s ease-in-out infinite alternate;
209
+ }
210
+ @keyframes stack-grip-dissolve { from { --_grip: 0; } to { --_grip: 1; } }
206
211
  @media (prefers-reduced-motion: reduce) {
207
- .sheet-stack--tap > .card.is-open.is-loading { animation: none; --_grip: 1; } /* no fade: the dots are simply full while it reloads */
212
+ .stack-card.is-open.is-loading { animation: none; --_grip-ink: currentColor; }
208
213
  }
209
214
  /* the front sheet — the one open on arrival — never travels up (it is not
210
215
  tappable), so its resting offset would leave its last n steps below the
@@ -24,7 +24,7 @@
24
24
  /* ---- size: four bases, the multipliers stay the system's ---- */
25
25
  --sp-base: 0.25rem; /* 4 the room BETWEEN things: every --sp-N = base × N, so paddings and gaps */
26
26
  --fs-base: 1.375rem; /* 22 the type — the body size; every --fs-* is a multiplier of it (17 · 22 · 26 · 32 · 40 · 55 · 64), the count badge, the inline icon */
27
- --r-base: 1rem; /* 16 the corners: every --r-* (0 = square; chips and buttons stay fully round) */
27
+ --r-base: 1.3rem; /* 20.8 the corners: every --r-* (0 = square; chips and buttons stay fully round) */
28
28
  --tap-base: 3rem; /* 48 the size of THINGS: touch targets, control heights, icons, avatars, ring, tiles
29
29
  (a touch target never goes under 44px, whatever this says) */
30
30
  /* a member's text-size setting is this same --fs-base, written on <html> by the app at run time (1.375rem = 100%, 2.2rem = 160%) — the words grow, the things keep their size */
package/css/tokens.css CHANGED
@@ -24,7 +24,7 @@
24
24
  --sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
25
25
  --fs-base: 1.375rem; /* 22 type: the BODY is the base (Lh 2026-09-22) — every --fs-* is a multiplier of it; nothing under 0.68 (15, minor links — was 0.775/17 until 2026-09-23). A new size is Lh's call only.
26
26
  Also the member's text-size setting: the app writes it on <html> at run time (2.2rem = 160%) — words grow, things don't */
27
- --r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
27
+ --r-base: 1.3rem; /* 20.8 radius, 130% of 1rem (Lh 2026-10-05): --r-* (--r-chip stays fully round) */
28
28
  --tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
29
29
  --header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
30
30
  Text-holders follow the type instead: --badge and --icon-xs (inline with captions) are on --fs-base.
@@ -79,10 +79,10 @@
79
79
 
80
80
  /* ---- radius ---- */
81
81
  --r-chip: 999px; /* pills, chips, buttons — always fully round */
82
- --r-card: calc(var(--r-base) * 2); /* 32 the one card radius: every card, row, tile, menu, sheet, fab */
83
- --r-media: var(--r-base); /* 16 images inside cards */
84
- --r-control: calc(var(--r-base) * 0.875); /* 14 fields, thumbs, menu rows */
85
- --r-badge: calc(var(--r-base) * 0.625); /* 10 tile badge, bars */
82
+ --r-card: calc(var(--r-base) * 2); /* 41.6 the one card radius: every card, row, tile, menu, sheet, fab */
83
+ --r-media: var(--r-base); /* 20.8 images inside cards */
84
+ --r-control: calc(var(--r-base) * 0.875); /* 18.2 fields, thumbs, menu rows */
85
+ --r-badge: calc(var(--r-base) * 0.625); /* 13 tile badge, bars */
86
86
 
87
87
  /* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
88
88
  --border-w: 1.5px;
package/dist/cardds.css CHANGED
@@ -65,7 +65,7 @@
65
65
  --sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
66
66
  --fs-base: 1.375rem; /* 22 type: the BODY is the base (Lh 2026-09-22) — every --fs-* is a multiplier of it; nothing under 0.68 (15, minor links — was 0.775/17 until 2026-09-23). A new size is Lh's call only.
67
67
  Also the member's text-size setting: the app writes it on <html> at run time (2.2rem = 160%) — words grow, things don't */
68
- --r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
68
+ --r-base: 1.3rem; /* 20.8 radius, 130% of 1rem (Lh 2026-10-05): --r-* (--r-chip stays fully round) */
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.
@@ -120,10 +120,10 @@
120
120
 
121
121
  /* ---- radius ---- */
122
122
  --r-chip: 999px; /* pills, chips, buttons — always fully round */
123
- --r-card: calc(var(--r-base) * 2); /* 32 the one card radius: every card, row, tile, menu, sheet, fab */
124
- --r-media: var(--r-base); /* 16 images inside cards */
125
- --r-control: calc(var(--r-base) * 0.875); /* 14 fields, thumbs, menu rows */
126
- --r-badge: calc(var(--r-base) * 0.625); /* 10 tile badge, bars */
123
+ --r-card: calc(var(--r-base) * 2); /* 41.6 the one card radius: every card, row, tile, menu, sheet, fab */
124
+ --r-media: var(--r-base); /* 20.8 images inside cards */
125
+ --r-control: calc(var(--r-base) * 0.875); /* 18.2 fields, thumbs, menu rows */
126
+ --r-badge: calc(var(--r-base) * 0.625); /* 13 tile badge, bars */
127
127
 
128
128
  /* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
129
129
  --border-w: 1.5px;
@@ -1163,7 +1163,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1163
1163
  overflow: clip;
1164
1164
  transition: translate var(--motion-sheet) ease; /* the only animated property here */
1165
1165
  transition-delay: calc(var(--stagger, 0) * 70ms); /* 0 for everything
1166
- except a staggered is-before card, set in JS as it tucks or un-tucks */
1166
+ except a staggered is-before card: the card renders --stagger as it tucks or un-tucks */
1167
1167
  --_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 */
1168
1168
  }
1169
1169
  .sheet-stack--tap > .card.is-open {
@@ -1184,16 +1184,18 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1184
1184
  }
1185
1185
  .sheet-stack > .card > .sheet__body.is-refreshing,
1186
1186
  .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
1187
- /* the GRIP (Lh 2026-10-05): four small grey dots at the middle of every sheet's top edge — just enough of a sign that this card
1187
+ /* the GRIP (Lh 2026-10-05): four small dots at the middle of every sheet's top edge — a sign that this card
1188
1188
  slides up and down. A mark, not a control: nothing to press, nothing added to the sheet (the card's own background, in the
1189
- room above its head). While the app reloads the open sheet (.is-loading, from `refreshing`) the same four dots fade up and
1190
- down until it clears — nothing travels, nobody knows how long a reload takes. --_grip is their strength, private. */
1191
- @property --_grip { syntax: "<number>"; inherits: false; initial-value: 0.35; }
1192
- .sheet-stack--tap > .card {
1189
+ room above its head). The card renders `.stack-card` when it is a sheet of a tap stack, so the dots are its own.
1190
+ At rest they are the card's own ink, faint. Only while THAT sheet reloads (`.is-loading`, from its `refreshing` prop,
1191
+ 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.
1192
+ Nothing travels. --_grip is the mix, 0 white → 1 black, private. */
1193
+ @property --_grip { syntax: "<number>"; inherits: false; initial-value: 0; }
1194
+ .stack-card {
1193
1195
  --_grip-dot: var(--sp-1);
1194
1196
  --_grip-step: calc(var(--sp-1) * 2.5);
1195
1197
  --_grip-y: calc((var(--card-pad) - var(--_grip-dot)) / 2);
1196
- --_grip-ink: color-mix(in srgb, currentColor calc(var(--_grip) * 100%), transparent);
1198
+ --_grip-ink: color-mix(in srgb, currentColor 35%, transparent);
1197
1199
  --_grip-img: radial-gradient(circle closest-side, var(--_grip-ink) 85%, transparent);
1198
1200
  background-image: var(--_grip-img), var(--_grip-img), var(--_grip-img), var(--_grip-img);
1199
1201
  background-repeat: no-repeat;
@@ -1202,10 +1204,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1202
1204
  calc(50% - 1.5 * var(--_grip-step)) var(--_grip-y), calc(50% - 0.5 * var(--_grip-step)) var(--_grip-y),
1203
1205
  calc(50% + 0.5 * var(--_grip-step)) var(--_grip-y), calc(50% + 1.5 * var(--_grip-step)) var(--_grip-y);
1204
1206
  }
1205
- .sheet-stack--tap > .card.is-open.is-loading { animation: stack-grip-breath 0.8s ease-in-out infinite alternate; }
1206
- @keyframes stack-grip-breath { from { --_grip: 0.2; } to { --_grip: 1; } }
1207
+ .stack-card.is-open.is-loading {
1208
+ --_grip-ink: color-mix(in srgb, black calc(var(--_grip) * 100%), white);
1209
+ animation: stack-grip-dissolve 0.4s ease-in-out infinite alternate;
1210
+ }
1211
+ @keyframes stack-grip-dissolve { from { --_grip: 0; } to { --_grip: 1; } }
1207
1212
  @media (prefers-reduced-motion: reduce) {
1208
- .sheet-stack--tap > .card.is-open.is-loading { animation: none; --_grip: 1; } /* no fade: the dots are simply full while it reloads */
1213
+ .stack-card.is-open.is-loading { animation: none; --_grip-ink: currentColor; }
1209
1214
  }
1210
1215
  /* the front sheet — the one open on arrival — never travels up (it is not
1211
1216
  tappable), so its resting offset would leave its last n steps below the
@@ -1437,10 +1442,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1437
1442
  ============================================================ */
1438
1443
 
1439
1444
  .sheet {
1440
- /* the strip a peeked sheet shows: the card's own top padding, the handle,
1441
- the gap, one title row (--tap — the head rule below makes every head at
1442
- least that tall) and the gap under it, so the cut lands exactly where
1443
- the first control starts and nothing of it leaks through. Published
1445
+ /* the strip a peeked sheet shows: the card's own top padding (the handle
1446
+ sits inside it), one title row (--tap — the head rule below makes every
1447
+ head at least that tall) and the gap under it, so the cut lands exactly
1448
+ where the first control starts and nothing of it leaks through. Published
1444
1449
  (and registered in tokens.css) so a sheet whose head is taller can
1445
1450
  retune it: .my-sheet { --sheet-peek: … } */
1446
1451
  --sheet-half: 0.5; /* a share of the sheet's own height */
@@ -1481,7 +1486,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1481
1486
  /* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
1482
1487
  have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
1483
1488
  inherits it. A sheet whose head is taller retunes it on the box. */
1484
- :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h) + var(--safe-bottom)); } /* + the gesture bar: the peeked head stands above it */
1489
+ :is(.step, .sheet-stage) { --_handle: var(--sp-0); --sheet-peek: calc(var(--card-pad-rest) + var(--sheet-head-h) + var(--card-gap-rest) + var(--safe-bottom)); } /* + the gesture bar: the peeked head stands above it. The handle sits IN the card's top padding, so the strip is that padding, the title row and the gap under it */
1485
1490
  /* the head's floor: a tap, or an h2 line with its Thai-mark padding when the type is bigger than that (a large text-size
1486
1491
  setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
1487
1492
  :root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
@@ -1565,25 +1570,55 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1565
1570
  .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
1566
1571
  .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover) + var(--_safe-b-open)); }
1567
1572
 
1568
- /* grab handle — first child of the sheet. Drawn as a small bar, but it is
1569
- the thumb's grab area for the whole strip above the title: a pseudo widens
1570
- its hit box to --tap tall and wider than the bar without changing the
1571
- picture. touch-action: none hands a touch drag to the Sheet component instead of to
1572
- the scroller behind it. */
1573
+ /* grab handle — first child of the sheet, but it does not take a row: it sits in the
1574
+ card's top padding, the same air as a stack sheet's four dots (stack.css --_grip-y),
1575
+ so a line and the dots leave the title in the same place (Lh 2026-10-05). The line
1576
+ is the default; .sheet__handle--dots is the four dots. Either way it is the thumb's
1577
+ grab: a pseudo widens its hit box to --tap tall and wider than the mark.
1578
+ touch-action: none hands a touch drag to the Sheet component instead of to the scroller. */
1573
1579
  .sheet__handle {
1574
- position: relative;
1580
+ position: absolute;
1581
+ z-index: 1;
1582
+ top: calc((var(--card-pad) - var(--sp-1)) / 2); /* the dots' top: (pad − 4) / 2. The line starts there too */
1583
+ left: 50%;
1584
+ translate: -50% 0;
1575
1585
  border: 0;
1576
- color: inherit; /* a <button> has its own UA colour; the bar is the card's ink */
1586
+ color: inherit; /* a <button> has its own UA colour; the mark is the card's ink */
1577
1587
  background: currentColor;
1578
1588
  opacity: 0.3;
1579
- width: var(--tap-sm);
1589
+ width: calc(var(--sp-base) * 18); /* 72: the line. The grab is the ::before, not this width */
1580
1590
  height: var(--_handle);
1581
1591
  border-radius: var(--sheet-handle-radius, var(--r-chip));
1582
- margin: 0 auto;
1592
+ margin: 0;
1583
1593
  padding: 0;
1584
1594
  touch-action: none;
1585
1595
  cursor: grab;
1586
1596
  }
1597
+ /* paper: the head keeps the sheet's own colour, the content is white (Lh 2026-10-05).
1598
+ The body and the foot read as paper; the handle and the head stay on the card. */
1599
+ .sheet--paper > .sheet__body {
1600
+ --card-bg: var(--color-paper, #fff);
1601
+ --card-ink: var(--color-ink);
1602
+ --card-muted: var(--_muted-on-paper);
1603
+ background: var(--card-bg);
1604
+ color: var(--card-ink);
1605
+ }
1606
+ .sheet--paper > .card__foot { --card-bg: var(--color-paper, #fff); color: var(--color-ink); }
1607
+ .sheet__handle--dots {
1608
+ --_dot: var(--sp-1);
1609
+ --_step: calc(var(--_dot) * 2.5);
1610
+ --_ink: color-mix(in srgb, currentColor 35%, transparent);
1611
+ --_img: radial-gradient(circle closest-side, var(--_ink) 85%, transparent);
1612
+ width: calc(3 * var(--_step) + var(--_dot));
1613
+ height: var(--_dot);
1614
+ opacity: 1;
1615
+ border-radius: 0;
1616
+ background-color: transparent;
1617
+ background-image: var(--_img), var(--_img), var(--_img), var(--_img);
1618
+ background-repeat: no-repeat;
1619
+ background-size: var(--_dot) var(--_dot);
1620
+ background-position: 0 0, var(--_step) 0, calc(2 * var(--_step)) 0, calc(3 * var(--_step)) 0;
1621
+ }
1587
1622
  .sheet__handle::before {
1588
1623
  content: "";
1589
1624
  position: absolute;