@usableapp/cardds 0.7.44 → 0.7.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
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.46 — 2026-10-03
6
+
7
+ ### Changed
8
+ - **A larger text pulls the controls with it, half as fast** (Lh 2026-10-03): `setTextScale(percent)` now writes `--tap-base` as well as `--fs-base` — 115% text → 107.5% controls · 130% → 115% · 145% → 122.5% · 160% → 130%; at 100% and under, the controls keep the theme's size. Before, only the words grew and outgrew what holds them (a number on its round's bottom edge, a title tight in the top bar). Nothing to change in an app that calls `setTextScale`: buttons, bars, rounds and icons grow by themselves — look at a dense screen at 160%, it has less room for content than before. An app that sets `--fs-base` on `<html>` by hand gets none of this: call `setTextScale`.
9
+ - New export **`controlsFor(percent)`** — the controls' share for a text size, for an app that wants to show or store the pair.
10
+
11
+ ### Fixed
12
+ - **`SheetSteps numbered`: the number sits in the middle of its disc at every text size** — the disc is cut from its own number (`1.408em`, 24px at the default text as before) instead of a fixed 24px, and the digit is lifted onto the middle. Knobs `--sheet-step-num-size` (now listed) · `--sheet-step-num-lift`. If your theme set `--sheet-step-num-size` in rem, it still wins — but then it no longer follows the text.
13
+
14
+ ## 0.7.45 — 2026-10-03
15
+
16
+ ### Changed
17
+ - **`Track` is as tall as the top bar, and a step is a mark, not a control** (Lh 2026-10-03): the pill is `--header-h` tall (60px, was 72) and each step's round is 32px (was 56 — the size of a control, it read as something to press), its glyph 18px, the connector 8px, a `num` at caption size. Nothing to change in a screen: same markup, same props. If your theme sets `--track-pad` or `--track-dot-font`, your value still wins — only the defaults moved (`--track-pad` 8px → 14px, `--track-dot-font` title → caption).
18
+
19
+ ### Added
20
+ - **A `Track` as the screen's top** (Lh 2026-10-03): `<Screen fill top={<Track>…</Track>}>` — the progress of a task in several sheets stands where a bar would, in the screen's inset, and stays in sight over every sheet. A sheet at full stops 16px under it and never touches it — the same 16px it keeps from the screen's sides and from the status bar. Knobs `--track-top-gap` (16px, the room under it) · `--track-top-lead` (16px, the air over it, under the status bar). Stories `Track · OnTop` (numbers) · `OnTopIcons`.
21
+ - **`CallSheet top`**: the template takes the Screen's `top` (before, it had none and could not be given one) — `<CallSheet top={<Track>…</Track>} …>`. Without it nothing changes: no top, the page starts at the lead.
22
+
5
23
  ## 0.7.44 — 2026-10-02
6
24
 
7
25
  ### Added
package/README.md CHANGED
@@ -653,7 +653,7 @@ Rules:
653
653
  any role can be set outright.
654
654
  - The member's text-size setting is the same `--fs-base`, written on `<html>`
655
655
  at run time by `setTextScale(percent)` (a share of the theme's base; 100 = the
656
- theme's own): the words grow, the things keep their size. (`--text-scale`, which multiplied the root and zoomed the UI too, is gone.)
656
+ 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.)
657
657
 
658
658
  Try it live: the gallery's nav (`npm run dev`) has a slider per size base, and
659
659
  palette `own` shows a colour well per colour base.
@@ -684,8 +684,8 @@ default) and steps narrow phones (≤360px) to `93.75%`, which keeps the
684
684
  column, its 48px taps and its type in proportion on 320–360 screens. Only
685
685
  `--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
686
686
  An app with its own text-size setting calls `setTextScale(percent)`, which writes
687
- `--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
688
- badge, the inline icon) grow; taps, spacing and radii keep their size.
687
+ `--fs-base` on `<html>` from the theme's base (100% = the theme's; 160% = 2.2rem by default): the type and its holders (the count
688
+ badge, the inline icon) grow, the controls grow half as much (`--tap-base`), spacing and radii keep their size.
689
689
  Components never write a raw length: spacing from `--sp-*`, control sizes from
690
690
  `--tap`, `--tap-lg`, `--btn-h`, `--bar-h`, `--header-h`, `--fab`, `--badge`, container
691
691
  padding from `--screen-pad` / `--card-pad`, radii from
package/cardds.css CHANGED
@@ -16,3 +16,4 @@
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
18
  @import url("css/layover.css"); /* ask drawer, popover menu, banner, float bar, callout */
