@usableapp/cardds 0.4.2 → 0.5.1

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,53 @@
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
+ ## Unreleased
6
+
7
+ ### Field type 26px, 56px tall (Lh 2026-09-22)
8
+ `--field-min-h` default is `--tap-lg` (56, was `--tap` 48) — every field stands as tall as the action field already did.
9
+ `--field-font` default is `400 var(--fs-lg) / 1.4 var(--font-ui)` (was `--type-body`, 22): the words being typed read a size up, level with the Composer. The field keeps its 48px height. A theme that set `--field-font` is unaffected.
10
+
11
+ ### Field: no browser focus ring; the field's border goes full ink while the cursor is in it (Lh 2026-09-22)
12
+ The inner input drew the browser's own ring as a second box inside the field. Gone. Knob `--field-active-border` (default `currentColor`); coloured palettes keep the 25% border at rest only.
13
+
14
+ ### Pin: the waiting cell shows a caret inside the pill, no focus ring (Lh 2026-09-22)
15
+ The `:focus-visible` outline sat outside the pill and was cropped by the sheet's edge. Gone. The cell the cursor waits in now has a full-ink border and a blinking bar drawn inside it (the native caret is hidden). Knobs `--pin-cell-active-border`, `--pin-caret-w`, `--pin-caret-h`.
16
+
17
+ ## 0.5.0 — 2026-09-22
18
+
19
+ ### Words are full ink; muted means inactive (Lh 2026-09-22)
20
+ `.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`).
21
+
22
+ ### Overline retired (Lh 2026-09-22) — BREAKING
23
+ 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).
24
+
25
+ ### h1 and h2 one size down (Lh 2026-09-22)
26
+ `--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.
27
+
28
+ ### TopBar: at most two trailing actions (Lh 2026-09-22)
29
+ 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`.
30
+
31
+ ### `--text-scale` retired — the text-size setting is `--fs-base`
32
+ `--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.
33
+
34
+ ### The type scale, rethought (Lh 2026-09-22) — BREAKING to the eye
35
+ 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:
36
+ | × | px | tokens |
37
+ |---|---|---|
38
+ | 0.775 | 17 | `--fs-xs` `--fs-caption` `--fs-label` `--fs-sm` (was 11 · 12 · 13 · 14) |
39
+ | 1 | 22 | `--fs-body` `--fs-md` `--fs-title` (was 15 · 16 · 17) |
40
+ | 1.2 | 26 | `--fs-h2` `--fs-lg` `--fs-2xl` (was 22 · 20 · 24) |
41
+ | 1.45 | 32 | `--fs-h1` `--fs-3xl` (was 28 · 32) |
42
+ | 1.8 | 40 | `--fs-display` `--fs-stat` (was clamp 34–42 · 30–38) |
43
+ | 2.5 | 55 | `--fs-hero` (was 56) |
44
+ | 2.9 | 64 | `--fs-giant` (was clamp 48–64) |
45
+ `--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.
46
+ - 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.
47
+ - The type page (`Text · Scale`) shows the 7 sizes with both faces and every job that shares each; the `/type` page is gone.
48
+
49
+ - `--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).
50
+ - `.base-content > img` keeps its ratio inside `--base-content-media-h` (`height: auto`).
51
+
5
52
  ## 0.4.2 — 2026-09-22
6
53
 
7
54
  ### Words on the base: a picture and the dots
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,10 +115,10 @@ 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) */
121
119
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
122
120
  the "where am I" dots under the words, centred — the same dots the centred card has under it */
123
- .base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; max-width: 100%; object-fit: contain; justify-self: start; }
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 */
124
122
  .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
125
123
  .base-content > .pager__at { justify-self: center; }
126
124
  .base-content .cta-pack {
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%;
@@ -204,10 +204,23 @@
204
204
  text-align: center;
205
205
  font: var(--pin-cell-font, var(--type-stat));
206
206
  font-variant-numeric: tabular-nums;
207
- caret-color: currentColor;
208
207
  }
209
208
  .pin__cell:not(:placeholder-shown) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
210
- .pin__cell:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
209
+ /* the cell the cursor waits in (Lh 2026-09-22): NO outline ring — it sat outside the pill and got cropped by the sheet's
210
+ edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
211
+ is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
212
+ .pin__cell { caret-color: transparent; }
213
+ .pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
214
+ .pin__cell:focus:placeholder-shown {
215
+ background-image: linear-gradient(currentColor, currentColor);
216
+ background-size: var(--pin-caret-w, 2px) var(--pin-caret-h, 1em);
217
+ background-position: center;
218
+ background-repeat: no-repeat;
219
+ }
220
+ @media (prefers-reduced-motion: no-preference) {
221
+ .pin__cell:focus:placeholder-shown { animation: pin-caret 1.1s steps(1) infinite; }
222
+ @keyframes pin-caret { 50% { background-size: 0 0; } }
223
+ }
211
224
 
212
225
  /* ---- composer: big text area with a bottom bar (icon · hint · send) ----
213
226
  <div class="composer"><textarea class="composer__input" placeholder="What do you need?"></textarea>
@@ -221,7 +234,7 @@
221
234
  padding: 0;
222
235
  background: transparent;
223
236
  color: inherit;
224
- font: 700 var(--fs-2xl)/1.25 var(--font-display);
237
+ font: var(--composer-input-font, 700 var(--fs-2xl)/1.25 var(--font-display));
225
238
  letter-spacing: var(--track-h2);
226
239
  outline: none;
227
240
  }
package/css/forms.css CHANGED
@@ -13,7 +13,7 @@
13
13
  background: var(--field-bg, var(--action-bg));
14
14
  color: var(--field-ink, var(--action-ink));
15
15
  padding-inline: var(--field-pad-x, var(--sp-4));
16
- min-height: var(--field-min-h, var(--tap));
16
+ min-height: var(--field-min-h, var(--tap-lg)); /* 56, the action field's height, for every field (Lh 2026-09-22: "ใหญ่ดี") */
17
17
  min-width: 0;
18
18
  }
19
19
 
@@ -22,19 +22,22 @@
22
22
  border: 0;
23
23
  background: transparent;
24
24
  color: inherit;
25
- font: var(--type-body);
25
+ font: var(--field-font, 400 var(--fs-lg) / 1.4 var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does */
26
26
  width: 100%;
27
27
  min-width: 0;
28
- outline-offset: var(--sp-1);
28
+ outline: none; /* the browser's ring drew a second box inside the field (Lh 2026-09-22) — the field itself tells focus */
29
+ caret-color: currentColor;
29
30
  }
30
31
  .field input::placeholder { color: inherit; opacity: 0.55; }
32
+ /* the field the cursor is in: its own border goes full ink, in every palette (the coloured ones fade it to 25% at rest) */
33
+ .field:has(:focus) { border-color: var(--field-active-border, currentColor); }
31
34
 
32
35
  /* field with its action inside: date pick, send, go — the button sits flush right */
33
36
  .field--action { padding-right: var(--sp-1); min-height: var(--field-action-min-h, var(--tap-lg)); }
34
37
  .field--action > .icon-btn { margin-left: auto; }
35
38
 
36
39
  /* colored palettes drop the visible outline elsewhere — fields keep one */
37
- [data-palette] .field { border-color: color-mix(in srgb, currentColor 25%, transparent); }
40
+ [data-palette] .field:not(:has(:focus)) { border-color: color-mix(in srgb, currentColor 25%, transparent); }
38
41
 
39
42
  /* ---- add-row: inline "+ add another" action ---- */
40
43
  .add-row {
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,10 +491,10 @@ 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
495
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
496
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; max-width: 100%; object-fit: contain; justify-self: start; }
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
498
  .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
499
499
  .base-content > .pager__at { justify-self: center; }
500
500
  .base-content .cta-pack {
@@ -561,19 +561,18 @@ button { font: inherit; cursor: pointer; }
561
561
  multi-line title can opt back out with .card__head--top. */
562
562
  .card__head {
563
563
  display: flex;
564
- 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 */
565
565
  align-items: center;
566
566
  justify-content: space-between;
567
567
  column-gap: var(--card-head-col-gap, var(--sp-3));
568
568
  row-gap: var(--card-head-row-gap, var(--sp-0));
569
569
  }
570
570
  /* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
571
- <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>
572
- The heading and the far-right thing share the first row; an overline sits
573
- on a row above (order −1), a caption on a row below (order 1), whatever the
574
- DOM order — so an author writes the words in reading order. */
575
- .card__head > :is(.t-overline, .t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
576
- .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; }
577
576
  .card__head--top { align-items: flex-start; }
578
577
 
579
578
  .card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
@@ -595,7 +594,7 @@ button { font: inherit; cursor: pointer; }
595
594
  /* Skeleton head — the heading in a card's OWN head, and only there.
596
595
  The heading wraps freely: no line budget, no squeeze, no clamp (Lh
597
596
  2026-09-16 — a height rule on words only distorts them; content that needs
598
- more rows takes more rows). The group that holds overline · heading ·
597
+ more rows takes more rows). The group that holds heading ·
599
598
  caption is the head's own wrapping row (above) — no wrapper element, no
600
599
  height, only the row gap. */
601
600
  .card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
@@ -1174,7 +1173,10 @@ button { font: inherit; cursor: pointer; }
1174
1173
  /* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
1175
1174
  have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
1176
1175
  inherits it. A sheet whose head is taller retunes it on the box. */
1177
- :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))); }
1178
1180
 
1179
1181
  /* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
1180
1182
  .sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
@@ -1286,7 +1288,7 @@ button { font: inherit; cursor: pointer; }
1286
1288
  - a label .row with its .check / .toggle may be the head when the sheet's
1287
1289
  whole ask is that switch: peek shows it (a row is --tap tall plus its own
1288
1290
  padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
1289
- .sheet > .card__head { min-height: var(--tap); }
1291
+ .sheet > .card__head { min-height: var(--sheet-head-h); }
1290
1292
  .sheet__head {
1291
1293
  display: grid;
1292
1294
  grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
@@ -1559,7 +1561,7 @@ button { font: inherit; cursor: pointer; }
1559
1561
  background: var(--field-bg, var(--action-bg));
1560
1562
  color: var(--field-ink, var(--action-ink));
1561
1563
  padding-inline: var(--field-pad-x, var(--sp-4));
1562
- min-height: var(--field-min-h, var(--tap));
1564
+ min-height: var(--field-min-h, var(--tap-lg)); /* 56, the action field's height, for every field (Lh 2026-09-22: "ใหญ่ดี") */
1563
1565
  min-width: 0;
1564
1566
  }
1565
1567
 
@@ -1568,19 +1570,22 @@ button { font: inherit; cursor: pointer; }
1568
1570
  border: 0;
1569
1571
  background: transparent;
1570
1572
  color: inherit;
1571
- font: var(--type-body);
1573
+ font: var(--field-font, 400 var(--fs-lg) / 1.4 var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does */
1572
1574
  width: 100%;
1573
1575
  min-width: 0;
1574
- outline-offset: var(--sp-1);
1576
+ outline: none; /* the browser's ring drew a second box inside the field (Lh 2026-09-22) — the field itself tells focus */
1577
+ caret-color: currentColor;
1575
1578
  }
1576
1579
  .field input::placeholder { color: inherit; opacity: 0.55; }
1580
+ /* the field the cursor is in: its own border goes full ink, in every palette (the coloured ones fade it to 25% at rest) */
1581
+ .field:has(:focus) { border-color: var(--field-active-border, currentColor); }
1577
1582
 
1578
1583
  /* field with its action inside: date pick, send, go — the button sits flush right */
1579
1584
  .field--action { padding-right: var(--sp-1); min-height: var(--field-action-min-h, var(--tap-lg)); }
1580
1585
  .field--action > .icon-btn { margin-left: auto; }
1581
1586
 
1582
1587
  /* colored palettes drop the visible outline elsewhere — fields keep one */
1583
- [data-palette] .field { border-color: color-mix(in srgb, currentColor 25%, transparent); }
1588
+ [data-palette] .field:not(:has(:focus)) { border-color: color-mix(in srgb, currentColor 25%, transparent); }
1584
1589
 
1585
1590
  /* ---- add-row: inline "+ add another" action ---- */
1586
1591
  .add-row {
@@ -1646,7 +1651,7 @@ button { font: inherit; cursor: pointer; }
1646
1651
  background: var(--tile-badge-bg, var(--card-3-bg));
1647
1652
  color: var(--tile-badge-ink, var(--card-3-ink));
1648
1653
  }
1649
- .tile-badge__label { font: var(--tile-badge-label-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; opacity: 0.7; }
1654
+ .tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); opacity: 0.7; }
1650
1655
  .tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
1651
1656
 
1652
1657
  /* plain: no fill — a date box beside a title (Jun / 18) */
@@ -1707,6 +1712,9 @@ button { font: inherit; cursor: pointer; }
1707
1712
  margin-block: -0.15em;
1708
1713
  }
1709
1714
  .topbar__actions { margin-left: auto; display: flex; gap: var(--sp-2); }
1715
+ /* at most TWO trailing controls (Lh 2026-09-22): the title must keep its line; a third and
1716
+ beyond is not drawn — more moves belong in a Menu behind one button, or on the base */
1717
+ .topbar__actions > :nth-child(n + 3) { display: none; }
1710
1718
 
1711
1719
  /* a screen whose one big move sits at the top right (home's "ขอความช่วยเหลือ" — Lh 2026-09-09:
1712
1720
  no bottom bar on the home screen) puts a primary .btn in the top bar's actions; it stands at
@@ -1810,7 +1818,9 @@ button { font: inherit; cursor: pointer; }
1810
1818
  text-decoration: none;
1811
1819
  justify-self: start; /* as a grid child it hugs its text, never stretches */
1812
1820
  }
1813
- .dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); } /* signal dot: chips, legends */
1821
+ .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.
1822
+ 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
1823
+ letters' own middle, in em so it follows the text size (Lh 2026-09-22: "ตัวหนังสือกับไฟเสมอกัน") */
1814
1824
  .dot--b { background: var(--dot-b); }
1815
1825
  .chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
1816
1826
  /* dark-slot inversion lives in card.css (.card--3 :is(...)) */
@@ -2000,7 +2010,7 @@ button { font: inherit; cursor: pointer; }
2000
2010
  .ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
2001
2011
  .ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
2002
2012
  .ring--xs { --_size: var(--tap-sm); --_w: var(--dot-xs); } /* inline counter (step 3 of 5) */
2003
- .ring--xs .ring__value { font: var(--type-overline); }
2013
+ .ring--xs .ring__value { font: var(--type-caption); }
2004
2014
 
2005
2015
  /* ---- track: step progress in a pill — done · done · todo ----
2006
2016
  <ol class="track"><li class="track__step is-done">icon</li> …</ol> */
@@ -2458,7 +2468,7 @@ button { font: inherit; cursor: pointer; }
2458
2468
  .calendar { display: grid; gap: var(--calendar-gap, var(--sp-3)); }
2459
2469
  .calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--calendar-head-gap, var(--sp-2)); }
2460
2470
  .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; }
2461
- .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)); }
2471
+ .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)); }
2462
2472
  .card--3 .calendar__wd { color: inherit; opacity: 0.7; }
2463
2473
  .calendar__day {
2464
2474
  width: 100%;
@@ -2524,10 +2534,23 @@ button { font: inherit; cursor: pointer; }
2524
2534
  text-align: center;
2525
2535
  font: var(--pin-cell-font, var(--type-stat));
2526
2536
  font-variant-numeric: tabular-nums;
2527
- caret-color: currentColor;
2528
2537
  }
2529
2538
  .pin__cell:not(:placeholder-shown) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
2530
- .pin__cell:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
2539
+ /* the cell the cursor waits in (Lh 2026-09-22): NO outline ring — it sat outside the pill and got cropped by the sheet's
2540
+ edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
2541
+ is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
2542
+ .pin__cell { caret-color: transparent; }
2543
+ .pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
2544
+ .pin__cell:focus:placeholder-shown {
2545
+ background-image: linear-gradient(currentColor, currentColor);
2546
+ background-size: var(--pin-caret-w, 2px) var(--pin-caret-h, 1em);
2547
+ background-position: center;
2548
+ background-repeat: no-repeat;
2549
+ }
2550
+ @media (prefers-reduced-motion: no-preference) {
2551
+ .pin__cell:focus:placeholder-shown { animation: pin-caret 1.1s steps(1) infinite; }
2552
+ @keyframes pin-caret { 50% { background-size: 0 0; } }
2553
+ }
2531
2554
 
2532
2555
  /* ---- composer: big text area with a bottom bar (icon · hint · send) ----
2533
2556
  <div class="composer"><textarea class="composer__input" placeholder="What do you need?"></textarea>
@@ -2541,7 +2564,7 @@ button { font: inherit; cursor: pointer; }
2541
2564
  padding: 0;
2542
2565
  background: transparent;
2543
2566
  color: inherit;
2544
- font: 700 var(--fs-2xl)/1.25 var(--font-display);
2567
+ font: var(--composer-input-font, 700 var(--fs-2xl)/1.25 var(--font-display));
2545
2568
  letter-spacing: var(--track-h2);
2546
2569
  outline: none;
2547
2570
  }
@@ -2696,8 +2719,7 @@ button { font: inherit; cursor: pointer; }
2696
2719
  .kv { margin: 0; display: grid; }
2697
2720
  .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)); }
2698
2721
  .kv--lined > .kv__row + .kv__row { border-top: var(--border-w) solid var(--hairline); }
2699
- .kv__key { font: var(--kv-key-font, var(--type-body)); color: var(--kv-key-ink, var(--card-muted)); }
2700
- .card--3 .kv__key { color: inherit; opacity: 0.7; }
2722
+ .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" */
2701
2723
  .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; }
2702
2724
 
2703
2725
  /* ---- kv grid: value over label, cells split by a rule (Sex | Weight) ----
@@ -2791,8 +2813,7 @@ button { font: inherit; cursor: pointer; }
2791
2813
  }
2792
2814
  .timeline__mark {
2793
2815
  justify-self: start;
2794
- font: var(--timeline-mark-font, var(--type-overline));
2795
- letter-spacing: var(--track-overline);
2816
+ font: var(--timeline-mark-font, var(--type-caption));
2796
2817
  text-transform: uppercase;
2797
2818
  padding: var(--timeline-mark-pad-y, var(--sp-1)) var(--timeline-mark-pad-x, var(--sp-2));
2798
2819
  border-radius: var(--timeline-mark-radius, var(--r-chip));
@@ -2832,7 +2853,7 @@ button { font: inherit; cursor: pointer; }
2832
2853
 
2833
2854
  /* ---- cover: the image IS the card; text over a scrim ----
2834
2855
  <article class="card card--cover"><div class="ph" role="img"></div>
2835
- <div class="card__head"><span class="t-overline">Featured</span><button class="icon-btn icon-btn--sm">♥</button></div>
2856
+ <div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
2836
2857
  <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
2837
2858
  <div class="row">…meta…</div></article> */
2838
2859
  .card--cover {
@@ -2857,7 +2878,7 @@ button { font: inherit; cursor: pointer; }
2857
2878
  border-radius: 0;
2858
2879
  min-height: 0;
2859
2880
  }
2860
- /* scrim: dark at both ends so overline and meta stay readable */
2881
+ /* scrim: dark at both ends so the meta stays readable */
2861
2882
  .card--cover::before {
2862
2883
  content: "";
2863
2884
  position: absolute;
@@ -2879,7 +2900,7 @@ button { font: inherit; cursor: pointer; }
2879
2900
  }
2880
2901
  .quote__text { margin: 0; font: var(--quote-text-font, var(--type-h1)); letter-spacing: var(--track-h1); text-wrap: balance; }
2881
2902
  .quote--sm .quote__text { font: var(--type-h2); letter-spacing: var(--track-h2); }
2882
- .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)); }
2903
+ .quote__by { font: var(--quote-by-font, var(--type-caption)); color: var(--quote-by-ink, var(--card-muted)); }
2883
2904
  .card--3 .quote__by { color: inherit; opacity: 0.7; }
2884
2905
 
2885
2906
  /* ---- 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.2",
3
+ "version": "0.5.1",
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.",