@usableapp/cardds 0.7.15 → 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 (77) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +29 -28
  3. package/cardds.css +3 -3
  4. package/css/actions.css +67 -22
  5. package/css/base.css +44 -18
  6. package/css/card.css +25 -26
  7. package/css/choice.css +21 -33
  8. package/css/forms.css +0 -3
  9. package/css/journey.css +1 -3
  10. package/css/layover.css +19 -41
  11. package/css/lists.css +1 -6
  12. package/css/media.css +9 -18
  13. package/css/numbers.css +6 -21
  14. package/css/people.css +3 -9
  15. package/css/sheet.css +15 -19
  16. package/css/stack.css +21 -29
  17. package/css/step.css +7 -12
  18. package/css/theme.template.css +2 -1
  19. package/css/tokens.css +22 -4
  20. package/dist/actions/Btn.d.ts +1 -1
  21. package/dist/actions/Dropdown.js +3 -9
  22. package/dist/actions/Fab.d.ts +1 -1
  23. package/dist/actions/Fab.js +3 -2
  24. package/dist/cardds.css +264 -267
  25. package/dist/cards/Card.d.ts +24 -8
  26. package/dist/cards/Card.js +23 -8
  27. package/dist/cards/CardHead.d.ts +0 -1
  28. package/dist/cards/CardHead.js +1 -1
  29. package/dist/choice/Calendar.js +2 -5
  30. package/dist/choice/Mood.d.ts +10 -5
  31. package/dist/choice/Mood.js +4 -2
  32. package/dist/forms/Field.js +2 -3
  33. package/dist/forms/FileBtn.js +1 -1
  34. package/dist/forms/Pin.js +7 -12
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +3 -3
  37. package/dist/layover/Banner.d.ts +10 -3
  38. package/dist/layover/Banner.js +6 -3
  39. package/dist/lists/Row.d.ts +2 -1
  40. package/dist/lists/Row.js +5 -5
  41. package/dist/media/Postcard.js +3 -5
  42. package/dist/numbers/Band.d.ts +3 -1
  43. package/dist/numbers/Picker.js +3 -10
  44. package/dist/numbers/Track.d.ts +4 -2
  45. package/dist/numbers/Track.js +3 -3
  46. package/dist/people/AvatarPick.d.ts +3 -1
  47. package/dist/people/AvatarPick.js +2 -2
  48. package/dist/scaffold/ActionBar.d.ts +3 -3
  49. package/dist/scaffold/ActionBar.js +2 -2
  50. package/dist/scaffold/Centre.d.ts +2 -2
  51. package/dist/scaffold/Centre.js +2 -2
  52. package/dist/scaffold/PagerAt.d.ts +1 -1
  53. package/dist/scaffold/PagerAt.js +1 -1
  54. package/dist/scaffold/Screen.js +4 -7
  55. package/dist/scaffold/TopBar.js +3 -5
  56. package/dist/sheets/Drawer.js +3 -4
  57. package/dist/sheets/Sheet.js +9 -22
  58. package/dist/sheets/SheetStack.d.ts +2 -2
  59. package/dist/sheets/SheetStack.js +20 -13
  60. package/dist/sheets/SheetSteps.js +8 -11
  61. package/dist/sheets/StackBack.js +2 -1
  62. package/dist/sheets/StackSheet.d.ts +3 -2
  63. package/dist/sheets/StackSheet.js +7 -11
  64. package/dist/state.d.ts +24 -0
  65. package/dist/state.js +57 -0
  66. package/dist/templates/CallSheet.js +2 -4
  67. package/dist/templates/Home.d.ts +3 -2
  68. package/dist/templates/Home.js +2 -4
  69. package/dist/templates/Splash.js +3 -3
  70. package/dist/templates/Walkthrough.js +4 -5
  71. package/dist/type/Icon.js +3 -5
  72. package/dist/type/Text.d.ts +1 -1
  73. package/dist/type/textScale.d.ts +6 -0
  74. package/dist/type/textScale.js +12 -0
  75. package/package.json +7 -4
  76. package/dist/layover/Deck.d.ts +0 -9
  77. package/dist/layover/Deck.js +0 -10