19
+ @import url("css/finish.css"); /* light, shadow, edges — only under data-finish="lit" */
package/css/finish.css ADDED
@@ -0,0 +1,77 @@
1
+ /* ============================================================
2
+ cardds/finish.css — FINISH: light, shadow and edges. A style of its own (Lh 2026-10-02: the plain
3
+ look read flat, "not finished"), apart from colour: palette.css says WHICH colours, this says how
4
+ the material is lit. Apply with <html data-finish="lit"> beside any palette (or none, or "own").
5
+ No attribute = nothing here applies — the flat look, unchanged.
6
+
7
+ The light comes from above:
8
+ - a thing that stands UP (card, sheet, button) has a lit top edge, a hairline ring and a shadow
9
+ that grows with its height: card 1 · sheet / float 2 — the layers cardds already has;
10
+ - a thing that is SUNK (a field, a pin cell) has its shadow inside, under its top edge;
11
+ - information inside a card stays flat: only a container or a control gets an edge.
12
+ Shadows are mixed from the ink (--on-surface), never plain black, so they sit in the palette.
13
+ ============================================================ */
14
+
15
+ [data-finish="lit"] {
16
+ --_shade: var(--on-surface);
17
+ --finish-edge: inset 0 1px 0 rgb(255 255 255 / 0.55); /* the lit top edge */
18
+ --finish-ring: 0 0 0 1px color-mix(in srgb, var(--_shade) 7%, transparent); /* the hairline round a raised thing */
19
+
20
+ /* the three shadows every component already reads — layered: one close and crisp, one far and soft */
21
+ --shadow-card: var(--finish-edge), var(--finish-ring),
22
+ 0 1px 2px color-mix(in srgb, var(--_shade) 8%, transparent),
23
+ 0 0.375rem 0.75rem -0.125rem color-mix(in srgb, var(--_shade) 8%, transparent),
24
+ 0 1.25rem 2.5rem -0.75rem color-mix(in srgb, var(--_shade) 14%, transparent);
25
+ --shadow-float: var(--finish-edge), var(--finish-ring),
26
+ 0 1px 2px color-mix(in srgb, var(--_shade) 12%, transparent),
27
+ 0 0.5rem 1.25rem -0.25rem color-mix(in srgb, var(--_shade) 22%, transparent);
28
+ --shadow-sheet: var(--finish-edge), var(--finish-ring),
29
+ 0 -1px 2px color-mix(in srgb, var(--_shade) 6%, transparent),
30
+ 0 -0.75rem 2.5rem color-mix(in srgb, var(--_shade) 18%, transparent);
31
+
32
+ /* a control: its own small lift, and the sheen that runs down its face */
33
+ --finish-control: var(--finish-edge), var(--finish-ring),
34
+ 0 1px 2px color-mix(in srgb, var(--_shade) 12%, transparent),
35
+ 0 0.25rem 0.5rem -0.125rem color-mix(in srgb, var(--_shade) 12%, transparent);
36
+ --finish-control-down: var(--finish-ring), inset 0 1px 2px color-mix(in srgb, var(--_shade) 18%, transparent);
37
+ --finish-sheen: linear-gradient(to bottom, rgb(255 255 255 / 0.16), rgb(255 255 255 / 0) 60%);
38
+ --finish-sunk: inset 0 1px 2px color-mix(in srgb, var(--_shade) 14%, transparent), 0 1px 0 rgb(255 255 255 / 0.5);
39
+
40
+ /* the shadow is the edge: the wireframe's lines step back (as in every colour palette) */
41
+ --card-border: transparent;
42
+ --action-border: transparent;
43
+ --outline: color-mix(in srgb, currentColor 14%, transparent);
44
+ }
45
+
46
+ /* ---- raised: cards, sheets, the stack ---- */
47
+ [data-finish="lit"] :is(.card, .band-stack) { box-shadow: var(--shadow-card); }
48
+ [data-finish="lit"] .band-stack > .card--band { box-shadow: none; }
49
+ /* a sheet lies on the floor and is lit from above: its shadow falls UP the screen, onto what it covers */
50
+ [data-finish="lit"] .sheet { box-shadow: var(--sheet-shadow, var(--shadow-sheet)); }
51
+ [data-finish="lit"] .modal { box-shadow: var(--modal-shadow, var(--shadow-float)); }
52
+ [data-finish="lit"] .sheet-stack > .card,
53
+ [data-finish="lit"] .sheet-stack > :not(.card, .sheet-stack__base) > .card { box-shadow: var(--shadow-sheet); }
54
+
55
+ /* ---- controls: a filled button stands up, and goes down under the finger ---- */
56
+ [data-finish="lit"] .btn:not(.btn--quiet),
57
+ [data-finish="lit"] .icon-btn:not(.icon-btn-group > *, .stack-sheet__tab:not(.icon-btn--invert)) {
58
+ background-image: var(--finish-sheen);
59
+ box-shadow: var(--finish-control);
60
+ }
61
+ /* the main button throws its own colour */
62
+ [data-finish="lit"] .btn--primary,
63
+ [data-finish="lit"] .icon-btn--invert:not(.icon-btn-group > *) {
64
+ box-shadow: var(--finish-edge),
65
+ 0 1px 2px color-mix(in srgb, var(--_shade) 20%, transparent),
66
+ 0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
67
+ }
68
+ [data-finish="lit"] :is(.btn:not(.btn--quiet), .icon-btn:not(.icon-btn-group > *, .stack-sheet__tab:not(.icon-btn--invert))):active:not(:disabled) {
69
+ background-image: none;
70
+ box-shadow: var(--finish-control-down);
71
+ }
72
+ [data-finish="lit"] .icon-btn-group { box-shadow: var(--finish-control); }
73
+ [data-finish="lit"] .toggle { box-shadow: var(--finish-sunk); }
74
+ [data-finish="lit"] .toggle::after { box-shadow: 0 1px 2px color-mix(in srgb, var(--_shade) 35%, transparent); }
75
+
76
+ /* ---- sunk: what the member types into ---- */
77
+ [data-finish="lit"] :is(.field, .pin__cell) { box-shadow: var(--finish-sunk); }
package/css/numbers.css CHANGED
@@ -39,8 +39,12 @@
39
39
  .ring--xs .ring__value { font: var(--type-caption); }
