@usableapp/cardds 0.7.94 → 0.7.96

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 +76 -0
  2. package/README.md +110 -11
  3. package/bin/cardds.mjs +1 -1
  4. package/css/actions.css +9 -12
  5. package/css/base.css +3 -6
  6. package/css/card.css +11 -17
  7. package/css/choice.css +14 -11
  8. package/css/finish.css +4 -2
  9. package/css/journey.css +7 -4
  10. package/css/layover.css +16 -11
  11. package/css/lists.css +4 -9
  12. package/css/media.css +20 -16
  13. package/css/numbers.css +16 -18
  14. package/css/palette.css +79 -97
  15. package/css/people.css +5 -4
  16. package/css/post.css +2 -1
  17. package/css/sheet.css +13 -10
  18. package/css/sticky.css +6 -5
  19. package/css/theme.template.css +16 -7
  20. package/css/tokens.css +69 -35
  21. package/dist/actions/Chip.d.ts +1 -1
  22. package/dist/actions/Chip.js +1 -1
  23. package/dist/actions/Dropdown.d.ts +1 -1
  24. package/dist/actions/Dropdown.js +1 -1
  25. package/dist/cardds.css +280 -260
  26. package/dist/cards/cardGrounds.d.ts +22 -0
  27. package/dist/cards/cardGrounds.js +84 -0
  28. package/dist/cards/inkGrounds.d.ts +34 -0
  29. package/dist/cards/inkGrounds.js +181 -0
  30. package/dist/catalog.json +1 -1
  31. package/dist/choice/Calendar.d.ts +1 -1
  32. package/dist/choice/Calendar.js +1 -1
  33. package/dist/choice/Check.d.ts +2 -2
  34. package/dist/choice/Check.js +2 -2
  35. package/dist/choice/DayStrip.d.ts +1 -1
  36. package/dist/choice/DayStrip.js +1 -1
  37. package/dist/choice/Mood.d.ts +1 -1
  38. package/dist/choice/Mood.js +1 -1
  39. package/dist/choice/Slider.d.ts +1 -1
  40. package/dist/choice/Slider.js +1 -1
  41. package/dist/forms/AddRow.d.ts +1 -1
  42. package/dist/forms/AddRow.js +1 -1
  43. package/dist/forms/Composer.d.ts +1 -1
  44. package/dist/forms/Composer.js +1 -1
  45. package/dist/forms/Field.d.ts +4 -2
  46. package/dist/forms/Field.js +4 -2
  47. package/dist/forms/FileBtn.d.ts +1 -1
  48. package/dist/forms/FileBtn.js +1 -1
  49. package/dist/forms/Pin.d.ts +1 -1
  50. package/dist/forms/Pin.js +1 -1
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.js +2 -0
  53. package/dist/media/MapPick.d.ts +1 -1
  54. package/dist/media/MapPick.js +1 -1
  55. package/dist/media/Tile.d.ts +1 -1
  56. package/dist/media/Tile.js +1 -1
  57. package/dist/numbers/Picker.d.ts +1 -1
  58. package/dist/numbers/Picker.js +1 -1
  59. package/dist/people/AvatarPick.d.ts +2 -2
  60. package/dist/people/AvatarPick.js +1 -1
  61. package/lib/guide.mjs +1 -1
  62. package/lint/rules.mjs +37 -0
  63. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,82 @@
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.96 — 2026-10-18
6
+
7
+ From timebank's help sheet (Lh 2026-10-18): "the card's colour is no different from the background at all". A card is one boundary; a boundary the member cannot see is no boundary (WCAG 2.2 SC 1.4.11, 3:1). No prop changes. Two looks change on existing screens: every card in coloured mode and under the `lit` and `sticker` finishes now has a visible edge, and a `Tile stat` takes the ink of the card it stands on.
8
+
9
+ Then the ink (Lh 2026-10-18). On `Tile · StatOnEachTone` at tone 3: "Text on a dark background like this must be light or white". The model, ruled the same day: "there are text colours ONLIGHT and ONDARK. When the ground is light, use onlight; when the ground is dark, use ondark. In future, with a dark mode, text uses ondark too, without changing the way of thinking", and "the onlight and ondark principle applies to other glyphs too". **Ink follows its ground** (README): two ink sets per palette, every ground declares only its side, and every word and glyph on it follows. Measured by the new check on every gallery story × 7 palettes × (as written + tones 1–5): 15,137 pieces under AA at 0a36c7a → 0, and 0 in a dark mode made of sides alone. **A theme that set a per-tone ink token must move to the two sets** (Removed, below). A theme that sets only the five `--color-*` bases (timebank) changes nothing: the sets derive from `--color-ink` and `--color-on-brand`.
10
+
11
+ ### Added
12
+ - **`cardGrounds()`: can the member see each card on your theme?** Before → after: nothing measured a card against its ground, and the README said cardds made no contrast check → a function the package exports builds a card of each tone (1–5) on the screen, in a sheet's body and in a `paper` sheet's body, where your theme is loaded, and returns per pair the fill's contrast, the edge's, `pass` (one of them 3:1) and a `problem` that names the tokens and the shortfall. It changes nothing; the theme's values stay yours. Run it on a dev page (`console.table(cardGrounds())`) or in a Playwright test (`await page.evaluate(cardGrounds)`). README "Can the member see the card?".
13
+ - **Story `Tile · StatOnEachTone`**: two stat tiles on each tone.
14
+ - **Two ink sets.** `--ink-onlight` and `--ink-onlight-muted` (words and glyphs on a light ground), `--ink-ondark` and `--ink-ondark-muted` (on a dark one). Each holds 4.5:1 on every ground of its side. Unset, they derive from `--color-ink` and `--color-on-brand`; the wireframe's are #141414 / #575757 and #ffffff / #a8a8a8.
15
+ - **Sides.** `--surface-dark`, `--card-1-dark` … `--card-5-dark`, `--accent-dark`, `--action-dark`, `--neg-dark`: `1` = a dark ground, `0` = a light one. The defaults: the screen, tones 1, 2, 4 and the paper light; tone 3, tone 5, the brand fill and a falling Delta dark. orchard and vers declare their tone 5 light. A surface sets `--dark` from its fill's side; the surface list in tokens.css gives it `--card-ink`, `--card-muted`, `--on-accent`, `--accent-ink` (the brand as a glyph: itself on a light ground, ondark on a dark one) and the four signals.
16
+ - **Signals by side.** `--signal-{ok,warn,error,info}-onlight` (the shade on a light ground, 4.5:1 on every one) and `--signal-…-ondark` (the ondark ink unless a palette gives its own: orchard does). `--signal-ok` etc. are now what a piece reads, derived on each surface.
17
+ - **Dark mode falls out.** Turn the light sides to 1 and darken their fills, in a block of your own (README "Ink follows its ground"); no rule per piece. `tests/ink.spec.js` holds the gallery to that block: 0 short.
18
+ - **`inkGrounds()`: can the member read every word and glyph on your theme?** A function the package exports walks every visible word, field value, `svg.icon` and glyph drawn as a fill (a signal dot, a day's dot, the pager's dots, a check's tick, a switch's knob, the strong calendar mark's ring) inside a root, finds the ground under it (every fill, through every opacity) and returns `{ piece, surface, side, set, kind, ink, ground, ratio, needs, pass, problem }`. `side` is what the ground declares, `set` the ink set the colour is; `problem` names both, e.g. `… on .card.card--3 (light side): #575757 (--card-muted = --ink-onlight-muted) on #141414 (--card-bg) is 2.55:1, words need 4.5:1`. Words 4.5:1; large words, icons and glyphs 3:1. It changes nothing. `console.table(inkGrounds().filter((r) => !r.pass))` or `await page.evaluate(inkGrounds)`. `tests/ink.spec.js` runs it over every story in every stock palette and in dark mode, in `npm run ci`.
19
+ - **Knobs.** `--sticky-dark` (a note's side, 0), `--bubble-meta-icon-ink`, plus the knobs #35 added for muted pieces: `--day-rest-ink`, `--segment-rest-ink`, `--float-bar-item-rest-ink`, `--picker-rest-ink`, `--tile-sub-ink`, `--tile-badge-label-ink`, `--callout-sub-ink`, `--snackbar-caption-ink`, `--modal-drawer-caption-ink`, `--route-via-ink`, `--sheet-step-summary-ink`, `--sticky-heading-caption-ink`, `--calendar-day-mark2-shadow`, `--sheet-stage-foot-bg` (docs/knobs.md).
20
+
21
+ ### Removed (before → after, for a theme that set them)
22
+ - `--card-1-ink`, `--card-2-ink`, `--card-4-ink` (and `--color-ink` where it meant them) → `--ink-onlight`: one ink for every light ground.
23
+ - `--card-3-ink`, `--card-5-ink` → `--ink-ondark`.
24
+ - `--card-muted` (on `:root`, the older name), `--card-1-muted`, `--card-2-muted`, `--card-4-muted` → `--ink-onlight-muted`. `--card-muted` stays as what a piece reads on its surface; set on `:root` it no longer retunes anything.
25
+ - `--card-3-muted`, `--card-5-muted` → `--ink-ondark-muted`.
26
+ - A tone whose words were set the other way (orchard's and vers's light tone 5) → `--card-5-dark: 0`.
27
+ - `--on-surface`, `--on-surface-muted`, `--on-accent`, `--action-ink`, `--action-muted` set directly → the screen's, the brand fill's and the paper's side (`--surface-dark`, `--accent-dark`, `--action-dark`). The names stay readable; they now derive.
28
+ - `--signal-ok`, `--signal-warn`, `--signal-error`, `--signal-info` set on `:root` → `--signal-…-onlight` (and `--signal-…-ondark`).
29
+ - `--dim` and `--rest` (#35) → the muted shade of the side; `--faint` → a pager dot at rest is the muted ink, a solid shade.
30
+
31
+ ### Changed
32
+ - **A card keeps an edge in coloured mode.** Before → after: `[data-palette]` set `--card-border: transparent`, so a card's only boundary was its fill and a 6–8% shadow: in every stock palette tones 1, 2 and 4 (and 5 in orchard and vers) sat under 3:1 on the screen, and a tone-1 card in a tone-1 sheet was 1:1 → `--card-border` is `--on-surface-muted` (the ground's muted words) under any `data-palette`, a 1.5px line (`--border-w`). Every tone on every ground now holds 3:1 in every stock palette and finish (525 pairs, `tests/card-grounds.spec.js`). The shadow stays. A project that wants its own edge sets `--card-border` in `theme.css` and runs `cardGrounds()`; the old look is `--card-border: transparent`, which the check will name.
33
+ - **The `lit` and `sticker` finishes keep the card's edge.** Before → after: every finish set `--card-border: transparent`; lit's 7% ring and sticker's white cut measured 1.15–1.41:1 on a light ground → only `--action-border` steps back under a finish; `print` and `ink` draw their own line as before.
34
+ - **`Tile stat` takes the ink of what it stands on** (timebank: in `Card tone={3}` the value and icon were rgb(42,36,28) on rgb(92,107,71)). Before → after: the stat tile drops the plain tile's fill (`--action-bg`) but kept its ink (`--action-ink`), dark on a dark card → its ink is inherited, as the card's heading's; `--tile-ink` still overrides. Its sub line is the card's muted words (`--card-muted`, which each tone holds to AA) instead of the full ink at `--dim`, which was 3.3:1 on clay's terracotta.
35
+ - **Every piece reads its ink from its ground's side.** Before → after (since 0a36c7a): `.card--3` set only its ink and the accent pair; a hand list in card.css faded ten named pieces by `opacity: var(--dim)`; six `.card--3 .x` patches in lists.css; per-tone ink tokens chosen one by one → a ground declares its side and the inks follow (the surface list in tokens.css: the screen, every card tone, a sheet, a tile, a chip, a switch, the drawer, a cover, each Postcard face, a note, the post paper, the float bar's pill, the snackbar, a Delta, the track and its steps, a tile badge, an avatar, a bubble, the map's hint). A piece with no fill that stands in for one takes its ground's side (`--dark: inherit`: a stat tile, an upload tile, a plain tile badge, an outline avatar, a light drawer).
36
+ - **A muted word or glyph is a shade, never an opacity.** Unpicked days, segments, picker values, float-bar items, a tile's sub line, captions: the side's muted ink. The pager's dots at rest were 30% of the ink (1.5:1) → the muted ink; the one where you are, the full ink (it was the accent, 1.6:1 on the wireframe's tone 5). A switch's knob at rest was 45% of the ink (2.4:1, and white on the paper switch on a dark card) → the paper side's muted ink.
37
+ - **One muted shade per side.** Wireframe: tone 1's #6b6b6b, tone 2's #5f5f5f, tone 4's #555555 and a note's 68% of the ink → #575757 (4.6:1 on the water note, the darkest light ground); tone 3's #b9b9b9 and tone 5's #c4c4c4 → #a8a8a8 (4.6:1 on tone 5). Each stock palette's sets are in palette.css with the ground that bounds them.
38
+ - **Each tone's words are its side's ink.** orchard's per-tone inks (sand #33291c, sage #2b3322, clay #3a2a1c) → its onlight umber #2a241c; clay's terracotta and mustard muted → #2a2723 (the terracotta is a middle ground: no lighter muted holds 4.5:1 on it). On the brand card the inverted primary button's words are onlight (they were the brand colour).
39
+ - **A chip is a ground of the paper's side.** A signal dot on a chip on a dark card took the card's ondark signal (1:1 on the white chip) → onlight. A picked chip or tile is a ground of the accent's side.
40
+ - **The track** lays down the accent fill of where it stands and is that fill's side; its steps re-point the accent (the `.card--3 .track` rule is gone).
41
+ - **Signals.** On a light ground, the onlight shade: wireframe ok #2b7a2f → #27692a, warn #7f6400 → #705903, error #c62828 → #af2525, info #2f5f9e → #2e5c98 (each 4.5:1 on tone 4's grey; tones 2 and 4 used to take the card's ink); orchard ok #497645 → #3e5a37, warn #8d641b → #6b4e1b, error #b04e39 → #82402f, info #4c6e95 → #40556c; vers ok #477242 → #3b5734, warn #856122 → #654b1e, error #a84a37 → #803c2d, info #49698f → #3c5169; clay ok #205622 → #1b2c18, warn #5d4a00 → #2f280d, error #931e1e → #471a17, info #264c7f → #1d2938 (4.5:1 on the terracotta: nearly the ink). On a dark ground, the ondark ink; orchard's own light shades (#f9ccc0, #f2d28b, #cfe6b8, #c5d7ef, 4.5:1 on the moss) now hold on every dark ground of orchard, not only tone 3.
42
+ - **Stock palette fills**, chosen so each side's ink holds 4.5:1 on the fill, on the fill's own hue (OKLCH, lightness only), and so a tone's ink holds on its 12% tint too (a banner, a step bar, today's day carry words there):
43
+
44
+ | palette | fill | before #35 | #35 | now | its ink | ink on it, before → now | on its 12% tint now |
45
+ |---|---|---|---|---|---|---|---|
46
+ | roast | tone 3 · --accent | #b97e3c | #805729 | #8c5402 | #fdf9f1 | 3.27 → 5.91 | 4.59 |
47
+ | hydro | tone 3 · --accent | #3d63e0 | #385bce | #365bd7 | #ffffff | 5.17 → 5.78 | 4.55 |
48
+ | orchard | tone 3 · --accent | #5c6b47 | #525f3f | #52613d | #f6f1e6 | 5.11 → 5.94 | 4.55 |
49
+ | vers | tone 3 · --accent | #678d58 | #4d6a42 | #496d3a | #ffffff | 3.8 → 5.95 | 4.55 |
50
+ | clay | tone 2 (light) | #e05c3a | #e36d4f | #eb6644 | #191713 | 4.91 → 5.55 | 4.56 |
51
+ | orchard | --highlight-1 | #d93a1f | #d1381e | #d03114 | #f6f1e6 | 4.07 → 4.51 | no words on a tint |
52
+ | orchard | --highlight-4 | #e0457b | #cd3f71 | #ca2e69 | #f6f1e6 | 3.51 → 4.54 | no words on a tint |
53
+ | orchard | --neg | #c4694f | #a25741 | #ad553b | #f6f1e6 | 3.4 → 4.5 | no words on a tint |
54
+ | vers | --neg | #dd9787 | #a84a37 | #a76557 | #ffffff | 2.37 → 4.52 | no words on a tint |
55
+
56
+ hydro's ondark ink #f2f6ff → #ffffff (#35). The highlight and Delta fills carry no tint, so they hold the fill alone. A palette's `--card-border` is still `--on-surface-muted`: now the onlight muted ink, a step darker.
57
+
58
+ ### Fixed
59
+ - **`pager-forward-primary` reads a conditional forward slot as one** (reported by timebank 2026-10-18, poc/help-flow phone.tsx). Before → after: `<Btn>Back</Btn>{last ? <Btn primary>Send</Btn> : <Btn primary>When</Btn>}` was flagged, the first branch counted as a back button → the two branches of a conditional are one slot, and both are the forward Btn that must be `primary`.
60
+
61
+ ### Known, not changed
62
+ - `inkGrounds()` reads ancestors only: words over a scrim drawn by `::after`, or over a sibling that is positioned under them, are measured against the ancestors' fills. A `url()` picture is skipped; a gradient is read as decoration unless it is one colour.
63
+ - A `Delta` pill (bg `--card-3-bg`) has no visible boundary on the dark card (its words pass); a mark, not words, so the ink check does not read it.
64
+ - A calendar `mark` is a 12% accent tint: the day's number reads on it, but the mark itself is not a 3:1 glyph (`mark2` adds a 3:1 ring). Decoration colours (`--dot-a`, `--dot-b`, the highlights) are not inks and are not measured: a brand-coloured legend dot vanishes on the brand card.
65
+ - The wireframe's card edge (#141414) and its brand fill are near-black: in a dark mode they need their own colours to keep a card's boundary (cardGrounds()); the ink holds without them.
66
+
67
+ ## 0.7.95 — 2026-10-18
68
+
69
+ From timebank's help-ask prototype (Lh 2026-10-18): its Composer, Dropdown and map stood on white cards inside the help sheet. Ruled: the sheet is the paper the member fills in.
70
+
71
+ Nothing in the package's look or behaviour changes: no component, class or knob is added or removed. Two new checks may fail a project's `cardds check`: a control in a card, and a worded pager whose forward button is not the primary one.
72
+
73
+ ### Added
74
+ - **`cardds check` rule `field-in-card` (error): the sheet is the paper the member fills in** (Lh 2026-10-18). Before → after: "Fields belong in a Sheet" was prose in `Field`'s doc, and nothing stopped a form on a card → a control the member fills in or picks, written inside a `<Card>`, is an error that names `Sheet`'s `SheetBody`, `SheetStep` and the stack sheet as its place. The controls: `Field`, `Composer`, `ComposerArea`, `Pin`, `Dropdown`, `Slider`, `Toggle`, `Check`, `ChipPick`, `Calendar`, `DayStrip`, `Day`, `Picker`, `Mood`, `AvatarPick`, `FileBtn`, `AddRow`, `MapPick`, a `Tile pick` and a `Tile upload`. A plain or `stat` Tile, a `Btn`, `IconBtn` or `Link`, and a raw `<input>` (the pickable row card's radio) are not flagged. A `Card` that is a sheet of a `SheetStack` is that stack's sheet and may hold them. To pass: move the controls onto the sheet's body (`<Sheet><CardHead …/><SheetBody>…controls…</SheetBody><CardFoot>…</CardFoot></Sheet>`), with no card round them. The ESLint plugin reports it under `cardds/check`.
75
+ - **`cardds check` rule `pager-forward-primary` (error).** Before → after: a worded pager could carry back and next with the same signal (Lh saw it on a timebank screen) → in an `ActionBar variant="pager"` or an `ActionBarTier pager` with two or more worded `Btn`s, the last (forward) one must be `primary` and the others not. The round pager (`IconBtn`s) is not checked.
76
+
77
+ ### Changed
78
+ - **Every control's doc says where it lives: on a sheet, never in a Card.** `Field`'s doc comment now leads with it, so `cardds get Field` and `cardds find "where does a form field go"` answer with the rule. README's sheet rules gain "The sheet is the paper the member fills in".
79
+ - **The gallery's stories put their controls on sheets.** 60 stories in 19 files moved from a card onto a sheet: a `Step` with a `Page` and a `Sheet` where the story is a step of a flow, a bare `SheetStage` with a `Sheet` for a part shown on its own. Story ids are unchanged. Two samples that stood in two cards of one story now stand on one sheet.
80
+
5
81
  ## 0.7.94 — 2026-10-18
6
82
 
7
83
  From timebank's help-ask sheet (Lh 2026-10-17). One look changes on existing screens: a row card's corner (41.6 → 13). Everything else is added, and a screen that does not use it is as it was.
package/README.md CHANGED
@@ -9,6 +9,9 @@
9
9
  > The other docs only point here: `CLAUDE.md` (AI working IN this repo — decisions and gotchas),
10
10
  > `docs/guides/building-with-cardds.md` (AI composing screens in React; shipped to Claude Design),
11
11
  > the `/cardds` skill (AI in another project — install + contract). To LOOK at the system: `npm run dev`.
12
+ >
13
+ > **Accessibility:** [Ink follows its ground](#ink-follows-its-ground). Two ink sets, onlight and ondark; every ground declares
14
+ > only its side, and every word and glyph on it follows. `inkGrounds()` checks your theme.
12
15
 
13
16
  Card-first mobile design system, **React-first**: `src/` is the component
14
17
  library — one thin component per pattern, emitting exactly the markup the CSS
@@ -555,8 +558,21 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
555
558
  - **A bottom sheet holds what the member acts on** — controls, or small
556
559
  content cards to pick from or read more — and it opens to the full height
557
560
  of its box. **Cards are content the system offers**: the words, the
558
- explanation, the thing to read. Do not put a form in a card when a sheet
559
- would do, and do not put the step's explanation in a sheet.
561
+ explanation, the thing to read. Do not put the step's explanation in a
562
+ sheet.
563
+ - **The sheet is the paper the member fills in** (Lh 2026-10-18). Every
564
+ control the member fills in or picks stands on a sheet: a `Sheet`'s
565
+ `SheetBody`, a `SheetStep`, or a stack sheet. That is `Field`, `Composer`
566
+ and `ComposerArea`, `Pin`, `Dropdown`, `Slider`, `Toggle`, `Check`,
567
+ `ChipPick`, `Calendar`, `DayStrip` and `Day`, `Picker`, `Mood`,
568
+ `AvatarPick`, `FileBtn`, `AddRow`, `MapPick`, a `Tile pick` and a
569
+ `Tile upload`. A card is for reading and pressing buttons only; never put
570
+ a form, or one control of it, in a card. `cardds check` rule
571
+ `field-in-card` (error) flags a control written inside a `<Card>`. A `Card`
572
+ that is a sheet of a `SheetStack` is that stack's sheet, so it may hold
573
+ them. Seen in timebank's help-ask prototype: a Composer, a Dropdown and a
574
+ map on white cards inside the help sheet, the cards the colour of the
575
+ ground.
560
576
  - **Two sheet systems, one material.** *Sheet single* (`.sheet`, `sheet.css`)
561
577
  is ONE sheet the member acts in, whatever it holds: a short entry in front
562
578
  of a step's card, a place on a map, a payment on a dim page — in a `.step`
@@ -916,7 +932,7 @@ That file is where a project's look lives. Bases and roles:
916
932
  | 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) |
917
933
  | faces | `--font-ui`, `--font-display` | |
918
934
  | colour | `--color-surface`, `--color-ink`, `--color-paper`, `--color-brand`, `--color-on-brand` — no default: unset = the wireframe | every role: surface / words, the three card tones (paper · the brand washed 14% into the surface · the brand), the muted inks (the ink washed into their ground), accent, neutral actions, `--dot-a` |
919
- | colour roles (the escape hatch) | `--surface`, `--on-surface(-muted)`, `--card-{1,2,3}-{bg,ink}`, `--card-border`, `--card-muted`, `--card-2-muted`, `--accent`, `--on-accent`, `--action-{bg,ink,border}`, `--pos`, `--neg`, `--dot-{a,b}`, `--signal-{ok,warn,error,info}` | a role set directly wins over its derivation; the tints derive from the roles |
935
+ | colour roles (the escape hatch) | the ink sets `--ink-{onlight,ondark}(-muted)`; the sides `--surface-dark`, `--card-{1,2,3,4,5}-dark`, `--accent-dark`, `--action-dark`, `--neg-dark`; `--surface`, `--card-{1,2,3,4,5}-bg`, `--card-border`, `--accent`, `--action-{bg,border}`, `--pos`, `--neg`, `--dot-{a,b}`, `--signal-{ok,warn,error,info}-{onlight,ondark}` | a role set directly wins over its derivation; the tints derive from the roles |
920
936
 
921
937
  Rules:
922
938
 
@@ -936,9 +952,13 @@ Rules:
936
952
  trades the wireframe's borders for the card's shadow and the controls' soft
937
953
  edge (`palette.css`, `forms.css`). Colours of your own → `<html data-palette="own">`.
938
954
  Without it the colours still apply, on wireframe borders.
939
- - **cardds does not judge the colours.** No contrast check, no warning — a
940
- theme's values are the project's call. The derivations are only starting points;
941
- any role can be set outright.
955
+ - **cardds does not pick the colours; it measures two things about them.** A theme's
956
+ values are the project's call and the derivations are only starting points; any
957
+ role can be set outright. What cardds checks is that the member can see each card
958
+ ([`cardGrounds()`](#can-the-member-see-the-card) names a tone whose fill and edge both
959
+ sit under 3:1 on its ground) and read what stands on it
960
+ ([`inkGrounds()`](#ink-follows-its-ground) names a word or icon under 4.5:1 or 3:1).
961
+ Both name the tokens to move.
942
962
  - The member's text-size setting is the same `--fs-base`, written on `<html>`
943
963
  at run time by `setTextScale(percent)` (a share of the theme's base; 100 = the
944
964
  theme's own): the words grow, and the things grow HALF as much (`--tap-base`; `controlsFor(percent)` — 130% text → 115% controls, 160% → 130%), so a larger text still fits what holds it. (`--text-scale`, which multiplied the root and zoomed the UI too, is gone.)
@@ -949,16 +969,95 @@ palette `own` shows a colour well per colour base.
949
969
  template to the defaults; the step / centre geometry runs at two extreme
950
970
  themes (`tight`, `loose`).
951
971
 
972
+ ## Ink follows its ground
973
+
974
+ The accessibility rule for colour (Lh 2026-10-18): "there are text colours ONLIGHT and ONDARK. When the ground is light, use
975
+ onlight; when the ground is dark, use ondark." It holds for words and for glyphs: icons, marks and dots that carry meaning, a
976
+ check's tick, a switch's knob, the pager's dots.
977
+
978
+ 1. **Two ink sets.** A palette (or your theme) sets `--ink-onlight` and `--ink-onlight-muted`, `--ink-ondark` and
979
+ `--ink-ondark-muted`. Each holds 4.5:1 on every ground of its side.
980
+ 2. **Every ground says only its side.** `--surface-dark`, `--card-1-dark` … `--card-5-dark`, `--accent-dark` (the brand fill),
981
+ `--action-dark` (the paper of buttons, chips and fields), `--neg-dark`: `1` is dark, `0` light. A surface turns its side into
982
+ the inks every piece on it reads: `--card-ink`, `--card-muted`, `--on-accent`, `--accent-ink` and the four signals.
983
+ 3. **A piece reads its ink only from the ground it stands on** (`currentColor`, `--card-ink`, `--card-muted`). It never names a
984
+ fixed colour, and a muted word or glyph is a shade, never an opacity.
985
+ 4. **4.5:1 for words, 3:1 for large words (24px, or 18.66px bold), icons and glyphs** (WCAG 2.2 SC 1.4.3, 1.4.11), on the ground
986
+ the piece is really drawn on. A tint that carries words (a banner, a step bar, today's day) is a ground of its card's side, so
987
+ a dark fill holds its ondark ink on its 12% tint too.
988
+ 5. **Signals** have an onlight and an ondark shade (`--signal-ok-onlight`, `--signal-ok-ondark`, …): no one shade of a hue holds
989
+ 4.5:1 on both. The ondark one is the ondark ink unless a palette gives its own. The icon and the words carry the meaning.
990
+
991
+ **Dark mode** is the sides turned to `1` with their fills darkened, and no rule per piece:
992
+
993
+ ```css
994
+ :root[data-mode="dark"] {
995
+ --surface-dark: 1; --card-1-dark: 1; --card-2-dark: 1; --card-4-dark: 1; --action-dark: 1;
996
+ --surface: #121212; --color-paper: #1e1e1e; --card-1-bg: #1e1e1e; --card-2-bg: #2a2a2a; --card-4-bg: #333333; --action-bg: #262626;
997
+ }
998
+ ```
999
+
1000
+ `tests/ink.spec.js` holds the gallery to exactly that block: every story, every tone, 0 pieces short.
1001
+
1002
+ Check your own theme where it is loaded. `inkGrounds()` walks every visible word, field value, icon and glyph inside a root and
1003
+ returns one row per piece, with the side its ground declares and the ink set its colour is:
1004
+
1005
+ ```ts
1006
+ import { inkGrounds } from '@usableapp/cardds';
1007
+ console.table(inkGrounds().filter((r) => !r.pass)); // a dev page
1008
+
1009
+ // a Playwright test on your app (Playwright sends the function to the page)
1010
+ const short = (await page.evaluate(inkGrounds)).filter((r) => !r.pass).map((r) => r.problem);
1011
+ expect(short).toEqual([]);
1012
+ // '.t-caption.t-muted "since May" on .card.card--3 (light side): #575757 (--card-muted = --ink-onlight-muted) on #141414 (--card-bg) is 2.55:1, words need 4.5:1'
1013
+ ```
1014
+
1015
+ A side and an ink set that disagree with the ground (`light side` on a dark fill) say the ground declared the wrong side. It
1016
+ reports and changes nothing. It does not measure a piece on a picture (a `url()` ground; a gradient counts as decoration unless it
1017
+ is one colour), a disabled control, a hidden one, words drawn by CSS `content`, decoration colours (`--dot-a`, `--dot-b`, the
1018
+ highlights), or what lies under a positioned element that is not its ancestor (a scrim drawn by `::after`). cardds holds itself to
1019
+ the same function: `tests/ink.spec.js` renders every gallery story in every stock palette and in the dark mode above, as written
1020
+ and with every card turned to each tone 1–5, about 27,000 pieces a pass, in `npm run ci`.
1021
+
1022
+ ## Can the member see the card?
1023
+
1024
+ A card is one boundary, and a boundary the member cannot see is no boundary (WCAG 2.2 SC 1.4.11, non-text contrast: 3:1 against what is
1025
+ next to it). A card's boundary is its fill against the ground it stands on, or its edge (`--card-border`, or a finish's line).
1026
+ In coloured mode the edge is `--on-surface-muted`, the ground's muted words, which a palette holds to AA on its surface; the wireframe's
1027
+ is its black line. A light card on a light ground cannot reach 3:1 by its fill (a tone-1 card in a tone-1 sheet is 1:1), so the edge
1028
+ carries it.
1029
+
1030
+ Your theme decides the colours, so run the check on your theme. `cardGrounds()` builds a card of each tone (1–5) on each ground a card
1031
+ stands on (the screen, a sheet's body, a `paper` sheet's body) where your theme is loaded, reads what the browser draws and returns one
1032
+ row per pair:
1033
+
1034
+ ```ts
1035
+ import { cardGrounds } from '@usableapp/cardds';
1036
+ console.table(cardGrounds()); // a dev page: ground · tone · fill · edge · pass · problem
1037
+
1038
+ // a Playwright test on your app (Playwright sends the function to the page)
1039
+ const rows = await page.evaluate(cardGrounds);
1040
+ expect(rows.filter((r) => !r.pass).map((r) => r.problem)).toEqual([]);
1041
+ ```
1042
+
1043
+ A failing row names the tokens and the shortfall, for example
1044
+ `tone 1 card on the screen: fill 1:1 (--card-1-bg #ffffff on --surface #ffffff), edge 1:1 (--card-border); one of them needs 3:1 — the fill is short by 2, or give --card-border a colour of 3:1 on #ffffff`.
1045
+ It reports and changes nothing: which knob you move is your call. It runs in a browser on purpose: your theme's `var()` aliases,
1046
+ `[data-palette]` blocks, finish and `color-mix()` are resolved by the same engine that draws the screen. A stock palette is held to the
1047
+ same check in every finish (`tests/card-grounds.spec.js`).
1048
+
952
1049
  ## New palette
953
1050
 
954
1051
  Copy any block in `palette.css`, rename, retune:
955
1052
 
956
1053
  ```css
957
1054
  [data-palette="mine"] {
958
- --surface: …; --on-surface: …;
959
- --card-1-bg: …; --card-1-ink: …; /* + card-2, card-3 */
960
- --accent: …; --on-accent: …;
961
- --action-bg: …; --action-ink: …;
1055
+ --surface: …;
1056
+ --ink-onlight: …; --ink-onlight-muted: …; /* words and glyphs on a light ground */
1057
+ --ink-ondark: …; --ink-ondark-muted: …; /* on a dark ground */
1058
+ --card-1-bg: …; /* + card-2 … card-5; a tone whose side is not the default's says so: --card-5-dark: 0; */
1059
+ --accent: …;
1060
+ --action-bg: …;
962
1061
  }
963
1062
  ```
964
1063
 
@@ -979,7 +1078,7 @@ Four values, one attribute (`css/finish.css`; the header names each and its knob
979
1078
  | `sticker` | a die-cut sticker: a white cut line, a short hard drop, a lone card leans (never one in a stack, a list or a feed), buttons with a bottom edge that press down | `--sticker-cut` (5px) · `--sticker-tilt` (0.8deg; `0deg` = straight) |
980
1079
  | `ink` | a poster / comic print: a thick line in the surface's ink round every boundary and control, a hard offset in the same ink | `--ink-w` (2.5px) · `--ink-off` (5px) |
981
1080
 
982
- Under every finish the wireframe's two borders step back (`--card-border`, `--action-border`; ink puts its own line there), a card in a closed stack's base stays flat, and the information inside a card stays flat — only a container or a control gets an edge. Colours are mixes of the roles, so a finish sits in any palette. The gallery's nav has a finish switch (key `f`); `Card · Finishes` shows the four side by side, `Home · Finish` a home under `print`.
1081
+ Under every finish the controls' wireframe border steps back (`--action-border`; ink puts its own line there) and the card keeps its edge (`--card-border`: lit's ring and sticker's cut do not reach 3:1 on a light ground, see [Can the member see the card?](#can-the-member-see-the-card)), a card in a closed stack's base stays flat, and the information inside a card stays flat — only a container or a control gets an edge. Colours are mixes of the roles, so a finish sits in any palette. The gallery's nav has a finish switch (key `f`); `Card · Finishes` shows the four side by side, `Home · Finish` a home under `print`.
983
1082
 
984
1083
  ## Scale: everything is rem
985
1084
 
package/bin/cardds.mjs CHANGED
@@ -84,7 +84,7 @@ import them, never copy their markup: the behaviour lives in the component. Gall
84
84
 
85
85
  // the exports beside the components that are not components (docs/roles.md rows `a function, not a component`…): shape `—` and nothing to tell them from a structural part, so they are named.
86
86
  // A new one joins here and in the list test of tests/catalog.spec.js, or it is listed as structure
87
- const HELPERS = new Set(['applyMask', 'slideTo', 'slideScreen', 'installScreenSlide', 'useLayer', 'useLayers', 'useOptionalLayer', 'registerIcons', 'LayerHistory', 'setWords', 'word', 'setTextScale', 'controlsFor']);
87
+ const HELPERS = new Set(['applyMask', 'cardGrounds', 'inkGrounds', 'slideTo', 'slideScreen', 'installScreenSlide', 'useLayer', 'useLayers', 'useOptionalLayer', 'registerIcons', 'LayerHistory', 'setWords', 'word', 'setTextScale', 'controlsFor']);
88
88
  const one = (text) => text.replace(/\s+/g, ' ').trim();
89
89
  const indent = (text, by = ' ') => text.replace(/^(?=.)/gm, by);
90
90
 
package/css/actions.css CHANGED
@@ -178,7 +178,8 @@
178
178
  toggles, tiles and the dropdown already use. */
179
179
  border: var(--border-w) solid var(--chip-border, var(--outline));
180
180
  background: var(--chip-bg, var(--action-bg));
181
- color: var(--chip-ink, var(--action-ink));
181
+ --dark: var(--action-dark); /* a chip is a surface of the paper's side: a signal dot on it reads onlight, on any card */
182
+ color: var(--chip-ink, var(--card-ink));
182
183
  padding: var(--chip-pad-y, var(--sp-2)) var(--chip-pad-x, var(--sp-3));
183
184
  font: var(--type-label);
184
185
  font-size: var(--chip-font-size, var(--fs-sm));
@@ -197,10 +198,9 @@
197
198
  .dot--info { background: var(--signal-info); }
198
199
  /* --sm: a small LABEL (a name on a note, a tag in a tight head) — low and tight; never a tap target */
199
200
  .chip--sm { min-height: 0; gap: var(--chip-sm-gap, var(--sp-1)); padding: var(--chip-sm-pad-y, var(--sp-0)) var(--chip-sm-pad-x, var(--sp-2)); }
200
- .chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
201
+ .chip--on { background: var(--chip-on-bg, var(--accent)); --dark: var(--accent-dark); color: var(--chip-on-ink, var(--card-ink)); border-color: transparent; }
201
202
  /* muted: an untouched default — the words wash out, the edge stays; emphasis is the difference from the neighbours (README 13) */
202
- .chip--muted { color: var(--chip-muted-ink, var(--card-muted)); }
203
- /* dark-slot inversion lives in card.css (.card--3 :is(...)) */
203
+ .chip--muted { color: var(--chip-muted-ink, var(--card-muted)); } /* the chip's own fill's muted words: a muted chip on the dark card is still a light chip */
204
204
 
205
205
  .chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
206
206
  .chip-row.bleed { flex-wrap: nowrap; } /* actions.css loads after card.css — the strip stays one line */
@@ -230,7 +230,7 @@
230
230
  text-align: left;
231
231
  }
232
232
  /* muted: the value is still the default (README 13) */
233
- .dropdown--muted .dropdown__btn { color: var(--dropdown-muted-ink, var(--card-muted)); }
233
+ .dropdown--muted .dropdown__btn { color: var(--dropdown-muted-ink, var(--action-muted)); }
234
234
  .dropdown__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
235
235
  .dropdown__btn > .icon { transition: rotate var(--motion-quick) ease; }
236
236
  .dropdown:has(.menu:popover-open) .dropdown__btn > .icon { rotate: 180deg; }
@@ -242,7 +242,7 @@
242
242
  /* ---- segment (day picker / tab strip) — items share width, shrink on small screens ---- */
243
243
  .segment { display: flex; gap: var(--sp-1); }
244
244
  .segment__item {
245
- opacity: var(--rest);
245
+ color: var(--segment-rest-ink, var(--card-muted)); /* unpicked: the ground's muted words */
246
246
  border-radius: var(--r-chip);
247
247
  padding: var(--sp-2) var(--sp-1);
248
248
  font: var(--type-label);
@@ -254,7 +254,6 @@
254
254
  .segment__item[aria-selected="true"] {
255
255
  background: var(--accent);
256
256
  color: var(--on-accent);
257
- opacity: 1;
258
257
  }
259
258
 
260
259
  /* ---- bottom CTA pack: the screen's bottom (Screen `bottom`) ----
@@ -325,10 +324,9 @@
325
324
  width: var(--dot);
326
325
  height: var(--dot);
327
326
  border-radius: 50%;
328
- background: currentColor;
329
- opacity: var(--faint);
327
+ background: var(--card-muted); /* a glyph: the dots at rest are the ground's muted ink (an opacity of 0.3 was 1.5:1), the one where you are its full ink */
330
328
  }
331
- .pager__at > .is-at { background: var(--accent); opacity: 1; }
329
+ .pager__at > .is-at { background: var(--card-ink); }
332
330
  /* ---- tiers: a control row above the buttons ----
333
331
  A screen whose one move comes with a control — the introduction's text-size
334
332
  slider above its pager. Each tier is a row; the bar stacks them, and any
@@ -378,11 +376,10 @@
378
376
  gap: var(--action-note-gap, var(--sp-1));
379
377
  padding-inline: var(--sp-2);
380
378
  font: var(--action-note-font, var(--type-body));
381
- color: var(--action-note-ink, var(--on-surface-muted));
379
+ color: var(--action-note-ink, var(--card-muted));
382
380
  text-align: center;
383
381
  cursor: default;
384
382
  }
385
- .card .action-note { color: var(--action-note-ink, var(--card-muted)); }
386
383
  .action-note__words { display: block; } /* ONE run of words: an icon inside sits in the line, the spaces around it kept */
387
384
  .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) */
388
385
 
package/css/base.css CHANGED
@@ -27,7 +27,7 @@ html { font-size: 100%; }
27
27
  body {
28
28
  margin: 0;
29
29
  background: var(--surface);
30
- color: var(--on-surface);
30
+ color: var(--card-ink);
31
31
  font: var(--type-body);
32
32
  -webkit-font-smoothing: antialiased;
33
33
  }
@@ -103,7 +103,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
103
103
  .screen > * { min-width: 0; } /* content never widens the column past the viewport */
104
104
  /* a screen that carries its own palette (data-palette on the element, not on <html>)
105
105
  paints its own surface, so a design tool can show one screen per palette */
106
- .screen[data-palette] { background: var(--surface); color: var(--on-surface); }
106
+ .screen[data-palette] { background: var(--surface); color: var(--card-ink); }
107
107
 
108
108
  /* app screen: exactly one phone height — a height, not a minimum, so that a
109
109
  .step (step.css) has a bound to fill and its card and sheet scroll INSIDE
@@ -140,9 +140,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
140
140
  .t-stat--mono { font-family: var(--font-mono); letter-spacing: 0; } /* clocks, ETAs, codes */
141
141
  .t-stat--xl { font-size: var(--fs-hero); } /* the one number on a sheet */
142
142
 
143
- .t-muted { color: var(--on-surface-muted); }
144
- .card .t-muted { color: var(--card-muted); } /* muted follows the card's ink, not the surface's */
145
- /* .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 */
143
+ .t-muted { color: var(--card-muted); } /* the muted words of whatever it stands on: the screen, a card of any tone, a drawer */
146
144
  .t-caps { text-transform: uppercase; }
147
145
  /* ---- mark: a highlighter stroke over a word INSIDE running text (a sender's name in a greeting) ----
148
146
  <p class="t-body"><mark class="mark">ธนาคารเวลา</mark> ส่งแสตมป์ดวงแรกให้คุณ</p>
@@ -211,4 +209,3 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
211
209
  .empty-state__action { margin-top: var(--sp-2); }
212
210
  /* inside a card (a card's own "nothing yet"): the card's ink */
213
211
  .card .empty-state { color: var(--card-ink); padding-block: var(--sp-6); }
214
- .card--3 .empty-state { color: inherit; }
package/css/card.css CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  .card {
8
8
  --card-bg: var(--card-1-bg); /* the slot, readable by children (avatar rings, fold) */
9
- --card-ink: var(--card-1-ink);
9
+ --dark: var(--card-1-dark); /* its side: the inks follow (tokens.css) */
10
10
  --card-pad: var(--card-pad-rest); /* published so .bleed can cancel it exactly */
11
11
  --card-gap: var(--card-gap-rest); /* published so a sheet's peek strip (sheet.css) is exact */
12
12
  background: var(--card-bg);
@@ -34,19 +34,13 @@
34
34
  .card--away { translate: 0 var(--_y); }
35
35
  @media (prefers-reduced-motion: reduce) { :is(.card--leaver, .post-desk__card) { transition: none; } }
36
36
 
37
- .card--2 { --card-bg: var(--card-2-bg); --card-ink: var(--card-2-ink); --card-muted: var(--card-2-muted); }
38
- /* dark/accent slot: accent-toned controls would vanish — the slot re-points
39
- the accent tokens, so every accent consumer (btn--primary, chip--on,
40
- check, toggle, ring, day…) inverts without per-component rules */
41
- .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); }
42
- .card--4 { --card-bg: var(--card-4-bg); --card-ink: var(--card-4-ink); --card-muted: var(--card-4-muted); }
43
- .card--5 { --card-bg: var(--card-5-bg); --card-ink: var(--card-5-ink); --card-muted: var(--card-5-muted); }
44
- /* the dark slot's captions/hints/footnotes: --card-muted is tuned for a light card, so these dim the full ink
45
- instead (one rule, matching specificity to what it replaces — .card--3 <space> <one class>) — merged from
46
- base.css/choice.css/media.css/numbers.css/journey.css/people.css (2026-09-27 review). lists.css's .legend
47
- differs (opacity 0.8, not --dim) and stays there. */
48
- .card--3 :is(.t-muted, .calendar__wd, .composer__hint, .quote__by, .ring__label, .band__foot, .picker__unit,
49
- .route__place, .note-row, .avatar-pick__role) { color: inherit; opacity: var(--dim); }
37
+ /* each tone is a fill and a side; its words and glyphs follow from the side (tokens.css) */
38
+ .card--2 { --card-bg: var(--card-2-bg); --dark: var(--card-2-dark); }
39
+ /* the brand slot: accent-toned controls would vanish on it — the slot re-points the accent to its own ink, and that fill's side is
40
+ the other one, so every accent consumer (btn--primary, chip--on, check, toggle, ring, day…) inverts without per-component rules */
41
+ .card--3 { --card-bg: var(--card-3-bg); --dark: var(--card-3-dark); --accent: var(--card-ink); --accent-dark: calc(1 - var(--dark)); }
42
+ .card--4 { --card-bg: var(--card-4-bg); --dark: var(--card-4-dark); }
43
+ .card--5 { --card-bg: var(--card-5-bg); --dark: var(--card-5-dark); }
50
44
 
51
45
  /* header row: title left, floating action right — centered, since the
52
46
  trailing control (chip/badge/icon-btn/link) is almost always taller than
@@ -317,7 +311,6 @@
317
311
  display: grid;
318
312
  place-items: center;
319
313
  font: var(--card-thumb-font, var(--type-label));
320
- opacity: 0.85;
321
314
  }
322
315
 
323
316
  /* ---- card feed: a queue of cards, all in sight — one big, the rest small (Lh 2026-10-09, the whole queue 2026-10-15) ----
@@ -364,9 +357,10 @@
364
357
  padding: var(--delta-pad-y, var(--sp-1)) var(--delta-pad-x, var(--sp-2));
365
358
  border-radius: var(--delta-radius, var(--r-chip));
366
359
  background: var(--delta-bg, var(--card-3-bg));
367
- color: var(--delta-ink, var(--card-3-ink));
360
+ --dark: var(--card-3-dark);
361
+ color: var(--delta-ink, var(--card-ink));
368
362
  }
369
- .delta--neg { background: var(--delta-neg-bg, var(--neg)); color: var(--delta-neg-ink, var(--card-3-ink)); } /* light ink on the signal color in every slot */
363
+ .delta--neg { background: var(--delta-neg-bg, var(--neg)); --dark: var(--neg-dark); color: var(--delta-neg-ink, var(--card-ink)); }
370
364
 
371
365
  /* ---- panes: a few panes side by side inside a card or a sheet, one in sight (Lh 2026-10-02) ----
372
366
  <div class="panes" role="group"><div class="panes__rail"><div class="panes__pane">…</div>…</div><span class="pager__at">…</span></div>
package/css/choice.css CHANGED
@@ -49,6 +49,8 @@
49
49
  border-radius: var(--toggle-radius, var(--r-chip));
50
50
  border: var(--border-w) solid var(--toggle-border, var(--outline));
51
51
  background: var(--toggle-bg, var(--action-bg));
52
+ --dark: var(--action-dark); /* the paper's side: its knob at rest is the paper's muted ink, on the accent the accent's */
53
+ color: var(--card-ink);
52
54
  position: relative;
53
55
  transition: background-color var(--motion-press) ease-out, border-color var(--motion-press) ease-out;
54
56
  }
@@ -60,12 +62,11 @@
60
62
  width: calc(var(--_h) - 2 * var(--_pad) - 2 * var(--border-w));
61
63
  height: calc(var(--_h) - 2 * var(--_pad) - 2 * var(--border-w));
62
64
  border-radius: 50%;
63
- background: currentColor;
64
- opacity: 0.45;
65
- transition: translate var(--motion-quick) ease, opacity var(--motion-quick) ease;
65
+ background: var(--card-muted); /* a glyph: a shade of its side, never an opacity (0.45 of the ink was 2.4:1 on the paper) */
66
+ transition: translate var(--motion-quick) ease;
66
67
  }
67
68
  .toggle:checked { background: var(--accent); border-color: transparent; }
68
- .toggle:checked::after { translate: calc(var(--_w) - var(--_h)) 0; background: var(--on-accent); opacity: 1; }
69
+ .toggle:checked::after { translate: calc(var(--_w) - var(--_h)) 0; background: var(--on-accent); }
69
70
 
70
71
  /* ---- chip pick: a chip that is a choice — a native radio/checkbox inside the label, the picked one filled with accent.
71
72
  Full-width in a .chip-grid (one slot / many options), or hugging its word on a .chip-row with --sign (an interest
@@ -87,7 +88,7 @@
87
88
  a chip, a mood dot, a tile (media.css), a sticky's tone swatch (sticky.css), a place on a map (sheet.css), a card that is a label round a radio
88
89
  (a pickable row card: the app gives the picked one its `tone`) — the native input lies over its whole box, unseen */
89
90
  :is(.chip--pick, .mood__opt, .tile--pick, .sticky-pick__swatch, .map-pick) > input, label.card > input[type="radio"] { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
90
- :is(.chip--pick, .tile--pick):has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
91
+ :is(.chip--pick, .tile--pick):has(:checked) { background: var(--accent); --dark: var(--accent-dark); color: var(--card-ink); border-color: transparent; }
91
92
  /* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
92
93
  .chip--sign { padding-right: var(--sp-2); }
93
94
  .chip--sign::after {
@@ -121,13 +122,13 @@
121
122
  is the same shape on every day */
122
123
  padding-inline: 0;
123
124
  border-radius: var(--day-radius, var(--r-chip));
124
- opacity: var(--rest);
125
+ color: var(--day-rest-ink, var(--card-muted)); /* an unpicked day at rest: the muted words of the ground it stands on (an opacity faded it under AA) */
125
126
  }
126
127
  .day__name { font: var(--day-name-font, var(--type-caption)); }
127
128
  .day__num { font: 700 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
128
129
  /* today: a light tone, no louder than a rest; the selected pill below wins when it is both (it comes after and weighs more) */
129
- .day--today { background: var(--day-today-bg, var(--ink-tint)); color: var(--day-today-ink, currentColor); }
130
- .day[aria-selected="true"] { background: var(--day-selected-bg, var(--accent)); color: var(--day-selected-ink, var(--on-accent)); opacity: 1; }
130
+ .day--today { background: var(--day-today-bg, var(--ink-tint)); color: var(--day-today-ink, var(--card-ink)); }
131
+ .day[aria-selected="true"] { background: var(--day-selected-bg, var(--accent)); color: var(--day-selected-ink, var(--on-accent)); }
131
132
  .day__dot { grid-row: 3; align-self: center; width: var(--day-dot-size, var(--dot-xs)); height: var(--day-dot-size, var(--dot-xs)); border-radius: 50%; background: currentColor; }
132
133
 
133
134
  /* ---- calendar: month grid, 44px cells, one accent day, optional marks ----
@@ -147,8 +148,10 @@
147
148
  font-variant-numeric: tabular-nums;
148
149
  grid-column-start: var(--start, auto); /* --start on the 1st: weekday column 1–7 */
149
150
  }
150
- .calendar__day--mark { background: var(--calendar-day-mark-bg, var(--accent-soft)); }
151
- .calendar__day--mark2 { background: var(--calendar-day-mark2-bg, var(--accent-mid)); color: var(--calendar-day-mark2-ink, var(--on-accent)); }
151
+ /* a marked day keeps the ground's ink, so its fill is a light tint (12%, as --ink-tint: every tone holds its ink 4.5:1 on that); the
152
+ strong mark adds a ring of the accent. A stronger fill sat at the middle, where neither the ink nor --on-accent holds 4.5:1 */
153
+ .calendar__day--mark { background: var(--calendar-day-mark-bg, color-mix(in srgb, var(--accent) 12%, transparent)); }
154
+ .calendar__day--mark2 { background: var(--calendar-day-mark2-bg, color-mix(in srgb, var(--accent) 12%, transparent)); color: var(--calendar-day-mark2-ink, inherit); box-shadow: var(--calendar-day-mark2-shadow, inset 0 0 0 var(--border-w) var(--accent)); }
152
155
  .calendar__day[aria-selected="true"] { background: var(--calendar-day-selected-bg, var(--accent)); color: var(--calendar-day-selected-ink, var(--on-accent)); font-weight: 700; }
153
156
  .calendar__day:disabled { opacity: 0.3; }
154
157
 
@@ -204,7 +207,7 @@
204
207
  edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
205
208
  is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
206
209
  }