package/CHANGELOG.md CHANGED
@@ -2,6 +2,59 @@
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
+
32
+ ## 0.7.16 — 2026-09-25
33
+
34
+ Refactor + additions. Same props, same markup, same computed style on every story.
35
+
36
+ ### Added
37
+ - `setTextScale(percent, root?)` — the member's text size as a share of the theme's `--fs-base` (100 = the theme's own). It reads the base from the CSS, so an app no longer copies `1.375rem`. Keeping the choice (localStorage…) stays the app's.
38
+
39
+ - PRESS: every control (Btn, IconBtn, Fab, Link, a/button Chip, ChipPick, Tile as button/link/pick, Day, SegmentItem, Calendar day, FloatBarItem, AvatarPickItem, AddRow, Picker item, StackBack, Dropdown, AppBar brand, MenuItem) gives under the finger (`--press-scale` 0.96, `--motion-press` 0.12s, off under reduced motion) and shows a focus ring; a choice's fill fades in. Zero specificity (`:where`), so a component's own rule wins.
40
+ - `Rows heading` takes an ELEMENT too: `<Rows heading={<Text level="title" as="h3">…</Text>}>` — that h3 is the group's name (role=group, aria-labelledby). Words still give the small label. Drop a hand-written `role="group" aria-label`.
41
+ - Token `--dim` (0.7): the one share secondary words fade to on the dark card, a drawer, a tile's sub line (was 14 hand-typed `opacity: 0.7`). A theme sets it once.
42
+ - `TrackStep num` — the step's number in the round instead of an Icon (the `.track__dot` look existed in CSS with no way in; story Track · Numbered).
43
+ - `AvatarPickItem selected` (aria-pressed; the picked person's avatar fills with the accent).
44
+
45
+ ### Removed (breaking for anyone using them)
46
+ - `Deck` and `DeckActions` (the swipe deck), the `.deck*` CSS and the `--deck-*` knobs — Lh 2026-09-26: no use case. Nothing replaces them; a card to pick from = `Card` + its own buttons.
47
+
48
+ ### Changed (may touch a screen)
49
+ - `Mood` is controlled like every other control: `value` + `onChange(value)` or `defaultValue`. Before: `value` was only the starting dot and `onChange` got the input event. Move a starting value to `defaultValue`; read the number from `onChange`.
50
+ - INFORMATION never looks tappable: `.tile` (a plain div) and `Avatar add` no longer set `cursor: pointer`.
51
+ - Stories: Segment, DayStrip, FloatBar (buttons), AvatarPick answer a tap; the Chip filter row is `ChipPick`s (plain `Chip on` is a label, not a choice).
52
+
53
+ ### Changed (internal)
54
+ - Every controlled/uncontrolled state (`value`+`onChange` or `defaultValue`, and the same for `state`, `open`, `at`, `tab`, `selected`, `flipped`) now runs on one internal hook, `useControlled`, instead of 12 hand copies.
55
+ - Dev warnings (Icon, TopBar, Screen) share one `warnOnce`.
56
+ - CSS: dead selectors removed (`.dropdown__trigger`, `.menu__item.is-on`, `.float-bar__item.is-on`), one overridden stack-head rule dropped, split rules merged. Two raw `0px` in the SheetSteps CSS → `0rem`.
57
+
5
58
  ## 0.7.15 — 2026-09-25
6
59
 
7
60
  ### Removed (breaking for anyone using them)
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.
@@ -307,11 +307,9 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
307
307
  above the centre (2 × (40% − the title bar)): a card taller than that is
308
308
  not a centred card — it is a plain scrolling `.screen` (the detail view).
309
309
  - **A small group at the same centre** — an empty state; a card with its quiet
310
- second button and the pager dots under it; a deck with its two round
311
- buttons — is `.screen__centre` (React: `Centre`), a block in the fill
310
+ second button and the pager dots under it — is `.screen__centre` (React: `Centre`), a block in the fill
312
311
  screen after the topbar. Same placement and cap; it runs edge to edge with
313
- the inset as padding, so a deck's fanned cards may overhang and clip at the
314
- screen's edges (the fold visual, not a bug), and stacks its children at the
312
+ the inset as padding, and stacks its children at the
315
313
  screen's gap. Never hand-lay `top: 40%` on a screen.
316
314
  - **The bar is the screen's bottom.** Plain `.action-bar` for one move; with a
317
315
  control that belongs to the screen (a slider), `.action-bar--tiers` stacks a
@@ -357,6 +355,10 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
357
355
  - **Foot** (`.card__foot`): the card's action buttons on one line. One
358
356
  button takes the width; up to four share it in even parts; from three
359
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).
360
362
  - A page with **one feature and one CTA** (an install page, a single
361
363
  decision) may be contained in one card: head, the pitch, foot.
362
364
  - **A bottom sheet holds what the member acts on** — controls, or small
@@ -375,7 +377,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
375
377
  same material — a stacked sheet is written as a `.card`, and the single
376
378
  sheet is `.card.sheet` — and both obey rule 8: rigid, full height, moved
377
379
  by `translate` only. (The stack was first named "card stack": wrong, and
378
- retired.) Demos: `sheet.html`, `stack.html`.
380
+ retired.) See them live in the gallery: `Sheet`, `SheetStack`.
379
381
  - **A step is one screen, one task: `.step`.** What the member reads is
