@usableapp/cardds 0.7.16 → 0.7.17

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 (63) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +23 -20
  3. package/cardds.css +3 -3
  4. package/css/actions.css +60 -38
  5. package/css/base.css +43 -17
  6. package/css/card.css +18 -13
  7. package/css/choice.css +21 -33
  8. package/css/forms.css +0 -3
  9. package/css/journey.css +0 -2
  10. package/css/layover.css +14 -24
  11. package/css/lists.css +1 -6
  12. package/css/media.css +7 -15
  13. package/css/numbers.css +5 -21
  14. package/css/people.css +1 -8
  15. package/css/sheet.css +12 -14
  16. package/css/stack.css +17 -21
  17. package/css/step.css +5 -8
  18. package/css/tokens.css +19 -4
  19. package/dist/actions/Btn.d.ts +1 -1
  20. package/dist/actions/Dropdown.js +1 -6
  21. package/dist/actions/Fab.d.ts +1 -1
  22. package/dist/actions/Fab.js +3 -2
  23. package/dist/cardds.css +228 -232
  24. package/dist/cards/Card.d.ts +23 -7
  25. package/dist/cards/Card.js +23 -8
  26. package/dist/cards/CardHead.d.ts +0 -1
  27. package/dist/cards/CardHead.js +1 -1
  28. package/dist/choice/Calendar.js +1 -2
  29. package/dist/choice/Mood.js +2 -2
  30. package/dist/forms/Field.js +2 -3
  31. package/dist/forms/FileBtn.js +1 -1
  32. package/dist/forms/Pin.js +6 -10
  33. package/dist/index.js +2 -2
  34. package/dist/layover/Banner.d.ts +10 -3
  35. package/dist/layover/Banner.js +6 -3
  36. package/dist/lists/Row.js +3 -3
  37. package/dist/media/Postcard.js +2 -2
  38. package/dist/numbers/Band.d.ts +3 -1
  39. package/dist/numbers/Picker.js +1 -7
  40. package/dist/scaffold/ActionBar.d.ts +3 -3
  41. package/dist/scaffold/ActionBar.js +2 -2
  42. package/dist/scaffold/Centre.d.ts +1 -1
  43. package/dist/scaffold/Centre.js +1 -1
  44. package/dist/scaffold/PagerAt.d.ts +1 -1
  45. package/dist/scaffold/PagerAt.js +1 -1
  46. package/dist/scaffold/Screen.js +2 -4
  47. package/dist/sheets/Drawer.js +3 -4
  48. package/dist/sheets/Sheet.js +8 -19
  49. package/dist/sheets/SheetStack.d.ts +2 -2
  50. package/dist/sheets/SheetStack.js +18 -10
  51. package/dist/sheets/SheetSteps.js +5 -7
  52. package/dist/sheets/StackBack.js +2 -1
  53. package/dist/sheets/StackSheet.d.ts +3 -2
  54. package/dist/sheets/StackSheet.js +6 -9
  55. package/dist/state.d.ts +19 -2
  56. package/dist/state.js +47 -7
  57. package/dist/templates/CallSheet.js +1 -2
  58. package/dist/templates/Home.d.ts +3 -2
  59. package/dist/templates/Home.js +1 -2
  60. package/dist/templates/Splash.js +2 -3
  61. package/dist/templates/Walkthrough.js +2 -2
  62. package/dist/type/Text.d.ts +1 -1
  63. package/package.json +7 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.7.17 — 2026-09-27
