@usableapp/cardds 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +72 -26
  3. package/css/actions.css +15 -0
  4. package/css/base.css +9 -6
  5. package/css/card.css +4 -4
  6. package/css/choice.css +11 -0
  7. package/css/layover.css +3 -0
  8. package/css/lists.css +4 -0
  9. package/css/sheet.css +8 -1
  10. package/css/stack.css +55 -0
  11. package/css/step.css +43 -13
  12. package/css/tokens.css +2 -1
  13. package/dist/actions/Btn.d.ts +1 -1
  14. package/dist/actions/IconBtn.d.ts +1 -1
  15. package/dist/actions/Link.d.ts +3 -1
  16. package/dist/actions/Link.js +2 -2
  17. package/dist/cardds.css +154 -25
  18. package/dist/choice/Check.d.ts +6 -3
  19. package/dist/choice/Check.js +7 -4
  20. package/dist/index.d.ts +4 -1
  21. package/dist/index.js +4 -1
  22. package/dist/scaffold/ActionBar.d.ts +3 -2
  23. package/dist/scaffold/ActionBar.js +3 -2
  24. package/dist/scaffold/AppBar.d.ts +4 -1
  25. package/dist/scaffold/AppBar.js +4 -3
  26. package/dist/scaffold/Centre.d.ts +1 -1
  27. package/dist/scaffold/Centre.js +1 -1
  28. package/dist/scaffold/Page.d.ts +17 -0
  29. package/dist/scaffold/Page.js +24 -0
  30. package/dist/scaffold/Screen.d.ts +13 -8
  31. package/dist/scaffold/Screen.js +24 -7
  32. package/dist/sheets/SheetStack.d.ts +1 -1
  33. package/dist/sheets/SheetStack.js +6 -1
  34. package/dist/sheets/SheetStage.d.ts +4 -1
  35. package/dist/sheets/SheetStage.js +2 -2
  36. package/dist/sheets/StackBack.d.ts +2 -1
  37. package/dist/sheets/StackBack.js +3 -2
  38. package/dist/sheets/StackSheet.d.ts +35 -0
  39. package/dist/sheets/StackSheet.js +24 -0
  40. package/dist/sheets/Step.d.ts +4 -4
  41. package/dist/sheets/Step.js +4 -4
  42. package/dist/templates/Home.d.ts +48 -0
  43. package/dist/templates/Home.js +40 -0
  44. package/dist/templates/Walkthrough.d.ts +42 -0
  45. package/dist/templates/Walkthrough.js +35 -0
  46. package/dist/type/Text.d.ts +3 -1
  47. package/dist/type/Text.js +2 -2
  48. package/dist/type/icons.d.ts +1 -1
  49. package/dist/type/icons.js +2 -2
  50. package/icons.svg +2 -0
  51. package/package.json +1 -1
  52. package/dist/scaffold/BaseContent.d.ts +0 -17
  53. package/dist/scaffold/BaseContent.js +0 -18
package/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.6.6 — 2026-09-23
6
+
7
+ ### BOTTOM is a layer (Lh 2026-09-23, replaces 0.6.5's "the bar is a row")
8
+ - In a `Screen fill`, top + content are one flex column and **the content runs to the screen's bottom edge**. The bottom
9
+ (`ActionBar`) is a **layer over the content's foot** — it covers what is under it, on purpose. Nothing is added to the
10
+ content to make room.
11
+ - **The walk's `PagerAt` is the bottom layer's first row**: a direct child of the `ActionBar`, before its tiers, centred.
12
+ It is no longer a child of the `Step`. (The 2026-09-16 rule "the dots never in the bar" is retired.)
13
+ - `ActionBar` in a fill screen has no top padding.
14
+
15
+ ### `BaseContent` → `Page`
16
+ - The content is a box. What is in it is always components one after another. A page of words (picture · heading · words ·
17
+ a CtaPack) is **`Page`** (class `.page`, was `.base-content`), and a walk is `Pages` of `Page`s.
18
+ - `BaseContent` still works for this version as an alias of `Page`; it warns once in the console. Rename it.
19
+ - The knobs keep their names for now (`--base-content-media-h`, `--base-content-gap`, …).
20
+
21
+ ### Fixes and small additions
22
+ - A `Step` under an `AppBar` touches it again, so a full sheet stops on the bar's bottom edge (0.6.5 left an 8px strip — timebank #137).
23
+ - `Link sm`: a minor link at a new size `--fs-2xs` (15px) on a 28px line; its tap target stays 44px. A `ctaLink` row is as tall as that line.
24
+
25
+ ## 0.6.5 — 2026-09-23
26
+
27
+ ### A screen is top · content · bottom, one flex column (Lh 2026-09-23)
28
+ - **`Screen top` / `bottom`** (new): `top` = `TopBar` / `AppBar` / `"lead"` (no bar, the words 15% down) · children = content · `bottom` = `ActionBar` / `FloatBar`. The browser measures the bar; the content takes the rest.
29
+ - **Before → after:** in a `Screen fill` the `ActionBar` and the `AppBar` are **rows of the screen's column**, not `position: fixed` over it. Put the bar INSIDE the Screen (as `bottom` or its last child). A bar after `</Screen>`, or one given `position` by hand, is now wrong (`validateScreen` in scripts/validate.mjs fails it). A bar outside a fill screen (an app shell) still works the old fixed way.
30
+ - The walk's dots (`PagerAt`) float over the foot of the pages; the pages run down to the bar.
31
+ - `Screen` warns once when a screen has both a `Sheet` and a bar.
32
+
33
+ ### Templates (new)
34
+ - **`Walkthrough`** — top `lead` · content `Pages` (media · heading · body) · bottom `ActionBar` (`controls` rows · the `cta` row — the pager by default, or a function of `{ at, count, go }` · `ctaLink`); children = layers over it.
35
+ - **`Home`** — top `AppBar` · content: `base` over a closed `SheetStack` · no bottom. `title` = the page's h1, read aloud, not shown. Only the open sheet's body is rendered.
36
+
37
+ ### New and changed
38
+ - **`StackSheet`** (new): one sheet of a SheetStack — head (back · title · icon tabs with a count) · body · foot. A tap on a tab of a closed sheet opens it on that tab; a tap on the open sheet's title folds it.
39
+ - **`StackBack`**: a small 32px disc with a chevron (was a 44px disc with an arrow); the tap target is still 44px.
40
+ - A stacked sheet with a `SheetBody` has the one sheet shape (head pinned, body scrolls, foot at the bottom). A closed stack sheet can't be text-selected.
41
+ - **`AppBar brandButton`**: the mark + the name become one button (`popoverTarget` opens a `Menu`).
42
+ - **`Toggle text`**: words shown before the switch, one tap target.
43
+ - **`Text sr`**: read by a screen reader, not shown.
44
+ - **`SheetStage over`**: a stage laid over its screen (a Modal a link opened).
45
+ - Icons `megaphone`, `log-out`.
46
+
5
47
  ## 0.6.4 — 2026-09-23