40
40
 
41
41
  /* ---- track: step progress in a pill — done · done · todo ----
42
- <ol class="track"><li class="track__step is-done">icon</li> …</ol> */
42
+ <ol class="track"><li class="track__step is-done">icon</li> …</ol>
43
+ As tall as the top bar (--header-h), and a step is a MARK, not a control: its round is well under
44
+ the tap floor, so it reads as progress and never as something to press (Lh 2026-10-03). */
43
45
  .track {
46
+ --_step: calc(var(--tap-base) * 2 / 3); /* 32 the round of one step */
47
+ --_pad: calc((var(--header-h) - var(--_step)) / 2); /* 14 round + its room = the top bar's height; the end rounds sit concentric in the pill */
44
48
  --_bg: var(--card-3-bg);
45
49
  --_ink: var(--card-3-ink);
46
50
  /* the track lays down its own dark ground, so like the dark card slot it
@@ -51,7 +55,7 @@
51
55
  --on-accent: var(--_bg);
52
56
  list-style: none;
53
57
  margin: 0;
54
- padding: var(--track-pad, var(--sp-2));
58
+ padding: var(--track-pad, var(--_pad));
55
59
  display: flex;
56
60
  align-items: center;
57
61
  border-radius: var(--track-radius, var(--r-chip));
@@ -70,22 +74,22 @@
70
74
  .track__step::before {
71
75
  content: "";
72
76
  flex: 1;
73
- height: var(--sp-3);
77
+ height: var(--sp-2);
74
78
  background: var(--ink-rail); /* the track's own ink at 30% — identical to color-mix(in srgb, var(--_ink) 30%, transparent): --ink-rail already reads currentColor, and .track sets color to --_ink */
75
79
  }
76
80
  .track__step:first-child::before { display: none; }
77
81
  .track__step.is-done::before { background: var(--accent); }
78
82
  .track__step > .icon,
79
83
  .track__step > .track__dot {
80
- width: var(--tap-lg);
81
- height: var(--tap-lg);
84
+ width: var(--_step);
85
+ height: var(--_step);
82
86
  flex: none;
83
- padding: calc((var(--tap-lg) - var(--icon)) / 2);
87
+ padding: calc(var(--_step) * 7 / 32); /* the glyph = 18 in the 32 round */
84
88
  border-radius: 50%;
85
89
  background: var(--ink-rail);
86
90
  color: var(--_ink);
87
91
  }
88
- .track__step > .track__dot { display: grid; place-items: center; padding: 0; font: var(--track-dot-font, var(--type-title)); font-variant-numeric: tabular-nums; } /* a number in the round (TrackStep num) */
92
+ .track__step > .track__dot { display: grid; place-items: center; padding: 0; font: var(--track-dot-font, var(--type-caption)); font-variant-numeric: tabular-nums; } /* a number in the round (TrackStep num) */
89
93
  .track__step.is-done > .icon,
90
94
  .track__step.is-done > .track__dot { background: var(--accent); color: var(--on-accent); }
91
95
 
package/css/sheet.css CHANGED
@@ -413,8 +413,10 @@
413
413
  }
