@usableapp/cardds 0.4.1 → 0.5.0

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 ADDED
@@ -0,0 +1,130 @@
1
+ # Changelog
2
+
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
+
5
+ ## 0.5.0 — 2026-09-22
6
+
7
+ ### Words are full ink; muted means inactive (Lh 2026-09-22)
8
+ `.base-content > .t-body` and `.kv__key` read `--on-surface` / `--card-ink` (were the muted grey). Knobs `--base-content-body-ink`, `--kv-key-ink` if a project wants them back. Muted stays on captions, `t-muted`, and inactive states (`.row--done`).
9
+
10
+ ### Overline retired (Lh 2026-09-22) — BREAKING
11
+ A small caps line above a heading is a second message nobody asked for. Gone: `Text level="overline"` (TypeScript rejects it), `CardHead overline` (drop the prop — a walk's progress is a `Track` in the body, or nothing), `.t-overline`, `--type-overline`, `--track-overline`. The small name over a control ("รหัส OTP") is `Text level="label" muted`. Ring xs, tile-badge label, calendar weekday, timeline mark and quote attribution now read `--type-caption` with no tracking or uppercase (their font knobs are unchanged).
12
+
13
+ ### h1 and h2 one size down (Lh 2026-09-22)
14
+ `--fs-h1` ×1.45 → ×1.2 (32 → 26: the screen title, so more of it fits the top bar) · `--fs-h2` ×1.2 → ×1 (26 → 22: a card's heading, told apart by the display face + weight). `--fs-3xl` (the xl button) stays 32; `--fs-lg`/`--fs-2xl` stay 26. Token names unchanged; a theme that sets `--fs-base` follows.
15
+
16
+ ### TopBar: at most two trailing actions (Lh 2026-09-22)
17
+ A third control in `actions` is no longer drawn (`.topbar__actions > :nth-child(n+3)`), and `TopBar` warns once in the console. A bar that showed three icon buttons now shows two — move the rest into a `Menu` behind one `IconBtn`, or onto the base. Story `TopBar · TooManyActions`.
18
+
19
+ ### `--text-scale` retired — the text-size setting is `--fs-base`
20
+ `--text-scale` multiplied the root font size, so a member's "larger text" zoomed every button, gap and icon too. Gone. An app's text-size setting now writes the type base on `<html>`: `document.documentElement.style.setProperty('--fs-base', '2.2rem')` (1.375rem = 100%, 2.2rem = 160%) — only the words and their holders (count badge, inline icon) grow. Remove `--text-scale` from your app; if a screen relied on the UI growing with the text, that is a gap to report.
21
+
22
+ ### The type scale, rethought (Lh 2026-09-22) — BREAKING to the eye
23
+ The base IS the body: `--fs-base` 1.375rem (22px). Nothing that reads is under 17px. Sizes are a job's fit, not rungs — jobs share a size. Multipliers only, no clamp/vw. 16 tokens → 7 sizes:
24
+ | × | px | tokens |
25
+ |---|---|---|
26
+ | 0.775 | 17 | `--fs-xs` `--fs-caption` `--fs-label` `--fs-sm` (was 11 · 12 · 13 · 14) |
27
+ | 1 | 22 | `--fs-body` `--fs-md` `--fs-title` (was 15 · 16 · 17) |
28
+ | 1.2 | 26 | `--fs-h2` `--fs-lg` `--fs-2xl` (was 22 · 20 · 24) |
29
+ | 1.45 | 32 | `--fs-h1` `--fs-3xl` (was 28 · 32) |
30
+ | 1.8 | 40 | `--fs-display` `--fs-stat` (was clamp 34–42 · 30–38) |
31
+ | 2.5 | 55 | `--fs-hero` (was 56) |
32
+ | 2.9 | 64 | `--fs-giant` (was clamp 48–64) |
33
+ `--badge` = base × 1.2 (26), `--icon-xs` = base × 0.775 (17). A project's `theme.css` that set `--fs-base: 1rem` now gets 16px body — set it to `1.375rem` (the template's new default), or leave it unset. Every token name is unchanged.
34
+ - The sheet's peek strip follows the head's floor `--sheet-head-h` = max(a tap, an h2 line); when a title still wraps (a narrow phone at a large text size) the `Sheet` measures its head and publishes the real strip on its box, so the whole title row stays above the cut.
35
+ - The type page (`Text · Scale`) shows the 7 sizes with both faces and every job that shares each; the `/type` page is gone.
36
+
37
+ - `--field-font` (the words typed into a `Field`, default `--type-body`) and `--composer-input-font` (the Composer's textarea) are knobs now — a project that wants bigger type in its inputs sets them in `theme.css`. Every input already follows `--text-scale` on `<html>` (rem); if yours does not, look for a `px` `font-size` on `input` in your own CSS (the iOS no-zoom rule is the usual one).
38
+ - `.base-content > img` keeps its ratio inside `--base-content-media-h` (`height: auto`).
39
+
40
+ ## 0.4.2 — 2026-09-22
41
+
42
+ ### Words on the base: a picture and the dots
43
+ Screens that only tell (onboarding, a walk of pages) live on the base — `Step › BaseContent` — never in a `Card centre` on an empty base. Two things the base could not hold well are fixed:
44
+ - an `<img>` (or `Placeholder`) among the words is capped by `--base-content-media-h` (14rem) so a tall picture never pushes the words under the bar.
45
+ - `PagerAt` as the last child of a `BaseContent` is centred under the words (it used to sit left).
46
+ Story: `Step · WalksWithArt` — pages of words with a picture and the dots, a slider in the bar's upper tier, the round pager below.
47
+
48
+ Where things go on such a screen (answers given to timebank): a `Postcard` shown with the words is a child of `BaseContent` after them (the picture's slot; never wrapped in a Card) · a summary the system hands over (an address, a ticket) stays a `Card centre` · a walk's progress is the `CardHead` overline ("ขั้นที่ 2 จาก 5") or a `Track` as the first child of `BaseContent` — nothing sits between the TopBar and the Step.
49
+
50
+ ## 0.4.1 — 2026-09-22
51
+
52
+ Breaking renames and removals. No aliases (nobody had installed 0.4.0).
53
+
54
+ ### `CardBack` → `StackBack`
55
+ The back button a tapped stack sheet reveals belongs to the stack, not the card.
56
+ - import: `CardBack` → `StackBack`
57
+ - class (if you styled it): `.card__back` → `.sheet-stack__back`
58
+ - where it goes: unchanged — first child of the open stack sheet's `CardHead`.
59
+
60
+ ### `SheetStage`: `ask` prop removed
61
+ The stage only does the ground; the thing in front places itself.
62
+ - `<SheetStage ask>` → `<SheetStage dim>` — the `Modal` inside centres itself now.
63
+ - A Modal on a clean ground (no scrim) is allowed: `<SheetStage>` with no `dim`.
64
+ - CSS: `.sheet-stage--ask` is gone; `--drawer-h` now lives on `.modal`.
65
+
66
+ ### `Sheet`: `raised` prop removed
67
+ A sheet always stands on the floor and never has a dim behind it.
68
+ - `<SheetStage dim><Sheet raised …>` → that screen is a `Modal`: `<SheetStage dim><Modal><Lift>…</Lift><Drawer>…</Drawer></Modal>`
69
+ - `.sheet--raised`, `--sheet-raised-radius` are gone. `SheetStage foot` (the footnote strip) stays.
70
+
71
+ ### `Postcard` reshaped
72
+ The shape of `Card cover`: the photo fills it, a band at the bottom names it. Not a 3:2 card inside a card any more.
73
+ - props kept: `photo` `alt` `name` `caption` `flipped` `defaultFlipped` `onFlip` `frontLabel` `backLabel`
74
+ - new: `line` (the words under the name, `t-body`) · `liked` + `onLike(liked)` (the member's own heart in the band; no heart without them) · `likeLabel` · `voice` (a voice note instead of words: whatever you put there stands at the middle of the back — the app plays it; see Progress)
75
+ - the band (`name` · `line` · heart) turns WITH the face. The back: `name` a fifth of the way down, `line`, then `caption` (or `voice`).
76
+ - a tap on the photo still turns it. The turn is `.postcard__turn` (a button filling the face); the faces are divs now.
77
+ - Don't wrap a Postcard in a `Card`. Its min-height is the cover's (`--postcard-min-h` 22.5rem); as a `SheetStage hero` it fills the band the sheet leaves open, with the screen's inset at both ends.
78
+ - Who liked it is YOUR data, shown outside the card — see `Row more`.
79
+
80
+ ### `Row`: `more` (new)
81
+ A row that expands: it becomes the summary of a `<details class="row-expand">`, `more` comes out under it.
82
+ - `<Row lead={…} meta="4 คนถูกใจ" end={<Icon name="chevron-down" />} open={open} onOpenChange={setOpen} more={<Rows>…one Row per person…</Rows>} />`
83
+ - `open` + `onOpenChange`, or `defaultOpen`. An `end` chevron turns when open. You fetch the list; the row shows it.
84
+
85
+ ### `Progress` (new)
86
+ One straight line, the done share solid, the rest faint.
87
+ - `<Progress at={0.4} label="0:05 of 0:12" />` — `at` 0–1; you keep the value (a timer, a download).
88
+ - knobs: `--progress-h` · `--progress-radius` · `--progress-done-ink` · `--progress-rest-bg`.
89
+
90
+ ### Stage hero
91
+ `.sheet-stage__hero` is now as tall as the band a half sheet leaves open, less `--screen-pad` at both ends, and what it holds fills it (a child's `min-height` gives way). If your hero relied on its own height, check it.
92
+
93
+ ### Gallery
94
+ One page per thing you name (Card, Sheet, Btn…); inner parts (CardHead, CardBody, SheetBody, MenuItem, BtnRow…) are variations on the parent's page. Nav groups by use. `/Parent/Part` URLs are gone.
95
+
96
+ ## 0.4.0 — 2026-09-21
97
+
98
+ Knobs, open icons, theme bases, gallery. Nothing removed from the component API.
99
+ - **Knobs**: every component reads its look through `--<component>-<what>` custom properties with the default as fallback (363, listed in `docs/knobs.md`). Set one on `:root` (your `theme.css`) to retune everywhere, on any element to retune what is inside it. Declared nowhere: unset = nothing moved.
100
+ - **Icons are open**: `registerIcons({ name: '<path…/>' })` → usable by name on every component that takes an icon; `<Icon>` draws a one-off from children; an unknown name warns once instead of throwing.
101
+ - **Theme bases** (`css/theme.template.css`): four size bases (`--sp-base` `--fs-base` `--r-base` `--tap-base`) and five colour bases (`--color-surface` `--color-ink` `--color-paper` `--color-brand` `--color-on-brand`) on `:root`. A project sets a base, never a multiplier. `<html data-palette="own">` for own colours.
102
+ - `Stat centre`; the pager places what sits between its two buttons in the middle; every component types its `ref`.
103
+
104
+ ## 0.3.0 — 2026-09-21
105
+
106
+ **Components only — the app wires them.** The biggest change since 0.1: every component with a state is controlled like an input (`x` + `onXChange`, or `defaultX`), keeps its own behaviour inside, and none knows another. `cardds.js` no longer does anything between components — it is `--kb` (the keyboard's height) and `.bleed`'s scrollbar, 35 lines. If your screen relied on cardds.js to connect two things, the app connects them now.
107
+
108
+ ### Gone
109
+ - `aria-controls` on a button calling a sheet → the app: `useState` of the sheet's state, the button sets it, `<Sheet state onStateChange>`.
110
+ - events `cardds:sheet` · `cardds:pin` · `cardds:modal` · `cardds:flip`, and the Dropdown's native `change` → the components' own `onXChange` callbacks.
111
+ - `window.carddsSheetSet` / `carddsSheetReveal` / `carddsStepSync`, `--sheet-room`, the bar guard → the Sheet does all of it itself.
112
+ - `data-answer` on Modal buttons → plain `onClick`s; `<Modal open onClosed>`.
113
+ - `IconBtn menu` / `menuId` / `menuStart` → any button opens a popover `Menu` with the platform's `popoverTarget={id}`.
114
+ - `SheetStack mode="tap"` + hand-set `is-open` / `is-before` / `is-after` → `open` (index | null) + `onOpenChange`, or `defaultOpen`; the component paints the classes.
115
+
116
+ ### Each component, before → after
117
+ - **Sheet**: `state="half"` alone used to be a starting point the sheet then moved from; now a fixed `state` PINS it (the sheet asks through `onStateChange` and stays if you don't answer). A sheet the member drags or taps the handle of: `defaultState="half"` (it keeps its own), or `state` + `onStateChange` (you keep it). Own behaviour, inside: handle tap ↔ peek, drag-snap to `states`, the raise when a field in a peeked sheet takes focus, the climb when the keyboard comes up. The `away` state (a called sheet, off-screen) is set-only.
118
+ - **SheetStack**: `open` + `onOpenChange` / `defaultOpen`. A peeked head's tap and the back button only ask.
119
+ - **Modal**: `open` + `onClosed` (it plays its opening backwards, then says so; you unmount it). Buttons are yours.
120
+ - **Menu**: `actions` (plain onClicks) or `value` + `onChange`; picking closes it. **Dropdown**: one component like `<select>`, `value` + `onChange`, the pill named by the chosen item.
121
+ - **Pin**: `value` + `onChange(code, complete)` or `defaultValue`; auto-advance, Backspace, arrows, paste inside. Tests that listened for `cardds:pin` type into the cells instead.
122
+ - **Slider**: publishes its own `--slider-at`. **Postcard**: `flipped` + `onFlip` or `defaultFlipped`.
123
+ - **Calendar**: `selected` + `onSelect` or `defaultSelected`; `onPrev` / `onNext`; label props (its buttons had no handlers before).
124
+ - **Picker**: `value` + `onChange` or `defaultValue` — the value IS the item at the rail's centre (it had no way to report one).
125
+
126
+ Guidance, no longer enforced: "pager or sheet", "a sheet finishes on its foot", "a form lives whole in one sheet".
127
+
128
+ ## 0.2.2 — 2026-09-21
129
+
130
+ One sheet shape: the head stays pinned, the body scrolls without limit, the foot's button stands at the bottom above the keyboard — at every state, on a step and on a stage. Stage sheets ride `--kb` too. (`carddsSheetSet` / `carddsSheetReveal` on `window` — removed again in 0.3.0.)
package/README.md CHANGED
@@ -141,7 +141,7 @@ whose markup has rules, because the sheet's box is the step itself:
141
141
  <!-- reads: the words only -->
142
142
  <section class="step">
143
143
  <section class="base-content">
144
- <div class="card__head"><span class="t-overline t-muted">ขั้นที่ 2 จาก 4</span><h2 class="t-h2">…</h2></div>
144
+ <div class="card__head"><h2 class="t-h2">…</h2></div>
145
145
  <p class="t-body">…</p>
146
146
  </section>
147
147
  </section>
@@ -222,7 +222,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
222
222
  <div class="screen screen--fill">
223
223
  <header class="topbar"><h1 class="t-h1">ธนาคารเวลายินดีต้อนรับ</h1></header>
224
224
  <article class="card card--centre">
225
- <div class="card__head"><div><span class="t-overline t-muted">ขั้นที่ 1 จาก 4</span><h2 class="t-h2">ปรับขนาดตัวหนังสือ</h2></div></div>
225
+ <div class="card__head"><div><h2 class="t-h2">ปรับขนาดตัวหนังสือ</h2></div></div>
226
226
  <p class="t-body">…</p>
227
227
  </article>
228
228
  </div>
@@ -283,10 +283,9 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
283
283
  the far right it runs all the way across. The heading wraps freely —
284
284
  no line budget, no squeeze, no clamp (Lh 2026-09-16): a height rule on
285
285
  words only distorts them. The head is FLAT markup — heading, then an
286
- optional overline / caption as spans, then the far-right thing; no
287
- wrapper element, no `<p>`: the head wraps, the overline takes the row
288
- above and the caption the row below (`order`), the gap tokens do the
289
- rest (Lh 2026-09-16).
286
+ optional caption as a span, then the far-right thing; no
287
+ wrapper element, no `<p>`: the head wraps, the caption takes the row
288
+ below (`order`), the gap tokens do the rest (Lh 2026-09-16).
290
289
  - **Content**: whatever the card frames — rows, key-values, a gauge, a
291
290
  paragraph. Frameless, per rule 9.
292
291
  - **Foot** (`.card__foot`): the card's action buttons on one line. One
@@ -500,8 +499,9 @@ Rules:
500
499
  - **cardds does not judge the colours.** No contrast check, no warning — a
501
500
  theme's values are the project's call. The derivations are only starting points;
502
501
  any role can be set outright.
503
- - `--text-scale` (below) is the member's setting, not the theme's: it scales
504
- everything, the bases included.
502
+ - The member's text-size setting is the same `--fs-base`, written on `<html>`
503
+ by the app at run time (2.2rem = 160%): the words grow, the things keep their
504
+ size. (`--text-scale`, which multiplied the root and zoomed the UI too, is gone.)
505
505
 
506
506
  Try it live: the gallery's nav (`npm run dev`) has a slider per size base, and
507
507
  palette `own` shows a colour well per colour base.
@@ -531,10 +531,9 @@ whole system still resizes from one number: the root font size. `base.css` keeps
531
531
  default) and steps narrow phones (≤360px) to `93.75%`, which keeps the 25rem
532
532
  column, its 48px taps and its type in proportion on 320–360 screens. Only
533
533
  `--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
534
- An app with its own text-size setting publishes it as `--text-scale` on
535
- `<html>` — a unitless number, `1` by default — and `base.css` multiplies it into
536
- the root, so a member's "larger" composes with the narrow-phone step rather
537
- than overwriting it, and the app never writes a length from script.
534
+ An app with its own text-size setting writes `--fs-base` on `<html>` (the type
535
+ base, 1.375rem = 100%; 2.2rem = 160%): only the type and its holders (the count
536
+ badge, the inline icon) grow; taps, spacing and radii keep their size.
538
537
  Components never write a raw length: spacing from `--sp-*`, control sizes from
539
538
  `--tap`, `--tap-lg`, `--btn-h`, `--bar-h`, `--header-h`, `--fab`, `--badge`, container
540
539
  padding from `--screen-pad` / `--card-pad`, radii from
@@ -567,10 +566,12 @@ The two bars across the top of a screen — `.topbar` (screen title) and
567
566
  `.appbar` (app context) — share one rule and one floor, `--header-h` (3.75rem /
568
567
  60px), so a title-only bar stands as tall as one carrying an icon button, a
569
568
  dropdown or a `.btn`. `.screen` reserves its top padding from the same token.
569
+ `.topbar__actions` takes at most TWO controls (Lh 2026-09-22): a third and beyond
570
+ is not drawn (`TopBar` warns once) — more moves go in a `Menu` behind one button.
570
571
 
571
572
  ## Typography levels
572
573
 
573
574
  `t-display` 34–42/800 · `t-h1` 28 · `t-h2` 22 · `t-title` 17 · `t-body` 15 ·
574
- `t-label` 13 · `t-caption` 12 · `t-overline` 11 mono caps · `t-stat` big tabular
575
+ `t-label` 13 · `t-caption` 12 · `t-stat` big tabular
575
576
  numbers (`t-stat--mono`, `t-stat--xl`). Pixel values are at the default root;
576
577
  each is an `--fs-*` rem token. Swap `--font-display` per app for character.
package/css/actions.css CHANGED
@@ -38,6 +38,9 @@
38
38
  margin-block: -0.15em;
39
39
  }
40
40
  .topbar__actions { margin-left: auto; display: flex; gap: var(--sp-2); }
41
+ /* at most TWO trailing controls (Lh 2026-09-22): the title must keep its line; a third and
42
+ beyond is not drawn — more moves belong in a Menu behind one button, or on the base */
43
+ .topbar__actions > :nth-child(n + 3) { display: none; }
41
44
 
42
45
  /* a screen whose one big move sits at the top right (home's "ขอความช่วยเหลือ" — Lh 2026-09-09:
43
46
  no bottom bar on the home screen) puts a primary .btn in the top bar's actions; it stands at
@@ -141,7 +144,9 @@
141
144
  text-decoration: none;
142
145
  justify-self: start; /* as a grid child it hugs its text, never stretches */
143
146
  }
144
- .dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); } /* signal dot: chips, legends */
147
+ .dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); translate: 0 var(--dot-drop, 0.09em); } /* signal dot: chips, legends.
148
+ The drop: a line box is centred on its ascender room (Thai marks), the letters beside the dot sit lower — the dot rides down to the
149
+ letters' own middle, in em so it follows the text size (Lh 2026-09-22: "ตัวหนังสือกับไฟเสมอกัน") */
145
150
  .dot--b { background: var(--dot-b); }
146
151
  .chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
147
152
  /* dark-slot inversion lives in card.css (.card--3 :is(...)) */
package/css/base.css CHANGED
@@ -16,14 +16,13 @@ button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn
16
16
  }
17
17
 
18
18
  /* the scale: everything is rem, so this one number resizes the whole system.
19
- 100% = the user's text size (16px by default); narrow phones step to 15/16 so
19
+ 100% = the browser's text size (16px by default); narrow phones step to 15/16 so
20
20
  the 25rem column and its 48px taps keep their proportions on 320–360 screens.
21
- --text-scale is an app's OWN text-size setting — a unitless number the app
22
- writes on <html> (1 = as the browser has it; 1.1875 = larger). It multiplies
23
- in here so it composes with the narrow-phone step instead of overwriting the
24
- root, and so the app never writes a length from script. */
25
- html { font-size: calc(100% * var(--text-scale, 1)); scrollbar-width: none; }
26
- @media (max-width: 22.5em) { html { font-size: calc(93.75% * var(--text-scale, 1)); } }
21
+ An app's OWN text-size setting is NOT here (Lh 2026-09-22: --text-scale, which
22
+ multiplied the root, zoomed the UI too — retired): it is --fs-base on <html>,
23
+ the type base, so the words grow and the things keep their size (tokens.css). */
24
+ html { font-size: 100%; scrollbar-width: none; }
25
+ @media (max-width: 22.5em) { html { font-size: 93.75%; } }
27
26
  html::-webkit-scrollbar { display: none; } /* Chromium/Safari */
28
27
 
29
28
  body {
@@ -96,7 +95,6 @@ button { font: inherit; cursor: pointer; }
96
95
  .t-body { font: var(--type-body); margin: 0; }
97
96
  .t-label { font: var(--type-label); }
98
97
  .t-caption { font: var(--type-caption); }
99
- .t-overline { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; }
100
98
  .t-stat { font: var(--type-stat); letter-spacing: var(--track-display); margin: 0; font-variant-numeric: tabular-nums; }
101
99
  .t-stat--mono { font-family: var(--font-mono); letter-spacing: 0; } /* clocks, ETAs, codes */
102
100
  .t-stat--xl { font-size: var(--fs-hero); } /* the one number on a sheet */
@@ -108,7 +106,7 @@ button { font: inherit; cursor: pointer; }
108
106
  .grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
109
107
 
110
108
  /* ---- base content: words straight on the base, no card around them — an
111
- overline, a heading, a paragraph, maybe a .cta-pack. Was .section until
109
+ a heading, a paragraph, maybe a .cta-pack. Was .section until
112
110
  2026-09-16 (Lh: the name said nothing about WHERE it lives; this one does).
113
111
  In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
114
112
  .base-content {
@@ -117,7 +115,12 @@ button { font: inherit; cursor: pointer; }
117
115
  align-content: start; /* a tall column never stretches the rows — the words hug the top */
118
116
  padding-block: var(--base-content-pad-y, var(--sp-4));
119
117
  }
120
- .base-content > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
118
+ .base-content > .t-body { color: var(--base-content-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
119
+ /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
120
+ the "where am I" dots under the words, centred — the same dots the centred card has under it */
121
+ .base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
122
+ .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
123
+ .base-content > .pager__at { justify-self: center; }
121
124
  .base-content .cta-pack {
122
125
  display: flex;
123
126
  align-items: start; /* buttons keep their own height, never the row's */
package/css/card.css CHANGED
@@ -39,19 +39,18 @@
39
39
  multi-line title can opt back out with .card__head--top. */
40
40
  .card__head {
41
41
  display: flex;
42
- flex-wrap: wrap; /* the caption / overline take a line of their own — no wrapper div */
42
+ flex-wrap: wrap; /* the caption takes a line of its own — no wrapper div */
43
43
  align-items: center;
44
44
  justify-content: space-between;
45
45
  column-gap: var(--card-head-col-gap, var(--sp-3));
46
46
  row-gap: var(--card-head-row-gap, var(--sp-0));
47
47
  }
48
48
  /* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
49
- <div class="card__head"><span class="t-overline t-muted">…</span><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
50
- The heading and the far-right thing share the first row; an overline sits
51
- on a row above (order −1), a caption on a row below (order 1), whatever the
52
- DOM order — so an author writes the words in reading order. */
53
- .card__head > :is(.t-overline, .t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
54
- .card__head > .t-overline { order: -1; }
49
+ <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
50
+ The heading and the far-right thing share the first row; a caption sits on
51
+ a row below (order 1), whatever the DOM order — so an author writes the
52
+ words in reading order. */
53
+ .card__head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
55
54
  .card__head--top { align-items: flex-start; }
56
55
 
57
56
  .card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
@@ -73,7 +72,7 @@
73
72
  /* Skeleton head — the heading in a card's OWN head, and only there.
74
73
  The heading wraps freely: no line budget, no squeeze, no clamp (Lh
75
74
  2026-09-16 — a height rule on words only distorts them; content that needs
76
- more rows takes more rows). The group that holds overline · heading ·
75
+ more rows takes more rows). The group that holds heading ·
77
76
  caption is the head's own wrapping row (above) — no wrapper element, no
78
77
  height, only the row gap. */
79
78
  .card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
package/css/choice.css CHANGED
@@ -138,7 +138,7 @@
138
138
  .calendar { display: grid; gap: var(--calendar-gap, var(--sp-3)); }
139
139
  .calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--calendar-head-gap, var(--sp-2)); }
140
140
  .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; }
141
- .calendar__wd { font: var(--calendar-wd-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; color: var(--calendar-wd-ink, var(--card-muted)); padding-block: var(--calendar-wd-pad-y, var(--sp-2)); }
141
+ .calendar__wd { font: var(--calendar-wd-font, var(--type-caption)); color: var(--calendar-wd-ink, var(--card-muted)); padding-block: var(--calendar-wd-pad-y, var(--sp-2)); }
142
142
  .card--3 .calendar__wd { color: inherit; opacity: 0.7; }
143
143
  .calendar__day {
144
144
  width: 100%;
@@ -221,7 +221,7 @@
221
221
  padding: 0;
222
222
  background: transparent;
223
223
  color: inherit;
224
- font: 700 var(--fs-2xl)/1.25 var(--font-display);
224
+ font: var(--composer-input-font, 700 var(--fs-2xl)/1.25 var(--font-display));
225
225
  letter-spacing: var(--track-h2);
226
226
  outline: none;
227
227
  }
package/css/forms.css CHANGED
@@ -22,7 +22,7 @@
22
22
  border: 0;
23
23
  background: transparent;
24
24
  color: inherit;
25
- font: var(--type-body);
25
+ font: var(--field-font, var(--type-body));
26
26
  width: 100%;
27
27
  min-width: 0;
28
28
  outline-offset: var(--sp-1);
package/css/journey.css CHANGED
@@ -34,7 +34,7 @@
34
34
  background: var(--tile-badge-bg, var(--card-3-bg));
35
35
  color: var(--tile-badge-ink, var(--card-3-ink));
36
36
  }
37
- .tile-badge__label { font: var(--tile-badge-label-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; opacity: 0.7; }
37
+ .tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); opacity: 0.7; }
38
38
  .tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
39
39
 
40
40
  /* plain: no fill — a date box beside a title (Jun / 18) */
package/css/lists.css CHANGED
@@ -82,8 +82,7 @@
82
82
  .kv { margin: 0; display: grid; }
83
83
  .kv__row { display: flex; align-items: center; justify-content: space-between; gap: var(--kv-row-gap, var(--sp-3)); min-height: var(--kv-row-min-h, var(--tap)); }
84
84
  .kv--lined > .kv__row + .kv__row { border-top: var(--border-w) solid var(--hairline); }
85
- .kv__key { font: var(--kv-key-font, var(--type-body)); color: var(--kv-key-ink, var(--card-muted)); }
86
- .card--3 .kv__key { color: inherit; opacity: 0.7; }
85
+ .kv__key { font: var(--kv-key-font, var(--type-body)); color: var(--kv-key-ink, var(--card-ink)); } /* full ink (Lh 2026-09-22): a key is read like the value; muted = inactive, not "secondary" */
87
86
  .kv__val { margin: 0; font: var(--kv-val-font, var(--type-title)); display: inline-flex; align-items: center; gap: var(--kv-val-gap, var(--sp-2)); text-align: right; min-width: 0; }
88
87
 
89
88
  /* ---- kv grid: value over label, cells split by a rule (Sex | Weight) ----
@@ -177,8 +176,7 @@
177
176
  }
178
177
  .timeline__mark {
179
178
  justify-self: start;
180
- font: var(--timeline-mark-font, var(--type-overline));
181
- letter-spacing: var(--track-overline);
179
+ font: var(--timeline-mark-font, var(--type-caption));
182
180
  text-transform: uppercase;
183
181
  padding: var(--timeline-mark-pad-y, var(--sp-1)) var(--timeline-mark-pad-x, var(--sp-2));
184
182
  border-radius: var(--timeline-mark-radius, var(--r-chip));
package/css/media.css CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  /* ---- cover: the image IS the card; text over a scrim ----
7
7
  <article class="card card--cover"><div class="ph" role="img"></div>
8
- <div class="card__head"><span class="t-overline">Featured</span><button class="icon-btn icon-btn--sm">♥</button></div>
8
+ <div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
9
9
  <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
10
10
  <div class="row">…meta…</div></article> */
11
11
  .card--cover {
@@ -30,7 +30,7 @@
30
30
  border-radius: 0;
31
31
  min-height: 0;
32
32
  }
33
- /* scrim: dark at both ends so overline and meta stay readable */
33
+ /* scrim: dark at both ends so the meta stays readable */
34
34
  .card--cover::before {
35
35
  content: "";
36
36
  position: absolute;
@@ -52,7 +52,7 @@
52
52
  }
53
53
  .quote__text { margin: 0; font: var(--quote-text-font, var(--type-h1)); letter-spacing: var(--track-h1); text-wrap: balance; }
54
54
  .quote--sm .quote__text { font: var(--type-h2); letter-spacing: var(--track-h2); }
55
- .quote__by { font: var(--quote-by-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; color: var(--quote-by-ink, var(--card-muted)); }
55
+ .quote__by { font: var(--quote-by-font, var(--type-caption)); color: var(--quote-by-ink, var(--card-muted)); }
56
56
  .card--3 .quote__by { color: inherit; opacity: 0.7; }
57
57
 
58
58
  /* ---- tile grid: big square tiles — icon top, label bottom ----
package/css/numbers.css CHANGED
@@ -37,7 +37,7 @@
37
37
  .ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
38
38
  .ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
39
39
  .ring--xs { --_size: var(--tap-sm); --_w: var(--dot-xs); } /* inline counter (step 3 of 5) */
40
- .ring--xs .ring__value { font: var(--type-overline); }
40
+ .ring--xs .ring__value { font: var(--type-caption); }
41
41
 
42
42
  /* ---- track: step progress in a pill — done · done · todo ----
43
43
  <ol class="track"><li class="track__step is-done">icon</li> …</ol> */
package/css/sheet.css CHANGED
@@ -89,7 +89,10 @@
89
89
  /* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
90
90
  have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
91
91
  inherits it. A sheet whose head is taller retunes it on the box. */
92
- :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--tap)); }
92
+ :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h)); }
93
+ /* the head's floor: a tap, or an h2 line with its Thai-mark padding when the type is bigger than that (a large text-size
94
+ setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
95
+ :root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
93
96
 
94
97
  /* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
95
98
  .sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
@@ -201,7 +204,7 @@
201
204
  - a label .row with its .check / .toggle may be the head when the sheet's
202
205
  whole ask is that switch: peek shows it (a row is --tap tall plus its own
203
206
  padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
204
- .sheet > .card__head { min-height: var(--tap); }
207
+ .sheet > .card__head { min-height: var(--sheet-head-h); }
205
208
  .sheet__head {
206
209
  display: grid;
207
210
  grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
@@ -23,11 +23,11 @@
23
23
  :root {
24
24
  /* ---- size: four bases, the multipliers stay the system's ---- */
25
25
  --sp-base: 0.25rem; /* 4 the room BETWEEN things: every --sp-N = base × N, so paddings and gaps */
26
- --fs-base: 1rem; /* 16 the type: every --fs-*, the count badge, the inline icon */
26
+ --fs-base: 1.375rem; /* 22 the type — the body size; every --fs-* is a multiplier of it (17 · 22 · 26 · 32 · 40 · 55 · 64), the count badge, the inline icon */
27
27
  --r-base: 1rem; /* 16 the corners: every --r-* (0 = square; chips and buttons stay fully round) */
28
28
  --tap-base: 3rem; /* 48 the size of THINGS: touch targets, control heights, icons, avatars, ring, tiles
29
29
  (a touch target never goes under 44px, whatever this says) */
30
- /* the whole system at once (a member's text-size setting) is --text-scale, a unitless number — not a theme's job */
30
+ /* a member's text-size setting is this same --fs-base, written on <html> by the app at run time (1.375rem = 100%, 2.2rem = 160%) — the words grow, the things keep their size */
31
31
 
32
32
  /* ---- faces ---- */
33
33
  --font-ui: "PK Nonthaburi", -apple-system, system-ui, sans-serif;
package/css/tokens.css CHANGED
@@ -20,7 +20,8 @@
20
20
  and never a multiplier, so the proportions of the system hold at any size.
21
21
  The px in the comments are at these defaults (1rem = 16px). */
22
22
  --sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
23
- --fs-base: 1rem; /* 16 type: --fs-* */
23
+ --fs-base: 1.375rem; /* 22 type: the BODY is the base (Lh 2026-09-22) — every --fs-* is a multiplier of it; nothing under 0.775 (17).
24
+ Also the member's text-size setting: the app writes it on <html> at run time (2.2rem = 160%) — words grow, things don't */
24
25
  --r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
25
26
  --tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
26
27
  --header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
@@ -54,7 +55,7 @@
54
55
  --screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
55
56
  --map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
56
57
  --fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
57
- --badge: calc(var(--fs-base) * 1.625); /* 26 count badge */
58
+ --badge: calc(var(--fs-base) * 1.2); /* 26 count badge — holds a 17px number */
58
59
  --dot: calc(var(--tap-base) * 5 / 24); /* 10 signal dot */
59
60
  --dot-xs: calc(var(--tap-base) / 8); /* 6 marker dot */
60
61
 
@@ -62,7 +63,7 @@
62
63
  --icon: calc(var(--tap-base) * 7 / 12); /* 28 default */
63
64
  --icon-sm: calc(var(--tap-base) * 7 / 12); /* one size everywhere; retune if small contexts appear */
64
65
  --icon-lg: calc(var(--tap-base) * 2 / 3); /* 32 empty states */
65
- --icon-xs: var(--fs-base); /* 16 inline with captions/chips only (row meta, link arrows) */
66
+ --icon-xs: calc(var(--fs-base) * 0.775); /* 17 inline with captions/chips only (row meta, link arrows) — the caption's size */
66
67
 
67
68
  /* ---- people & big elements ---- */
68
69
  --avatar-sm: calc(var(--tap-base) * 5 / 6); /* 40 row lead */
@@ -100,22 +101,24 @@
100
101
  --font-display: "FC Pride", var(--font-ui); /* the character face for headings and big numbers (fonts.css) */
101
102
  --font-mono: "SF Mono", "JetBrains Mono", ui-monospace, monospace;
102
103
 
103
- --fs-xs: calc(var(--fs-base) * 0.6875); /* 11 overline */
104
- --fs-caption: calc(var(--fs-base) * 0.75); /* 12 */
105
- --fs-label: calc(var(--fs-base) * 0.8125); /* 13 */
106
- --fs-sm: calc(var(--fs-base) * 0.875); /* 14 chips, segments */
107
- --fs-body: calc(var(--fs-base) * 0.9375); /* 15 */
108
- --fs-md: var(--fs-base); /* 16 buttons */
109
- --fs-title: calc(var(--fs-base) * 1.0625); /* 17 */
110
- --fs-lg: calc(var(--fs-base) * 1.25); /* 20 day numbers, tile values */
111
- --fs-h2: calc(var(--fs-base) * 1.375); /* 22 */
112
- --fs-2xl: calc(var(--fs-base) * 1.5); /* 24 composer, small ring */
113
- --fs-h1: calc(var(--fs-base) * 1.75); /* 28 */
114
- --fs-3xl: calc(var(--fs-base) * 2); /* 32 xl button */
115
- --fs-display: clamp(calc(var(--fs-base) * 2.125), 9vw, calc(var(--fs-base) * 2.625)); /* 34–42 */
116
- --fs-stat: clamp(calc(var(--fs-base) * 1.875), 8vw, calc(var(--fs-base) * 2.375)); /* 30–38 big numbers */
117
- --fs-hero: calc(var(--fs-base) * 3.5); /* 56 the one number on a sheet */
118
- --fs-giant: clamp(calc(var(--fs-base) * 3), 15vw, calc(var(--fs-base) * 4)); /* 48–64 data bands */
104
+ /* THE SCALE (Lh 2026-09-22): 7 sizes for 16 jobs — a size is a job's fit, not a rung; jobs share sizes.
105
+ Nothing that reads is under 17 (the smallest ui: chips). No clamp/vw: a size is one number. */
106
+ --fs-xs: calc(var(--fs-base) * 0.775); /* 17 the smallest ui words: ring xs, a tile-badge label */
107
+ --fs-caption: calc(var(--fs-base) * 0.775); /* 17 */
108
+ --fs-label: calc(var(--fs-base) * 0.775); /* 17 chips, buttons' small words */
109
+ --fs-sm: calc(var(--fs-base) * 0.775); /* 17 chips, segments — the floor */
110
+ --fs-body: var(--fs-base); /* 22 body, the words in a field */
111
+ --fs-md: var(--fs-base); /* 22 buttons, fields */
112
+ --fs-title: var(--fs-base); /* 22 row title (weight tells it apart) */
113
+ --fs-h2: var(--fs-base); /* 22 a card's heading (Lh 2026-09-22: one size down — the display face + weight tell it) */
114
+ --fs-lg: calc(var(--fs-base) * 1.2); /* 26 day numbers, tile values */
115
+ --fs-2xl: calc(var(--fs-base) * 1.2); /* 26 composer, small ring */
116
+ --fs-h1: calc(var(--fs-base) * 1.2); /* 26 screen title (Lh 2026-09-22: was 1.45 — a smaller title = more of it fits the bar) */
117
+ --fs-3xl: calc(var(--fs-base) * 1.45); /* 32 xl button */
118
+ --fs-display: calc(var(--fs-base) * 1.8); /* 40 hero words */
119
+ --fs-stat: calc(var(--fs-base) * 1.8); /* 40 big numbers */
120
+ --fs-hero: calc(var(--fs-base) * 2.5); /* 55 the one number on a sheet */
121
+ --fs-giant: calc(var(--fs-base) * 2.9); /* 64 data bands */
119
122
 
120
123
  /* level weight size / line-height family */
121
124
  --type-display: 800 var(--fs-display) / 1.15 var(--font-display); /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
@@ -128,8 +131,6 @@
128
131
  --type-body: 400 var(--fs-body) / 1.5 var(--font-ui);
129
132
  --type-label: 600 var(--fs-label) / 1.2 var(--font-ui); /* chips, buttons */
130
133
  --type-caption: 500 var(--fs-caption) / 1.35 var(--font-ui);
131
- --type-overline:700 var(--fs-xs) / 1.2 var(--font-mono); /* uppercase micro-labels */
132
- --track-overline: 0.08em;
133
134
  --type-stat: 800 var(--fs-stat) / 1.05 var(--font-display); /* big numbers */
134
135
 
135
136
  /* ============================================================
@@ -208,5 +209,6 @@
208
209
  falls back to when its own declaration cannot resolve.
209
210
  ============================================================ */
210
211
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
212
+ @property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; } /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
211
213
  @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
212
214
  @property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
package/dist/cardds.css CHANGED
@@ -61,7 +61,8 @@
61
61
  and never a multiplier, so the proportions of the system hold at any size.
62
62
  The px in the comments are at these defaults (1rem = 16px). */
63
63
  --sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
64
- --fs-base: 1rem; /* 16 type: --fs-* */
64
+ --fs-base: 1.375rem; /* 22 type: the BODY is the base (Lh 2026-09-22) — every --fs-* is a multiplier of it; nothing under 0.775 (17).
65
+ Also the member's text-size setting: the app writes it on <html> at run time (2.2rem = 160%) — words grow, things don't */
65
66
  --r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
66
67
  --tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
67
68
  --header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
@@ -95,7 +96,7 @@
95
96
  --screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
96
97
  --map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
97
98
  --fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
98
- --badge: calc(var(--fs-base) * 1.625); /* 26 count badge */
99
+ --badge: calc(var(--fs-base) * 1.2); /* 26 count badge — holds a 17px number */
99
100
  --dot: calc(var(--tap-base) * 5 / 24); /* 10 signal dot */
100
101
  --dot-xs: calc(var(--tap-base) / 8); /* 6 marker dot */
101
102
 
@@ -103,7 +104,7 @@
103
104
  --icon: calc(var(--tap-base) * 7 / 12); /* 28 default */
104
105
  --icon-sm: calc(var(--tap-base) * 7 / 12); /* one size everywhere; retune if small contexts appear */
105
106
  --icon-lg: calc(var(--tap-base) * 2 / 3); /* 32 empty states */
106
- --icon-xs: var(--fs-base); /* 16 inline with captions/chips only (row meta, link arrows) */
107
+ --icon-xs: calc(var(--fs-base) * 0.775); /* 17 inline with captions/chips only (row meta, link arrows) — the caption's size */
107
108
 
108
109
  /* ---- people & big elements ---- */
109
110
  --avatar-sm: calc(var(--tap-base) * 5 / 6); /* 40 row lead */
@@ -141,22 +142,24 @@
141
142
  --font-display: "FC Pride", var(--font-ui); /* the character face for headings and big numbers (fonts.css) */
142
143
  --font-mono: "SF Mono", "JetBrains Mono", ui-monospace, monospace;
143
144
 
144
- --fs-xs: calc(var(--fs-base) * 0.6875); /* 11 overline */
145
- --fs-caption: calc(var(--fs-base) * 0.75); /* 12 */
146
- --fs-label: calc(var(--fs-base) * 0.8125); /* 13 */
147
- --fs-sm: calc(var(--fs-base) * 0.875); /* 14 chips, segments */
148
- --fs-body: calc(var(--fs-base) * 0.9375); /* 15 */
149
- --fs-md: var(--fs-base); /* 16 buttons */
150
- --fs-title: calc(var(--fs-base) * 1.0625); /* 17 */
151
- --fs-lg: calc(var(--fs-base) * 1.25); /* 20 day numbers, tile values */
152
- --fs-h2: calc(var(--fs-base) * 1.375); /* 22 */
153
- --fs-2xl: calc(var(--fs-base) * 1.5); /* 24 composer, small ring */
154
- --fs-h1: calc(var(--fs-base) * 1.75); /* 28 */
155
- --fs-3xl: calc(var(--fs-base) * 2); /* 32 xl button */
156
- --fs-display: clamp(calc(var(--fs-base) * 2.125), 9vw, calc(var(--fs-base) * 2.625)); /* 34–42 */
157
- --fs-stat: clamp(calc(var(--fs-base) * 1.875), 8vw, calc(var(--fs-base) * 2.375)); /* 30–38 big numbers */
158
- --fs-hero: calc(var(--fs-base) * 3.5); /* 56 the one number on a sheet */
159
- --fs-giant: clamp(calc(var(--fs-base) * 3), 15vw, calc(var(--fs-base) * 4)); /* 48–64 data bands */
145
+ /* THE SCALE (Lh 2026-09-22): 7 sizes for 16 jobs — a size is a job's fit, not a rung; jobs share sizes.
146
+ Nothing that reads is under 17 (the smallest ui: chips). No clamp/vw: a size is one number. */
147
+ --fs-xs: calc(var(--fs-base) * 0.775); /* 17 the smallest ui words: ring xs, a tile-badge label */
148
+ --fs-caption: calc(var(--fs-base) * 0.775); /* 17 */
149
+ --fs-label: calc(var(--fs-base) * 0.775); /* 17 chips, buttons' small words */
150
+ --fs-sm: calc(var(--fs-base) * 0.775); /* 17 chips, segments — the floor */
151
+ --fs-body: var(--fs-base); /* 22 body, the words in a field */
152
+ --fs-md: var(--fs-base); /* 22 buttons, fields */
153
+ --fs-title: var(--fs-base); /* 22 row title (weight tells it apart) */
154
+ --fs-h2: var(--fs-base); /* 22 a card's heading (Lh 2026-09-22: one size down — the display face + weight tell it) */
155
+ --fs-lg: calc(var(--fs-base) * 1.2); /* 26 day numbers, tile values */
156
+ --fs-2xl: calc(var(--fs-base) * 1.2); /* 26 composer, small ring */
157
+ --fs-h1: calc(var(--fs-base) * 1.2); /* 26 screen title (Lh 2026-09-22: was 1.45 — a smaller title = more of it fits the bar) */
158
+ --fs-3xl: calc(var(--fs-base) * 1.45); /* 32 xl button */
159
+ --fs-display: calc(var(--fs-base) * 1.8); /* 40 hero words */
160
+ --fs-stat: calc(var(--fs-base) * 1.8); /* 40 big numbers */
161
+ --fs-hero: calc(var(--fs-base) * 2.5); /* 55 the one number on a sheet */
162
+ --fs-giant: calc(var(--fs-base) * 2.9); /* 64 data bands */
160
163
 
161
164
  /* level weight size / line-height family */
162
165
  --type-display: 800 var(--fs-display) / 1.15 var(--font-display); /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
@@ -169,8 +172,6 @@
169
172
  --type-body: 400 var(--fs-body) / 1.5 var(--font-ui);
170
173
  --type-label: 600 var(--fs-label) / 1.2 var(--font-ui); /* chips, buttons */
171
174
  --type-caption: 500 var(--fs-caption) / 1.35 var(--font-ui);
172
- --type-overline:700 var(--fs-xs) / 1.2 var(--font-mono); /* uppercase micro-labels */
173
- --track-overline: 0.08em;
174
175
  --type-stat: 800 var(--fs-stat) / 1.05 var(--font-display); /* big numbers */
175
176
 
176
177
  /* ============================================================
@@ -249,6 +250,7 @@
249
250
  falls back to when its own declaration cannot resolve.
250
251
  ============================================================ */
251
252
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
253
+ @property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; } /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
252
254
  @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
253
255
  @property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
254
256
 
@@ -390,14 +392,13 @@ button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn
390
392
  }
391
393
 
392
394
  /* the scale: everything is rem, so this one number resizes the whole system.
393
- 100% = the user's text size (16px by default); narrow phones step to 15/16 so
395
+ 100% = the browser's text size (16px by default); narrow phones step to 15/16 so
394
396
  the 25rem column and its 48px taps keep their proportions on 320–360 screens.
395
- --text-scale is an app's OWN text-size setting — a unitless number the app
396
- writes on <html> (1 = as the browser has it; 1.1875 = larger). It multiplies
397
- in here so it composes with the narrow-phone step instead of overwriting the
398
- root, and so the app never writes a length from script. */
399
- html { font-size: calc(100% * var(--text-scale, 1)); scrollbar-width: none; }
400
- @media (max-width: 22.5em) { html { font-size: calc(93.75% * var(--text-scale, 1)); } }
397
+ An app's OWN text-size setting is NOT here (Lh 2026-09-22: --text-scale, which
398
+ multiplied the root, zoomed the UI too — retired): it is --fs-base on <html>,
399
+ the type base, so the words grow and the things keep their size (tokens.css). */
400
+ html { font-size: 100%; scrollbar-width: none; }
401
+ @media (max-width: 22.5em) { html { font-size: 93.75%; } }
401
402
  html::-webkit-scrollbar { display: none; } /* Chromium/Safari */
402
403
 
403
404
  body {
@@ -470,7 +471,6 @@ button { font: inherit; cursor: pointer; }
470
471
  .t-body { font: var(--type-body); margin: 0; }
471
472
  .t-label { font: var(--type-label); }
472
473
  .t-caption { font: var(--type-caption); }
473
- .t-overline { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; }
474
474
  .t-stat { font: var(--type-stat); letter-spacing: var(--track-display); margin: 0; font-variant-numeric: tabular-nums; }
475
475
  .t-stat--mono { font-family: var(--font-mono); letter-spacing: 0; } /* clocks, ETAs, codes */
476
476
  .t-stat--xl { font-size: var(--fs-hero); } /* the one number on a sheet */
@@ -482,7 +482,7 @@ button { font: inherit; cursor: pointer; }
482
482
  .grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
483
483
 
484
484
  /* ---- base content: words straight on the base, no card around them — an
485
- overline, a heading, a paragraph, maybe a .cta-pack. Was .section until
485
+ a heading, a paragraph, maybe a .cta-pack. Was .section until
486
486
  2026-09-16 (Lh: the name said nothing about WHERE it lives; this one does).
487
487
  In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
488
488
  .base-content {
@@ -491,7 +491,12 @@ button { font: inherit; cursor: pointer; }
491
491
  align-content: start; /* a tall column never stretches the rows — the words hug the top */
492
492
  padding-block: var(--base-content-pad-y, var(--sp-4));
493
493
  }
494
- .base-content > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
494
+ .base-content > .t-body { color: var(--base-content-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
495
+ /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
496
+ the "where am I" dots under the words, centred — the same dots the centred card has under it */
497
+ .base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
498
+ .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
499
+ .base-content > .pager__at { justify-self: center; }
495
500
  .base-content .cta-pack {
496
501
  display: flex;
497
502
  align-items: start; /* buttons keep their own height, never the row's */
@@ -556,19 +561,18 @@ button { font: inherit; cursor: pointer; }
556
561
  multi-line title can opt back out with .card__head--top. */
557
562
  .card__head {
558
563
  display: flex;
559
- flex-wrap: wrap; /* the caption / overline take a line of their own — no wrapper div */
564
+ flex-wrap: wrap; /* the caption takes a line of its own — no wrapper div */
560
565
  align-items: center;
561
566
  justify-content: space-between;
562
567
  column-gap: var(--card-head-col-gap, var(--sp-3));
563
568
  row-gap: var(--card-head-row-gap, var(--sp-0));
564
569
  }
565
570
  /* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
566
- <div class="card__head"><span class="t-overline t-muted">…</span><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
567
- The heading and the far-right thing share the first row; an overline sits
568
- on a row above (order −1), a caption on a row below (order 1), whatever the
569
- DOM order — so an author writes the words in reading order. */
570
- .card__head > :is(.t-overline, .t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
571
- .card__head > .t-overline { order: -1; }
571
+ <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
572
+ The heading and the far-right thing share the first row; a caption sits on
573
+ a row below (order 1), whatever the DOM order — so an author writes the
574
+ words in reading order. */
575
+ .card__head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
572
576
  .card__head--top { align-items: flex-start; }
573
577
 
574
578
  .card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
@@ -590,7 +594,7 @@ button { font: inherit; cursor: pointer; }
590
594
  /* Skeleton head — the heading in a card's OWN head, and only there.
591
595
  The heading wraps freely: no line budget, no squeeze, no clamp (Lh
592
596
  2026-09-16 — a height rule on words only distorts them; content that needs
593
- more rows takes more rows). The group that holds overline · heading ·
597
+ more rows takes more rows). The group that holds heading ·
594
598
  caption is the head's own wrapping row (above) — no wrapper element, no
595
599
  height, only the row gap. */
596
600
  .card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
@@ -1169,7 +1173,10 @@ button { font: inherit; cursor: pointer; }
1169
1173
  /* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
1170
1174
  have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
1171
1175
  inherits it. A sheet whose head is taller retunes it on the box. */
1172
- :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--tap)); }
1176
+ :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h)); }
1177
+ /* the head's floor: a tap, or an h2 line with its Thai-mark padding when the type is bigger than that (a large text-size
1178
+ setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
1179
+ :root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
1173
1180
 
1174
1181
  /* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
1175
1182
  .sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
@@ -1281,7 +1288,7 @@ button { font: inherit; cursor: pointer; }
1281
1288
  - a label .row with its .check / .toggle may be the head when the sheet's
1282
1289
  whole ask is that switch: peek shows it (a row is --tap tall plus its own
1283
1290
  padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
1284
- .sheet > .card__head { min-height: var(--tap); }
1291
+ .sheet > .card__head { min-height: var(--sheet-head-h); }
1285
1292
  .sheet__head {
1286
1293
  display: grid;
1287
1294
  grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
@@ -1563,7 +1570,7 @@ button { font: inherit; cursor: pointer; }
1563
1570
  border: 0;
1564
1571
  background: transparent;
1565
1572
  color: inherit;
1566
- font: var(--type-body);
1573
+ font: var(--field-font, var(--type-body));
1567
1574
  width: 100%;
1568
1575
  min-width: 0;
1569
1576
  outline-offset: var(--sp-1);
@@ -1641,7 +1648,7 @@ button { font: inherit; cursor: pointer; }
1641
1648
  background: var(--tile-badge-bg, var(--card-3-bg));
1642
1649
  color: var(--tile-badge-ink, var(--card-3-ink));
1643
1650
  }
1644
- .tile-badge__label { font: var(--tile-badge-label-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; opacity: 0.7; }
1651
+ .tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); opacity: 0.7; }
1645
1652
  .tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
1646
1653
 
1647
1654
  /* plain: no fill — a date box beside a title (Jun / 18) */
@@ -1702,6 +1709,9 @@ button { font: inherit; cursor: pointer; }
1702
1709
  margin-block: -0.15em;
1703
1710
  }
1704
1711
  .topbar__actions { margin-left: auto; display: flex; gap: var(--sp-2); }
1712
+ /* at most TWO trailing controls (Lh 2026-09-22): the title must keep its line; a third and
1713
+ beyond is not drawn — more moves belong in a Menu behind one button, or on the base */
1714
+ .topbar__actions > :nth-child(n + 3) { display: none; }
1705
1715
 
1706
1716
  /* a screen whose one big move sits at the top right (home's "ขอความช่วยเหลือ" — Lh 2026-09-09:
1707
1717
  no bottom bar on the home screen) puts a primary .btn in the top bar's actions; it stands at
@@ -1805,7 +1815,9 @@ button { font: inherit; cursor: pointer; }
1805
1815
  text-decoration: none;
1806
1816
  justify-self: start; /* as a grid child it hugs its text, never stretches */
1807
1817
  }
1808
- .dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); } /* signal dot: chips, legends */
1818
+ .dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); translate: 0 var(--dot-drop, 0.09em); } /* signal dot: chips, legends.
1819
+ The drop: a line box is centred on its ascender room (Thai marks), the letters beside the dot sit lower — the dot rides down to the
1820
+ letters' own middle, in em so it follows the text size (Lh 2026-09-22: "ตัวหนังสือกับไฟเสมอกัน") */
1809
1821
  .dot--b { background: var(--dot-b); }
1810
1822
  .chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
1811
1823
  /* dark-slot inversion lives in card.css (.card--3 :is(...)) */
@@ -1995,7 +2007,7 @@ button { font: inherit; cursor: pointer; }
1995
2007
  .ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
1996
2008
  .ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
1997
2009
  .ring--xs { --_size: var(--tap-sm); --_w: var(--dot-xs); } /* inline counter (step 3 of 5) */
1998
- .ring--xs .ring__value { font: var(--type-overline); }
2010
+ .ring--xs .ring__value { font: var(--type-caption); }
1999
2011
 
2000
2012
  /* ---- track: step progress in a pill — done · done · todo ----
2001
2013
  <ol class="track"><li class="track__step is-done">icon</li> …</ol> */
@@ -2453,7 +2465,7 @@ button { font: inherit; cursor: pointer; }
2453
2465
  .calendar { display: grid; gap: var(--calendar-gap, var(--sp-3)); }
2454
2466
  .calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--calendar-head-gap, var(--sp-2)); }
2455
2467
  .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; }
2456
- .calendar__wd { font: var(--calendar-wd-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; color: var(--calendar-wd-ink, var(--card-muted)); padding-block: var(--calendar-wd-pad-y, var(--sp-2)); }
2468
+ .calendar__wd { font: var(--calendar-wd-font, var(--type-caption)); color: var(--calendar-wd-ink, var(--card-muted)); padding-block: var(--calendar-wd-pad-y, var(--sp-2)); }
2457
2469
  .card--3 .calendar__wd { color: inherit; opacity: 0.7; }
2458
2470
  .calendar__day {
2459
2471
  width: 100%;
@@ -2536,7 +2548,7 @@ button { font: inherit; cursor: pointer; }
2536
2548
  padding: 0;
2537
2549
  background: transparent;
2538
2550
  color: inherit;
2539
- font: 700 var(--fs-2xl)/1.25 var(--font-display);
2551
+ font: var(--composer-input-font, 700 var(--fs-2xl)/1.25 var(--font-display));
2540
2552
  letter-spacing: var(--track-h2);
2541
2553
  outline: none;
2542
2554
  }
@@ -2691,8 +2703,7 @@ button { font: inherit; cursor: pointer; }
2691
2703
  .kv { margin: 0; display: grid; }
2692
2704
  .kv__row { display: flex; align-items: center; justify-content: space-between; gap: var(--kv-row-gap, var(--sp-3)); min-height: var(--kv-row-min-h, var(--tap)); }
2693
2705
  .kv--lined > .kv__row + .kv__row { border-top: var(--border-w) solid var(--hairline); }
2694
- .kv__key { font: var(--kv-key-font, var(--type-body)); color: var(--kv-key-ink, var(--card-muted)); }
2695
- .card--3 .kv__key { color: inherit; opacity: 0.7; }
2706
+ .kv__key { font: var(--kv-key-font, var(--type-body)); color: var(--kv-key-ink, var(--card-ink)); } /* full ink (Lh 2026-09-22): a key is read like the value; muted = inactive, not "secondary" */
2696
2707
  .kv__val { margin: 0; font: var(--kv-val-font, var(--type-title)); display: inline-flex; align-items: center; gap: var(--kv-val-gap, var(--sp-2)); text-align: right; min-width: 0; }
2697
2708
 
2698
2709
  /* ---- kv grid: value over label, cells split by a rule (Sex | Weight) ----
@@ -2786,8 +2797,7 @@ button { font: inherit; cursor: pointer; }
2786
2797
  }
2787
2798
  .timeline__mark {
2788
2799
  justify-self: start;
2789
- font: var(--timeline-mark-font, var(--type-overline));
2790
- letter-spacing: var(--track-overline);
2800
+ font: var(--timeline-mark-font, var(--type-caption));
2791
2801
  text-transform: uppercase;
2792
2802
  padding: var(--timeline-mark-pad-y, var(--sp-1)) var(--timeline-mark-pad-x, var(--sp-2));
2793
2803
  border-radius: var(--timeline-mark-radius, var(--r-chip));
@@ -2827,7 +2837,7 @@ button { font: inherit; cursor: pointer; }
2827
2837
 
2828
2838
  /* ---- cover: the image IS the card; text over a scrim ----
2829
2839
  <article class="card card--cover"><div class="ph" role="img"></div>
2830
- <div class="card__head"><span class="t-overline">Featured</span><button class="icon-btn icon-btn--sm">♥</button></div>
2840
+ <div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
2831
2841
  <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
2832
2842
  <div class="row">…meta…</div></article> */
2833
2843
  .card--cover {
@@ -2852,7 +2862,7 @@ button { font: inherit; cursor: pointer; }
2852
2862
  border-radius: 0;
2853
2863
  min-height: 0;
2854
2864
  }
2855
- /* scrim: dark at both ends so overline and meta stay readable */
2865
+ /* scrim: dark at both ends so the meta stays readable */
2856
2866
  .card--cover::before {
2857
2867
  content: "";
2858
2868
  position: absolute;
@@ -2874,7 +2884,7 @@ button { font: inherit; cursor: pointer; }
2874
2884
  }
2875
2885
  .quote__text { margin: 0; font: var(--quote-text-font, var(--type-h1)); letter-spacing: var(--track-h1); text-wrap: balance; }
2876
2886
  .quote--sm .quote__text { font: var(--type-h2); letter-spacing: var(--track-h2); }
2877
- .quote__by { font: var(--quote-by-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; color: var(--quote-by-ink, var(--card-muted)); }
2887
+ .quote__by { font: var(--quote-by-font, var(--type-caption)); color: var(--quote-by-ink, var(--card-muted)); }
2878
2888
  .card--3 .quote__by { color: inherit; opacity: 0.7; }
2879
2889
 
2880
2890
  /* ---- tile grid: big square tiles — icon top, label bottom ----
@@ -3,7 +3,6 @@ export interface CardHeadProps extends ComponentProps<'div'> {
3
3
  /** the heading (a string becomes an h2 at level `t-h2`) — wraps freely, as many lines as the words need */
4
4
  heading?: ReactNode;
5
5
  /** small caps above the heading ("Step 1 of 4", "Time bank") */
6
- overline?: ReactNode;
7
6
  /** the muted line under the heading (a date, a subtitle) */
8
7
  caption?: ReactNode;
9
8
  /** the contextual thing at the far right: a chip, an IconBtn (more), a Link, a small Ring, an Avatar, a Badge (a count in a stack head) */
@@ -15,7 +14,7 @@ export interface CardHeadProps extends ComponentProps<'div'> {
15
14
  }
16
15
  /**
17
16
  * CardHead — the card's head: the heading on the left takes the room, the contextual info hugs the far right.
18
- * Use `heading` (+ `overline`/`caption`) and `end` for the standard head, or pass children for a custom one
19
- * (the FIRST child takes the room). Flat markup: no wrapper element, no <p> — the overline and caption are spans on rows of their own (card.css). Never put buttons here — they go in the CardFoot.
17
+ * Use `heading` (+ `caption`) and `end` for the standard head, or pass children for a custom one
18
+ * (the FIRST child takes the room). Flat markup: no wrapper element, no <p> — the caption is a span on a row of its own (card.css). Never put buttons here — they go in the CardFoot.
20
19
  */
21
- export declare function CardHead({ heading, overline, caption, end, top, level, className, children, ...rest }: CardHeadProps): import("react").JSX.Element;
20
+ export declare function CardHead({ heading, caption, end, top, level, className, children, ...rest }: CardHeadProps): import("react").JSX.Element;
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  /**
4
4
  * CardHead — the card's head: the heading on the left takes the room, the contextual info hugs the far right.
5
- * Use `heading` (+ `overline`/`caption`) and `end` for the standard head, or pass children for a custom one
6
- * (the FIRST child takes the room). Flat markup: no wrapper element, no <p> — the overline and caption are spans on rows of their own (card.css). Never put buttons here — they go in the CardFoot.
5
+ * Use `heading` (+ `caption`) and `end` for the standard head, or pass children for a custom one
6
+ * (the FIRST child takes the room). Flat markup: no wrapper element, no <p> — the caption is a span on a row of its own (card.css). Never put buttons here — they go in the CardFoot.
7
7
  */
8
- export function CardHead({ heading, overline, caption, end, top, level = 'h2', className, children, ...rest }) {
8
+ export function CardHead({ heading, caption, end, top, level = 'h2', className, children, ...rest }) {
9
9
  const H = level === 'h1' ? 'h1' : 'h2';
10
- return (_jsxs("div", { className: cx('card__head', top && 'card__head--top', className), ...rest, children: [overline != null && _jsx("span", { className: "t-overline t-muted", children: overline }), heading != null && _jsx(H, { className: level === 'display' ? 't-display' : `t-${level}`, children: heading }), caption != null && _jsx("span", { className: "t-caption t-muted", children: caption }), children, end] }));
10
+ return (_jsxs("div", { className: cx('card__head', top && 'card__head--top', className), ...rest, children: [heading != null && _jsx(H, { className: level === 'display' ? 't-display' : `t-${level}`, children: heading }), caption != null && _jsx("span", { className: "t-caption t-muted", children: caption }), children, end] }));
11
11
  }
@@ -5,7 +5,7 @@ export interface StatProps extends ComponentProps<'div'> {
5
5
  /** centred: the one number of a sheet or a stage, under a centred Avatar */
6
6
  centre?: boolean;
7
7
  }
8
- /** Stat — an inline stat block: an overline, a big `Text level="stat"` number, a caption; StatRows put a Delta beside the label. */
8
+ /** Stat — an inline stat block: a label, a big `Text level="stat"` number, a caption; StatRows put a Delta beside the label. */
9
9
  export declare function Stat({ grow, centre, className, ...rest }: StatProps): import("react").JSX.Element;
10
10
  export interface StatRowProps extends ComponentProps<'div'> {
11
11
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
- /** Stat — an inline stat block: an overline, a big `Text level="stat"` number, a caption; StatRows put a Delta beside the label. */
3
+ /** Stat — an inline stat block: a label, a big `Text level="stat"` number, a caption; StatRows put a Delta beside the label. */
4
4
  export function Stat({ grow, centre, className, ...rest }) {
5
5
  return _jsx("div", { className: cx('stat', grow && 'grow', centre && 'stat--centre', className), ...rest });
6
6
  }
@@ -7,5 +7,5 @@ export interface QuoteProps extends ComponentProps<'blockquote'> {
7
7
  /** h2-size text instead of h1 */
8
8
  sm?: boolean;
9
9
  }
10
- /** Quote — a giant opening mark, big text, an overline attribution. */
10
+ /** Quote — a giant opening mark, big text, a small attribution. */
11
11
  export declare function Quote({ text, by, sm, className, ...rest }: QuoteProps): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
- /** Quote — a giant opening mark, big text, an overline attribution. */
3
+ /** Quote — a giant opening mark, big text, a small attribution. */
4
4
  export function Quote({ text, by, sm, className, ...rest }) {
5
5
  return (_jsxs("blockquote", { className: cx('quote', sm && 'quote--sm', className), ...rest, children: [_jsx("p", { className: "quote__text", children: text }), by != null && _jsx("footer", { className: "quote__by", children: by })] }));
6
6
  }
@@ -2,7 +2,7 @@ import type { ComponentProps } from 'react';
2
2
  export interface BaseContentProps extends ComponentProps<'section'> {
3
3
  }
4
4
  /**
5
- * BaseContent — words straight on the base, no card around them: an overline, a heading, a paragraph,
5
+ * BaseContent — words straight on the base, no card around them: a heading, a paragraph,
6
6
  * maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
7
7
  * Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
8
8
  * In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  /**
4
- * BaseContent — words straight on the base, no card around them: an overline, a heading, a paragraph,
4
+ * BaseContent — words straight on the base, no card around them: a heading, a paragraph,
5
5
  * maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
6
6
  * Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
7
7
  * In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
@@ -4,8 +4,9 @@ export interface TopBarProps extends Omit<ComponentProps<'header'>, 'title'> {
4
4
  title: ReactNode;
5
5
  /** a leading control — usually `<IconBtn sm icon="arrow-left" label="Back" />` */
6
6
  back?: ReactNode;
7
- /** trailing controls, right-aligned: chips, small icon buttons, a Dropdown — or the screen's one big move as a
8
- * `Btn primary` (home's ขอความช่วยเหลือ, --tap-lg tall there; Lh 2026-09-09: no bottom bar on home) */
7
+ /** trailing controls, right-aligned, AT MOST TWO (Lh 2026-09-22 — a third is not drawn and warns once): chips,
8
+ * small icon buttons, a Dropdown — or the screen's one big move as a `Btn primary` (home's ขอความช่วยเหลือ,
9
+ * --tap-lg tall there; Lh 2026-09-09: no bottom bar on home). More moves go in a Menu behind one button. */
9
10
  actions?: ReactNode;
10
11
  }
11
12
  /**
@@ -1,9 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
2
3
  import { cx } from '../cx.js';
3
4
  /**
4
5
  * TopBar — the screen's title bar: back · TITLE · actions. Same 60px floor as the AppBar.
5
6
  * One per screen, first child of the Screen; a Step that follows it touches it.
6
7
  */
7
8
  export function TopBar({ title, back, actions, className, children, ...rest }) {
8
- return (_jsxs("header", { className: cx('topbar', className), ...rest, children: [back, _jsx("h1", { className: "t-h1", children: title }), children, actions != null && _jsx("div", { className: "topbar__actions", children: actions })] }));
9
+ const acts = useRef(null);
10
+ useEffect(() => {
11
+ const n = acts.current?.childElementCount ?? 0;
12
+ if (n > MAX_ACTIONS && !warned) {
13
+ warned = true;
14
+ console.warn(`cardds TopBar: ${n} actions — at most ${MAX_ACTIONS} are drawn; put the rest in a Menu behind one button`);
15
+ }
16
+ });
17
+ return (_jsxs("header", { className: cx('topbar', className), ...rest, children: [back, _jsx("h1", { className: "t-h1", children: title }), children, actions != null && _jsx("div", { className: "topbar__actions", ref: acts, children: actions })] }));
9
18
  }
19
+ const MAX_ACTIONS = 2;
20
+ let warned = false;
@@ -105,6 +105,26 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
105
105
  ro.observe(el.parentElement);
106
106
  return () => ro.disconnect();
107
107
  }, [reveal]);
108
+ /* the peek strip holds the handle and the WHOLE title row. CSS gives the box the strip for a one-line head (--sheet-peek in
109
+ sheet.css); when the head is taller than that floor — a title wrapped on a narrow phone at a large text size — the sheet
110
+ measures its head and publishes the real strip on its box, so nothing of the title is under the cut (2026-09-22, with the
111
+ 22-base type scale). Removed when the head fits the floor again, so the CSS number is the one in force. */
112
+ useEffect(() => {
113
+ const el = ref.current, box = el?.parentElement, head = el?.querySelector(':scope > .card__head, :scope > .sheet__head');
114
+ if (!el || !box || !head || typeof ResizeObserver === 'undefined')
115
+ return;
116
+ const publish = () => {
117
+ box.style.removeProperty('--sheet-peek'); // read the CSS strip, not our own
118
+ const cs = getComputedStyle(el);
119
+ const strip = parseFloat(cs.getPropertyValue('--sheet-peek')) || 0, floor = parseFloat(cs.getPropertyValue('--sheet-head-h')) || 0;
120
+ const over = head.getBoundingClientRect().height - floor;
121
+ if (over > 0.5)
122
+ box.style.setProperty('--sheet-peek', `${strip + over}px`);
123
+ };
124
+ const ro = new ResizeObserver(publish);
125
+ ro.observe(head);
126
+ return () => { ro.disconnect(); box.style.removeProperty('--sheet-peek'); };
127
+ }, []);
108
128
  const focus = (e) => {
109
129
  onFocus?.(e);
110
130
  if (current !== 'peek' || e.target.closest('.sheet__handle'))
@@ -1,8 +1,8 @@
1
- export type TextLevel = 'display' | 'h1' | 'h2' | 'title' | 'body' | 'label' | 'caption' | 'overline' | 'stat';
1
+ export type TextLevel = 'display' | 'h1' | 'h2' | 'title' | 'body' | 'label' | 'caption' | 'stat';
2
2
  export interface TextProps extends React.HTMLAttributes<HTMLElement> {
3
3
  /** the element it renders (React 19 passes `ref` as a prop) */
4
4
  ref?: React.Ref<HTMLElement>;
5
- /** the type level: display (hero words) · h1 · h2 (a card's heading) · title · body · label · caption · overline (small caps above a heading) · stat (a big number) */
5
+ /** the type level: display (hero words) · h1 · h2 (a card's heading) · title · body · label · caption · stat (a big number) */
6
6
  level: TextLevel;
7
7
  /** stat only: the one giant number on a sheet (`.t-stat--xl`) */
8
8
  xl?: boolean;
package/dist/type/Text.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createElement } from 'react';
2
2
  import { cx } from '../cx.js';
3
3
  const DEFAULT_AS = {
4
- display: 'h2', h1: 'h2', h2: 'h2', title: 'p', body: 'p', label: 'span', caption: 'span', overline: 'span', stat: 'p',
4
+ display: 'h2', h1: 'h2', h2: 'h2', title: 'p', body: 'p', label: 'span', caption: 'span', stat: 'p',
5
5
  };
6
6
  /**
7
7
  * Text — one of cardds's type levels (`.t-display` … `.t-stat`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
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.",
@@ -48,7 +48,8 @@
48
48
  "cardds.css",
49
49
  "cardds.js",
50
50
  "icons.svg",
51
- "README.md"
51
+ "README.md",
52
+ "CHANGELOG.md"
52
53
  ],
53
54
  "peerDependencies": {
54
55
  "react": ">=18",