207
- .pin__cell:not(:placeholder-shown) { color: var(--pin-cell-filled-ink, var(--accent)); } /* a typed digit: coloured words on the cell's own ground — no fill (Lh 2026-09-24; was an accent-filled pill) */
210
+ .pin__cell:not(:placeholder-shown) { color: var(--pin-cell-filled-ink, var(--action-accent)); } /* a typed digit: coloured words on the cell's own ground — no fill (Lh 2026-09-24; was an accent-filled pill) */
208
211
  /* a PIN (secret): the cell DRAWS the dot, the character itself is not painted — except for a moment, when it has just been typed
209
212
  (Lh 2026-10-01: "each digit must flash, then go at once"). It was `-webkit-text-security: disc`: a phone's browser then shows
210
213
  the last typed character of a masked field for a second or more ("password echo", Android) — per FIELD, and a Pin is six
package/css/finish.css CHANGED
@@ -20,8 +20,10 @@
20
20
  so a finish sits in any palette; the one plain white is the sticker's backing paper and the lit edge's light.
21
21
  ============================================================ */
22
22
 
23
- /* every finish: the finish IS the edge, so the wireframe's two borders step back (as [data-palette] in palette.css); ink puts its own line there below */
24
- [data-finish] { --card-border: transparent; --action-border: transparent; }
23
+ /* every finish: the finish IS the controls' edge, so the wireframe's control border steps back (as [data-palette] in palette.css). The card
24
+ keeps its --card-border: lit's ring and sticker's white cut do not reach 3:1 on a light ground (palette.css, THE CARD KEEPS AN EDGE);
25
+ print and ink draw their own line below */
26
+ [data-finish] { --action-border: transparent; }
25
27
 