414
414
  .sheet-step__title { font: var(--sheet-step-title-font, var(--type-label)); margin: 0; flex: 0 1 auto; min-width: 0; outline: none; /* the label size, not a body title: a pile of rows must stay small (Lh 2026-09-24) */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
415
415
  .sheet-step__num { /* `numbered`: the step's number in a disc BEFORE its title (Lh 2026-09-25) — a white disc on the bar, the number in the ink */
416
- flex: none; display: grid; place-items: center; inline-size: var(--sheet-step-num-size, calc(var(--tap-base) / 2)); block-size: var(--sheet-step-num-size, calc(var(--tap-base) / 2));
417
- border-radius: 50%; font: var(--sheet-step-num-font, var(--type-caption)); font-variant-numeric: tabular-nums;
416
+ flex: none; display: grid; place-items: center; box-sizing: border-box; inline-size: var(--sheet-step-num-size, 1.408em); block-size: var(--sheet-step-num-size, 1.408em); /* em, not rem (Lh 2026-10-03, as the StackSheet's count): the disc is cut from ITS OWN number's size —
417
+ at a large text size the number grew and a 24px disc did not, so the digit sat on its bottom edge; 1.408em of a 17px caption = the 24px it always was */
418
+ padding-bottom: var(--sheet-step-num-lift, 0.1em); /* the room UNDER the number lifts it onto the disc's middle: a digit has no descender, so a centred line box sets it low */
419
+ border-radius: 50%; font: var(--sheet-step-num-font, var(--type-caption)); line-height: 1; font-variant-numeric: tabular-nums;
418
420
  background: var(--sheet-step-num-bg, var(--card-bg)); color: var(--sheet-step-num-ink, inherit);
419
421
  }
420
422
  .sheet-step__summary { color: inherit; opacity: 0.8; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
package/css/step.css CHANGED
@@ -141,6 +141,15 @@
141
141
  them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
142
142
  .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
143
143
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
144
+ /* a TRACK as the screen's TOP (Lh 2026-10-03): the walk's progress stands where a bar would, a row of the column, in the screen's
145
+ inset — and it never touches what comes up under it: the step (and so a sheet at full) starts --track-top-gap below it. That
146
+ room is the track's own, not the screen's gap; --track-top-lead is the air over it, under the status bar. Both = the screen's side inset (16): the pill
147
+ stands the same distance from everything around it (Lh 2026-10-03: 8 over was too little, 24 under too much). */
148
+ .screen--fill > .track:first-child {
149
+ flex: none;
150
+ margin-top: var(--track-top-lead, var(--sp-4));
151
+ margin-bottom: calc(var(--track-top-gap, var(--sp-4)) - var(--screen-gap));
152
+ }
144
153
  /* a walk page that OPENS on a picture (Lh 2026-09-23): the picture takes the lead's room — as wide as the content (edge to edge), from the top edge, as tall as
145
154
  the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
146
155
  .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
package/css/tokens.css CHANGED
@@ -46,7 +46,7 @@
46
46
  --tap-floor: 2.75rem; /* 44 the HIG floor — no --tap-base takes a touch target under it */
47
47
  --tap: max(var(--tap-floor), var(--tap-base)); /* 48 primary touch target */
48
48
  --tap-sm: max(var(--tap-floor), calc(var(--tap-base) * 11 / 12)); /* 44 minimum touch target */
49
- --tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: track steps, send, field with action */
49
+ --tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: send, field with action */
50
50
  --btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
51
51
  --btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
52
52
  --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
package/dist/cardds.css CHANGED
@@ -87,7 +87,7 @@
87
87
  --tap-floor: 2.75rem; /* 44 the HIG floor — no --tap-base takes a touch target under it */
88
88
  --tap: max(var(--tap-floor), var(--tap-base)); /* 48 primary touch target */
89
89
  --tap-sm: max(var(--tap-floor), calc(var(--tap-base) * 11 / 12)); /* 44 minimum touch target */
90
- --tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: track steps, send, field with action */
90
+ --tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: send, field with action */
91
91
  --btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
92
92
  --btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
93
93
  --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
@@ -1737,8 +1737,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1737
1737
  }
1738
1738
  .sheet-step__title { font: var(--sheet-step-title-font, var(--type-label)); margin: 0; flex: 0 1 auto; min-width: 0; outline: none; /* the label size, not a body title: a pile of rows must stay small (Lh 2026-09-24) */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1739
1739
  .sheet-step__num { /* `numbered`: the step's number in a disc BEFORE its title (Lh 2026-09-25) — a white disc on the bar, the number in the ink */
1740
- flex: none; display: grid; place-items: center; inline-size: var(--sheet-step-num-size, calc(var(--tap-base) / 2)); block-size: var(--sheet-step-num-size, calc(var(--tap-base) / 2));
1741
- border-radius: 50%; font: var(--sheet-step-num-font, var(--type-caption)); font-variant-numeric: tabular-nums;
1740
+ flex: none; display: grid; place-items: center; box-sizing: border-box; inline-size: var(--sheet-step-num-size, 1.408em); block-size: var(--sheet-step-num-size, 1.408em); /* em, not rem (Lh 2026-10-03, as the StackSheet's count): the disc is cut from ITS OWN number's size —
1741
+ at a large text size the number grew and a 24px disc did not, so the digit sat on its bottom edge; 1.408em of a 17px caption = the 24px it always was */
1742
+ padding-bottom: var(--sheet-step-num-lift, 0.1em); /* the room UNDER the number lifts it onto the disc's middle: a digit has no descender, so a centred line box sets it low */
1743
+ border-radius: 50%; font: var(--sheet-step-num-font, var(--type-caption)); line-height: 1; font-variant-numeric: tabular-nums;
1742
1744
  background: var(--sheet-step-num-bg, var(--card-bg)); color: var(--sheet-step-num-ink, inherit);
1743
1745
  }
1744
1746
  .sheet-step__summary { color: inherit; opacity: 0.8; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -1899,6 +1901,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1899
1901
  them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
1900
1902
  .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
1901
1903
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
1904
+ /* a TRACK as the screen's TOP (Lh 2026-10-03): the walk's progress stands where a bar would, a row of the column, in the screen's
1905
+ inset — and it never touches what comes up under it: the step (and so a sheet at full) starts --track-top-gap below it. That
1906
+ room is the track's own, not the screen's gap; --track-top-lead is the air over it, under the status bar. Both = the screen's side inset (16): the pill
1907
+ stands the same distance from everything around it (Lh 2026-10-03: 8 over was too little, 24 under too much). */
1908
+ .screen--fill > .track:first-child {
1909
+ flex: none;
1910
+ margin-top: var(--track-top-lead, var(--sp-4));
1911
+ margin-bottom: calc(var(--track-top-gap, var(--sp-4)) - var(--screen-gap));
1912
+ }
1902
1913
  /* a walk page that OPENS on a picture (Lh 2026-09-23): the picture takes the lead's room — as wide as the content (edge to edge), from the top edge, as tall as
1903
1914
  the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
1904
1915
  .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
@@ -2516,8 +2527,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2516
2527
  .ring--xs .ring__value { font: var(--type-caption); }
2517
2528
 
2518
2529
  /* ---- track: step progress in a pill — done · done · todo ----
2519
- <ol class="track"><li class="track__step is-done">icon</li> …</ol> */
2530
+ <ol class="track"><li class="track__step is-done">icon</li> …</ol>
2531
+ As tall as the top bar (--header-h), and a step is a MARK, not a control: its round is well under
2532
+ the tap floor, so it reads as progress and never as something to press (Lh 2026-10-03). */
2520
2533
  .track {
2534
+ --_step: calc(var(--tap-base) * 2 / 3); /* 32 the round of one step */
2535
+ --_pad: calc((var(--header-h) - var(--_step)) / 2); /* 14 round + its room = the top bar's height; the end rounds sit concentric in the pill */
2521
2536
  --_bg: var(--card-3-bg);
2522
2537
  --_ink: var(--card-3-ink);
2523
2538
  /* the track lays down its own dark ground, so like the dark card slot it
@@ -2528,7 +2543,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2528
2543
  --on-accent: var(--_bg);
2529
2544
  list-style: none;
2530
2545
  margin: 0;
2531
- padding: var(--track-pad, var(--sp-2));
2546
+ padding: var(--track-pad, var(--_pad));
2532
2547
  display: flex;
2533
2548
  align-items: center;
2534
2549
  border-radius: var(--track-radius, var(--r-chip));
@@ -2547,22 +2562,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2547
2562
  .track__step::before {
2548
2563
  content: "";
2549
2564
  flex: 1;
2550
- height: var(--sp-3);
2565
+ height: var(--sp-2);
2551
2566
  background: var(--ink-rail); /* the track's own ink at 30% — identical to color-mix(in srgb, var(--_ink) 30%, transparent): --ink-rail already reads currentColor, and .track sets color to --_ink */
2552
2567
  }
2553
2568
  .track__step:first-child::before { display: none; }
2554
2569
  .track__step.is-done::before { background: var(--accent); }
2555
2570
  .track__step > .icon,
2556
2571
  .track__step > .track__dot {
2557
- width: var(--tap-lg);
2558
- height: var(--tap-lg);
2572
+ width: var(--_step);
2573
+ height: var(--_step);
2559
2574
  flex: none;
2560
- padding: calc((var(--tap-lg) - var(--icon)) / 2);
2575
+ padding: calc(var(--_step) * 7 / 32); /* the glyph = 18 in the 32 round */
2561
2576
  border-radius: 50%;
2562
2577
  background: var(--ink-rail);
2563
2578
  color: var(--_ink);
2564
2579
  }
2565
- .track__step > .track__dot { display: grid; place-items: center; padding: 0; font: var(--track-dot-font, var(--type-title)); font-variant-numeric: tabular-nums; } /* a number in the round (TrackStep num) */
2580
+ .track__step > .track__dot { display: grid; place-items: center; padding: 0; font: var(--track-dot-font, var(--type-caption)); font-variant-numeric: tabular-nums; } /* a number in the round (TrackStep num) */
2566
2581
  .track__step.is-done > .icon,
2567
2582
  .track__step.is-done > .track__dot { background: var(--accent); color: var(--on-accent); }
2568
2583
 
@@ -3987,3 +4002,82 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3987
4002
  .snackbar__caption { font: var(--snackbar-caption-font, var(--type-caption)); opacity: var(--dim); }
3988
4003
  @media (prefers-reduced-motion: reduce) { .snackbar { transition: none; } }
3989
4004
 
4005
+ /* ---- css/finish.css — light, shadow, edges — only under data-finish="lit" ---- */
4006
+ /* ============================================================
4007
+ cardds/finish.css — FINISH: light, shadow and edges. A style of its own (Lh 2026-10-02: the plain
4008
+ look read flat, "not finished"), apart from colour: palette.css says WHICH colours, this says how
4009
+ the material is lit. Apply with <html data-finish="lit"> beside any palette (or none, or "own").
4010
+ No attribute = nothing here applies — the flat look, unchanged.
4011
+
4012
+ The light comes from above:
4013
+ - a thing that stands UP (card, sheet, button) has a lit top edge, a hairline ring and a shadow
4014
+ that grows with its height: card 1 · sheet / float 2 — the layers cardds already has;
4015
+ - a thing that is SUNK (a field, a pin cell) has its shadow inside, under its top edge;
4016
+ - information inside a card stays flat: only a container or a control gets an edge.
4017
+ Shadows are mixed from the ink (--on-surface), never plain black, so they sit in the palette.
4018
+ ============================================================ */
4019
+
4020
+ [data-finish="lit"] {
4021
+ --_shade: var(--on-surface);
4022
+ --finish-edge: inset 0 1px 0 rgb(255 255 255 / 0.55); /* the lit top edge */
4023
+ --finish-ring: 0 0 0 1px color-mix(in srgb, var(--_shade) 7%, transparent); /* the hairline round a raised thing */
4024
+
4025
+ /* the three shadows every component already reads — layered: one close and crisp, one far and soft */
4026
+ --shadow-card: var(--finish-edge), var(--finish-ring),
4027
+ 0 1px 2px color-mix(in srgb, var(--_shade) 8%, transparent),
4028
+ 0 0.375rem 0.75rem -0.125rem color-mix(in srgb, var(--_shade) 8%, transparent),
4029
+ 0 1.25rem 2.5rem -0.75rem color-mix(in srgb, var(--_shade) 14%, transparent);
4030
+ --shadow-float: var(--finish-edge), var(--finish-ring),
4031
+ 0 1px 2px color-mix(in srgb, var(--_shade) 12%, transparent),
4032
+ 0 0.5rem 1.25rem -0.25rem color-mix(in srgb, var(--_shade) 22%, transparent);
4033
+ --shadow-sheet: var(--finish-edge), var(--finish-ring),
4034
+ 0 -1px 2px color-mix(in srgb, var(--_shade) 6%, transparent),
4035
+ 0 -0.75rem 2.5rem color-mix(in srgb, var(--_shade) 18%, transparent);
4036
+
4037
+ /* a control: its own small lift, and the sheen that runs down its face */
4038
+ --finish-control: var(--finish-edge), var(--finish-ring),
4039
+ 0 1px 2px color-mix(in srgb, var(--_shade) 12%, transparent),
4040
+ 0 0.25rem 0.5rem -0.125rem color-mix(in srgb, var(--_shade) 12%, transparent);
4041
+ --finish-control-down: var(--finish-ring), inset 0 1px 2px color-mix(in srgb, var(--_shade) 18%, transparent);
4042
+ --finish-sheen: linear-gradient(to bottom, rgb(255 255 255 / 0.16), rgb(255 255 255 / 0) 60%);
4043
+ --finish-sunk: inset 0 1px 2px color-mix(in srgb, var(--_shade) 14%, transparent), 0 1px 0 rgb(255 255 255 / 0.5);
4044
+
4045
+ /* the shadow is the edge: the wireframe's lines step back (as in every colour palette) */
4046
+ --card-border: transparent;
4047
+ --action-border: transparent;
4048
+ --outline: color-mix(in srgb, currentColor 14%, transparent);
4049
+ }
4050
+
4051
+ /* ---- raised: cards, sheets, the stack ---- */
4052
+ [data-finish="lit"] :is(.card, .band-stack) { box-shadow: var(--shadow-card); }
4053
+ [data-finish="lit"] .band-stack > .card--band { box-shadow: none; }
4054
+ /* a sheet lies on the floor and is lit from above: its shadow falls UP the screen, onto what it covers */
4055
+ [data-finish="lit"] .sheet { box-shadow: var(--sheet-shadow, var(--shadow-sheet)); }
4056
+ [data-finish="lit"] .modal { box-shadow: var(--modal-shadow, var(--shadow-float)); }
4057
+ [data-finish="lit"] .sheet-stack > .card,
4058
+ [data-finish="lit"] .sheet-stack > :not(.card, .sheet-stack__base) > .card { box-shadow: var(--shadow-sheet); }
4059
+
4060
+ /* ---- controls: a filled button stands up, and goes down under the finger ---- */
4061
+ [data-finish="lit"] .btn:not(.btn--quiet),
4062
+ [data-finish="lit"] .icon-btn:not(.icon-btn-group > *, .stack-sheet__tab:not(.icon-btn--invert)) {
4063
+ background-image: var(--finish-sheen);
4064
+ box-shadow: var(--finish-control);
4065
+ }
4066
+ /* the main button throws its own colour */
4067
+ [data-finish="lit"] .btn--primary,
4068
+ [data-finish="lit"] .icon-btn--invert:not(.icon-btn-group > *) {
4069
+ box-shadow: var(--finish-edge),
4070
+ 0 1px 2px color-mix(in srgb, var(--_shade) 20%, transparent),
4071
+ 0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
4072
+ }
4073
+ [data-finish="lit"] :is(.btn:not(.btn--quiet), .icon-btn:not(.icon-btn-group > *, .stack-sheet__tab:not(.icon-btn--invert))):active:not(:disabled) {
4074
+ background-image: none;
4075
+ box-shadow: var(--finish-control-down);
4076
+ }
4077
+ [data-finish="lit"] .icon-btn-group { box-shadow: var(--finish-control); }
4078
+ [data-finish="lit"] .toggle { box-shadow: var(--finish-sunk); }
4079
+ [data-finish="lit"] .toggle::after { box-shadow: 0 1px 2px color-mix(in srgb, var(--_shade) 35%, transparent); }
4080
+
4081
+ /* ---- sunk: what the member types into ---- */
4082
+ [data-finish="lit"] :is(.field, .pin__cell) { box-shadow: var(--finish-sunk); }
4083
+
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { cx } from './cx.js';
4
4
  export * from './type/Icon.js';
5
5
  export { ICON_NAMES } from './type/icons.js';
6
6
  export * from './type/Text.js';
7
- export { setTextScale } from './type/textScale.js';
7
+ export { setTextScale, controlsFor } from './type/textScale.js';
8
8
  export { setWords, word, type WordKey } from './words.js';
9
9
  export * from './scaffold/Screen.js';
10
10
  export * from './scaffold/AppBar.js';
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ export { cx } from './cx.js';
8
8
  export * from './type/Icon.js';
9
9
  export { ICON_NAMES } from './type/icons.js';
10
10
  export * from './type/Text.js';
11
- export { setTextScale } from './type/textScale.js';
11
+ export { setTextScale, controlsFor } from './type/textScale.js';
12
12
  export { setWords, word } from './words.js';
13
13
  export * from './scaffold/Screen.js';
14
14
  export * from './scaffold/AppBar.js';
@@ -21,10 +21,13 @@ export interface CallSheetWay {
21
21
  /** what that button brings up */
22
22
  sheet: CallSheetSheet;
23
23
  }
24
- export interface CallSheetProps extends Omit<ComponentProps<typeof Screen>, 'children' | 'fill' | 'top' | 'bottom' | 'title'> {
24
+ export interface CallSheetProps extends Omit<ComponentProps<typeof Screen>, 'children' | 'fill' | 'bottom' | 'title'> {
25
+ /** TOP (optional, the Screen's own): none by default — the page starts at the lead (15%). A walk in several sheets may stand
26
+ * its `Track` here: it stays in sight over every sheet, a full sheet stops under it. */
27
+ top?: ComponentProps<typeof Screen>['top'];
25
28
  /** a picture above the words (optional) */
26
29
  media?: ReactNode;
27
- /** the page's one heading — its h1 (there is no top) */
30
+ /** the page's one heading — its h1 */
28
31
  heading: ReactNode;
29
32
  /** the words: a string is one paragraph */
30
33
  body?: ReactNode;
@@ -46,7 +49,7 @@ export interface CallSheetProps extends Omit<ComponentProps<typeof Screen>, 'chi
46
49
  children?: ReactNode;
47
50
  }
48
51
  /**
49
- * CallSheet — TEMPLATE, a `Screen` with its parts filled — no top · content: a `Step` holding ONE `Page` (picture · heading (the h1) ·
52
+ * CallSheet — TEMPLATE, a `Screen` with its parts filled — no top (or the one given: `top`) · content: a `Step` holding ONE `Page` (picture · heading (the h1) ·
50
53
  * words · the button, or several — `ways`) and the `Sheet` each button calls · no bottom (a sheet finishes on its own foot). The page
51
54
  * starts at the screen's lead (15%). The sheets wait `away`; a button brings its own up full; the handle takes it down to peek.
52
55
  * One sheet up at a time — a sheet holds that way's WHOLE form.
@@ -12,7 +12,7 @@ import { Text } from '../type/Text.js';
12
12
  import { Btn } from '../actions/Btn.js';
13
13
  const index = (o) => (o === true ? 0 : typeof o === 'number' ? o : null);
14
14
  /**
15
- * CallSheet — TEMPLATE, a `Screen` with its parts filled — no top · content: a `Step` holding ONE `Page` (picture · heading (the h1) ·
15
+ * CallSheet — TEMPLATE, a `Screen` with its parts filled — no top (or the one given: `top`) · content: a `Step` holding ONE `Page` (picture · heading (the h1) ·
16
16
  * words · the button, or several — `ways`) and the `Sheet` each button calls · no bottom (a sheet finishes on its own foot). The page
17
17
  * starts at the screen's lead (15%). The sheets wait `away`; a button brings its own up full; the handle takes it down to peek.
18
18
  * One sheet up at a time — a sheet holds that way's WHOLE form.
@@ -1,6 +1,14 @@
1
1
  /**
2
- * The member's text size: `--fs-base` on <html> as a share of the theme's own base (100 = as the theme set it).
3
- * Words grow, things keep their size. The base is read from the CSS (cardds's default or the project's theme.css) — never
4
- * copied into app code. Keeping the choice (localStorage, an account) is the app's.
2
+ * How much the CONTROLS grow for a text size (both a %, 100 = as the theme set it): half as fast as the words. A text-holder of
3
+ * a fixed size (a round with a number, a button, the top bar) is drawn for the theme's text — at a larger text the words outgrew
4
+ * it; growing the things at the same rate would zoom the whole UI and leave no room for content (Lh 2026-10-03, pairs set by
5
+ * measuring the gallery: 115 → 107.5 · 130 → 115 · 145 → 122.5 · 160 → 130). A smaller text never shrinks a control.
6
+ */
7
+ export declare const controlsFor: (percent: number) => number;
8
+ /**
9
+ * The member's text size: `--fs-base` on <html> as a share of the theme's own base (100 = as the theme set it), and with it
10
+ * `--tap-base` — the things grow half as much as the words (`controlsFor`), so a larger text still fits what holds it.
11
+ * The bases are read from the CSS (cardds's default or the project's theme.css) — never copied into app code. Keeping the choice
12
+ * (localStorage, an account) is the app's.
5
13
  */
6
14
  export declare function setTextScale(percent: number, root?: HTMLElement): void;
@@ -1,12 +1,25 @@
1
1
  /**
2
- * The member's text size: `--fs-base` on <html> as a share of the theme's own base (100 = as the theme set it).
3
- * Words grow, things keep their size. The base is read from the CSS (cardds's default or the project's theme.css) — never
4
- * copied into app code. Keeping the choice (localStorage, an account) is the app's.
2
+ * How much the CONTROLS grow for a text size (both a %, 100 = as the theme set it): half as fast as the words. A text-holder of
3
+ * a fixed size (a round with a number, a button, the top bar) is drawn for the theme's text — at a larger text the words outgrew
4
+ * it; growing the things at the same rate would zoom the whole UI and leave no room for content (Lh 2026-10-03, pairs set by
5
+ * measuring the gallery: 115 → 107.5 · 130 → 115 · 145 → 122.5 · 160 → 130). A smaller text never shrinks a control.
6
+ */
7
+ export const controlsFor = (percent) => 100 + Math.max(0, percent - 100) / 2;
8
+ /**
9
+ * The member's text size: `--fs-base` on <html> as a share of the theme's own base (100 = as the theme set it), and with it
10
+ * `--tap-base` — the things grow half as much as the words (`controlsFor`), so a larger text still fits what holds it.
11
+ * The bases are read from the CSS (cardds's default or the project's theme.css) — never copied into app code. Keeping the choice
12
+ * (localStorage, an account) is the app's.
5
13
  */
6
14
  export function setTextScale(percent, root = document.documentElement) {
7
15
  root.style.removeProperty('--fs-base');
16
+ root.style.removeProperty('--tap-base');
8
17
  if (percent === 100)
9
18
  return;
10
- const base = parseFloat(getComputedStyle(root).getPropertyValue('--fs-base')) || 1.375; // rem
11
- root.style.setProperty('--fs-base', `${(base * percent) / 100}rem`);
19
+ const cs = getComputedStyle(root);
20
+ const fs = parseFloat(cs.getPropertyValue('--fs-base')) || 1.375, tap = parseFloat(cs.getPropertyValue('--tap-base')) || 3; // rem
21
+ root.style.setProperty('--fs-base', `${(fs * percent) / 100}rem`);
22
+ const controls = controlsFor(percent);
23
+ if (controls !== 100)
24
+ root.style.setProperty('--tap-base', `${(tap * controls) / 100}rem`);
12
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.44",
3
+ "version": "0.7.46",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",