@usableapp/cardds 0.5.2 → 0.6.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,10 +2,22 @@
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.1 — 2026-09-22
6
+
7
+ - **A screen with no bar** (Lh 2026-09-22): a `Step` that is the `Screen`'s first child starts its words 15% of the screen's height down — `--screen-lead` (a knob, `15cqh`; `Screen fill` is a size container, `--_lead` is registered so the share resolves against the screen). Story `Step · NoBar`.
8
+ - **The base's own h1**: `CardHead level="h1"` inside a `BaseContent` reads at the card heading's size (`--fs-h2`) — h1 is the semantic level; `--fs-h1` stays the bar title's size.
9
+
10
+ ## 0.6.0 — 2026-09-22
11
+
12
+ Also in 0.6.0 (same commit): `slideTo(direction, update, scope?)` names the moving page for the move only — CSS no longer names every walk's current page (two walks on one page aborted the transition); on a base page the picture comes FIRST, then the heading, then the words; `--card-3-muted` is a real colour role (the five tone-3 opacity rules in lists.css read it; default = the wireframe's old look); `--dot-drop` (0.09em) sits a signal dot on the letters' middle.
13
+
14
+ ### The walk's dots are the Step's, not each page's (Lh 2026-09-22) — BREAKING
15
+ Reverses 0.5.2. A walk of `BaseContent` pages now carries **one** `PagerAt`, written after `Pages` — not one inside every page. `.step > .pager__at` sits at the base's foot, above the bar; `.base-content:has(> .pager__at:last-child)` (the 0.5.2 per-page flex-column form) is gone. An app with a `PagerAt` at the end of each page's `BaseContent` should move it to a single `PagerAt` after the `Pages` block instead.
16
+
5
17
  ## 0.5.2 — 2026-09-22
6
18
 
7
19
  ### Dots at the foot (Lh 2026-09-22)
8
- A `BaseContent` whose last child is `PagerAt` now sits its dots at the bottom of the reading block, above the bar; the block fills the step for it. Nothing to change in an app.
20
+ A `BaseContent` whose last child is `PagerAt` now sits its dots at the bottom of the reading block, above the bar; the block fills the step for it. Nothing to change in an app. Superseded by 0.6.0 — see above.
9
21
 
10
22
  ## 0.5.1 — 2026-09-22
11
23
 
package/css/base.css CHANGED
@@ -78,6 +78,7 @@ button { font: inherit; cursor: pointer; }
78
78
  height: 100dvh;
79
79
  overflow: clip;
80
80
  position: relative; /* the box a .card--centre or a .screen__centre floats in (card.css) */
81
+ container-type: size; /* a share of the SCREEN's height (a no-bar step's lead, step.css) reads it as cqh — a real phone's height, or the gallery's frame */
81
82
  }
82
83
  /* min-height: 0 — a flex item's floor is its content, and the stack's content
83
84
  is its own full-height sheets, so without it the stack grew to the whole
@@ -117,15 +118,12 @@ button { font: inherit; cursor: pointer; }
117
118
  }
118
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) */
119
120
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
120
- the "where am I" dots under the words, centred — the same dots the centred card has under it */
121
+ (the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
122
+ /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
123
+ 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); }
121
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 */
122
126
  .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