6
+
7
+ Second refactor pass (what the first skipped). Most of it is internal; these can touch a screen:
8
+
9
+ ### Changed (may touch a screen)
10
+ - `Banner` is controlled like every disclosure: `open` + `onOpenChange`, or `defaultOpen`. Before: `open` was the native attribute. `<Banner open>` meaning "start open" → `defaultOpen`.
11
+ - A controlled component no longer asks for the value it already shows: tapping the picked day (`Calendar onSelect`), the picked dot (`Mood onChange`) calls nothing.
12
+ - `FileBtn`: its file input is visually hidden instead of `hidden` — Tab reaches it, Enter/Space opens the picker, the button shows the focus ring.
13
+ - `SheetStack` (tap): a head's title takes the keyboard's focus where a tap on it works (every title while none is open; the open one's while one is) — Enter/Space opens or folds.
14
+ - peerDependencies: React `>=19` (components take `ref` as a prop; React 18 never worked).
15
+
16
+ ### Added
17
+ - `Card` SLOTS (Lh 2026-09-27): `heading` · `caption` · `end` · `level` fill its head, `foot` · `footLink` its foot; the children are the content between — the card writes head first, foot last. `<CardHead>`/`<CardFoot>` as children still work (a head of its own shape); both at once warns. `<Card heading="…" foot={<Btn primary>Join</Btn>}>…</Card>` replaces a CardHead + CardFoot pair.
18
+ - Tap answer for every tappable thing (the PRESS block, actions.css): a finger-sized control GIVES (`--press-scale`); a card-wide one — a row as a link/label, a Row `more` line, a Banner line, a card as a link, a Toggle with words — DIMS (`--press-dim` 0.6); a peeked stack sheet and a Postcard SHADE (`--press-shade` 0.93, so nothing behind shows through). New in it: Check, Toggle, Mood, Sheet handle, Avatar as a link/button, the Slider's knob, SheetSteps' step bar. A disabled control has no pointer, no press, and fades to `--off` (0.4); a label whose input is disabled too.
19
+ - Tokens: `--press-dim`, `--press-shade`, `--off`, `--rest` (0.6, an unpicked choice), `--faint` (0.3, far dots, picker items), `--motion-quick` (0.2s: chevrons, the toggle, menus), `--z-bar` / `--z-fab` / `--z-top`.
20
+
21
+ ### Fixed
22
+ - Enter in a SheetSteps field could open a `FileBtn`'s picker (a `label.btn` counted as the step's button).
23
+ - The Toggle's height and knob gap were raw rem — now on `--tap-base` (same at the default).
24
+ - The sheet's foot strip ignored the `--link-sm-min-h` knob; `.sheet-stage > .float-bar` defeated `--float-bar-pad-x`.
25
+ - The Banner opens in 0.2s (was 0.25s — the one value that moved).
26
+
27
+ ### Internal
28
+ - `src/state.ts`: `useControlled(value, initial, onChange)` now returns `[shown, request]` — the no-change guard, the own copy and the ask in one stable function (12 copies gone; the Sheet no longer rebuilt its ResizeObserver every render); `useDisclosure` (Row `more`, Banner); `watchSize` (5 ResizeObserver copies); `useLatest`. `Tone` type exported once. StackBack, Fab and the StackSheet tabs render through `IconBtn`.
29
+ - CSS: `.card--3` muted words one rule; one zero-specificity button reset; one hidden-input rule; one scrollbar-hiding rule; dead `.dotgrid--stars` and float-bar padding rule gone; split rules merged; stale comments (cardds.js doing behaviour, bars outside the screen) rewritten.
30
+ - Tests: `tests/helpers.js` (shared rect/near/tok/settle/story…); PIN + Modal flakes fixed with the page clock and finished animations; the knob probe focuses its own field. Fixtures lost ~500 lines nothing asserted (`stack.html` gone). `scripts/catalog.mjs` lost the dropped MCP server's code; `validate.mjs` accepts `.js` imports and `ComponentProps`. New scripts: `typecheck`, `snapshot:write`, `snapshot:check`.
31
+
5
32
  ## 0.7.16 — 2026-09-25
6
33
 
7
34
  Refactor + additions. Same props, same markup, same computed style on every story.
package/README.md CHANGED
@@ -18,8 +18,8 @@ from props. The demo is the gallery (`npm run dev`):
18
18
  every component's stories, live, searchable.
19
19
 
20
20
  Repo: **https://github.com/everysundays/cardds** (private — this is the
21
- source of truth; every consuming project vendors a copy from here, never
22
- the other way round).
21
+ source of truth; every consuming project installs the npm package
22
+ `@usableapp/cardds`, never the other way round).
23
23
 
24
24
  **Wireframe by default** — grayscale with visible outlines. Color is opt-in,
25
25
  lives only in one file, applied with one attribute. Two faces ship with it
@@ -42,12 +42,12 @@ import '@usableapp/cardds/dist/cardds.css';
42
42
  import './theme.css'; // the project's own look, after the system's — see "A project's theme"