380
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
381
383
  `.sheet` in front of it; the screen's one move is `.action-bar`, the screen's
@@ -387,8 +389,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
387
389
  slides in: it starts where the top bar ends and ends where the bar's room
388
390
  begins. Nobody lays a step out by hand and nobody writes a
389
391
  sheet's height. In short: **member acts → card + sheet, member reads →
390
- card.** Markup: *Step markup*. Demo: `sheet.html`, `elements.html` #ask,
391
- `index.html` "step".
392
+ card.** Markup: *Step markup*. Live: the gallery's `Step` page.
392
393
 
393
394
  - **Where the centre is.** To the person holding a phone, the bottom fifth of
394
395
  the screen is the thumb's — the bar, the controls — so what is meant to be
@@ -397,7 +398,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
397
398
  with one card and nothing else to read puts it there (`.card--centre`, the
398
399
  introduction screens); a splash puts its mark there.
399
400
 
400
- Demo: the *skeleton* block at the top of `elements.html`.
401
+ Live: every screen in the gallery (`npm run dev`).
401
402
 
402
403
  ## Rules of the system
403
404
 
@@ -463,7 +464,7 @@ Demo: the *skeleton* block at the top of `elements.html`.
463
464
  get covered. Closing reverses it in two phases: the after-pack returns
464
465
  while the card drops into its slot, then the before-pack slides back up
465
466
  from behind. Slots, offsets and paint order are all CSS `:nth-child`, so
466
- a stack renders correctly with no script; `cardds.js` only toggles the
467
+ a stack renders correctly with no script; `SheetStack` only sets the
467
468
  state classes. Don't reintroduce height, `display` or z-index switching
468
469
  here — every past bug in this animation came from exactly that.
469
470
  7. **Everything shrinks to 320px.** `.screen > *`, `.card` and `.card > *`
@@ -490,14 +491,14 @@ Demo: the *skeleton* block at the top of `elements.html`.
490
491
  revealed above the cut, not under it. `--sheet-half` is a share of the
491
492
  sheet's height (`0.5`), one number serving both the translate and that
492
493
  padding. Two boxes hold sheets and the states
493
- mean the same in both: `.step` (an app screen, the bar outside — Anatomy)
494
+ mean the same in both: `.step` (an app screen — Anatomy)
494
495
  and `.sheet-stage` (a page-level overlay: a sheet over a map, the dim page
495
496
  stage `--dim` + `.modal` = `__lift` + `__drawer` — a sheet always stands on the floor, never raised).
496
497
  A stage keeps the top bar's line free (`--_top`), so `--full` stops under
497
498
  `--header-h` and keeps its corners. Both sheet tokens are published — a
498
499
  sheet with a taller head retunes `--sheet-peek` — and registered in
499
- `tokens.css`, which is how `cardds.js` reads them to snap a drag. Demo:
500
- `sheet.html`, `elements.html`.
500
+ `tokens.css`, which is how `Sheet` reads them to snap a drag. Live: the
501
+ gallery's `Sheet` page.
501
502
  9. **Elements inside a card are frameless.** Rows, key-values, pickers,