123
- .base-content > .pager__at { justify-self: center; }
124
- /* the dots at the FOOT of the block (Lh 2026-09-22): a block that ends on .pager__at becomes a flex column so the dots
125
- ride its bottom edge — the words keep the top, the free room opens between. The block must fill its cell for that:
126
- in .pages it is stretched to the cell; a lone .step > .base-content grows for it (step.css). */
127
- .base-content:has(> .pager__at:last-child) { display: flex; flex-direction: column; }
128
- .base-content > .pager__at:last-child { margin-top: auto; align-self: center; }
129
127
  .base-content .cta-pack {
130
128
  display: flex;
131
129
  align-items: start; /* buttons keep their own height, never the row's */
package/css/card.css CHANGED
@@ -208,7 +208,8 @@
208
208
  and updates inside it; a browser without them (the floor: Chrome 105 / iOS 16)
209
209
  simply switches. Only the card is named; the rest of the page must not
210
210
  cross-fade. */
211
- .screen__centre > .card, .pages > [aria-current] { view-transition-name: centre-card; }
211
+ /* the name is set by slideTo() on the moving element for the move only (2026-09-22): a static name on every walk's current
212
+ page made two walks on one page — the gallery — share it, and the browser aborts a transition with a duplicate name */
212
213
 
213
214
  /* ---- pages: the cards of a walk, all one height ----
214
215
  A sequence the member walks (an introduction) reads steadier when every card
@@ -221,7 +222,11 @@
221
222
  <article class="card" aria-current="step">…</article>
222
223
  <article class="card" inert>…</article>
223
224
  </div><div class="pager__at">…</div></div> */
224
- .pages { display: grid; }
225
+ .pages {
226
+ display: grid;
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
+ touch-action: pan-y; /* leaves vertical scroll to the browser; horizontal is the app's own drag, never a native pan */
229
+ }
225
230
  .pages > :is(.card, .base-content) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
226
231
  .pages > [inert] { visibility: hidden; }
227
232
  /* pages of WORDS (Lh, 2026-09-20): the same walk with .base-content pages, on the base — in a .step the
package/css/lists.css CHANGED
@@ -32,7 +32,7 @@
32
32
  .row__body { flex: 1; min-width: 0; display: grid; gap: var(--row-body-gap, var(--sp-0)); }
33
33
  .row__title { font: var(--row-title-font, var(--type-title)); overflow-wrap: anywhere; } /* wraps freely — no clip, so no padding trick for Thai marks (Lh 2026-09-16, same call as the card heading) */
34
34
  .row__meta { font: var(--row-meta-font, var(--type-caption)); color: var(--row-meta-ink, var(--card-muted)); display: flex; align-items: center; gap: var(--row-meta-gap, var(--sp-1)); }
35
- .card--3 .row__meta { color: inherit; opacity: 0.7; }
35
+ .card--3 .row__meta { color: var(--card-3-muted); }
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); }
@@ -51,7 +51,7 @@
51
51
  at --tap-sm, but not an input: nothing to press. */
52
52
  .row--done { pointer-events: none; cursor: default; }
53
53
  .row--done .row__title { color: var(--card-muted); }
54
- .card--3 .row--done .row__title { color: inherit; opacity: 0.7; }
54
+ .card--3 .row--done .row__title { color: var(--card-3-muted); }
55
55
  .row__done {
56
56
  width: var(--row-done-size, var(--tap-sm));
57
57
  height: var(--row-done-size, var(--tap-sm));
@@ -93,7 +93,7 @@
93
93
  .kv-cell > .icon { margin-bottom: var(--sp-1); }
94
94
  .kv-cell__val { font: var(--kv-cell-val-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
95
95
  .kv-cell__key { font: var(--kv-cell-key-font, var(--type-caption)); color: var(--kv-cell-key-ink, var(--card-muted)); }
96
- .card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
96
+ .card--3 .kv-cell__key { color: var(--card-3-muted); }
97
97
 
98
98
  /* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
99
99
  No tail: a rounded block whose corner nearest the sender is square-ish
@@ -143,7 +143,7 @@
143
143
  .bubble__meta { display: flex; align-items: center; gap: var(--bubble-meta-gap, var(--sp-1)); font: var(--bubble-meta-font, var(--type-caption)); color: var(--bubble-meta-ink, var(--on-surface-muted)); }
144
144
  .bubble--right > .bubble__meta { justify-self: end; }
145
145
  .bubble__meta > .icon { color: var(--accent); }
146
- .card--3 .bubble__meta { color: inherit; opacity: 0.7; }
146
+ .card--3 .bubble__meta { color: var(--card-3-muted); }
147
147
 
148
148
  /* ---- timeline: a hairline with date chips, entries to the right ----
149
149
  <ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
@@ -206,4 +206,4 @@
206
206
  cursor: pointer;
207
207
  }
208
208
  .link--muted { color: var(--link-muted-ink, var(--card-muted)); }
209
- .card--3 .link--muted { color: inherit; opacity: 0.7; }
209
+ .card--3 .link--muted { color: var(--card-3-muted); }
package/css/media.css CHANGED
@@ -116,7 +116,7 @@
116
116
  border-color: transparent;
117
117
  border-bottom-right-radius: 0;
118
118
  background-image: linear-gradient(to top left,
119
- var(--surface) 0 50%,
119
+ var(--card-bg) 0 50%, /* the card's own background, not --surface (the page's) — a mismatch here drew a seam at the flap's edge */
120
120
  color-mix(in srgb, currentColor 18%, var(--card-bg)) 50% 100%);
121
121
  background-repeat: no-repeat;
122
122
  background-position: bottom right;
package/css/step.css CHANGED
@@ -54,6 +54,10 @@
54
54
  that thing. */
55
55
  .topbar + .step { margin-top: calc(-1 * var(--screen-gap)); --_lead: var(--screen-gap); }
56
56
  .appbar + .step { margin-top: calc(-1 * var(--header-gap)); --_lead: var(--screen-gap); }
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
+ 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
+ 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); }
57
61
 