43
43
  ```
44
44
 
45
- `src/index.ts` imports `cardds.js` for its side effects, so a React host gets
46
- the behaviours for free: the `.sheet` handle (tap, drag, keyboard) with the
47
- focus and on-screen-keyboard raises, `.sheet-stack--tap` opening and closing,
48
- the `.pin` auto-advance (a digit moves on, Backspace moves back, a paste fills
49
- the code), the modal's close, and the `.dropdown` writing its picked value back. `Icon`
50
- inlines the 56 symbols of `icons.svg` at build time, so no sprite file ships.
45
+ Every behaviour is the component's own React code — the `Sheet`'s handle (tap, drag,
46
+ keyboard) with the focus and on-screen-keyboard raises, the `SheetStack` opening and
47
+ closing, the `Pin` auto-advance, the `Modal`'s leaving — each controlled like an input
48
+ (`x` + `onXChange`, or `defaultX`). `src/index.ts` imports `cardds.js` for the environment
49
+ only: the keyboard's height (`--kb`) and `.bleed`'s scrollbar. `Icon` inlines the 61
50
+ symbols of `icons.svg` at build time, so no sprite file ships.
51
51
 
52
52
  **The gallery** (`gallery/`, `npm run dev` → http://localhost:5174): Vite + React Router, `cardds`
53
53
  aliased to `src/`, so the gallery and the package share one truth with no build between them.
@@ -160,7 +160,7 @@ the card is the material, the sheet is what it is in the stack.
160
160
  sixth would render at slot 0, on top of the first.
161
161
  - **Every head needs its own `.sheet-stack__back`** if the stack is `--tap`. It stays
162
162
  hidden until that sheet is open, then slides into a reserved header slot.
163
- - **No ids, no wiring.** `cardds.js` finds any `.sheet-stack--tap` on the page.
163
+ - **No ids, no wiring.** `SheetStack` holds which sheet is open (`open` + `onOpenChange`, or `defaultOpen`).
164
164
  - **`.card-stack` is gone.** That was this component's first name and the wrong
165
165
  one — the members are sheets and the stack is the menu, not a list of cards.
166
166
  Never bring it back, not even as an alias.
@@ -355,6 +355,10 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
355
355
  - **Foot** (`.card__foot`): the card's action buttons on one line. One
356
356
  button takes the width; up to four share it in even parts; from three
357
357
  on they drop their side padding so the labels keep the room.
358
+ - In React the head and the foot are the card's **slots**:
359
+ `<Card heading caption end foot footLink>` with the content as children —
360
+ the card writes the order (head first, foot last). A head of its own shape
361
+ is a `<CardHead>` child instead; never both (Lh 2026-09-27).
358
362
  - A page with **one feature and one CTA** (an install page, a single
359
363
  decision) may be contained in one card: head, the pitch, foot.
360
364
  - **A bottom sheet holds what the member acts on** — controls, or small
@@ -373,7 +377,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
373
377
  same material — a stacked sheet is written as a `.card`, and the single
374
378
  sheet is `.card.sheet` — and both obey rule 8: rigid, full height, moved
375
379
  by `translate` only. (The stack was first named "card stack": wrong, and
376
- retired.) Demos: `sheet.html`, `stack.html`.
380
+ retired.) See them live in the gallery: `Sheet`, `SheetStack`.
377
381
  - **A step is one screen, one task: `.step`.** What the member reads is
378
382
  words on the base (`.page` — the same block as anywhere on the base; it was `.step__card` until 2026-09-16); what the member does, if the step asks for it, is a
379
383
  `.sheet` in front of it; the screen's one move is `.action-bar`, the screen's
@@ -385,8 +389,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
385
389
  slides in: it starts where the top bar ends and ends where the bar's room
386
390
  begins. Nobody lays a step out by hand and nobody writes a
387
391
  sheet's height. In short: **member acts → card + sheet, member reads →
388
- card.** Markup: *Step markup*. Demo: `sheet.html`, `elements.html` #ask,
389
- `index.html` "step".
392
+ card.** Markup: *Step markup*. Live: the gallery's `Step` page.
390
393
 
391
394
  - **Where the centre is.** To the person holding a phone, the bottom fifth of
392
395
  the screen is the thumb's — the bar, the controls — so what is meant to be
@@ -395,7 +398,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
395
398
  with one card and nothing else to read puts it there (`.card--centre`, the
396
399
  introduction screens); a splash puts its mark there.
397
400
 
398
- Demo: the *skeleton* block at the top of `elements.html`.
401
+ Live: every screen in the gallery (`npm run dev`).
399
402
 
400
403
  ## Rules of the system
401
404
 
@@ -461,7 +464,7 @@ Demo: the *skeleton* block at the top of `elements.html`.
461
464
  get covered. Closing reverses it in two phases: the after-pack returns
462
465
  while the card drops into its slot, then the before-pack slides back up
463
466
  from behind. Slots, offsets and paint order are all CSS `:nth-child`, so
464
- a stack renders correctly with no script; `cardds.js` only toggles the
467
+ a stack renders correctly with no script; `SheetStack` only sets the
465
468
  state classes. Don't reintroduce height, `display` or z-index switching
466
469
  here — every past bug in this animation came from exactly that.
467
470
  7. **Everything shrinks to 320px.** `.screen > *`, `.card` and `.card > *`
@@ -488,14 +491,14 @@ Demo: the *skeleton* block at the top of `elements.html`.
488
491
  revealed above the cut, not under it. `--sheet-half` is a share of the
489
492
  sheet's height (`0.5`), one number serving both the translate and that
490
493
  padding. Two boxes hold sheets and the states
491
- mean the same in both: `.step` (an app screen, the bar outside — Anatomy)
494
+ mean the same in both: `.step` (an app screen — Anatomy)
492
495
  and `.sheet-stage` (a page-level overlay: a sheet over a map, the dim page
493
496
  stage `--dim` + `.modal` = `__lift` + `__drawer` — a sheet always stands on the floor, never raised).
494
497
  A stage keeps the top bar's line free (`--_top`), so `--full` stops under
495
498
  `--header-h` and keeps its corners. Both sheet tokens are published — a
496
499
  sheet with a taller head retunes `--sheet-peek` — and registered in
497
- `tokens.css`, which is how `cardds.js` reads them to snap a drag. Demo:
498
- `sheet.html`, `elements.html`.
500
+ `tokens.css`, which is how `Sheet` reads them to snap a drag. Live: the
501
+ gallery's `Sheet` page.
499
502
  9. **Elements inside a card are frameless.** Rows, key-values, pickers,
500
503
  gauges and tiles never draw a second card boundary; hairlines
501
504
  (`--hairline`) separate rows, `--outline` keeps controls legible on
@@ -557,7 +560,7 @@ That file is where a project's look lives. Bases and roles:
557
560
  | what | tokens | moves |
558
561
  |---|---|---|
559
562
  | 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 |
560
- | type | `--fs-base` (1rem = 16) | every `--fs-*`, `--badge`, `--icon-xs` |
563
+ | type | `--fs-base` (1.375rem = 22 — the body) | every `--fs-*`, `--badge`, `--icon-xs` |
561
564
  | corners | `--r-base` (1rem = 16) | every `--r-*`; `0` = square. `--r-chip` stays fully round |
562
565
  | 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) |
563
566
  | faces | `--font-ui`, `--font-display` | |
@@ -612,8 +615,8 @@ Copy any block in `palette.css`, rename, retune:
612
615
 
613
616
  Every length token resolves to rem — a multiplier × one of four rem bases
614
617
  (`--sp-4` = `--sp-base` × 4 = 1rem, `--tap` = `--tap-base` = 3rem, `--r-card` =
615
- `--r-base` × 2, `--fs-h1` = `--fs-base` × 1.75; see "A project's theme") — so the
616
- whole system still resizes from one number: the root font size. `base.css` keeps it at `100%` (the user's text-size setting, 16px by
618
+ `--r-base` × 2, `--fs-h1` = `--fs-base` × 1.2; see "A project's theme") — so the
619
+ whole system scales with the root font size. `base.css` keeps it at `100%` (the browser's setting, 16px by
617
620
  default) and steps narrow phones (≤360px) to `93.75%`, which keeps the 25rem
618
621
  column, its 48px taps and its type in proportion on 320–360 screens. Only
619
622
  `--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