502
503
  gauges and tiles never draw a second card boundary; hairlines
503
504
  (`--hairline`) separate rows, `--outline` keeps controls legible on
@@ -559,7 +560,7 @@ That file is where a project's look lives. Bases and roles:
559
560
  | what | tokens | moves |
560
561
  |---|---|---|
561
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 |
562
- | type | `--fs-base` (1rem = 16) | every `--fs-*`, `--badge`, `--icon-xs` |
563
+ | type | `--fs-base` (1.375rem = 22 — the body) | every `--fs-*`, `--badge`, `--icon-xs` |
563
564
  | corners | `--r-base` (1rem = 16) | every `--r-*`; `0` = square. `--r-chip` stays fully round |
564
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) |
565
566
  | faces | `--font-ui`, `--font-display` | |
@@ -588,8 +589,8 @@ Rules:
588
589
  theme's values are the project's call. The derivations are only starting points;
589
590
  any role can be set outright.
590
591
  - The member's text-size setting is the same `--fs-base`, written on `<html>`
591
- by the app at run time (2.2rem = 160%): the words grow, the things keep their
592
- size. (`--text-scale`, which multiplied the root and zoomed the UI too, is gone.)
592
+ at run time by `setTextScale(percent)` (a share of the theme's base; 100 = the
593
+ theme's own): the words grow, the things keep their size. (`--text-scale`, which multiplied the root and zoomed the UI too, is gone.)
593
594
 
594
595
  Try it live: the gallery's nav (`npm run dev`) has a slider per size base, and
595
596
  palette `own` shows a colour well per colour base.
@@ -614,13 +615,13 @@ Copy any block in `palette.css`, rename, retune:
614
615
 
615
616
  Every length token resolves to rem — a multiplier × one of four rem bases
616
617
  (`--sp-4` = `--sp-base` × 4 = 1rem, `--tap` = `--tap-base` = 3rem, `--r-card` =
617
- `--r-base` × 2, `--fs-h1` = `--fs-base` × 1.75; see "A project's theme") — so the
618
- 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
619
620
  default) and steps narrow phones (≤360px) to `93.75%`, which keeps the 25rem
620
621
  column, its 48px taps and its type in proportion on 320–360 screens. Only
621
622
  `--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
622
- An app with its own text-size setting writes `--fs-base` on `<html>` (the type
623
- base, 1.375rem = 100%; 2.2rem = 160%): only the type and its holders (the count
623
+ An app with its own text-size setting calls `setTextScale(percent)`, which writes
624
+ `--fs-base` on `<html>` from the theme's base (100% = the theme's; 160% = 2.2rem by default): only the type and its holders (the count
624
625
  badge, the inline icon) grow; taps, spacing and radii keep their size.
625
626
  Components never write a raw length: spacing from `--sp-*`, control sizes from
626
627
  `--tap`, `--tap-lg`, `--btn-h`, `--bar-h`, `--header-h`, `--fab`, `--badge`, container
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: 2px; }
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>
@@ -333,3 +327,54 @@
333
327
  .card .action-note { color: var(--action-note-ink, var(--card-muted)); }
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) */
330
+
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) {
339
+ cursor: pointer;
340
+ touch-action: manipulation;
341
+ transition: scale var(--motion-press) ease-out;
342
+ }
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;
347
+ }
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;
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; }
@@ -9,7 +9,7 @@
9
9
  the member wants to copy), and inputs are untouched. Native buttons and
10
10
  links are matched by element; the rest by the class that makes them one */
11
11
  button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn, a.icon-btn, a.chip, label.chip, .chip--pick,
12
- .segment__item, .day, .calendar__day, .mood__opt, .pin__cell, .slider, .picker__item, .float-bar__item, .menu__item, .sheet__handle, .dropdown__trigger, .track__step, .pager__at {
12
+ .segment__item, .day, .calendar__day, .mood__opt, .pin__cell, .slider, .picker__item, .float-bar__item, .menu__item, .sheet__handle, .track__step, .pager__at {
13
13
  user-select: none;
14
14
  -webkit-user-select: none;
15
15
  -webkit-touch-callout: none;
@@ -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: 0.7; } /* 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; }