6
48
 
7
49
  - **Fold card: the corner is cut** (Lh 2026-09-23): the triangle under the flap shows what is behind the card, instead of the card's own colour, which read as a second card underneath. Nothing to change in an app.
package/README.md CHANGED
@@ -37,7 +37,7 @@ npm test # Playwright: the tokens and the step/sheet/centre geometry
37
37
  ```
38
38
 
39
39
  ```tsx
40
- import { Screen, TopBar, Step, BaseContent, Sheet, SheetBody, CardHead, Field, Pin, ActionBar, Btn } from '@usableapp/cardds';
40
+ import { Screen, TopBar, Step, Page, Sheet, SheetBody, CardHead, Field, Pin, ActionBar, Btn } from '@usableapp/cardds';
41
41
  import '@usableapp/cardds/dist/cardds.css';
42
42
  import './theme.css'; // the project's own look, after the system's — see "A project's theme"
43
43
  ```
@@ -74,16 +74,30 @@ import '@usableapp/cardds/dist/cardds.css'; // the CSS, fonts resolve from the
74
74
  ```
75
75
 
76
76
 
77
- **Browsers** (checked 2026-09-23): works on browsers from **early 2025** on.
77
+ **เบราว์เซอร์** (เช็ก 2026-09-23): ใช้ได้กับเบราว์เซอร์ตั้งแต่**ต้นปี 2025** ขึ้นไป
78
78
 
79
- | Browser | Minimum | Released | Phones |
79
+ | เบราว์เซอร์ | ขั้นต่ำ | ออกเมื่อ | เครื่องที่ใช้ได้ |
80
80
  |---|---|---|---|
81
- | Safari / iOS | 18.3 | Jan 2025 | iPhone XS / XR (2018) and newer, updated |
82
- | Chrome | 114 | May 2023 | most Android phones with Chrome kept up to date |
83
- | Firefox | 128 | Jul 2024 | — |
81
+ | Safari / iOS | 18.3 | ม.ค. 2025 | iPhone XS / XR (2018) ขึ้นไป ที่อัปเดตแล้ว |
82
+ | Chrome | 114 | พ.ค. 2023 | Android ส่วนใหญ่ที่ Chrome อัปเดตอยู่ |
83
+ | Firefox | 128 | ก.ค. 2024 | — |
84
84
 
85
- Older than that: menus and dropdowns don't open. On browsers between the minimum and
86
- the newest, a menu opens in the middle of the screen instead of under its button.
85
+ เก่ากว่านี้: เมนูและ dropdown เปิดไม่ได้ ส่วนเบราว์เซอร์ที่ผ่านขั้นต่ำแต่ยังไม่ใช่รุ่นล่าสุด
86
+ เมนูจะขึ้นกลางจอแทนที่จะอยู่ใต้ปุ่ม
87
+
88
+ สำหรับโปรเจกต์ที่อยากเช็กเบราว์เซอร์ก่อนเริ่ม (เช่นหน้าติดตั้ง): เช็กเฉพาะฟีเจอร์ที่**จำเป็น**
89
+ ตัวอื่น cardds มีตัวสำรองไว้แล้ว ไม่ต้องเช็ก
90
+
91
+ | จำเป็น: ไม่มีแล้วหน้าจอพัง | วิธีเช็ก |
92
+ |---|---|
93
+ | popover | `'popover' in HTMLElement.prototype` |
94
+ | `@property` | `'registerProperty' in CSS` |
95
+ | `color-mix()` | `CSS.supports('color', 'color-mix(in srgb, red, blue)')` |
96
+ | container queries | `CSS.supports('container-type: size') && CSS.supports('height: 1cqh')` |
97
+ | `:has()` | `CSS.supports('selector(:has(a))')` |
98
+
99
+ มีตัวสำรองแล้ว ไม่ต้องเช็ก: anchor positioning, view transitions, `@starting-style`, `text-wrap`
100
+ iOS 17–18.2 เช็กผ่าน แต่ยังรองรับไม่เต็ม เมนูอาจแสดงผิดเล็กน้อย
87
101
 
88
102
  Working with Claude in another project? The `/cardds` skill
89
103
  (`~/.claude/skills/cardds/`) carries the install and the contract, and points
@@ -143,6 +157,36 @@ the card is the material, the sheet is what it is in the stack.
143
157
  one — the members are sheets and the stack is the menu, not a list of cards.