package/cardds.css CHANGED
@@ -2,7 +2,7 @@
2
2
  @import url("css/fonts.css"); /* the two faces: PK Nonthaburi (body), FC Pride (display) */
3
3
  @import url("css/tokens.css"); /* structure + wireframe defaults */
4
4
  @import url("css/palette.css"); /* color settings — the file you tune */
5
- @import url("css/base.css"); /* reset, surface, type levels, section */
5
+ @import url("css/base.css"); /* reset, surface, type levels, page */
6
6
  @import url("css/card.css"); /* card boundary + list variants */
7
7
  @import url("css/stack.css"); /* sheet stack: .sheet-stack — the menu, sheets stacked, heads peek, tap opens */
8
8
  @import url("css/sheet.css"); /* sheet single: .sheet — one card as a bottom sheet, peek / half / full */
@@ -12,7 +12,7 @@
12
12
  @import url("css/actions.css"); /* topbar, chips, buttons, CTA pack */
13
13
  @import url("css/numbers.css"); /* ring, track, band, dot grid, picker, badge, bars */
14
14
  @import url("css/people.css"); /* avatar, avatar stack, chooser row */
15
- @import url("css/choice.css"); /* check, toggle, toggle chip, chip grid, days, calendar, mood, pin, composer */
15
+ @import url("css/choice.css"); /* check, toggle, chip grid, days, calendar, mood, pin, composer */
16
16
  @import url("css/lists.css"); /* row, rows, key-value, kv grid, timeline, legend, link */
17
17
  @import url("css/media.css"); /* cover card, quote, tile grid, fold, mosaic, wave */
18
- @import url("css/layover.css"); /* ask drawer, popover menu, banner, float bar, callout, deck */
18
+ @import url("css/layover.css"); /* ask drawer, popover menu, banner, float bar, callout */
package/css/actions.css CHANGED
@@ -55,12 +55,12 @@
55
55
  max-width: var(--screen-w);
56
56
  margin-inline: auto;
57
57
  padding-inline: var(--appbar-pad-x, var(--screen-pad)); /* fixed + full-bleed: the screen's own inset, so the bar's contents line up with the title bar under it (Lh 2026-09-15) */
58
- z-index: 40; /* topmost — card animations never cover it */
58
+ z-index: var(--z-top); /* topmost — card animations never cover it */
59
59
  background: var(--appbar-bg, var(--surface)); /* solid: sliding cards never show through */
60
60
  }
61
61
  /* fixed, so it covers the top of the screen it sits in — reserve its height
62
- there, from the same token, the way the bottom bars reserve theirs. The
63
- breathing under it is published: a .step cancels it to touch the bar. */
62
+ there, from the same header-h token. The breathing under it is published:
63
+ a .step cancels it to touch the bar. */
64
64
  .screen:has(> .appbar) { --header-gap: var(--sp-2); padding-top: calc(var(--header-h) + var(--header-gap)); }
65
65
  .appbar__brand { flex: 1; display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
66
66
  .appbar__text { display: grid; min-width: 0; flex: 1; } /* pushes trailing button to the right edge */
@@ -68,11 +68,9 @@
68
68
  .appbar__who {
69
69
  flex: 1; min-width: 0; min-height: var(--tap);
70
70
  display: flex; align-items: center; gap: var(--sp-2);
71
- padding: 0; margin: 0; border: 0; background: none; color: inherit; font: inherit; text-align: start; cursor: pointer;
71
+ margin: 0; text-align: start;
72
72
  border-radius: var(--appbar-who-radius, var(--r-chip));
73
- -webkit-tap-highlight-color: transparent;
74
73
  }
75
- .appbar__who:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
76
74
  .appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.35; } /* clipped lines: room for Thai marks */
77
75
  .appbar__title { font: var(--type-label); }
78
76
  .appbar__sub { font: var(--type-caption); color: var(--on-surface-muted); }
@@ -96,7 +94,7 @@
96
94
  .icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
97
95
  .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); }
98
96
  /* fab: the square floating action, bottom-right of a stage or screen */
99
- .fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3)); box-shadow: var(--shadow-float); z-index: 31; }
97
+ .fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3)); box-shadow: var(--shadow-float); z-index: var(--z-fab); }
100
98
 
101
99
  /* ---- buttons ---- */
102
100
  .btn {
@@ -129,7 +127,6 @@
129
127
  /* disabled keeps the slot — a control that vanishes moves everything else,
130
128
  and a pager whose "previous" disappeared on page one would shift the bar
131
129
  under the thumb on every page */
132
- :is(.btn, .icon-btn):disabled { opacity: 0.4; cursor: default; }
133
130
  /* a row of buttons sharing the width (twin CTAs inside a card) */
134
131
  .btn-row { display: flex; gap: var(--sp-2); }
135
132
  .btn-row > .btn { flex: 1; min-width: 0; }
@@ -169,7 +166,7 @@
169
166
  /* ---- dropdown: the standard for every select — a pill trigger + .menu popover ----
170
167
  <div class="dropdown"><button class="dropdown__btn" popovertarget="f1"><span class="dropdown__label">this week</span><svg class="icon icon--xs"><use href="icons.svg#chevron-down"/></svg></button>
171
168
  <div class="menu" id="f1" popover><button class="menu__item" aria-current="true" data-value="week">this week</button>…</div></div>
172
- cardds.js copies the picked item into the label and fires "change" on .dropdown.
169
+ The Dropdown component copies the picked item into the label and reports the change.
173
170
  .menu (layover.css) draws the panel; anchor positioning pins it under the trigger. */
174
171
  .dropdown { position: relative; min-width: 0; display: inline-grid; }
175
172
  .dropdown__btn {
@@ -188,12 +185,11 @@
188
185
  font: var(--type-label);
189
186
  font-size: var(--fs-body);
190
187
  text-align: left;
191
- cursor: pointer;
192
188
  }
193
189
  /* muted: the value is still the default (README 13) */
194
190
  .dropdown--muted .dropdown__btn { color: var(--dropdown-muted-ink, var(--card-muted)); }
195
191
  .dropdown__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
196
- .dropdown__btn > .icon { transition: rotate 0.2s ease; }
192
+ .dropdown__btn > .icon { transition: rotate var(--motion-quick) ease; }
197
193
  .dropdown:has(.menu:popover-open) .dropdown__btn > .icon { rotate: 180deg; }
198
194
  .dropdown:has(.menu.\:popover-open) .dropdown__btn > .icon { rotate: 180deg; } /* the polyfill's open (layover.css) */
199
195
  .dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards (= .menu--end, layover.css) */
@@ -203,10 +199,7 @@
203
199
  /* ---- segment (day picker / tab strip) — items share width, shrink on small screens ---- */
204
200
  .segment { display: flex; gap: var(--sp-1); }
205
201
  .segment__item {
206
- border: 0;
207
- background: transparent;
208
- color: inherit;
209
- opacity: 0.6;
202
+ opacity: var(--rest);
210
203
  border-radius: var(--r-chip);
211
204
  padding: var(--sp-2) var(--sp-1);
212
205
  font: var(--type-label);
@@ -230,7 +223,7 @@
230
223
  :root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
231
224
  .action-bar {
232
225
  position: relative;
233
- z-index: 30; /* above .sheet-stack sheets, which paint at --level 1-5 */
226
+ z-index: var(--z-bar); /* above .sheet-stack sheets, which paint at --level 1-5 */
234
227
  max-width: var(--screen-w);
235
228
  margin-inline: auto;
236
229
  padding: var(--sp-3) var(--sp-4) var(--sp-5);
@@ -260,10 +253,11 @@
260
253
  NEVER carry words (Lh 2026-09-16, for good): an arrow is the whole meaning,
261
254
  and a word that isn't a direction ("Get started", "Done") is not a pager
262
255
  button — that step COMMITS and goes back to the plain .action-bar with one
263
- wide button. Where you are is said by the .pager__at dots UNDER the card
264
- that changes (a .screen__centre group, card.css), never in the bar — the
265
- dots move with what moves (Lh, 2026-09-12). Between the buttons goes a
266
- "skip" link or nothing.
256
+ wide button. Where you are is said by the .pager__at dots — under the card
257
+ that changes (a .screen__centre group, card.css) for a card walk, never in
258
+ that bar (Lh, 2026-09-12); a walk of words instead floats its dots as the
259
+ bar's own first row (step.css, Lh 2026-09-23) — either way the dots move
260
+ with what moves. Between the buttons goes a "skip" link or nothing.
267
261
 
268
262
  <div class="action-bar action-bar--pager">
269
263
  <button class="icon-btn" aria-label="ย้อนกลับ" disabled>…</button>
@@ -280,14 +274,14 @@
280
274
  height: var(--dot);
281
275
  border-radius: 50%;
282
276
  background: currentColor;
283
- opacity: 0.3;
277
+ opacity: var(--faint);
284
278
  }
285
279
  .pager__at > .is-at { background: var(--accent); opacity: 1; }
286
280
  /* ---- tiers: a control row above the buttons ----
287
281
  A screen whose one move comes with a control — the introduction's text-size
288
282
  slider above its pager. Each tier is a row; the bar stacks them, and any
289
283
  parent hosting the bar publishes the room for both, the way the plain
290
- reserve is published above, so a .screen or a .card--centre stops clear.
284
+ reserve is published above, so a .card--centre stops clear of it.
291
285
  <div class="action-bar action-bar--tiers">
292
286
  <div class="action-bar__tier"><label class="slider">…</label></div>
293
287
  <div class="action-bar__tier action-bar__tier--pager">…the round pager…</div>
@@ -334,25 +328,53 @@
334
328
  .action-note__words { display: block; } /* ONE run of words: an icon inside sits in the line, the spaces around it kept */
335
329
  .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) */
336
330
 
337
- /* PRESS (Lh 2026-09-25): every CONTROL answers a tap — it gives under the finger and springs back, its choice fill fades in;
338
- INFORMATION never does (no pointer, no press). One list, zero specificity: a component's own rule always wins. */
339
- :where(.btn, .icon-btn, .fab, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item,
340
- .calendar__day, .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .sheet-stack__back, .dropdown__btn, .appbar__who, .menu__item) {
331
+ /* PRESS (Lh 2026-09-25; every tappable thing, 2026-09-27): every CONTROL answers a tap, INFORMATION never does (no pointer, no
332
+ press). Two answers, by size: GIVE — a control the size of a finger shrinks under it (--press-scale) and springs back; DIM —
333
+ something as wide as its card (a row, a summary line, a card, a step bar, a peeked sheet) fades under the finger (--press-dim)
334
+ instead, because a whole row shrinking reads as the card moving. A choice's fill fades in as it is picked. Disabled = no
335
+ pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here. */
336
+ :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
337
+ .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
338
+ a.avatar, button.avatar, .menu__item) {
341
339
  cursor: pointer;
342
340
  touch-action: manipulation;
343
341
  transition: scale var(--motion-press) ease-out;
344
342
  }
345
- /* a CHOICE's fill fades in as it is picked (a button's colour never changes on a tap, so it does not fade) */
346
- :where(.chip--pick, .tile--pick, .day, .segment__item, .calendar__day, .float-bar__item, .menu__item) {
347
- transition: scale var(--motion-press) ease-out, background-color var(--motion-press) ease-out, color var(--motion-press) ease-out, border-color var(--motion-press) ease-out;
348
- }
349
- :where(.btn, .icon-btn, .fab, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item,
350
- .calendar__day, .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .sheet-stack__back, .dropdown__btn, .appbar__who, .menu__item):active:not(:disabled, [aria-disabled="true"]) {
351
- scale: var(--press-scale);
343
+ :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field) {
344
+ cursor: pointer;
345
+ touch-action: manipulation;
346
+ transition: opacity var(--motion-press) ease-out;
352
347
  }
353
- :where(.btn, .icon-btn, .fab, .link, a.chip, button.chip, a.tile, button.tile, .day, .segment__item, .calendar__day, .float-bar__item,
354
- .avatar-pick__item, .add-row, .picker__item, .sheet-stack__back, .dropdown__btn):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
355
- @media (prefers-reduced-motion: reduce) {
356
- :where(.btn, .icon-btn, .fab, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item,
357
- .calendar__day, .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .sheet-stack__back, .dropdown__btn, .appbar__who, .menu__item):active { scale: none; }
348
+ /* 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) */
349
+ :where(.chip--pick, button.chip, .tile--pick, button.tile, .day, .segment__item, .calendar__day, .float-bar__item, .picker__item, .check,
350
+ .icon-btn[role="tab"], .avatar-pick__item > .avatar) {
351
+ transition-property: scale, background-color, color, border-color, opacity;
352
+ transition-duration: var(--motion-press);
353
+ transition-timing-function: ease-out;
358
354
  }
355
+ :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
356
+ .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
357
+ a.avatar, button.avatar, .menu__item):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
358
+ :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
359
+ .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
360
+ /* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back) must not turn see-through:
361
+ it darkens a touch instead (--press-shade) */
362
+ :where(.sheet-stack--tap > .card:not(.is-open), .postcard__turn) { cursor: pointer; touch-action: manipulation; transition: filter var(--motion-press) ease-out; }
363
+ :where(.sheet-stack--tap > .card:not(.is-open), .postcard__turn):active { filter: brightness(var(--press-shade)); }
364
+ /* 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) */
365
+ :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
366
+ .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
367
+ a.avatar, button.avatar, .menu__item):focus-visible,
368
+ :where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
369
+ :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field):not(label, .toggle-field):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
370
+ /* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
371
+ :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
372
+ .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
373
+ a.avatar, button.avatar, .menu__item, a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field):is(:disabled, [aria-disabled="true"]),
374
+ :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn, label.row):has(:disabled) { cursor: default; }
375
+ :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
376
+ .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
377
+ a.avatar, button.avatar, .menu__item):not(.link):is(:disabled, [aria-disabled="true"]),
378
+ :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
379
+ .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 */
380
+ @media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
package/css/base.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* ============================================================
2
- cardds/base.css — reset, surface, typography levels, section
2
+ cardds/base.css — reset, surface, typography levels, page
3
3
  ============================================================ */
4
4
 
5
5
  *, *::before, *::after { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
@@ -21,9 +21,8 @@ button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn
21
21
  An app's OWN text-size setting is NOT here (Lh 2026-09-22: --text-scale, which
22
22
  multiplied the root, zoomed the UI too — retired): it is --fs-base on <html>,
23
23
  the type base, so the words grow and the things keep their size (tokens.css). */
24
- html { font-size: 100%; scrollbar-width: none; }
24
+ html { font-size: 100%; }
25
25
  @media (max-width: 22.5em) { html { font-size: 93.75%; } }
26
- html::-webkit-scrollbar { display: none; } /* Chromium/Safari */
27
26
 
28
27
  body {
29
28
  margin: 0;
@@ -39,7 +38,30 @@ body {
39
38
  [hidden] { display: none !important; }
40
39
 
41
40
  img, svg { display: block; max-width: 100%; }
42
- button { font: inherit; cursor: pointer; }
41
+ button { font: inherit; }
42
+ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
43
+
44
+ /* zero-specificity reset for controls that draw nothing of their own (2026-09-27 review):
45
+ a component's own rule always wins, at any specificity, so this never fights it — it only
46
+ fills in for a selector that never says otherwise. Not everything below shares every
47
+ property: two exceptions read as they always have — .calendar__day has never set its own
48
+ padding (the UA button default), and .sheet-step__go has never set its own color (the UA
49
+ button default too) — giving either one now would be a look change, so neither is here. */
50
+ :where(.appbar__who, .segment__item, .day, .calendar__day, .menu__item, .float-bar__item, .link,
51
+ .add-row, .avatar-pick__item, .picker__item, .postcard__turn, .sheet-step__go) {
52
+ border: 0;
53
+ background: transparent;
54
+ font: inherit;
55
+ }
56
+ :where(.appbar__who, .segment__item, .day, .calendar__day, .menu__item, .float-bar__item, .link,
57
+ .add-row, .avatar-pick__item, .picker__item, .postcard__turn) { color: inherit; }
58
+ :where(.appbar__who, .segment__item, .day, .menu__item, .float-bar__item, .link, .add-row,
59
+ .avatar-pick__item, .picker__item, .postcard__turn, .sheet-step__go) { padding: 0; }
60
+
61
+ /* scrollbar hidden, scroll kept: the track scrolls by touch/drag, never by a visible bar —
62
+ .bleed (card.css) is the one exception, its bar is designed, not hidden */
63
+ :where(html, .sheet-stack--tap > .card, .step > .pages, .picker__rail, .avatar-pick) { scrollbar-width: none; }
64
+ :where(html, .sheet-stack--tap > .card, .step > .pages, .picker__rail, .avatar-pick)::-webkit-scrollbar { display: none; }
43
65
 
44
66
  /* icon from icons.svg sprite: <svg class="icon"><use href="icons.svg#bell"/></svg> */
45
67
  .icon { width: var(--icon-size, var(--icon)); height: var(--icon-size, var(--icon)); flex: none; }
@@ -48,10 +70,16 @@ button { font: inherit; cursor: pointer; }
48
70
  .icon--lg { width: var(--icon-lg-size, var(--icon-lg)); height: var(--icon-lg-size, var(--icon-lg)); } /* empty states */
49
71
 
50
72
  /* mobile screen column — the standard horizontal inset, and nothing vertical.
51
- Top and bottom are zero on purpose: vertical room is only ever a RESERVE for
52
- something fixed parked over the screen, so each bar adds its own and a page
53
- without bars needs no override. .appbar reserves the top (actions.css),
54
- .action-bar (actions.css) and .float-bar (layover.css) the bottom. */
73
+ Top is zero on purpose: room there is only ever a RESERVE for something
74
+ FIXED parked over the screen — .appbar reserves it (actions.css). A bottom
75
+ bar is not fixed (0.7.1): it is the Screen's own last child, in flow on a
76
+ scrolling screen (a sticky row, actions.css) or, in a fill screen, a layer
77
+ over the content's foot (step.css) — nothing here reserves room for it.
78
+ It also reads like a native app (Lh 2026-09-25): words are not highlighted
79
+ by a long press or a drag, no copy / web-search callout — only what is
80
+ typed into, and words marked selectable (`Text selectable` → .t-select: a
81
+ code, an address). Knob --screen-select: `text` = the web page's old way
82
+ everywhere. */
55
83
  .screen {
56
84
  --screen-gap: var(--sp-5); /* published: a .step cancels it to touch the bar it follows (step.css) */
57
85
  max-width: var(--screen-w);
@@ -60,6 +88,7 @@ button { font: inherit; cursor: pointer; }
60
88
  display: grid;
61
89
  gap: var(--screen-gap);
62
90
  align-content: start; /* a screen taller than its content (a fixed phone) never spreads its rows — the top bar stays at the top, a row keeps its height */
91
+ -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); -webkit-touch-callout: none;
63
92
  }
64
93
  .screen > * { min-width: 0; } /* content never widens the column past the viewport */
65
94
  /* a screen that carries its own palette (data-palette on the element, not on <html>)
@@ -102,21 +131,18 @@ button { font: inherit; cursor: pointer; }
102
131
 
103
132
  .t-muted { color: var(--on-surface-muted); }
104
133
  .card .t-muted { color: var(--card-muted); } /* muted follows the card's ink, not the surface's */
105
- .card--3 .t-muted { color: inherit; opacity: var(--dim); } /* card-muted is tuned for light cards; dim instead on the dark slot */
134
+ /* .card--3 .t-muted (dim instead of --card-muted on the dark slot) is declared once in card.css, with the rest of that family */
106
135
  .t-caps { text-transform: uppercase; }
107
136
  .grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
108
137
  /* 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.
109
138
  The 1px is the one the platform needs to keep it in the accessibility tree, not a length of the design. */
110
- /* a screen reads like a native app (Lh 2026-09-25): words are not highlighted by a long press or a drag, no copy / web-search
111
- callout — only what is typed into, and words marked selectable (`Text selectable` → .t-select: a code, an address).
112
- Knob --screen-select: `text` = the web page's old way everywhere. */
113
- .screen { -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); -webkit-touch-callout: none; }
114
139
  .screen :is(input, textarea, [contenteditable="true"]), .screen .t-select { -webkit-user-select: text; user-select: text; -webkit-touch-callout: default; }