58
62
  /* the bar's room — read from actions.css, never recomputed. Margin, not
59
63
  padding, so the clip cuts at the box edge and nothing shows behind the
@@ -89,7 +93,9 @@
89
93
  .step > :is(.base-content, .pages) {
90
94
  flex: 0 1 auto;
91
95
  }
92
- .step:has(> .base-content > .pager__at:last-child, > .pages > .base-content > .pager__at:last-child) > :is(.base-content, .pages) { flex: 1 1 auto; } /* dots at the foot: the block takes the column (base.css) */
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; }
93
99
  .step > :is(.base-content, .pages) {
94
100
  min-height: 0;
95
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 */
package/css/tokens.css CHANGED
@@ -155,6 +155,7 @@
155
155
  --_muted-on-paper: color-mix(in srgb, var(--color-ink) 66%, var(--color-paper, #ffffff));
156
156
  --_tone-2: color-mix(in srgb, var(--color-brand) 14%, var(--color-surface, #f2f2f0));
157
157
  --_muted-on-tone-2: color-mix(in srgb, var(--color-ink, #141414) 72%, var(--_tone-2));
158
+ --_muted-on-brand: color-mix(in srgb, var(--color-on-brand) 70%, var(--color-brand)); /* the tone-3 card's secondary words: on-brand faded into brand — was opacity 0.7, which no theme could set (timebank #132, 2026-09-22) */
158
159
 
159
160
  /* app background ("earth-toned surface or shiny color, up to the app") */
160
161
  --surface: var(--color-surface, #f2f2f0);
@@ -168,6 +169,7 @@
168
169
 
169
170
  --card-border: #141414; /* wireframe: visible outline; palettes may set transparent */
170
171
  --card-muted: var(--_muted-on-paper, #6b6b6b); /* secondary text inside cards (the default, tone-1 card) */
172
+ --card-3-muted: var(--_muted-on-brand, #b9b9b9); /* the same text on the tone-3 card — a real colour, like the two above: a theme whose brand is not near-black sets it to reach its contrast (the wireframe's #b9b9b9 = white at 0.7 on #141414, what the opacity gave) */
171
173
  --card-2-muted: var(--_muted-on-tone-2, #5f5f5f); /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
172
174
  a palette whose tone-2 ground is strong
173
175
  sets this to its tone-2 ink — a muted shade cannot reach AA there (clay) */
@@ -209,6 +211,7 @@
209
211
  falls back to when its own declaration cannot resolve.
210
212
  ============================================================ */
211
213
  @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 */
214
+ @property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; }
215
+ @property --_lead { syntax: "<length>"; inherits: true; initial-value: 0px; } /* the room above a step's words: registered so a cqh share (a no-bar step, 15% of the SCREEN) resolves where it is declared — on the step, against the screen — not on the words, against the step */ /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
213
216
  @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
214
217
  @property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
package/dist/cardds.css CHANGED
@@ -196,6 +196,7 @@
196
196
  --_muted-on-paper: color-mix(in srgb, var(--color-ink) 66%, var(--color-paper, #ffffff));
197
197
  --_tone-2: color-mix(in srgb, var(--color-brand) 14%, var(--color-surface, #f2f2f0));
198
198
  --_muted-on-tone-2: color-mix(in srgb, var(--color-ink, #141414) 72%, var(--_tone-2));
199
+ --_muted-on-brand: color-mix(in srgb, var(--color-on-brand) 70%, var(--color-brand)); /* the tone-3 card's secondary words: on-brand faded into brand — was opacity 0.7, which no theme could set (timebank #132, 2026-09-22) */
199
200
 
200
201
  /* app background ("earth-toned surface or shiny color, up to the app") */
201
202
  --surface: var(--color-surface, #f2f2f0);
@@ -209,6 +210,7 @@
209
210
 
210
211
  --card-border: #141414; /* wireframe: visible outline; palettes may set transparent */
211
212
  --card-muted: var(--_muted-on-paper, #6b6b6b); /* secondary text inside cards (the default, tone-1 card) */
213
+ --card-3-muted: var(--_muted-on-brand, #b9b9b9); /* the same text on the tone-3 card — a real colour, like the two above: a theme whose brand is not near-black sets it to reach its contrast (the wireframe's #b9b9b9 = white at 0.7 on #141414, what the opacity gave) */
212
214
  --card-2-muted: var(--_muted-on-tone-2, #5f5f5f); /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
213
215
  a palette whose tone-2 ground is strong
214
216
  sets this to its tone-2 ink — a muted shade cannot reach AA there (clay) */
@@ -250,7 +252,8 @@
250
252
  falls back to when its own declaration cannot resolve.
251
253
  ============================================================ */
252
254
  @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 */
255
+ @property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; }
256
+ @property --_lead { syntax: "<length>"; inherits: true; initial-value: 0px; } /* the room above a step's words: registered so a cqh share (a no-bar step, 15% of the SCREEN) resolves where it is declared — on the step, against the screen — not on the words, against the step */ /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
254
257
  @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
255
258
  @property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
256
259
 
@@ -454,6 +457,7 @@ button { font: inherit; cursor: pointer; }
454
457
  height: 100dvh;
455
458
  overflow: clip;
456
459
  position: relative; /* the box a .card--centre or a .screen__centre floats in (card.css) */
460
+ container-type: size; /* a share of the SCREEN's height (a no-bar step's lead, step.css) reads it as cqh — a real phone's height, or the gallery's frame */
457
461
  }
458
462
  /* min-height: 0 — a flex item's floor is its content, and the stack's content
459
463
  is its own full-height sheets, so without it the stack grew to the whole
@@ -493,15 +497,12 @@ button { font: inherit; cursor: pointer; }
493
497
  }
494
498
  .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
499
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
496
- the "where am I" dots under the words, centred — the same dots the centred card has under it */
500
+ (the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
501
+ /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
502
+ h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
503
+ .base-content > .card__head > .t-h1 { font-size: var(--fs-h2); }
497
504
  .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 */
498
505
  .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
499
- .base-content > .pager__at { justify-self: center; }
500
- /* the dots at the FOOT of the block (Lh 2026-09-22): a block that ends on .pager__at becomes a flex column so the dots
501
- ride its bottom edge — the words keep the top, the free room opens between. The block must fill its cell for that:
502
- in .pages it is stretched to the cell; a lone .step > .base-content grows for it (step.css). */
503
- .base-content:has(> .pager__at:last-child) { display: flex; flex-direction: column; }
504
- .base-content > .pager__at:last-child { margin-top: auto; align-self: center; }
505
506
  .base-content .cta-pack {
506
507
  display: flex;
507
508
  align-items: start; /* buttons keep their own height, never the row's */
@@ -735,7 +736,8 @@ button { font: inherit; cursor: pointer; }
735
736
  and updates inside it; a browser without them (the floor: Chrome 105 / iOS 16)
736
737
  simply switches. Only the card is named; the rest of the page must not
737
738
  cross-fade. */
738
- .screen__centre > .card, .pages > [aria-current] { view-transition-name: centre-card; }
739
+ /* the name is set by slideTo() on the moving element for the move only (2026-09-22): a static name on every walk's current
740
+ page made two walks on one page — the gallery — share it, and the browser aborts a transition with a duplicate name */
739
741
 
740
742
  /* ---- pages: the cards of a walk, all one height ----
741
743
  A sequence the member walks (an introduction) reads steadier when every card
@@ -748,7 +750,11 @@ button { font: inherit; cursor: pointer; }
748
750
  <article class="card" aria-current="step">…</article>
749
751
  <article class="card" inert>…</article>
750
752
  </div><div class="pager__at">…</div></div> */
751
- .pages { display: grid; }
753
+ .pages {
754
+ display: grid;
755
+ 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) */
756
+ touch-action: pan-y; /* leaves vertical scroll to the browser; horizontal is the app's own drag, never a native pan */
757
+ }
752
758
  .pages > :is(.card, .base-content) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
753
759
  .pages > [inert] { visibility: hidden; }
754
760
  /* pages of WORDS (Lh, 2026-09-20): the same walk with .base-content pages, on the base — in a .step the
@@ -1508,6 +1514,10 @@ button { font: inherit; cursor: pointer; }
1508
1514
  that thing. */
1509
1515
  .topbar + .step { margin-top: calc(-1 * var(--screen-gap)); --_lead: var(--screen-gap); }
1510
1516
  .appbar + .step { margin-top: calc(-1 * var(--header-gap)); --_lead: var(--screen-gap); }
1517
+ /* 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):
1518
+ the words start a share of the screen down — --screen-lead, 15% of its height (Lh 2026-09-22), so a tall phone gives
1519
+ more air and a short one less; a screen with a bar keeps the bar's own room */
1520
+ .screen > .step:first-child { --_lead: var(--screen-lead, 15cqh); }
1511
1521
 
1512
1522
  /* the bar's room — read from actions.css, never recomputed. Margin, not
1513
1523
  padding, so the clip cuts at the box edge and nothing shows behind the
@@ -1543,7 +1553,9 @@ button { font: inherit; cursor: pointer; }
1543
1553
  .step > :is(.base-content, .pages) {
1544
1554
  flex: 0 1 auto;
1545
1555
  }
1546
- .step:has(> .base-content > .pager__at:last-child, > .pages > .base-content > .pager__at:last-child) > :is(.base-content, .pages) { flex: 1 1 auto; } /* dots at the foot: the block takes the column (base.css) */
1556
+ /* 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
1557
+ 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) */
1558
+ .step > .pager__at { margin-top: auto; align-self: center; padding-block: var(--sp-3); flex: none; }
1547
1559
  .step > :is(.base-content, .pages) {
1548
1560
  min-height: 0;
1549
1561
  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 */
@@ -2675,7 +2687,7 @@ button { font: inherit; cursor: pointer; }
2675
2687
  .row__body { flex: 1; min-width: 0; display: grid; gap: var(--row-body-gap, var(--sp-0)); }
2676
2688
  .row__title { font: var(--row-title-font, var(--type-title)); overflow-wrap: anywhere; } /* wraps freely — no clip, so no padding trick for Thai marks (Lh 2026-09-16, same call as the card heading) */
2677
2689
  .row__meta { font: var(--row-meta-font, var(--type-caption)); color: var(--row-meta-ink, var(--card-muted)); display: flex; align-items: center; gap: var(--row-meta-gap, var(--sp-1)); }
2678
- .card--3 .row__meta { color: inherit; opacity: 0.7; }
2690
+ .card--3 .row__meta { color: var(--card-3-muted); }
2679
2691
  .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
2680
2692
  .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 */
2681
2693
  .row--lg .row__title { font: 700 var(--fs-lg)/1.2 var(--font-display); }
@@ -2694,7 +2706,7 @@ button { font: inherit; cursor: pointer; }
2694
2706
  at --tap-sm, but not an input: nothing to press. */
2695
2707
  .row--done { pointer-events: none; cursor: default; }
2696
2708
  .row--done .row__title { color: var(--card-muted); }
2697
- .card--3 .row--done .row__title { color: inherit; opacity: 0.7; }
2709
+ .card--3 .row--done .row__title { color: var(--card-3-muted); }
2698
2710
  .row__done {
2699
2711
  width: var(--row-done-size, var(--tap-sm));
2700
2712
  height: var(--row-done-size, var(--tap-sm));
@@ -2736,7 +2748,7 @@ button { font: inherit; cursor: pointer; }
2736
2748
  .kv-cell > .icon { margin-bottom: var(--sp-1); }
2737
2749
  .kv-cell__val { font: var(--kv-cell-val-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
2738
2750
  .kv-cell__key { font: var(--kv-cell-key-font, var(--type-caption)); color: var(--kv-cell-key-ink, var(--card-muted)); }
2739
- .card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
2751
+ .card--3 .kv-cell__key { color: var(--card-3-muted); }
2740
2752
 
2741
2753
  /* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
2742
2754
  No tail: a rounded block whose corner nearest the sender is square-ish
@@ -2786,7 +2798,7 @@ button { font: inherit; cursor: pointer; }
2786
2798
  .bubble__meta { display: flex; align-items: center; gap: var(--bubble-meta-gap, var(--sp-1)); font: var(--bubble-meta-font, var(--type-caption)); color: var(--bubble-meta-ink, var(--on-surface-muted)); }
2787
2799
  .bubble--right > .bubble__meta { justify-self: end; }
2788
2800
  .bubble__meta > .icon { color: var(--accent); }
2789
- .card--3 .bubble__meta { color: inherit; opacity: 0.7; }
2801
+ .card--3 .bubble__meta { color: var(--card-3-muted); }
2790
2802
 
2791
2803
  /* ---- timeline: a hairline with date chips, entries to the right ----
2792
2804
  <ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
@@ -2849,7 +2861,7 @@ button { font: inherit; cursor: pointer; }
2849
2861
  cursor: pointer;
2850
2862
  }
2851
2863
  .link--muted { color: var(--link-muted-ink, var(--card-muted)); }
2852
- .card--3 .link--muted { color: inherit; opacity: 0.7; }
2864
+ .card--3 .link--muted { color: var(--card-3-muted); }
2853
2865
 
2854
2866
  /* ---- css/media.css — cover card, quote, tile grid, fold, mosaic, wave ---- */
2855
2867
  /* ============================================================
@@ -2970,7 +2982,7 @@ button { font: inherit; cursor: pointer; }
2970
2982
  border-color: transparent;
2971
2983
  border-bottom-right-radius: 0;
2972
2984
  background-image: linear-gradient(to top left,
2973
- var(--surface) 0 50%,
2985
+ var(--card-bg) 0 50%, /* the card's own background, not --surface (the page's) — a mismatch here drew a seam at the flap's edge */
2974
2986
  color-mix(in srgb, currentColor 18%, var(--card-bg)) 50% 100%);
2975
2987
  background-repeat: no-repeat;
2976
2988
  background-position: bottom right;
@@ -29,4 +29,4 @@ export type SlideDirection = 'next' | 'back';
29
29
  * (card.css, `--motion-slide`); the heading and the dots stay still. Without view transitions (the browser
30
30
  * floor) it just runs the update.
31
31
  */
32
- export declare function slideTo(direction: SlideDirection, update: () => void): void;
32
+ export declare function slideTo(direction: SlideDirection, update: () => void, scope?: ParentNode): void;
@@ -29,15 +29,25 @@ export function Pages({ className, ...rest }) {
29
29
  * (card.css, `--motion-slide`); the heading and the dots stay still. Without view transitions (the browser
30
30
  * floor) it just runs the update.
31
31
  */
32
- export function slideTo(direction, update) {
32
+ export function slideTo(direction, update, scope = document) {
33
33
  const root = document.documentElement;
34
34
  if (typeof document.startViewTransition !== 'function') {
35
35
  update();
36
36
  return;
37
37
  }
38
+ /* the moving thing is NAMED for the transition only while it moves, and only inside `scope` (the walk's Step or Centre —
39
+ an app has one walk per screen and can leave it out; the gallery shows several on one page, and two elements with the
40
+ same name abort the transition). CSS names nothing (2026-09-22). */
41
+ const moving = () => scope.querySelector('.pages > [aria-current], .screen__centre > .card');
42
+ const name = (el, v) => { if (el)
43
+ el.style.viewTransitionName = v; };
44
+ const before = moving();
45
+ name(before, 'centre-card');
38
46
  root.dataset.slide = direction;
39
- const transition = document.startViewTransition(() => flushSync(update));
47
+ let after = null;
48
+ const transition = document.startViewTransition(() => { flushSync(update); name(before, ''); after = moving(); name(after, 'centre-card'); });
40
49
  void transition.finished.finally(() => {
41
50
  delete root.dataset.slide;
51
+ name(after, '');
42
52
  });
43
53
  }
@@ -7,5 +7,5 @@ export interface PagerAtProps extends Omit<ComponentProps<'span'>, 'children'> {
7
7
  /** accessible name; defaults to "Page {at} of {of}" */
8
8
  label?: string;
9
9
  }
10
- /** PagerAt — the "where am I" dots, centred under the card that changes (a child of Centre): one accent dot at the current page. Never inside an ActionBar. */
10
+ /** PagerAt — the "where am I" dots: one accent dot at the current page. Under the card that changes (a child of Centre), or a Step's child after its Pages — pinned to the base's foot. One per walk, never inside an ActionBar, never inside a page. */
11
11
  export declare function PagerAt({ at, of, label, className, ...rest }: PagerAtProps): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
- /** PagerAt — the "where am I" dots, centred under the card that changes (a child of Centre): one accent dot at the current page. Never inside an ActionBar. */
3
+ /** PagerAt — the "where am I" dots: one accent dot at the current page. Under the card that changes (a child of Centre), or a Step's child after its Pages — pinned to the base's foot. One per walk, never inside an ActionBar, never inside a page. */
4
4
  export function PagerAt({ at, of, label, className, ...rest }) {
5
5
  return (_jsx("span", { className: cx('pager__at', className), role: "img", "aria-label": label ?? `Page ${at} of ${of}`, ...rest, children: Array.from({ length: of }, (_, i) => _jsx("i", { className: i + 1 === at ? 'is-at' : undefined }, i)) }));
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.5.2",
3
+ "version": "0.6.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.",