144
158
  Never bring it back, not even as an alias.
145
159
 
160
+ ## Screen: top · content · bottom
161
+
162
+ **Every screen is three parts in ONE flex column** (Lh 2026-09-23) — the words used to ask for a screen, and the props
163
+ of `Screen`:
164
+
165
+ | Part | Prop | What fills it |
166
+ |---|---|---|
167
+ | **top** | `top` | `TopBar` · `AppBar` · `"lead"` (no bar: the content starts `--screen-lead`, 15% of the screen, down) |
168
+ | **content** | children | a `Step` (`Pages` · `Page` · `Page` + `Sheet`) · `Centre` · `SheetStack` … |
169
+ | **bottom** | `bottom` | `ActionBar` (rows: `controls` · the one `cta` row — the pager ← → or one wide button · a `ctaLink`) · `FloatBar` · nothing |
170
+
171
+ ```tsx
172
+ <Screen fill top="lead" bottom={<ActionBar variant="pager">…</ActionBar>}>
173
+ <Step><Pages>…</Pages><PagerAt at={1} of={3} /></Step>
174
+ </Screen>
175
+ ```
176
+
177
+ - **Top and content are one flex column; the bottom is a LAYER** (Lh 2026-09-23). In `Screen fill` the content runs to the
178
+ screen's bottom edge; the bottom (`ActionBar`, with the walk's `PagerAt` as its first row) lies over it at the foot and
179
+ covers what is under it — on purpose, nothing is added to the content to make room. The keyboard lifts the layer (`--kb`).
180
+ - **The bar is INSIDE the screen** — `bottom`, never a sibling after `</Screen>`, never pinned with a hand-set `position`.
181
+ A `FloatBar` in `bottom` floats the same way.
182
+ - **A screen with a `Sheet` has no bottom.** The sheet finishes on its own `CardFoot`; `Screen` warns once when both
183
+ are there.
184
+ - **Checked:** `validateScreen` (`scripts/validate.mjs`, run on every story and template by
185
+ `tests/composition.spec.js`) fails a bar after `</Screen>` or a pinned bar, warns on a Sheet + ActionBar.
186
+ - **Templates** are `Screen`s with the three parts filled (`Walkthrough` = top `lead` · content `Pages` · bottom
187
+ `ActionBar`); the gallery draws those parts on a template's page. The gallery's nav groups the components by
188
+ the part they fill: *top* · *content* · *bottom*.
189
+
146
190
  ## Step markup
147
191
 
148
192
  A step is one screen, one task (see *Anatomy*). It is the other component
@@ -151,16 +195,16 @@ whose markup has rules, because the sheet's box is the step itself:
151
195
  ```html
152
196
  <!-- reads: the words only -->
153
197
  <section class="step">
154
- <section class="base-content">
198
+ <section class="page">
155
199
  <div class="card__head"><h2 class="t-h2">…</h2></div>
156
200
  <p class="t-body">…</p>
157
201
  </section>
158
202
  </section>
159
- <div class="action-bar action-bar--pager">…</div>
203
+ <div class="action-bar action-bar--pager">…</div> <!-- the screen's bottom: inside .screen, after the step -->
160
204
 
161
205
  <!-- acts: the words + a sheet -->
162
206
  <section class="step">
163
- <section class="base-content">…the words…</section>
207
+ <section class="page">…the words…</section>
164
208
  <article class="card sheet sheet--half">
165
209
  <button class="sheet__handle" type="button" aria-label="ปรับความสูง"></button>
166
210
  <div class="card__head"><h2 class="t-h2">…</h2><span class="chip">…</span></div>
@@ -184,7 +228,7 @@ whose markup has rules, because the sheet's box is the step itself:
184
228
  card takes the inset back as its side margins — so the card and the bar's
185
229
  buttons stand on the inset line, and the sheet runs edge to edge like the
186
230
  bar and like a sheet in a `.sheet-stage`.
187
- - **`.base-content` first, `.sheet` last.** The block carries the step's words
231
+ - **`.page` first, `.sheet` last.** The block carries the step's words
188
232
  ON THE BASE — no frame, no area colour (it was a `.card` until 2026-09-16;
189
233
  Lh: a step reads like a page, not a card in a page). It keeps a card's
190
234
  rhythm: a `.card__head`, `--card-gap` between things. The sheet is
@@ -201,9 +245,10 @@ whose markup has rules, because the sheet's box is the step itself:
201
245
  breathing (`--header-gap`), and the card takes that room as its lead. A
202
246
  step that follows anything else keeps the screen's gap, and the sheet
203
247
  stops under that thing.
204
- - **The bar is outside the step**, after it. `.step` reads the bar's room
205
- from `--bar-reserve`; any parent that hosts the `.action-bar` counts
206
- (`<body>`, an app shell, a demo frame).
248
+ - **The bar is after the step, inside the screen** (the screen's bottom — *Screen: top · content · bottom*). In a
249
+ `.screen--fill` the step and the bar share one flex column and touch; the step takes the rest. (A bar outside a
250
+ fill screen — an app shell — is still fixed to the viewport and the step reads `--bar-reserve`: the old way,
251
+ kept until 0.7.0.)
207
252
  - **Every sheet has one shape** (Lh 2026-09-21): the handle and the head stay at the top and never scroll · the
208
253
  foot's button stands at the bottom — above the keyboard when there is one (`--kb`: a step gives the keyboard
209
254
  its room, a stage sheet gives it up itself as `--_lift`) · everything between them scrolls, as far as it
@@ -259,15 +304,14 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
259
304
  the inset as padding, so a deck's fanned cards may overhang and clip at the
260
305
  screen's edges (the fold visual, not a bug), and stacks its children at the
261
306
  screen's gap. Never hand-lay `top: 40%` on a screen.
262
- - **The bar is outside, as always.** Plain `.action-bar` for one move; with a
307
+ - **The bar is the screen's bottom.** Plain `.action-bar` for one move; with a
263
308
  control that belongs to the screen (a slider), `.action-bar--tiers` stacks a
264
- control tier above the buttons and publishes the taller reserve
265
- (`--bar-reserve`) from whatever parent hosts the bar.
309
+ control tier above the buttons (it publishes the taller `--bar-reserve` the centred card still reads for its cap).
266
310
  - **The round pager** is `.action-bar--pager` (or an `.action-bar__tier--pager`)
267
311
  holding two `.icon-btn`s and nothing else: they stand at the two edges. The
268
312
  step that commits still goes back to one wide button (rule 3).
269
- - **BaseContent that moves** (Lh 2026-09-20) — two ways, never both on one screen:
270
- - *Pages of words:* `.step > .pages > .base-content` (one `aria-current="step"`, the rest `inert`) — every page
313
+ - **Page that moves** (Lh 2026-09-20) — two ways, never both on one screen:
314
+ - *Pages of words:* `.step > .pages > .page` (one `aria-current="step"`, the rest `inert`) — every page
271
315
  one height, the round pager turns them (`slideTo()`), the words slide like a page. No sheet there.
272
316
  - *Sheets the app calls:* the base offers WAYS IN — sign in · new phone · join — as buttons (`.cta-pack`); each
273
317
  way has a sheet that holds its WHOLE form and its own submit on its foot, starting `.sheet--away` (not on the
@@ -282,8 +326,8 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
282
326
  - **A pager turns pages of words, never a screen with a sheet** (Lh 2026-09-20). A
283
327
  sheet means this screen has work that cannot be walked past: no ← →, no bar at all — the sheet's
284
328
  own foot finishes it. (Guidance — not enforced.)
285
- - A screen is **base, top menu, bottom menu, content**. The base is the
286
- surface; the menus are the chrome above and below (`.topbar` / `.appbar`,
329
+ - A screen is **top · content · bottom** on the base (*Screen: top · content · bottom*). The base is the
330
+ surface; top and bottom are the chrome above and below (`.topbar` / `.appbar`,
287
331
  `.action-bar` / `.float-bar`); content is everything between.
288
332
  - Content has three homes: **on the base**, **in a card**, **in a bottom
289
333
  sheet**. Content normally comes in cards; static data (a blurb, a legal
@@ -322,9 +366,9 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
322
366
  by `translate` only. (The stack was first named "card stack": wrong, and
323
367
  retired.) Demos: `sheet.html`, `stack.html`.
324
368
  - **A step is one screen, one task: `.step`.** What the member reads is
325
- words on the base (`.base-content` — the same block as anywhere on the base; it was `.step__card` until 2026-09-16); what the member does, if the step asks for it, is a
326
- `.sheet` in front of it; the screen's one move is `.action-bar`, outside
327
- the step, below it. A step where the member only READS is the card alone.
369
+ words on the base (`.page` — the same block as anywhere on the base; it was `.step__card` until 2026-09-16); what the member does, if the step asks for it, is a
370
+ `.sheet` in front of it; the screen's one move is `.action-bar`, the screen's
371
+ bottom, after the step. A step where the member only READS is the card alone.
328
372
  A step where the member ACTS (types, picks, shoots a photo, records) adds
329
373
  the sheet: peek shows its handle and title row, half shows half of it,
330
374
  full slides up until its top edge touches the top bar — and at every one
@@ -349,7 +393,7 @@ Demo: the *skeleton* block at the top of `elements.html`.
349
393
  1. **One card = one boundary.** A card frames one thing: a category, a stat
350
394
  group, a list row. Tone slots: `.card` (card-1), `.card--2`, `.card--3`.
351
395
  2. **Sections are not cards.** Page sections = frameless on-surface content
352
- (`.base-content`) + a lone/packed CTA (`.cta-pack`).
396
+ (`.page`) + a lone/packed CTA (`.cta-pack`).
353
397
  3. **Surrounding UI is separate.** `.topbar`, `.filter-row`, `.chip-row`,
354
398
  `.action-bar` live outside cards, above/below. The bottom bar has two
355
399
  arrangements and they mean different things: the default pack (one wide
@@ -374,6 +418,8 @@ Demo: the *skeleton* block at the top of `elements.html`.
374
418
  room itself, as a bottom margin, reading the same `--bar-reserve`. The
375
419
  reserves use the nominal control height, so a bar whose button wraps to two
376
420
  lines can still overlap the last line of content.
421
+ **In a `.screen--fill` none of this applies to a bar in the screen** (2026-09-23): it is the column's last
422
+ part and the browser measures it — the reserves are the old way for a bar outside a fill screen.
377
423
  **Padding is published, not copied.** `.screen` sets `--screen-pad` and
378
424
  `--screen-gap`, its appbar reserve sets `--header-gap`, and `.card` sets
379
425
  `--card-pad` and `--card-gap`; anything that has to cancel a container's
package/css/actions.css CHANGED
@@ -64,6 +64,15 @@
64
64
  .screen:has(> .appbar) { --header-gap: var(--sp-2); padding-top: calc(var(--header-h) + var(--header-gap)); }
65
65
  .appbar__brand { flex: 1; display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
66
66
  .appbar__text { display: grid; min-width: 0; flex: 1; } /* pushes trailing button to the right edge */
67
+ /* the mark + the words as ONE button (AppBar brandButton — the member's name opens their menu, Lh 2026-09-23): the same row, a tap target */
68
+ .appbar__who {
69
+ flex: 1; min-width: 0; min-height: var(--tap);
70
+ display: flex; align-items: center; gap: var(--sp-2);
71
+ padding: 0; margin: 0; border: 0; background: none; color: inherit; font: inherit; text-align: start; cursor: pointer;
72
+ border-radius: var(--appbar-who-radius, var(--r-chip));
73
+ -webkit-tap-highlight-color: transparent;
74
+ }
75
+ .appbar__who:focus-visible { outline: var(--focus); outline-offset: 2px; }
67
76
  .appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.35; } /* clipped lines: room for Thai marks */
68
77
  .appbar__title { font: var(--type-label); }
69
78
  .appbar__sub { font: var(--type-caption); color: var(--on-surface-muted); }
@@ -280,7 +289,13 @@
280
289
  shadow and read on their own (Lh, 2026-09-12; replaces the 09-11 fade) */
281
290
  .action-bar__tier { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--tap); }
282
291
  .action-bar__tier > .btn { flex: 1; }
292
+ .action-bar__tier--link { justify-content: center; min-height: 0; } /* a Link under the buttons, e.g. "details" that opens a Modal — its row as tall as a small link, not a tap (Lh 2026-09-23) */
283
293
  :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
294
+ /* more rows (Lh 2026-09-23: a bar may hold several rows of controls — keeping it short is the designer's job): each row past two
295
+ adds a tap-tall row and its gap to the reserve, so the words above never run under the bar */
296
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(3)) { --bar-reserve: calc(2 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
297
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(4)) { --bar-reserve: calc(3 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
298
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(5)) { --bar-reserve: calc(4 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
284
299
 
285
300
  /* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
286
301
  :is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
package/css/base.css CHANGED
@@ -105,26 +105,29 @@ button { font: inherit; cursor: pointer; }
105
105
  .card--3 .t-muted { color: inherit; opacity: 0.7; } /* card-muted is tuned for light cards; dim instead on the dark slot */
106
106
  .t-caps { text-transform: uppercase; }
107
107
  .grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
108
+ /* read, not shown (Lh 2026-09-23): words a screen reader needs — a page's h1 the design leaves out — kept in the page, taking no room.
109
+ The 1px is the one the platform needs to keep it in the accessibility tree, not a length of the design. */
110
+ .t-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
108
111
 
109
112
  /* ---- base content: words straight on the base, no card around them — an
110
113
  a heading, a paragraph, maybe a .cta-pack. Was .section until
111
114
  2026-09-16 (Lh: the name said nothing about WHERE it lives; this one does).
112
115
  In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
113
- .base-content {
116
+ .page {
114
117
  display: grid;
115
118
  gap: var(--base-content-gap, var(--sp-3));
116
119
  align-content: start; /* a tall column never stretches the rows — the words hug the top */
117
120
  padding-block: var(--base-content-pad-y, var(--sp-4));
118
121
  }
119
- .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) */
122
+ .page > .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) */
120
123
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
121
124
  (the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
122
125
  /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
123
126
  h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
124
- .base-content > .card__head > .t-h1 { font-size: var(--fs-h2); }
125
- .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; align-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
126
- .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
127
- .base-content .cta-pack {
127
+ .page > .card__head > .t-h1 { font-size: var(--fs-h2); }
128
+ .page > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; align-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
129
+ .page > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
130
+ .page .cta-pack {
128
131
  display: flex;
129
132
  align-items: start; /* buttons keep their own height, never the row's */
130
133
  gap: var(--sp-2);
package/css/card.css CHANGED
@@ -227,13 +227,13 @@
227
227
  user-select: none; /* a walk is dragged, not read-and-selected — without this, a finger crossing the words highlights them instead of turning the page (timebank #59) */
228
228
  touch-action: pan-y; /* leaves vertical scroll to the browser; horizontal is the app's own drag, never a native pan */
229
229
  }
230
- .pages > :is(.card, .base-content) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
230
+ .pages > :is(.card, .page) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
231
231
  .pages > [inert] { visibility: hidden; }
232
- /* pages of WORDS (Lh, 2026-09-20): the same walk with .base-content pages, on the base — in a .step the
232
+ /* pages of WORDS (Lh, 2026-09-20): the same walk with .page pages, on the base — in a .step the
233
233
  .pages is the reading block (step.css), the round pager in the bar turns it, the same slideTo().
234
234
  <section class="step"><div class="pages">
235
- <section class="base-content" aria-current="step">…</section>
236
- <section class="base-content" inert>…</section>
235
+ <section class="page" aria-current="step">…</section>
236
+ <section class="page" inert>…</section>
237
237
  </div></section> <div class="action-bar action-bar--pager">← →</div> */
238
238
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
239
239
  ::view-transition-old(centre-card), ::view-transition-new(centre-card) {
package/css/choice.css CHANGED
@@ -303,3 +303,14 @@
303
303
  box-shadow: var(--shadow-float);
304
304
  }
305
305
  .slider__range:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
306
+
307
+ /* ---- a switch that names itself (Toggle text, Lh 2026-09-23 — "อยู่บ้าน" on the home's top): the words, then the switch,
308
+ one label — the whole of it is the tap target ---- */
309
+ .toggle-field {
310
+ display: inline-flex; align-items: center; gap: var(--toggle-field-gap, var(--sp-2));
311
+ min-height: var(--tap);
312
+ font: var(--toggle-field-font, var(--type-label));
313
+ cursor: pointer;
314
+ -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent;
315
+ flex: none;
316
+ }
package/css/layover.css CHANGED
@@ -190,6 +190,9 @@
190
190
  align-items: center;
191
191
  z-index: 30;
192
192
  }
193
+ /* in a fill screen (its `bottom`) the float bar floats over the SCREEN's foot, not the window's — it lies over the content
194
+ (a map, a stack), so it is not a row of the column; the screen is its box (Lh 2026-09-23) */
195
+ .screen--fill > .float-bar { position: absolute; }
193
196
  .sheet-stage > .float-bar { position: absolute; padding-inline: var(--sp-4); }
194
197
  /* .screen's default bottom padding clears an .action-bar; a .float-bar is
195
198
  taller (--bar-h + its own bottom offset), so reserve the difference. Any
package/css/lists.css CHANGED
@@ -205,5 +205,9 @@
205
205
  padding: 0;
206
206
  cursor: pointer;
207
207
  }
208
+ /* a SMALL link (Lh 2026-09-23): a thing tapped rarely — "details", a ctaLink — at the minor-link size (15) and 28px of height;
209
+ its 44px tap target stays, an invisible ::after around it */
210
+ .link--sm { position: relative; font-size: var(--link-sm-font-size, var(--fs-2xs)); min-height: var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)); }
211
+ .link--sm::after { content: ""; position: absolute; inset: calc((var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) - var(--tap-sm)) / 2) 0; }
208
212
  .link--muted { color: var(--link-muted-ink, var(--card-muted)); }
209
213
  .card--3 .link--muted { color: var(--card-3-muted); }
package/css/sheet.css CHANGED
@@ -94,7 +94,7 @@
94
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
95
  :root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
96
96
 
97
- /* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
97
+ /* ---- called sheets (Lh, 2026-09-20): a .step whose .page has buttons that each CALL a sheet ----
98
98
  .sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
99
99
  changes), and hidden once it has left so its shadow does not lie on the step's bottom edge; write it
100
100
  with `inert` (the React Sheet does). WHO brings it onto the screen is the app's business — a button, a route:
@@ -243,6 +243,13 @@
243
243
  that bar — the stage keeps the bar's line free itself (--_top), so the reserve
244
244
  would be paid twice and the page would scroll by it */
245
245
  .screen > .sheet-stage { margin-inline: calc(-1 * var(--screen-pad)); width: auto; } /* auto: 100% would keep the inset width */
246
+ /* a stage laid OVER its screen (a Modal a link opened, Lh 2026-09-23): the page shows through, the stage covers it and its bar */
247
+ .sheet-stage--over, .screen > .sheet-stage--over {
248
+ position: absolute; inset: 0; z-index: 40;
249
+ width: auto; height: auto; max-width: none; margin: 0;
250
+ background: transparent;
251
+ --_top: 0rem;
252
+ }
246
253
  .appbar + .sheet-stage { margin-top: calc(-1 * (var(--header-h) + var(--header-gap, 0rem))); }
247
254
  .sheet-stage__bg { position: absolute; inset: 0; } /* map / previous cards behind */
248
255
  /* the hero: ONE thing shown in the band a half sheet leaves open (a postcard being made,
package/css/stack.css CHANGED
@@ -230,6 +230,9 @@
230
230
  gap: var(--card-gap, var(--sp-5));
231
231
  overflow: clip;
232
232
  }
233
+ /* a base that is ONE BaseContent is a page's words (a title, a welcome, the news): it reads from the TOP, like any page —
234
+ loose things (a picture, a line) still sit against the peeks (Lh 2026-09-23, the home) */
235
+ .sheet-stack__base:has(> .page:only-child) { align-content: start; }
233
236
 
234
237
  /* back button: lives in a slot the header reserves once open, and DISSOLVES
235
238
  into it — an opacity fade in place, never a slide in from the left (the
@@ -256,3 +259,55 @@
256
259
  opacity: 1;
257
260
  visibility: visible;
258
261
  }
262
+
263
+ /* ---- a stacked sheet with a SheetBody has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
264
+ pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a CardFoot, the CTA) at the bottom.
265
+ The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no SheetBody
266
+ keeps the old way: the whole open card scrolls. ---- */
267
+ .sheet-stack--tap > .card:has(> .sheet__body) { display: flex; flex-direction: column; gap: var(--card-gap); }
268
+ .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
269
+ .sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
270
+ .sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
271
+
272
+ /* ---- StackSheet's tabs (Lh 2026-09-23): the head's end is a row of small icon buttons, the sheet's pages — the chosen one
273
+ filled (.icon-btn--invert); a count is a small circle over the icon's top right corner ---- */
274
+ .stack-sheet__tabs { display: flex; gap: var(--stack-sheet-tabs-gap, 0rem); flex: none; } /* no gap: each tab is a 40px target already, and an open head (back · title · tabs) must fit one line — Lh 2026-09-23 */
275
+ .stack-sheet__tab { position: relative; }
276
+ .stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
277
+ position: absolute;
278
+ top: calc(-1 * var(--sp-1));
279
+ right: calc(-1 * var(--sp-1));
280
+ min-width: var(--stack-sheet-count-h, 1.25rem);
281
+ height: var(--stack-sheet-count-h, 1.25rem);
282
+ padding: 0 var(--sp-1);
283
+ font: var(--type-caption);
284
+ line-height: 1;
285
+ pointer-events: none;
286
+ }
287
+ /* a tab not chosen is the icon alone — no ring, no ground (Lh 2026-09-23: "the circle around is not pretty"); the chosen one turns
288
+ to the opposite colour (.icon-btn--invert). */
289
+ .stack-sheet__tab:not(.icon-btn--invert) { border-color: transparent; background: transparent; box-shadow: none; color: inherit; } /* the sheet's own ink, on any paper */
290
+ /* the last tab's ICON lines up with the inset, not its button's edge: an unchosen tab shows the glyph only, so the button's own
291
+ margin around it reaches into the card's padding — and an open head (back · title · three tabs) keeps one line (Lh 2026-09-23) */
292
+ .card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
293
+
294
+ /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
295
+ tappable"): a SMALL DISC — its ring kept, 32px instead of 44, the chevron in it at 20 — so the head gives it 32px + a gap
296
+ (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
297
+ .sheet-stack--tap > .card.is-open > .card__head { padding-left: var(--stack-back-room, calc(var(--stack-back-size, calc(var(--tap-base) * 2 / 3)) + var(--sp-2))); }
298
+ .sheet-stack--tap .sheet-stack__back {
299
+ width: var(--stack-back-size, calc(var(--tap-base) * 2 / 3));
300
+ height: var(--stack-back-size, calc(var(--tap-base) * 2 / 3));
301
+ min-width: 0;
302
+ min-height: 0;
303
+ padding: 0;
304
+ }
305
+ .sheet-stack--tap .sheet-stack__back > .icon { width: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); height: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); flex: none; } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
306
+ .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--stack-back-size, calc(var(--tap-base) * 2 / 3)) - var(--tap-sm)) / 2); }
307
+
308
+ /* the open sheet's title folds it (SheetStack.tsx): it says so to a pointer */
309
+ .sheet-stack--tap > .card.is-open > .card__head > :is(h1, h2, h3) { cursor: pointer; }
310
+ /* a stacked sheet is a control until it is open — tapped, never read into: no text selection (a drag, a double tap, a long press),
311
+ no grey flash, no iOS callout; its head stays one when open. Only the OPEN sheet's body is text to select (Lh 2026-09-23) */
312
+ .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; }
313
+ .sheet-stack--tap > .card.is-open > :not(.card__head) { -webkit-user-select: text; user-select: text; -webkit-touch-callout: default; }
package/css/step.css CHANGED
@@ -7,7 +7,7 @@
7
7
  (see "LAYERS" below).
8
8
 
9
9
  <section class="step">
10
- <section class="base-content">…the words…</section> ← first
10
+ <section class="page">…the words…</section> ← first
11
11
  <article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
12
12
  </section>
13
13
  <div class="action-bar">…</div> ← only when there is no sheet
@@ -57,20 +57,23 @@
57
57
  /* no bar at all — the step is the screen's first child (a page whose one heading is the base's own, Lh 2026-09-22):
58
58
  the words start a share of the screen down — --screen-lead, 15% of its height (Lh 2026-09-22), so a tall phone gives
59
59
  more air and a short one less; a screen with a bar keeps the bar's own room */
60
- .screen > .step:first-child { --_lead: var(--screen-lead, 15cqh); }
60
+ .screen > .step:first-child, .screen--lead > .step { --_lead: var(--screen-lead, 15cqh); } /* top="lead" says it; a Step first says it too */
61
61
 
62
62
  /* the bar's room — read from actions.css, never recomputed. Margin, not
63
63
  padding, so the clip cuts at the box edge and nothing shows behind the
64
64
  bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
65
65
  frame — the bar is fixed to the viewport wherever it sits. */
66
66
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
67
+ /* a walk (the dots float on its foot): the reading block runs right to the bar's top edge — the reserve's breathing gap (--sp-6)
68
+ is given back, the dots keep the words clear of the bar (Lh 2026-09-23) */
69
+
67
70
  /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
68
71
  move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
69
72
  (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
70
73
  own screens are composed (the guide); the framework does not police it — a component never changes
71
74
  because another one is there. */
72
75
 
73
- /* the reading block: a .base-content (base.css) — the same words-on-the-base
76
+ /* the reading block: a .page (base.css) — the same words-on-the-base
74
77
  block as anywhere else (it was .step__card, and a .card before 2026-09-16;
75
78
  Lh: a step reads like a page, not a card in a page — so it IS the page's
76
79
  block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
@@ -78,7 +81,7 @@
78
81
  gets the flex rule: it hugs its content, and when the column runs out (a
79
82
  short phone, large text) it shrinks and scrolls inside itself instead of
80
83
  pushing the bar or the sheet */
81
- .step > .base-content, .step > .pages > .base-content {
84
+ .step > .page, .step > .pages > .page {
82
85
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
83
86
  --card-gap: var(--sp-4);
84
87
  gap: var(--card-gap);
@@ -86,17 +89,14 @@
86
89
  align-content: start;
87
90
  color: var(--on-surface);
88
91
  }
89
- /* the reading block is the .base-content — or the .pages that holds several of them, a walk of words
92
+ /* the reading block is the .page — or the .pages that holds several of them, a walk of words
90
93
  (card.css). Behind a PEEKED sheet the words keep the strip's room under them, so whatever is down there
91
94
  (the buttons that call the other sheets) scrolls clear of it — CSS alone: the box declares --sheet-peek. */
92
- .step:has(> .sheet--peek) > :is(.base-content, .pages) { padding-bottom: var(--sheet-peek); }
93
- .step > :is(.base-content, .pages) {
95
+ .step:has(> .sheet--peek) > :is(.page, .pages) { padding-bottom: var(--sheet-peek); }
96
+ .step > :is(.page, .pages) {
94
97
  flex: 0 1 auto;
95
98
  }
96
- /* the "where am I" dots of a walk are the STEP's, one for all its pages — written after .pages, pinned to the base's foot above the bar's
97
- room, so a short page never leaves them stranded under its last line (Lh 2026-09-22: the dots belong to the page, not a card, not a paragraph) */
98
- .step > .pager__at { margin-top: auto; align-self: center; padding-block: var(--sp-3); flex: none; }
99
- .step > :is(.base-content, .pages) {
99
+ .step > :is(.page, .pages) {
100
100
  min-height: 0;
101
101
  margin: var(--_lead, 0) var(--screen-pad) 0; /* the screen's gap, moved in here from above the step; the inset, taken back from the bleed */
102
102
  overflow-y: auto;
@@ -106,10 +106,40 @@
106
106
  /* a picture that opens the words stays put, only the words scroll under it (Lh 2026-09-23, timebank's walk at large text:
107
107
  the picture scrolled away with the words). Sticky at the top of the reading block's scroller; its height is
108
108
  --base-content-media-h as everywhere, never shrunk (Lh 2026-09-23: a fixed height, no scaling). */
109
- .step > .base-content > :is(img, .ph):first-child,
110
- .step > .pages > .base-content > :is(img, .ph):first-child {
109
+ .step > .page > :is(img, .ph):first-child,
110
+ .step > .pages > .page > :is(img, .ph):first-child {
111
111
  position: sticky;
112
112
  top: 0;
113
113
  z-index: 1;
114
114
  background-color: var(--surface); /* the words pass UNDER it, never through a transparent picture */
115
115
  }
116
+
117
+ /* ============================================================
118
+ TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
119
+ in a .screen--fill the content runs to the screen's bottom edge, and the bar lies over it at the foot — it covers what is
120
+ under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
121
+ (A bar OUTSIDE a fill screen — an app shell, a scrolling screen — is still fixed to the viewport, actions.css.)
122
+ ============================================================ */
123
+ .screen--fill > .action-bar {
124
+ position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
125
+ max-width: none;
126
+ margin: 0;
127
+ padding-top: 0; /* no ground of its own, never a solid strip */
128
+ }
129
+ .screen--fill:has(> .action-bar) > .step { margin-bottom: 0; }
130
+ /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
131
+ .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
132
+ /* the TOP is in the column too (Lh 2026-09-23): an .appbar in a fill screen is its first row, not fixed over it — the screen
133
+ reserves nothing for it; the breathing under it stays the one it had (--sp-2), not the screen's gap */
134
+ .screen--fill > .appbar {
135
+ position: relative; inset: auto; top: auto;
136
+ flex: none;
137
+ max-width: none;
138
+ margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge; its own padding is the inset */
139
+ margin-bottom: calc(var(--sp-2) - var(--screen-gap));
140
+ }
141
+ .screen.screen--fill:has(> .appbar) { padding-top: 0; --header-gap: 0rem; }
142
+ /* …and a STEP touches it, as it touches a TopBar: a full sheet's top edge on the bar's bottom edge (0.6.5 left 8px of base between
143
+ them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
144
+ .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
145
+ .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
package/css/tokens.css CHANGED
@@ -20,7 +20,7 @@
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: 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).
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.68 (15, minor links — was 0.775/17 until 2026-09-23). A new size is Lh's call only.
24
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 */
25
25
  --r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
26
26
  --tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
@@ -103,6 +103,7 @@
103
103
 
104
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
105
  Nothing that reads is under 17 (the smallest ui: chips). No clamp/vw: a size is one number. */
106
+ --fs-2xs: calc(var(--fs-base) * 0.68); /* 15 minor links — a ctaLink, a "details" (Lh 2026-09-23: added by Lh; ONLY Lh adds a size) */
106
107
  --fs-xs: calc(var(--fs-base) * 0.775); /* 17 the smallest ui words: ring xs, a tile-badge label */
107
108
  --fs-caption: calc(var(--fs-base) * 0.775); /* 17 */
108
109
  --fs-label: calc(var(--fs-base) * 0.775); /* 17 chips, buttons' small words */
@@ -307,6 +307,6 @@ export declare function Btn({ primary, quiet, block, xl, href, className, type,
307
307
  onTransitionStart?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
308
308
  onTransitionStartCapture?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
309
309
  href: string | undefined;
310
- type: "button" | "reset" | "submit" | undefined;
310
+ type: "button" | "submit" | "reset" | undefined;
311
311
  className: string;
312
312
  }, HTMLButtonElement>;
@@ -306,6 +306,6 @@ export declare function IconBtn({ label, icon, sm, invert, href, className, type
306
306
  onTransitionStart?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
307
307
  onTransitionStartCapture?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
308
308
  href: string | undefined;
309
- type: "button" | "reset" | "submit" | undefined;
309
+ type: "button" | "submit" | "reset" | undefined;
310
310
  className: string;
311
311
  }, HTMLButtonElement>;
@@ -5,11 +5,13 @@ export interface LinkProps extends ComponentProps<'a'> {
5
5
  muted?: boolean;
6
6
  /** a trailing xs icon (arrow-up-right for "See all") */
7
7
  icon?: IconName;
8
+ /** SMALL: the minor-link size (15) and a 28px line, for what is tapped rarely (details, a ctaLink); the tap target stays 44px */
9
+ sm?: boolean;
8
10
  /** without an href it renders as a button */
9
11
  href?: string;
10
12
  }
11
13
  /** Link — an inline text action big enough to tap (40px): "See all ↗" in a card head, "Skip" between pager buttons. */
12
- export declare function Link({ muted, icon, href, className, children, ...rest }: LinkProps): import("react").DetailedReactHTMLElement<{
14
+ export declare function Link({ muted, sm, icon, href, className, children, ...rest }: LinkProps): import("react").DetailedReactHTMLElement<{
13
15
  ref?: import("react").Ref<HTMLAnchorElement> | undefined;
14
16
  key?: import("react").Key | null | undefined;
15
17
  download?: any;