115
140
  .t-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
116
141
 
117
- /* ---- base content: words straight on the base, no card around them — an
118
- a heading, a paragraph, maybe a .cta-pack. Was .section until
119
- 2026-09-16 (Lh: the name said nothing about WHERE it lives; this one does).
142
+ /* ---- page: words straight on the base, no card around them — a
143
+ heading, a paragraph, maybe a .cta-pack. Was .section until 2026-09-16
144
+ (Lh: the name said nothing about WHERE it lives), then .base-content
145
+ until 2026-09-23 (Lh: the content is a BOX; what's in it is .page).
120
146
  In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
121
147
  .page {
122
148
  display: grid;
@@ -150,6 +176,6 @@ button { font: inherit; cursor: pointer; }
150
176
  color: var(--empty-state-ink, var(--on-surface)); /* full ink, icon and words alike: muted read as "disabled" at the middle of an empty screen (Lh 2026-09-16) — muted is for meta, not a sentence */
151
177
  }
152
178
  .empty-state__action { margin-top: var(--sp-2); }
153
- /* inside a card (a section's "nothing yet"): the card's ink */
179
+ /* inside a card (a card's own "nothing yet"): the card's ink */
154
180
  .card .empty-state { color: var(--card-ink); padding-block: var(--sp-6); }