26
28
  /* ============================================================
27
29
  lit — the light comes from above:
package/css/journey.css CHANGED
@@ -27,7 +27,7 @@
27
27
  .route__time { font: var(--route-time-font, var(--type-body)); font-variant-numeric: tabular-nums; }
28
28
  .route__place { font: var(--route-place-font, var(--type-caption)); color: var(--route-place-ink, var(--card-muted)); }
29
29
 
30
- .route__via { grid-column: 2; grid-row: 2; align-self: center; display: flex; align-items: center; opacity: 0.65; font-size: var(--route-via-font-size, var(--fs-body)); }
30
+ .route__via { grid-column: 2; grid-row: 2; align-self: center; display: flex; align-items: center; color: var(--route-via-ink, var(--card-muted)); font-size: var(--route-via-font-size, var(--fs-body)); }
31
31
 
32
32
  /* ---- tile badge: tiny label over a big value (gate, seat, zone) ---- */
33
33
  .tile-badge {
@@ -37,13 +37,16 @@
37
37
  padding: var(--tile-badge-pad-y, var(--sp-1)) var(--tile-badge-pad-x, var(--sp-2));
38
38
  border-radius: var(--tile-badge-radius, var(--r-badge));
39
39
  background: var(--tile-badge-bg, var(--card-3-bg));
40
- color: var(--tile-badge-ink, var(--card-3-ink));
40
+ --dark: var(--card-3-dark);
41
+ color: var(--tile-badge-ink, var(--card-ink));
41
42
  }
42
- .tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); opacity: var(--dim); }
43
+ /* the label is the badge's own ink, smaller: a badge is a small fill of any colour (a palette paints it yellow), and only its own ink is
44
+ sure to hold 4.5:1 on it; --dim faded it to 2.4:1 */
45
+ .tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); color: var(--tile-badge-label-ink, inherit); }
43
46
  .tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
44
47
 
45
48
  /* plain: no fill — a date box beside a title (Jun / 18) */
46
- .tile-badge--plain { background: transparent; color: inherit; padding: 0; min-width: var(--tile-badge-plain-min-w, var(--tap-sm)); }
49
+ .tile-badge--plain { background: transparent; --dark: inherit; color: inherit; padding: 0; min-width: var(--tile-badge-plain-min-w, var(--tap-sm)); }
47
50
  .tile-badge--plain .tile-badge__value { font: 800 var(--fs-h1)/1 var(--font-display); }
48
51
 
49
52
  /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */