@usableapp/cardds 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,95 @@
1
+ # Changelog
2
+
3
+ > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
+
5
+ ## 0.4.2 — 2026-09-22
6
+
7
+ ### Words on the base: a picture and the dots
8
+ Screens that only tell (onboarding, a walk of pages) live on the base — `Step › BaseContent` — never in a `Card centre` on an empty base. Two things the base could not hold well are fixed:
9
+ - an `<img>` (or `Placeholder`) among the words is capped by `--base-content-media-h` (14rem) so a tall picture never pushes the words under the bar.
10
+ - `PagerAt` as the last child of a `BaseContent` is centred under the words (it used to sit left).
11
+ Story: `Step · WalksWithArt` — pages of words with a picture and the dots, a slider in the bar's upper tier, the round pager below.
12
+
13
+ Where things go on such a screen (answers given to timebank): a `Postcard` shown with the words is a child of `BaseContent` after them (the picture's slot; never wrapped in a Card) · a summary the system hands over (an address, a ticket) stays a `Card centre` · a walk's progress is the `CardHead` overline ("ขั้นที่ 2 จาก 5") or a `Track` as the first child of `BaseContent` — nothing sits between the TopBar and the Step.
14
+
15
+ ## 0.4.1 — 2026-09-22
16
+
17
+ Breaking renames and removals. No aliases (nobody had installed 0.4.0).
18
+
19
+ ### `CardBack` → `StackBack`
20
+ The back button a tapped stack sheet reveals belongs to the stack, not the card.
21
+ - import: `CardBack` → `StackBack`
22
+ - class (if you styled it): `.card__back` → `.sheet-stack__back`
23
+ - where it goes: unchanged — first child of the open stack sheet's `CardHead`.
24
+
25
+ ### `SheetStage`: `ask` prop removed
26
+ The stage only does the ground; the thing in front places itself.
27
+ - `<SheetStage ask>` → `<SheetStage dim>` — the `Modal` inside centres itself now.
28
+ - A Modal on a clean ground (no scrim) is allowed: `<SheetStage>` with no `dim`.
29
+ - CSS: `.sheet-stage--ask` is gone; `--drawer-h` now lives on `.modal`.
30
+
31
+ ### `Sheet`: `raised` prop removed
32
+ A sheet always stands on the floor and never has a dim behind it.
33
+ - `<SheetStage dim><Sheet raised …>` → that screen is a `Modal`: `<SheetStage dim><Modal><Lift>…</Lift><Drawer>…</Drawer></Modal>`
34
+ - `.sheet--raised`, `--sheet-raised-radius` are gone. `SheetStage foot` (the footnote strip) stays.
35
+
36
+ ### `Postcard` reshaped
37
+ The shape of `Card cover`: the photo fills it, a band at the bottom names it. Not a 3:2 card inside a card any more.
38
+ - props kept: `photo` `alt` `name` `caption` `flipped` `defaultFlipped` `onFlip` `frontLabel` `backLabel`
39
+ - new: `line` (the words under the name, `t-body`) · `liked` + `onLike(liked)` (the member's own heart in the band; no heart without them) · `likeLabel` · `voice` (a voice note instead of words: whatever you put there stands at the middle of the back — the app plays it; see Progress)
40
+ - the band (`name` · `line` · heart) turns WITH the face. The back: `name` a fifth of the way down, `line`, then `caption` (or `voice`).
41
+ - a tap on the photo still turns it. The turn is `.postcard__turn` (a button filling the face); the faces are divs now.
42
+ - Don't wrap a Postcard in a `Card`. Its min-height is the cover's (`--postcard-min-h` 22.5rem); as a `SheetStage hero` it fills the band the sheet leaves open, with the screen's inset at both ends.
43
+ - Who liked it is YOUR data, shown outside the card — see `Row more`.
44
+
45
+ ### `Row`: `more` (new)
46
+ A row that expands: it becomes the summary of a `<details class="row-expand">`, `more` comes out under it.
47
+ - `<Row lead={…} meta="4 คนถูกใจ" end={<Icon name="chevron-down" />} open={open} onOpenChange={setOpen} more={<Rows>…one Row per person…</Rows>} />`
48
+ - `open` + `onOpenChange`, or `defaultOpen`. An `end` chevron turns when open. You fetch the list; the row shows it.
49
+
50
+ ### `Progress` (new)
51
+ One straight line, the done share solid, the rest faint.
52
+ - `<Progress at={0.4} label="0:05 of 0:12" />` — `at` 0–1; you keep the value (a timer, a download).
53
+ - knobs: `--progress-h` · `--progress-radius` · `--progress-done-ink` · `--progress-rest-bg`.
54
+
55
+ ### Stage hero
56
+ `.sheet-stage__hero` is now as tall as the band a half sheet leaves open, less `--screen-pad` at both ends, and what it holds fills it (a child's `min-height` gives way). If your hero relied on its own height, check it.
57
+
58
+ ### Gallery
59
+ One page per thing you name (Card, Sheet, Btn…); inner parts (CardHead, CardBody, SheetBody, MenuItem, BtnRow…) are variations on the parent's page. Nav groups by use. `/Parent/Part` URLs are gone.
60
+
61
+ ## 0.4.0 — 2026-09-21
62
+
63
+ Knobs, open icons, theme bases, gallery. Nothing removed from the component API.
64
+ - **Knobs**: every component reads its look through `--<component>-<what>` custom properties with the default as fallback (363, listed in `docs/knobs.md`). Set one on `:root` (your `theme.css`) to retune everywhere, on any element to retune what is inside it. Declared nowhere: unset = nothing moved.
65
+ - **Icons are open**: `registerIcons({ name: '<path…/>' })` → usable by name on every component that takes an icon; `<Icon>` draws a one-off from children; an unknown name warns once instead of throwing.
66
+ - **Theme bases** (`css/theme.template.css`): four size bases (`--sp-base` `--fs-base` `--r-base` `--tap-base`) and five colour bases (`--color-surface` `--color-ink` `--color-paper` `--color-brand` `--color-on-brand`) on `:root`. A project sets a base, never a multiplier. `<html data-palette="own">` for own colours.
67
+ - `Stat centre`; the pager places what sits between its two buttons in the middle; every component types its `ref`.
68
+
69
+ ## 0.3.0 — 2026-09-21
70
+
71
+ **Components only — the app wires them.** The biggest change since 0.1: every component with a state is controlled like an input (`x` + `onXChange`, or `defaultX`), keeps its own behaviour inside, and none knows another. `cardds.js` no longer does anything between components — it is `--kb` (the keyboard's height) and `.bleed`'s scrollbar, 35 lines. If your screen relied on cardds.js to connect two things, the app connects them now.
72
+
73
+ ### Gone
74
+ - `aria-controls` on a button calling a sheet → the app: `useState` of the sheet's state, the button sets it, `<Sheet state onStateChange>`.
75
+ - events `cardds:sheet` · `cardds:pin` · `cardds:modal` · `cardds:flip`, and the Dropdown's native `change` → the components' own `onXChange` callbacks.
76
+ - `window.carddsSheetSet` / `carddsSheetReveal` / `carddsStepSync`, `--sheet-room`, the bar guard → the Sheet does all of it itself.
77
+ - `data-answer` on Modal buttons → plain `onClick`s; `<Modal open onClosed>`.
78
+ - `IconBtn menu` / `menuId` / `menuStart` → any button opens a popover `Menu` with the platform's `popoverTarget={id}`.
79
+ - `SheetStack mode="tap"` + hand-set `is-open` / `is-before` / `is-after` → `open` (index | null) + `onOpenChange`, or `defaultOpen`; the component paints the classes.
80
+
81
+ ### Each component, before → after
82
+ - **Sheet**: `state="half"` alone used to be a starting point the sheet then moved from; now a fixed `state` PINS it (the sheet asks through `onStateChange` and stays if you don't answer). A sheet the member drags or taps the handle of: `defaultState="half"` (it keeps its own), or `state` + `onStateChange` (you keep it). Own behaviour, inside: handle tap ↔ peek, drag-snap to `states`, the raise when a field in a peeked sheet takes focus, the climb when the keyboard comes up. The `away` state (a called sheet, off-screen) is set-only.
83
+ - **SheetStack**: `open` + `onOpenChange` / `defaultOpen`. A peeked head's tap and the back button only ask.
84
+ - **Modal**: `open` + `onClosed` (it plays its opening backwards, then says so; you unmount it). Buttons are yours.
85
+ - **Menu**: `actions` (plain onClicks) or `value` + `onChange`; picking closes it. **Dropdown**: one component like `<select>`, `value` + `onChange`, the pill named by the chosen item.
86
+ - **Pin**: `value` + `onChange(code, complete)` or `defaultValue`; auto-advance, Backspace, arrows, paste inside. Tests that listened for `cardds:pin` type into the cells instead.
87
+ - **Slider**: publishes its own `--slider-at`. **Postcard**: `flipped` + `onFlip` or `defaultFlipped`.
88
+ - **Calendar**: `selected` + `onSelect` or `defaultSelected`; `onPrev` / `onNext`; label props (its buttons had no handlers before).
89
+ - **Picker**: `value` + `onChange` or `defaultValue` — the value IS the item at the rail's centre (it had no way to report one).
90
+
91
+ Guidance, no longer enforced: "pager or sheet", "a sheet finishes on its foot", "a form lives whole in one sheet".
92
+
93
+ ## 0.2.2 — 2026-09-21
94
+
95
+ One sheet shape: the head stays pinned, the body scrolls without limit, the foot's button stands at the bottom above the keyboard — at every state, on a step and on a stage. Stage sheets ride `--kb` too. (`carddsSheetSet` / `carddsSheetReveal` on `window` — removed again in 0.3.0.)
package/README.md CHANGED
@@ -105,7 +105,7 @@ the card is the material, the sheet is what it is in the stack.
105
105
  <div class="sheet-stack sheet-stack--tap"> <!-- drop --tap for a static stack -->
106
106
  <article class="card card--2">
107
107
  <div class="card__head">
108
- <button class="icon-btn icon-btn--sm card__back" aria-label="Back">…</button>
108
+ <button class="icon-btn icon-btn--sm sheet-stack__back" aria-label="Back">…</button>
109
109
  <h2 class="t-h2 t-caps">Shopping</h2>
110
110
  <span class="icon-row">…</span> <!-- optional trailing icons -->
111
111
  </div>
@@ -125,7 +125,7 @@ the card is the material, the sheet is what it is in the stack.
125
125
  A tap opens any sheet over it; back drops it home.
126
126
  - **Five sheets max.** Slots are declared per `:nth-child` in `stack.css`; a
127
127
  sixth would render at slot 0, on top of the first.
128
- - **Every head needs its own `.card__back`** if the stack is `--tap`. It stays
128
+ - **Every head needs its own `.sheet-stack__back`** if the stack is `--tap`. It stays
129
129
  hidden until that sheet is open, then slides into a reserved header slot.
130
130
  - **No ids, no wiring.** `cardds.js` finds any `.sheet-stack--tap` on the page.
131
131
  - **`.card-stack` is gone.** That was this component's first name and the wrong
@@ -433,7 +433,7 @@ Demo: the *skeleton* block at the top of `elements.html`.
433
433
  padding. Two boxes hold sheets and the states
434
434
  mean the same in both: `.step` (an app screen, the bar outside — Anatomy)
435
435
  and `.sheet-stage` (a page-level overlay: a sheet over a map, the dim page
436
- sheet `--dim` + `--raised`, the ask `--ask` + `.modal` = `__lift` + `__drawer`).
436
+ stage `--dim` + `.modal` = `__lift` + `__drawer` — a sheet always stands on the floor, never raised).
437
437
  A stage keeps the top bar's line free (`--_top`), so `--full` stops under
438
438
  `--header-h` and keeps its corners. Both sheet tokens are published — a
439
439
  sheet with a taller head retunes `--sheet-peek` — and registered in
package/css/base.css CHANGED
@@ -118,6 +118,11 @@ button { font: inherit; cursor: pointer; }
118
118
  padding-block: var(--base-content-pad-y, var(--sp-4));
119
119
  }
120
120
  .base-content > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
121
+ /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
122
+ 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; }
124
+ .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
125
+ .base-content > .pager__at { justify-self: center; }
121
126
  .base-content .cta-pack {
122
127
  display: flex;
123
128
  align-items: start; /* buttons keep their own height, never the row's */
package/css/layover.css CHANGED
@@ -5,8 +5,8 @@
5
5
  lives in sheet.css; these compose with .sheet-stage.
6
6
  ============================================================ */
7
7
 
8
- /* ---- ask: the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
9
- <div class="sheet-stage sheet-stage--ask">
8
+ /* ---- the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
9
+ <div class="sheet-stage sheet-stage--dim"> (dim = the page under a scrim; a Modal on a clean ground is the stage without it — Lh 2026-09-22, `--ask` retired: the stage does the ground, the modal centres itself)
10
10
  <article class="card modal">
11
11
  <div class="modal__lift">…the card's content (a head, rows, a timeline)…</div>
12
12
  <div class="modal__drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
@@ -17,30 +17,27 @@
17
17
  .modal is the card — no border (the shadow is its edge on the scrim);
18
18
  .modal__lift the light part that holds the thing being asked about (it
19
19
  scrolls when tall); .modal__drawer the dark end that asks. The ground is
20
- the page under a scrim (like --dim). Two beats (2 × --motion-sheet) when
20
+ whatever the stage shows (dim it or not). Two beats (2 × --motion-sheet) when
21
21
  the stage appears, and the same two backwards when it closes:
22
22
  1. the card drops in from above the stage to the screen's centre — its
23
23
  drawer still inside it, height 0: nothing of the ask shows yet
24
24
  2. the drawer comes out of the card's bottom edge while the lift moves
25
25
  up, both at once: the card grows from its centre, which never moves
26
- (the stage centres it; a growing card keeps its middle where it is)
26
+ (the modal centres itself — absolute, margin auto; a growing card keeps its middle where it is)
27
27
  Closing: a click on any button in the drawer plays the same animations in
28
28
  reverse (cardds.js: Animation.reverse() on each — nothing duplicated), then
29
29
  fires "cardds:modal" on the .sheet-stage (bubbles, detail.answer from the
30
30
  button's data-answer) for the app to remove the stage.
31
31
  Reduced motion: the finished state, no beats; closing fires at once. */
32
- .sheet-stage--ask {
33
- --drawer-h: 14rem;
34
- display: grid;
35
- align-content: center; /* the card at the screen's centre — and kept there as it grows */
36
- padding-inline: var(--screen-pad);
37
- }
38
- .sheet-stage--ask::after { content: ""; position: absolute; inset: 0; background: var(--scrim); } /* the page dimmed */
39
- .sheet-stage--ask > .sheet-stage__bg { filter: blur(var(--sp-2)); }
40
32
  .modal {
41
- position: relative;
42
- z-index: 1; /* over the scrim */
43
- max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) — a % here would be of the grid area, which is the card itself */
33
+ --drawer-h: 14rem;
34
+ position: absolute; /* at the stage's centre, and kept there as it grows: absolute + margin auto re-centres on every height */
35
+ inset: 0;
36
+ margin: auto;
37
+ height: fit-content; /* inset 0 alone would stretch it to the stage: its own height, then the margins share the rest */
38
+ width: calc(100% - 2 * var(--screen-pad));
39
+ z-index: 2; /* over the dim stage's scrim (z 1) */
40
+ max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) */
44
41
  padding: 0; /* the parts pad themselves */
45
42
  border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
46
43
  display: grid;
package/css/lists.css CHANGED
@@ -36,6 +36,15 @@
36
36
  .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
37
37
  .row--lg { min-height: var(--row-lg-min-h, var(--fab)); } /* menu rows: a taller target, round arrow at the end; .rows gives them more air */
38
38
  .row--lg .row__title { font: 700 var(--fs-lg)/1.2 var(--font-display); }
39
+ /* a row that EXPANDS (Lh 2026-09-22): the row is a <details>' summary, what it shows comes out under it —
40
+ who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
41
+ <details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
42
+ .row-expand { display: block; }
43
+ .row-expand > summary { list-style: none; cursor: pointer; }
44
+ .row-expand > summary::-webkit-details-marker { display: none; }
45
+ .row-expand > summary > .icon:last-child { transition: rotate var(--motion-slide); }
46
+ .row-expand[open] > summary > .icon:last-child { rotate: 180deg; }
47
+ .row-expand__more { padding-top: var(--row-expand-gap, var(--sp-3)); }
39
48
  /* a finished item in a checklist (a to-do list, an onboarding "complete your introduction" list):
40
49
  the words go muted, a filled tick sits at the end, and the row is no longer a tap target —
41
50
  pointer-events off here, aria-disabled from the markup. The tick is the .check's checked look
package/css/media.css CHANGED
@@ -152,59 +152,75 @@
152
152
  .mosaic > .ph { opacity: 0.4; border-width: var(--border-w); }
153
153
  .mosaic > img { width: 100%; height: 100%; object-fit: cover; }
154
154
 
155
- /* ---- postcard: a member's introduction, a card in the hand — photo in front,
156
- words on the back; tap to turn it over (Lh, 2026-09-12; card #63) ----
155
+ /* ---- postcard: a member's introduction in the shape of a cover card — the photo fills it,
156
+ the band at the bottom names it; tap the photo to turn it over (Lh 2026-09-12; reshaped 2026-09-22) ----
157
157
  <div class="postcard">
158
- <button class="postcard__face postcard__front" type="button" aria-label="พลิกดูด้านหลัง">
159
- <img src="…" alt=""> — or <div class="ph"></div> —
160
- <span class="postcard__label">ป้าแจ๋ว</span>
161
- </button>
162
- <button class="postcard__face postcard__back" type="button" aria-label="พลิกดูด้านหน้า">
163
- <p class="postcard__caption">ชอบชงกาแฟดริปตอนเช้า</p>
164
- <span class="postcard__label">ป้าแจ๋ว</span>
165
- </button>
158
+ <div class="postcard__face postcard__front">
159
+ <button class="postcard__turn" type="button" aria-label="พลิกดูด้านหลัง"><img src="…" alt=""> — or <div class="ph"></div> —</button>
160
+ <div class="postcard__band">
161
+ <div class="postcard__words"><span class="t-h1">ป้าแจ๋ว</span><span class="t-body">บ้านสวนผัก</span></div>
162
+ <button class="icon-btn icon-btn--invert" aria-label="ถูกใจ" aria-pressed="false">♥</button>
163
+ </div>
164
+ </div>
165
+ <div class="postcard__face postcard__back">
166
+ <button class="postcard__turn" type="button" aria-label="พลิกดูด้านหน้า">
167
+ <div class="postcard__words">…</div><p class="postcard__caption">ชอบชงกาแฟดริปตอนเช้า</p>
168
+ </button>
169
+ <div class="postcard__voice"><div class="progress"></div><span class="t-caption">0:07 / 0:12</span><button class="icon-btn icon-btn--invert" aria-label="ฟัง">▶</button></div> (a voice note: no caption — the line, its time, the control under them, at the middle)
170
+ </div>
166
171
  </div>
167
- Each face is the whole tap target; cardds.js toggles .is-flipped on the
168
- postcard (either face, Enter/Space too). A voice note, a like, a reply live
169
- OUTSIDE the postcard (a Row under it): a face is a button, and a button
170
- holds no button. The faces share one box (a 3:2 landscape card, the media
171
- radius); the turn is a Y rotation at --motion-flip, a plain swap under
172
- prefers-reduced-motion. Reads ink from the card slot it sits in. */
172
+ The two faces share one box and turn (a Y rotation at --motion-flip, a plain
173
+ swap under prefers-reduced-motion). A face is a div: the TURN is the button
174
+ that fills it (the photo, or the caption), the band lies over its foot and
175
+ turns with it — words and the heart; the back sets the name a fifth of the way down, the words after it; a
176
+ button in the band is never inside the turn button. The size is the cover
177
+ card's (--postcard-min-h = the cover's min-height, the card radius). Who
178
+ liked it, a voice note, a reply live OUTSIDE the postcard, in a Row under it. */
173
179
  .postcard {
174
180
  position: relative;
175
- aspect-ratio: 3 / 2;
181
+ min-height: var(--postcard-min-h, 22.5rem);
176
182
  perspective: 60rem;
177
- border-radius: var(--postcard-radius, var(--r-media));
183
+ border-radius: var(--postcard-radius, var(--r-card));
184
+ isolation: isolate;
178
185
  }
179
186
  .postcard__face {
180
187
  position: absolute;
181
188
  inset: 0;
182
189
  display: grid;
183
190
  align-content: end;
184
- padding: var(--postcard-face-pad, var(--sp-4));
185
- gap: var(--postcard-face-gap, var(--sp-2));
186
191
  border: var(--border-w) solid var(--postcard-face-border, var(--card-border));
187
192
  border-radius: inherit;
188
193
  overflow: hidden;
189
194
  isolation: isolate;
190
- color: inherit;
191
- text-align: left;
192
195
  backface-visibility: hidden;
193
196
  -webkit-backface-visibility: hidden;
194
197
  transition: transform var(--motion-flip) ease-in-out;
195
198
  box-shadow: var(--postcard-face-shadow, var(--shadow-float));
196
199
  }
197
- .postcard__face:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
198
- /* the front: the photo (or the wireframe box) fills it, the name reads on a scrim */
200
+ /* the turn: the button that fills the face, under the band */
201
+ .postcard__turn {
202
+ position: absolute;
203
+ inset: 0;
204
+ display: grid;
205
+ align-content: start;
206
+ padding: var(--postcard-face-pad, var(--card-pad-rest));
207
+ border: 0;
208
+ border-radius: inherit;
209
+ background: transparent;
210
+ color: inherit;
211
+ text-align: left;
212
+ cursor: pointer;
213
+ }
214
+ .postcard__turn:focus-visible { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
215
+ /* the front: the photo (or the wireframe box) fills it; the band reads on a scrim */
199
216
  .postcard__front { background: var(--postcard-front-bg, var(--card-3-bg)); color: var(--postcard-front-ink, var(--card-3-ink)); }
200
- .postcard__front > img,
201
- .postcard__front > .ph {
217
+ .postcard__front .postcard__turn > img,
218
+ .postcard__front .postcard__turn > .ph {
202
219
  position: absolute;
203
220
  inset: 0;
204
221
  width: 100%;
205
222
  height: 100%;
206
223
  object-fit: cover;
207
- z-index: -2;
208
224
  border: 0;
209
225
  border-radius: 0;
210
226
  min-height: 0;
@@ -213,20 +229,40 @@
213
229
  content: "";
214
230
  position: absolute;
215
231
  inset: 0;
216
- z-index: -1;
232
+ z-index: 1;
233
+ pointer-events: none;
217
234
  background: linear-gradient(to top, var(--scrim), transparent 45%);
218
235
  }
219
- /* the back: paper — the words, big enough to read at arm's length */
236
+ /* the back: paper — the caption and the name packed at the middle, big enough to read at arm's length */
220
237
  .postcard__back {
221
238
  background: var(--postcard-back-bg, var(--card-2-bg));
222
239
  color: var(--postcard-back-ink, var(--card-2-ink));
223
- align-content: space-between;
224
240
  transform: rotateY(180deg);
225
241
  }
242
+ .postcard__back .postcard__turn { align-content: start; padding-top: var(--postcard-back-top, 20%); gap: var(--postcard-back-gap, var(--sp-5)); } /* the name a fifth down the paper, the words after it */
226
243
  .postcard__caption { margin: 0; font: var(--postcard-caption-font, var(--type-title)); text-wrap: pretty; }
227
- .postcard__label { font: var(--postcard-label-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; opacity: 0.85; }
244
+ /* the band: the face's foot — name and line left, the heart right; in flow at the face's end, over the turn.
245
+ Taps between its words fall through to the turn; its buttons take their own. */
246
+ .postcard__band {
247
+ position: relative;
248
+ z-index: 2;
249
+ display: flex;
250
+ align-items: end;
251
+ gap: var(--postcard-band-gap, var(--sp-3));
252
+ padding: var(--postcard-face-pad, var(--card-pad-rest));
253
+ pointer-events: none;
254
+ }
255
+ .postcard__band > * { pointer-events: auto; }
256
+ .postcard__words { display: grid; gap: var(--postcard-words-gap, var(--sp-1)); flex: 1; min-width: 0; pointer-events: none; }
257
+ .postcard__front .postcard__band { --accent: var(--postcard-front-ink, var(--card-3-ink)); --on-accent: var(--postcard-front-bg, var(--card-3-bg)); }
258
+ .postcard__band .icon-btn[aria-pressed="true"] .icon { fill: currentColor; }
228
259
  .postcard.is-flipped > .postcard__front { transform: rotateY(-180deg); }
229
260
  .postcard.is-flipped > .postcard__back { transform: rotateY(0); }
261
+ /* a voice note on the back: no words to read — its timeline (a Progress), its length and the control to hear it
262
+ under them, at the middle over the turn, on the face's inset; the app plays and keeps the time */
263
+ .postcard__voice { position: absolute; inset: 0; z-index: 2; display: grid; align-content: center; justify-items: center; gap: var(--postcard-voice-gap, var(--sp-3)); padding: var(--postcard-face-pad, var(--card-pad-rest)); pointer-events: none; }
264
+ .postcard__voice > * { pointer-events: auto; }
265
+ .postcard__voice > .progress { justify-self: stretch; pointer-events: none; }
230
266
  @media (prefers-reduced-motion: reduce) {
231
267
  .postcard__face { transition: none; }
232
268
  }
package/css/numbers.css CHANGED
@@ -256,3 +256,19 @@
256
256
  }
257
257
  .bar--on::after { background: var(--accent); }
258
258
  .bar--on > span { background: var(--accent); color: var(--on-accent); padding: var(--sp-0) var(--sp-2); border-radius: var(--r-chip); }
259
+
260
+ /* ---- progress: one straight line, the done share solid, the rest faint (Lh 2026-09-22) — a voice note's
261
+ timeline, a download, a step count as a line. <div class="progress" role="progressbar" style="--progress-at: 0.4"></div>
262
+ --progress-at is DATA the component writes (0–1), not a knob (scripts/knobs.mjs lists it). The done part is a
263
+ gradient of the line's ink sized by the share; the rest is the background colour. */
264
+ .progress {
265
+ display: block;
266
+ width: 100%;
267
+ height: var(--progress-h, var(--sp-1));
268
+ border-radius: var(--progress-radius, var(--r-chip));
269
+ color: var(--progress-done-ink, inherit);
270
+ background-color: var(--progress-rest-bg, color-mix(in srgb, currentColor 25%, transparent));
271
+ background-image: linear-gradient(currentColor, currentColor);
272
+ background-size: calc(var(--progress-at, 0) * 100%) 100%;
273
+ background-repeat: no-repeat;
274
+ }
package/css/sheet.css CHANGED
@@ -250,8 +250,13 @@
250
250
  inset-inline: var(--screen-pad);
251
251
  top: var(--_band-centre);
252
252
  translate: 0 -50%;
253
- /* no z-index: it follows the background in DOM order and the sheet, later, covers it at full */
253
+ /* as tall as the band less the screen's inset at both ends (Lh 2026-09-22: the room under the hero = the room at its sides);
254
+ what it holds fills it (a postcard's min-height gives way) */
255
+ height: calc((100cqh - var(--_top, 0rem)) / 2 - 2 * var(--screen-pad));
256
+ display: grid;
254
257
  }
258
+ .sheet-stage > .sheet-stage__hero > * { min-height: 0; } /* outranks a component's own min-height (the postcard's) */
259
+ /* no z-index on the hero: it follows the background in DOM order and the sheet, later, covers it at full */
255
260
 
256
261
  /* ---- map: the box a map engine draws in, behind a stage sheet ----
257
262
  <div class="sheet-stage__bg"><div class="map"><div>…the engine's canvas…</div>
@@ -327,10 +332,9 @@
327
332
  /* EVERY sheet has one shape (Lh, 2026-09-21): the handle and the head stay at the top and never scroll · the
328
333
  foot's button stands at the bottom, and ABOVE the keyboard when there is one · everything between them
329
334
  scrolls, as far as it needs to. In a .step the keyboard's room is the step's (--kb, step.css); on a stage
330
- the sheet gives it up itself, as --_lift — the same token a raised sheet floats on. */
335
+ the sheet gives it up itself, as --_lift. (A sheet ALWAYS stands on the floor — `.sheet--raised`, the one that
336
+ floated above a footnote strip on a dim stage, was retired 2026-09-22: that is a Modal's job.) */
331
337
  .sheet-stage > .sheet { --_lift: var(--kb, 0rem); bottom: var(--_lift); }
332
- .sheet-stage > .sheet--raised { --_lift: calc(var(--tap-lg) + var(--kb, 0rem)); }
333
- .sheet--raised { --_lift: var(--tap-lg); bottom: var(--_lift); border-radius: var(--sheet-raised-radius, var(--r-card)); border-bottom: var(--border-w) solid var(--card-border); }
334
338
  .sheet-stage__foot {
335
339
  position: absolute;
336
340
  inset-inline: 0;
package/css/stack.css CHANGED
@@ -19,7 +19,7 @@
19
19
  <div class="sheet-stack sheet-stack--tap"> (drop --tap for a static stack)
20
20
  <article class="card card--2">
21
21
  <div class="card__head">
22
- <button class="icon-btn icon-btn--sm card__back" aria-label="Back">…</button>
22
+ <button class="icon-btn icon-btn--sm sheet-stack__back" aria-label="Back">…</button>
23
23
  <h2 class="t-h2 t-caps">Shopping</h2>
24
24
  <span class="icon-row">…</span> (optional trailing icons)
25
25
  </div>
@@ -243,7 +243,7 @@
243
243
  .sheet-stack--tap > .card.is-open > .card__head {
244
244
  padding-left: calc(var(--tap-sm) + var(--sp-3));
245
245
  }
246
- .sheet-stack--tap .card__back {
246
+ .sheet-stack--tap .sheet-stack__back {
247
247
  position: absolute;
248
248
  left: 0;
249
249
  top: 50%;
@@ -252,7 +252,7 @@
252
252
  visibility: hidden;
253
253
  transition: opacity 0.35s ease, visibility 0.35s;
254
254
  }
255
- .sheet-stack--tap > .card.is-open .card__back {
255
+ .sheet-stack--tap > .card.is-open .sheet-stack__back {
256
256
  opacity: 1;
257
257
  visibility: visible;
258
258
  }