155
181
  .card--3 .empty-state { color: inherit; }
package/css/card.css CHANGED
@@ -1,6 +1,7 @@
1
1
  /* ============================================================
2
2
  cardds/card.css — the card boundary + list variants
3
- One card = one boundary. Tone via .card--1/2/3 (role slots).
3
+ One card = one boundary. The base .card is tone 1; further tones
4
+ via .card--2/3/4/5 (role slots).
4
5
  ============================================================ */
5
6
 
6
7
  .card {
@@ -25,14 +26,18 @@
25
26
  .card > * { min-width: 0; }
26
27
 
27
28
  .card--2 { --card-bg: var(--card-2-bg); --card-ink: var(--card-2-ink); --card-muted: var(--card-2-muted); }
28
- .card--3 { --card-bg: var(--card-3-bg); --card-ink: var(--card-3-ink); }
29
- .card--4 { --card-bg: var(--card-4-bg); --card-ink: var(--card-4-ink); --card-muted: var(--card-4-muted); }
30
- .card--5 { --card-bg: var(--card-5-bg); --card-ink: var(--card-5-ink); --card-muted: var(--card-5-muted); }
31
-
32
29
  /* dark/accent slot: accent-toned controls would vanish — the slot re-points
33
30
  the accent tokens, so every accent consumer (btn--primary, chip--on,
34
31
  check, toggle, ring, day…) inverts without per-component rules */
35
- .card--3 { --accent: var(--card-3-ink); --on-accent: var(--card-3-bg); }
32
+ .card--3 { --card-bg: var(--card-3-bg); --card-ink: var(--card-3-ink); --accent: var(--card-3-ink); --on-accent: var(--card-3-bg); }
33
+ .card--4 { --card-bg: var(--card-4-bg); --card-ink: var(--card-4-ink); --card-muted: var(--card-4-muted); }
34
+ .card--5 { --card-bg: var(--card-5-bg); --card-ink: var(--card-5-ink); --card-muted: var(--card-5-muted); }
35
+ /* the dark slot's captions/hints/footnotes: --card-muted is tuned for a light card, so these dim the full ink
36
+ instead (one rule, matching specificity to what it replaces — .card--3 <space> <one class>) — merged from
37
+ base.css/choice.css/media.css/numbers.css/journey.css/people.css (2026-09-27 review). lists.css's .legend
38
+ differs (opacity 0.8, not --dim) and stays there. */
39
+ .card--3 :is(.t-muted, .calendar__wd, .composer__hint, .quote__by, .ring__label, .band__foot, .picker__unit,
40
+ .route__place, .note-row, .avatar-pick__role) { color: inherit; opacity: var(--dim); }
36
41
 
37
42
  /* header row: title left, floating action right — centered, since the
38
43
  trailing control (chip/badge/icon-btn/link) is almost always taller than
@@ -135,7 +140,7 @@
135
140
  linear-gradient(to top left, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px));
136
141
  border: var(--border-w) solid currentColor;
137
142
  border-radius: var(--r-media);
138
- opacity: 0.4;
143
+ opacity: var(--off);
139
144
  min-height: 10rem;
140
145
  }
141
146
  .ph--sm { min-height: 7.5rem; }
@@ -191,9 +196,9 @@
191
196
  likes when not (Lh, 2026-09-12) ----
192
197
  The member sees one content area — whatever is between the top bar and the
193
198
  bar — and reads it as a whole page: a short card sits at its middle, a long
194
- one simply makes the page longer, runs under the bar (the bar's reserve is
195
- already the screen's bottom padding, actions.css) and scrolls up like any
196
- page — nothing is capped, nothing shrinks, no scroll box inside a box.
199
+ one simply makes the page longer, runs under the bar (a sticky last row,
200
+ actions.css) and scrolls up like any page — nothing is capped, nothing
201
+ shrinks, no scroll box inside a box.
197
202
  So on a .screen that is NOT --fill the group is in flow: the column is at
198
203
  least one phone tall, the group takes the room after the top bar and
199
204
  centres its children in it. The screen's own inset stays the inset.
@@ -211,9 +216,9 @@
211
216
  the React adapter's slideTo() sets data-slide on :root, starts the transition
212
217
  and updates inside it; a browser without them (the floor: Chrome 105 / iOS 16)
213
218
  simply switches. Only the card is named; the rest of the page must not
214
- cross-fade. */
215
- /* the name is set by slideTo() on the moving element for the move only (2026-09-22): a static name on every walk's current
216
- page made two walks on one page — the gallery — share it, and the browser aborts a transition with a duplicate name */
219
+ cross-fade. The name is set by slideTo() on the moving element for the move only (2026-09-22): a static name on
220
+ every walk's current page made two walks on one page — the gallery — share it, and the browser aborts a
221
+ transition with a duplicate name. */
217
222
 
218
223
  /* ---- pages: the cards of a walk, all one height ----
219
224
  A sequence the member walks (an introduction) reads steadier when every card