@usableapp/cardds 0.7.103 → 0.7.105

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 (66) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/css/actions.css +112 -37
  3. package/css/base.css +139 -15
  4. package/css/card.css +331 -86
  5. package/css/choice.css +25 -33
  6. package/css/finish.css +29 -28
  7. package/css/forms.css +6 -14
  8. package/css/journey.css +8 -6
  9. package/css/layover.css +26 -24
  10. package/css/lists.css +8 -6
  11. package/css/media.css +13 -74
  12. package/css/numbers.css +37 -32
  13. package/css/palette.css +2 -2
  14. package/css/people.css +3 -8
  15. package/css/post.css +23 -19
  16. package/css/sheet.css +84 -56
  17. package/css/stack.css +40 -216
  18. package/css/stamp.css +41 -29
  19. package/css/step.css +1 -107
  20. package/css/sticky.css +43 -41
  21. package/css/theme.template.css +12 -0
  22. package/css/tokens.css +101 -26
  23. package/dist/actions/Chip.d.ts +1 -1
  24. package/dist/actions/Chip.js +3 -2
  25. package/dist/actions/IconBtn.d.ts +12 -3
  26. package/dist/actions/IconBtn.js +9 -3
  27. package/dist/cardds.css +1094 -881
  28. package/dist/cards/Card.d.ts +8 -6
  29. package/dist/cards/Card.js +4 -4
  30. package/dist/cards/CardFeed.js +7 -9
  31. package/dist/cards/CardFoot.d.ts +7 -2
  32. package/dist/cards/CardFoot.js +4 -3
  33. package/dist/catalog.json +1 -1
  34. package/dist/journey/JourneyBadge.d.ts +1 -1
  35. package/dist/journey/JourneyBadge.js +1 -1
  36. package/dist/layover/Snackbar.js +2 -2
  37. package/dist/lists/Bubble.d.ts +1 -1
  38. package/dist/lists/Bubble.js +1 -1
  39. package/dist/lists/Row.js +5 -7
  40. package/dist/numbers/Picker.js +11 -18
  41. package/dist/people/Avatar.d.ts +1 -1
  42. package/dist/people/Avatar.js +3 -2
  43. package/dist/post/PostDesk.js +6 -9
  44. package/dist/post/PostLetter.js +4 -4
  45. package/dist/pressable.d.ts +9 -0
  46. package/dist/pressable.js +6 -0
  47. package/dist/room.d.ts +5 -0
  48. package/dist/room.js +9 -0
  49. package/dist/scaffold/Panes.js +8 -14
  50. package/dist/sheets/Sheet.d.ts +1 -1
  51. package/dist/sheets/Sheet.js +2 -3
  52. package/dist/sheets/SheetSteps.js +3 -3
  53. package/dist/sheets/StackSheet.d.ts +1 -1
  54. package/dist/sheets/sheetStepsPile.js +2 -3
  55. package/dist/sheets/stackCard.js +67 -115
  56. package/dist/state.d.ts +21 -0
  57. package/dist/state.js +28 -1
  58. package/dist/sticky/Sticky.js +3 -7
  59. package/dist/templates/Home.d.ts +1 -1
  60. package/dist/templates/Splash.js +2 -2
  61. package/dist/templates/Walkthrough.js +4 -5
  62. package/docs/reference.md +76 -12
  63. package/lib/catalog.mjs +23 -7
  64. package/lint/rules.mjs +7 -5
  65. package/lint/values.mjs +144 -0
  66. package/package.json +6 -4
package/css/card.css CHANGED
@@ -29,10 +29,10 @@
29
29
  card once `sent` and a `Card` with `leave` read the same two knobs; the time is --motion-sheet. `.card--leaver` is a card whose app may ask it
30
30
  to go (it holds the transition, so it comes back as it went); `.card--away` is one that has been asked. A knob of the time would read dead:
31
31
  the knob walk switches motion off */
32
- :is(.card--leaver, .post-desk__card) { transition: translate var(--motion-sheet) ease, rotate var(--motion-sheet) ease; }
33
- :is(.card--away, .post-desk--sent .post-desk__card) { --_y: calc(var(--leave-rise, 160%) * -1); rotate: var(--leave-turn, -5deg); }
32
+ .card--leaver { transition: translate var(--motion-sheet) var(--ease), rotate var(--motion-sheet) var(--ease); }
33
+ .card--away { --_y: calc(var(--leave-rise, 160%) * -1); rotate: var(--leave-turn, calc(var(--tilt-base) * -5)); }
34
34
  .card--away { translate: 0 var(--_y); }
35
- @media (prefers-reduced-motion: reduce) { :is(.card--leaver, .post-desk__card) { transition: none; } }
35
+ @media (prefers-reduced-motion: reduce) { .card--leaver { transition: none; } }
36
36
 
37
37
  /* each tone is a fill and a side; its words and glyphs follow from the side (tokens.css) */
38
38
  .card--2 { --card-bg: var(--card-2-bg); --dark: var(--card-2-dark); }
@@ -49,18 +49,13 @@
49
49
  multi-line title can opt back out with .card__head--top. */
50
50
  .card__head {
51
51
  display: flex;
52
- flex-wrap: wrap; /* the caption takes a line of its own — no wrapper div */
52
+ flex-wrap: var(--card-head-wrap, wrap); /* the caption takes a line of its own — no wrapper div */
53
53
  align-items: center;
54
54
  justify-content: space-between;
55
55
  column-gap: var(--card-head-col-gap, var(--sp-3));
56
56
  row-gap: var(--card-head-row-gap, var(--sp-0));
57
+ min-height: var(--card-head-min-h, auto);
57
58
  }
58
- /* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
59
- <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
60
- The heading and the far-right thing share the first row; a caption sits on
61
- a row below (order 1), whatever the DOM order — so an author writes the
62
- words in reading order. */
63
- .card__head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
64
59
  .card__head--top { align-items: flex-start; }
65
60
 
66
61
  .card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
@@ -80,12 +75,23 @@
80
75
  .card__head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
81
76
 
82
77
  /* Skeleton head — the heading in a card's OWN head, and only there.
83
- The heading wraps freely: no line budget, no squeeze, no clamp (Lh
84
- 2026-09-16 — a height rule on words only distorts them; content that needs
85
- more rows takes more rows). The group that holds heading ·
78
+ The heading wraps, no squeeze, up to three lines and cut there with an
79
+ ellipsis (Lh 2026-10-21: "มันควรจะ max สามบรรทัด" — a request's title ran to
80
+ four lines on a phone; reverses 2026-09-16's "no clamp"). The count is the knob
81
+ --card-head-title-lines. The group that holds heading ·
86
82
  caption is the head's own wrapping row (above) — no wrapper element, no
87
83
  height, only the row gap. */
88
- .card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
84
+ .card__head :is(h1, h2, h3) {
85
+ overflow-wrap: anywhere;
86
+ display: -webkit-box;
87
+ -webkit-box-orient: vertical;
88
+ -webkit-line-clamp: var(--card-head-title-lines, 3);
89
+ line-clamp: var(--card-head-title-lines, 3);
90
+ overflow: hidden;
91
+ padding-block-start: var(--_mark-room); /* a stacked Thai mark rises over the first line's box: room inside the clip, given back outside */
92
+ margin-block-start: calc(-1 * var(--_mark-room));
93
+ --_mark-room: 0.2em;
94
+ }
89
95
 
90
96
  /* a Stamp in a card's head (`Card end={<Stamp …/>}`): the words stand at the left, the stamp at the right BESIDE BOTH LINES — the heading and the
91
97
  caption pair up beside it, not the stamp beside the heading alone with the caption left under it. The stamp is its own size (--stamp-size,
@@ -93,15 +99,13 @@
93
99
  <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><span class="stamp">…</span></div> */
94
100
  .card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
95
101
  .card__head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
96
- .card__head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
97
- .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
98
102
 
99
103
  /* a JourneyBadge at the edge of the card's own first head (the date box in the top-left or top-right corner) keeps its corner concentric
100
104
  with the card's: the card's corner (--r-card) less the room the padding leaves round it. journey.css reads it behind the theme's --tile-badge-radius,
101
105
  so a theme that sets that knob still wins. */
102
106
  .card > .card__head:first-child > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); }
103
107
 
104
- /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are white paper bands edge to
108
+ /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are paper bands edge to
105
109
  edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
106
110
  clips, so the bands keep its corners. A two-faced card (below) carries the look on each face. */
107
111
  :is(.card--bands, .card--bands > .card__face) {
@@ -122,12 +126,14 @@
122
126
 
123
127
  /* ---- two faces: the WHOLE card turns (`Card back`, Lh 2026-10-19). The article is the turning box (.flip, base.css) and only that;
124
128
  each face is the card's paper — head · middle · foot — and both lie in one grid cell. The head and the foot are the same on both
125
- faces; the middle is one fixed height (--card-middle-h, the theme's call), so the card never changes height as it turns. The hidden
129
+ faces; the card is as tall as its taller face and each face fills it, the middle taking what the head and the foot leave (2026-10-21,
130
+ timebank: a fixed middle left a short front a tall empty band), so the card never changes height as it turns; --card-middle-h fixes
131
+ the middle's height instead, the theme's call. The hidden
126
132
  face is inert. Each face holds an unseen turn button: the keyboard's way to turn it; its ring is drawn on the face. */
127
133
  .card--turns { background: none; border: 0; padding: 0; cursor: pointer; touch-action: manipulation; }
128
134
  .card__face {
129
- display: grid;
130
- align-content: start;
135
+ display: flex;
136
+ flex-direction: column;
131
137
  gap: var(--card-gap);
132
138
  min-width: 0;
133
139
  padding: var(--card-pad);
@@ -138,13 +144,10 @@
138
144
  position: relative;
139
145
  }
140
146
  .card__face > * { min-width: 0; }
141
- .card__middle { display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, 18.75rem); overflow: hidden; }
142
- .card__turn { position: absolute; inset: 0; width: 1px; height: 1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
147
+ .card__middle { flex: 1 0 auto; display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, auto); overflow: hidden; }
148
+ .card__turn { position: absolute; inset: 0; width: var(--px); height: var(--px); padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
143
149
  .card__face:has(> .card__turn:focus-visible) { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
144
- /* a head's ChipRow takes a line of its own under the title: at most two lines of chips, a chip's words cut with an ellipsis */
145
- .card__head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * 1.5 + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
146
- .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.5; }
147
- .card__head > .chip-row > .chip > .icon { display: inline-block; vertical-align: -0.15em; margin-inline-end: var(--chip-sm-gap, var(--sp-1)); }
150
+ .card__head > .chip-row > .chip > .icon { --icon-display: inline-block; --icon-align: -0.15em; --icon-margin: 0 var(--chip-sm-gap, var(--sp-1)) 0 0; }
148
151
 
149
152
  /* ---- bleed: a scrolling track that runs to the container's edges ----
150
153
  For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
@@ -165,15 +168,15 @@
165
168
  nothing jumps. Chromium/Safari paint it through the ::-webkit-scrollbar
166
169
  parts; Firefox gets its thin bar in the same ink. */
167
170
  .bleed {
168
- margin-inline: calc(-1 * var(--card-pad, 0px));
169
- padding-inline: var(--card-pad, 0px);
171
+ margin-inline: calc(-1 * var(--card-pad, 0rem));
172
+ padding-inline: var(--card-pad, 0rem);
170
173
  padding-block-end: var(--sp-2);
171
- scroll-padding-inline: var(--card-pad, 0px);
174
+ scroll-padding-inline: var(--card-pad, 0rem);
172
175
  flex-wrap: nowrap;
173
176
  overflow-x: auto;
174
177
  }
175
178
  .bleed::-webkit-scrollbar { height: var(--scroll-w); }
176
- .bleed::-webkit-scrollbar-track { background: transparent; margin-inline: var(--card-pad, 0px); }
179
+ .bleed::-webkit-scrollbar-track { background: transparent; margin-inline: var(--card-pad, 0rem); }
177
180
  .bleed::-webkit-scrollbar-thumb { background: transparent; }
178
181
  .bleed:is(:hover, .is-scrolling)::-webkit-scrollbar-thumb { background: currentColor; }
179
182
  @supports not selector(::-webkit-scrollbar) {
@@ -187,29 +190,28 @@
187
190
  /* foot: the card's action buttons on one line. One button takes the width,
188
191
  up to four share it in even parts — so the gaps read even too — and from
189
192
  three on they drop their side padding so the labels keep the room. */
190
- .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; }
191
- .card__foot > .btn { flex: 1 1 0; min-width: 0; }
192
- /* …unless the button says its own size: `Btn form` is as wide as its words in a foot too, centred (Lh 2026-10-10: one button filling
193
- a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
194
- .card__foot > .btn--form { flex: 0 1 auto; }
195
- .card__foot:has(> :nth-child(3)) > .btn { padding-inline: var(--sp-2); }
193
+ .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; color: var(--card-foot-ink, inherit); }
194
+ .card__foot:has(> :nth-child(3)) > .btn { --btn-pad-x: var(--sp-2); }
196
195
  /* a small link under the buttons (CardFoot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
197
196
  whole short row of its own, centred — it never shares the buttons' line */
198
197
  .card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
199
198
  .card__foot-link { flex: 1 0 100%; display: flex; justify-content: center; }
199
+ /* round (CardFoot round): the foot holds a few round IconBtns, each its own size (they never share the line), as ONE group in the middle with
200
+ a gap that does not follow the card's width. The gap is wider than the Btns' (a miss between two round targets): --card-foot-gap sets it */
201
+ .card__foot--round { justify-content: center; gap: var(--card-foot-gap, var(--sp-6)); }
200
202
 
201
203
  /* media placeholder for wireframes — currentColor so it survives any palette */
202
204
  .ph {
203
205
  background:
204
- linear-gradient(to top right, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px)),
205
- linear-gradient(to top left, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px));
206
+ linear-gradient(to top right, transparent calc(50% - var(--px)), currentColor calc(50% - var(--px)), currentColor calc(50% + var(--px)), transparent calc(50% + var(--px))),
207
+ linear-gradient(to top left, transparent calc(50% - var(--px)), currentColor calc(50% - var(--px)), currentColor calc(50% + var(--px)), transparent calc(50% + var(--px)));
206
208
  border: var(--border-w) solid currentColor;
207
209
  border-radius: var(--r-media);
208
210
  opacity: var(--off);
209
- min-height: 10rem;
211
+ min-height: calc(var(--tap-base) * 10 / 3);
210
212
  }
211
- .ph--sm { min-height: 7.5rem; }
212
- .ph--md { min-height: 9rem; }
213
+ .ph--sm { min-height: calc(var(--tap-base) * 5 / 2); }
214
+ .ph--md { min-height: calc(var(--tap-base) * 3); }
213
215
  /* ---- centred card: one thing to read, at the human centre of a fill screen ----
214
216
  (.screen__centre below is the same placement for a small GROUP of things)
215
217
  Where the centre of a phone IS, to the person holding it: the bottom fifth is
@@ -222,8 +224,7 @@
222
224
  The card hugs its words and keeps the screen's inset. It is capped so that it
223
225
  never reaches the title bar above it nor the bar's room below it — past that it
224
226
  scrolls inside — from the same tokens those bars reserve their room with. */
225
- .card--centre,
226
- .screen--fill > .screen__centre {
227
+ .card--centre {
227
228
  position: absolute;
228
229
  top: var(--screen-centre);
229
230
  translate: 0 -50%;
@@ -240,38 +241,6 @@
240
241
  :is(.topbar, .appbar) ~ :is(.card--centre, .screen__centre) { --_lead: var(--header-h); }
241
242
  .card--centre { inset-inline: var(--screen-pad); }
242
243
 
243
- /* ---- centred block: the same place, for one thing to read that is not a
244
- lone card — an empty state, a card with its quiet second button and the
245
- pager under it. The block runs edge to
246
- edge and keeps the screen's inset as padding, and
247
- its children stack at the screen's own rhythm (Lh, 2026-09-11):
248
- <div class="screen screen--fill"><header class="topbar">…</header>
249
- <div class="screen__centre"><article class="card">…</article><button class="btn btn--quiet btn--block">…</button></div>
250
- </div> */
251
- .screen__centre {
252
- inset-inline: 0;
253
- padding-inline: var(--screen-pad);
254
- display: grid;
255
- gap: var(--screen-gap);
256
- align-content: start;
257
- }
258
- .screen__centre > * { min-width: 0; }
259
-
260
- /* ---- the same group on a plain .screen: centred when short, as long as it
261
- likes when not (Lh, 2026-09-12) ----
262
- The member sees one content area — whatever is between the top bar and the
263
- bar — and reads it as a whole page: a short card sits at its middle, a long
264
- one simply makes the page longer, runs under the bar (a sticky last row,
265
- actions.css) and scrolls up like any page — nothing is capped, nothing
266
- shrinks, no scroll box inside a box.
267
- So on a .screen that is NOT --fill the group is in flow: the column is at
268
- least one phone tall (step.css), the group takes the room after the top bar and
269
- centres its children in it. The screen's own inset stays the inset.
270
- <div class="screen"><header class="topbar">…</header>
271
- <div class="screen__centre"><article class="card">…</article><div class="pager__at">…</div></div>
272
- </div><div class="action-bar">…</div> */
273
- .screen:not(.screen--fill) > .screen__centre { flex: 1; padding-inline: 0; align-content: center; }
274
-
275
244
  /* ---- the card that changes slides: out one side, the next in from the other ----
276
245
  A .screen__centre group that walks pages (data-slide="next" | "back" on the
277
246
  root while a step changes) turns its card like a page: the old card slides
@@ -300,7 +269,7 @@
300
269
  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) */
301
270
  touch-action: pan-y; /* leaves vertical scroll to the browser; horizontal is the app's own drag, never a native pan */
302
271
  }
303
- .pages > :is(.card, .page) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
272
+ .pages > .card { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
304
273
  .pages > [inert] { visibility: hidden; }
305
274
  /* pages of WORDS (Lh, 2026-09-20): the same walk with .page pages, on the base — in a .step the
306
275
  .pages is the reading block (step.css), the round pager in the bar turns it, the same slideTo().
@@ -312,7 +281,7 @@
312
281
  ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
313
282
  ::view-transition-old(screen-out), ::view-transition-new(screen-in) { /* screen-*: slideScreen — one whole screen to the next (Lh 2026-09-25) */
314
283
  animation-duration: var(--motion-slide);
315
- animation-timing-function: ease-out;
284
+ animation-timing-function: var(--ease-out);
316
285
  mix-blend-mode: normal;
317
286
  }
318
287
  :root[data-slide="next"]::view-transition-old(centre-card-out), :root[data-slide="next"]::view-transition-old(screen-out) { animation-name: centre-card-out-left; }
@@ -327,9 +296,6 @@
327
296
  ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
328
297
  ::view-transition-old(screen-out), ::view-transition-new(screen-in) { animation: none; }
329
298
  }
330
- /* the page dots under the card sit centred under it — the indicator belongs
331
- with the page that changes, never in the bar (Lh, 2026-09-12) */
332
- .screen__centre > .pager__at { justify-self: center; }
333
299
 
334
300
  /* compact card inside a card (nested note, promo) */
335
301
  .card--sm { padding: var(--card-sm-pad, var(--sp-4)); --card-gap: var(--sp-2); border-radius: var(--card-sm-radius, var(--r-card)); }
@@ -338,7 +304,7 @@
338
304
  stack.css; a stacked sheet is a plain .card, the material */
339
305
 
340
306
  /* trailing icon cluster in a card header */
341
- .icon-row { display: flex; align-items: center; gap: var(--icon-row-gap, var(--sp-4)); opacity: 0.85; }
307
+ .icon-row { display: flex; align-items: center; gap: var(--icon-row-gap, var(--sp-4)); opacity: var(--fade-near); }
342
308
 
343
309
  /* ---- card list: rows, each row its own boundary ---- */
344
310
  .card-list { display: grid; gap: var(--card-list-gap, var(--sp-3)); }
@@ -374,7 +340,7 @@
374
340
  .card__remove {
375
341
  --_size: var(--card-remove-size, calc(var(--tap-base) / 2));
376
342
  position: relative;
377
- z-index: 1;
343
+ z-index: var(--z-over);
378
344
  flex: none;
379
345
  align-self: flex-start;
380
346
  display: grid;
@@ -392,7 +358,7 @@
392
358
  cursor: pointer;
393
359
  }
394
360
  .card__remove::after { content: ""; position: absolute; inset: 50% auto auto 50%; translate: -50% -50%; width: max(var(--tap-floor), var(--_size)); aspect-ratio: 1; border-radius: 50%; }
395
- .card__remove > .icon { width: 62%; height: 62%; }
361
+ .card__remove > .icon { --icon-size: 62%; }
396
362
  .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
397
363
 
398
364
  /* leading thumb/icon in a row */
@@ -435,7 +401,7 @@
435
401
  .feed-card__rest > :first-child { margin-top: var(--card-gap); }
436
402
  .feed-card--small .feed-card__rest { overflow: clip; visibility: hidden; }
437
403
  @media (prefers-reduced-motion: no-preference) {
438
- .feed-card__more { transition: grid-template-rows var(--motion-sheet) ease; }
404
+ .feed-card__more { transition: grid-template-rows var(--motion-sheet) var(--ease); }
439
405
  .feed-card--small .feed-card__rest { transition: visibility 0s var(--motion-sheet); }
440
406
  }
441
407
 
@@ -446,7 +412,7 @@
446
412
 
447
413
  .delta {
448
414
  font: var(--delta-font, var(--type-caption));
449
- font-weight: 700;
415
+ font-weight: var(--fw-bold);
450
416
  padding: var(--delta-pad-y, var(--sp-1)) var(--delta-pad-x, var(--sp-2));
451
417
  border-radius: var(--delta-radius, var(--r-chip));
452
418
  background: var(--delta-bg, var(--card-3-bg));
@@ -473,4 +439,283 @@
473
439
  scroll-snap-align: start; scroll-snap-stop: always;
474
440
  display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--card-gap); align-content: start;
475
441
  }
476
- .panes > .pager__at { justify-self: center; }
442
+
443
+ /* ==== the card's variants another family draws: Cover and the fold (media), Band (numbers) (#62: a block is written in its own file) ==== */
444
+
445
+ /* ---- cover: the image IS the card; text over a scrim ----
446
+ <article class="card card--cover"><div class="ph" role="img"></div>
447
+ <div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
448
+ <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
449
+ <div class="row">…meta…</div></article> */
450
+ .card--cover {
451
+ --card-bg: var(--card-3-bg);
452
+ --dark: var(--card-3-dark);
453
+ --accent: var(--card-ink);
454
+ --accent-dark: calc(1 - var(--dark));
455
+ min-height: var(--_cover-min-h);
456
+ align-content: space-between;
457
+ overflow: hidden;
458
+ isolation: isolate;
459
+ }
460
+ .card--cover > img,
461
+ .card--cover > .ph {
462
+ position: absolute;
463
+ inset: 0;
464
+ width: 100%;
465
+ height: 100%;
466
+ object-fit: cover;
467
+ z-index: var(--z-under-2);
468
+ border: 0;
469
+ border-radius: 0;
470
+ min-height: 0;
471
+ }
472
+ /* scrim: dark at both ends so the meta stays readable */
473
+ .card--cover::before {
474
+ content: "";
475
+ position: absolute;
476
+ inset: 0;
477
+ z-index: var(--z-under);
478
+ background: linear-gradient(to bottom, var(--scrim), transparent 40% 60%, var(--scrim));
479
+ }
480
+
481
+ /* ---- fold: a sticky-note corner on any card (decorative) ----
482
+ No border. The card's outline follows its border box, so it ran straight
483
+ across the squared bottom-right corner and cut through the fold's diagonal —
484
+ the corner read as a notch with a line through it rather than a lifted flap.
485
+ The COLOUR goes transparent rather than the border going to 0, so the box
486
+ keeps its geometry and a fold card still measures the same as the cards
487
+ beside it. */
488
+ .card--fold {
489
+ border-color: transparent;
490
+ border-bottom-right-radius: 0;
491
+ background-image: linear-gradient(to top left,
492
+ 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 */
493
+ color-mix(in srgb, currentColor 18%, var(--card-bg)) 50% 100%);
494
+ background-repeat: no-repeat;
495
+ background-position: bottom right;
496
+ background-size: var(--sp-8) var(--sp-8);
497
+ background-origin: border-box; /* the flap sits on the true corner, not inside the (transparent) border */
498
+ /* the corner is CUT: the triangle under the flap shows what is behind the card. Painted in the card's own colour it
499
+ read as the square corner of a second card underneath (Lh 2026-09-23). The polygon reaches --sp-12 past the other
500
+ edges so a coloured palette's shadow is not clipped there. */
501
+ clip-path: polygon(calc(-1 * var(--sp-12)) calc(-1 * var(--sp-12)), calc(100% + var(--sp-12)) calc(-1 * var(--sp-12)), calc(100% + var(--sp-12)) calc(100% - var(--sp-8) - var(--sp-12)),
502
+ calc(100% - var(--sp-8) - var(--sp-12)) calc(100% + var(--sp-12)), calc(-1 * var(--sp-12)) calc(100% + var(--sp-12)));
503
+ }
504
+
505
+ /* ---- band: data card; numbers.css draws its parts and the band stack ---- */
506
+ .card--band {
507
+ grid-template-columns: 1fr auto;
508
+ grid-template-areas: "label num" "foot num";
509
+ align-items: end;
510
+ gap: var(--sp-2) var(--sp-4);
511
+ min-height: var(--card-band-min-h, var(--tile-h));
512
+ }
513
+
514
+ /* ---- where it sits in what base.css draws (docs/reference.md rule 15) ---- */
515
+ .page > :is(img, .ph) { max-height: var(--page-media-h, calc(var(--tap-base) * 14 / 3)); 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 */
516
+ .page > .ph { width: 100%; height: var(--page-media-h, calc(var(--tap-base) * 14 / 3)); }
517
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
518
+ .sheet-stack > .card {
519
+ grid-area: 1 / 1; /* every card shares the stack's cell; later ones offset down */
520
+ border-bottom-left-radius: 0;
521
+ border-bottom-right-radius: 0;
522
+ border-bottom: 0;
523
+ align-content: start;
524
+ align-self: start; /* own natural height (padding + head) by default */
525
+ z-index: var(--level, var(--z-over)); /* explicit paint order, 1 = back of the stack */
526
+ }
527
+ /* THE FRONT SHEET is the last .card, not the last child: a --closed stack
528
+ may carry a .sheet-stack__base after its sheets (below). Written once here
529
+ as "a card no card follows" so every rule about the front sheet agrees. */
530
+ .sheet-stack > .card:not(:has(~ .card)) {
531
+ align-self: stretch; /* the front card fills the remaining stack height */
532
+ }
533
+ .sheet-stack > .card:nth-child(2) { margin-top: var(--stack-step); }
534
+ .sheet-stack > .card:nth-child(3) { margin-top: calc(2 * var(--stack-step)); }
535
+ .sheet-stack > .card:nth-child(4) { margin-top: calc(3 * var(--stack-step)); }
536
+ .sheet-stack > .card:nth-child(5) { margin-top: calc(4 * var(--stack-step)); }
537
+ /* stack headers: uniform — [back?] title left, icons right, one height,
538
+ same gap to content on every card */
539
+ .sheet-stack > .card > .card__head {
540
+ align-items: center;
541
+ justify-content: flex-start;
542
+ min-height: var(--tap);
543
+ }
544
+ .sheet-stack--display > .card > .card__head { align-items: start; min-height: 0; }
545
+ /* ============================================================
546
+ .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
547
+
548
+ A sheet (a .card — the material, sheet.css's single sheet is the same
549
+ stuff) is rigid: it can move in, move out, or be covered — it can
550
+ never change shape. So every sheet here is the SAME full height,
551
+ always, set once and never touched again by anything. A "peek" is
552
+ not a short sheet; it's a full sheet the next one happens to cover,
553
+ showing only the strip above it, exactly like a sheet of paper part
554
+ way out of a real stack.
555
+
556
+ Because of that, the whole animation is one property: translate.
557
+ Nothing resizes, nothing re-stacks — z-index is the plain resting
558
+ --level order and is never touched, since the order already says
559
+ what has to happen. Cards after the tapped one sit IN FRONT of it,
560
+ so they're what's covering it, so they're what must leave. Cards
561
+ before it are already BEHIND it, so they need do nothing at all
562
+ except let themselves be covered as it rises over them.
563
+
564
+ .is-open — the tapped card, moved up to the top of the stack. Its
565
+ own resting offset is margin-top (below); opening just
566
+ translates that offset back off, landing it flush at
567
+ the stack's top with its full height exactly filling
568
+ to the bottom.
569
+ .is-before — an earlier card, nudged one step down so it slips
570
+ under the next card's resting strip and is covered
571
+ there. Each carries its own --stagger (set in JS: 0
572
+ for the one nearest the tapped card, counting up
573
+ further back) so they tuck away, and later un-tuck,
574
+ one at a time.
575
+ .is-after — a later card, moved out below, together, no stagger.
576
+ ============================================================ */
577
+ .sheet-stack--tap > .card {
578
+ height: 100%; /* every card identical and full length, always — the one
579
+ size in this whole component, never toggled, never transitioned. At a
580
+ resting margin-top of n steps it simply overhangs the stack's bottom
581
+ by n steps (clipped by .screen--fill), so translating it up by exactly
582
+ that much lands it filling the stack precisely. */
583
+ overflow: clip;
584
+ transition: translate var(--motion-sheet) var(--ease); /* the only animated property here */
585
+ transition-delay: calc(var(--stagger, 0) * var(--motion-stagger)); /* 0 for everything
586
+ except a staggered is-before card: the card renders --stagger as it tucks or un-tucks */
587
+ }
588
+ .sheet-stack--tap > .card.is-open {
589
+ /* up by its own resting offset — pure movement, nothing resizes */
590
+ translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step) + var(--_pull, 0rem));
591
+ cursor: default;
592
+ overflow-y: auto;
593
+ overflow-x: clip;
594
+ overscroll-behavior: contain;
595
+ }
596
+ .sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
597
+ .sheet-stack--tap > .card > .card__head { touch-action: none; }
598
+ .sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
599
+ padding-top: calc(var(--card-pad) + var(--_refresh, 0rem));
600
+ transition: translate var(--motion-sheet) var(--ease), padding-top var(--motion-sheet) var(--ease);
601
+ }
602
+ /* the front sheet — the one open on arrival — never travels up (it is not
603
+ tappable), so its resting offset would leave its last n steps below the
604
+ screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
605
+ alone is cut to the room it actually has, and it scrolls like an opened
606
+ sheet does. Every other sheet keeps the one full height. */
607
+ .sheet-stack--tap:not(.sheet-stack--closed) > .card:not(:has(~ .card)) {
608
+ height: calc(100% - var(--slot, 0) * var(--stack-step));
609
+ overflow-y: auto;
610
+ overflow-x: clip;
611
+ overscroll-behavior: contain;
612
+ }
613
+ /* a scrolling sheet shows no scrollbar (a desktop with a mouse would paint one
614
+ down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself
615
+ (the hiding rule is base.css's, shared with every other scrolling track) */
616
+ .sheet-stack--tap > .card.is-before { translate: 0 var(--stack-step); }
617
+ .sheet-stack--tap > .card.is-after { translate: 0 100vh; }
618
+ .sheet-stack--closed > .card { translate: 0 calc(var(--_headroom) - var(--_lift, 0rem)); }
619
+ .sheet-stack--closed > .card.is-dragging { transition: none; }
620
+ .sheet-stack--tap:not(.sheet-stack--closed) > .card.is-dragging:not(.is-open) {
621
+ translate: 0 calc(-1 * var(--_lift, 0rem));
622
+ transition: none;
623
+ }
624
+ .sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
625
+ .sheet-stack--closed > .card.is-after { translate: 0 100vh; }
626
+ /* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
627
+ line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
628
+ :is([data-palette], [data-finish]) .sheet-stack__base .card { box-shadow: none; }
629
+ /* at the screen's top the open sheet's top corners are SQUARE (Lh 2026-10-08): round, they left two small pockets of the
630
+ bar's ground at the glass. They square as the sheet rises and round again as it folds, in the slide's own time. Paint
631
+ only, the sheet's size is untouched. A pull on the head keeps them square until the finger lets go. */
632
+ .sheet-stack--flood > .card { transition: translate var(--motion-sheet) var(--ease), border-radius var(--motion-sheet) var(--ease); }
633
+ .sheet-stack--flood > .card.is-open { border-top-left-radius: 0; border-top-right-radius: 0; }
634
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head {
635
+ position: relative;
636
+ padding-left: 0;
637
+ transition: padding-left var(--motion-sheet) var(--ease);
638
+ }
639
+ /* ---- a stacked sheet with a SheetBody has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
640
+ pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a CardFoot, the CTA) at the bottom.
641
+ The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no SheetBody
642
+ keeps the old way: the whole open card scrolls. ---- */
643
+ .sheet-stack--tap > .card:has(> .sheet__body) { display: flex; flex-direction: column; gap: var(--card-gap); }
644
+ .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
645
+ /* a paper stack's foot is paper to the card's edges, and it covers the tinted flex gap above it so the
646
+ button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
647
+ where it was — the padding gives back what the bleed takes. */
648
+ .sheet-stack > .card.sheet--paper > .card__foot {
649
+ background: var(--action-bg);
650
+ color: var(--card-ink);
651
+ margin-top: calc(-1 * var(--card-gap));
652
+ margin-inline: calc(-1 * var(--card-pad));
653
+ margin-bottom: calc(-1 * var(--card-pad));
654
+ padding-top: var(--card-gap);
655
+ padding-inline: var(--card-pad);
656
+ padding-bottom: var(--card-pad);
657
+ }
658
+ .stack-sheet > .card__head { flex-wrap: nowrap; }
659
+ /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
660
+ 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
661
+ (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
662
+ .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back:not(.sheet-stack__back--end)) { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
663
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) var(--ease); }
664
+ .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) { padding-right: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
665
+ /* 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),
666
+ 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) */
667
+ .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
668
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
669
+ .sheet > .card__foot {
670
+ position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: var(--z-over); /* the sheet's border box: the same height the sheet's own % reads */
671
+ align-items: end;
672
+ align-content: end;
673
+ padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
674
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
675
+ pointer-events: none;
676
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
677
+ transition: translate var(--motion-sheet) var(--ease), visibility 0s;
678
+ }
679
+ .sheet.is-dragging > .card__foot { transition: none; }
680
+ /* at peek and away the foot is NOT held on the box's edge: it stands on the sheet's own bottom edge (no counter-slide), so it
681
+ LEAVES AND ARRIVES WITH ITS SHEET — solid the whole way, hidden only once the slide is over. It used to stay in place and
682
+ fade: the body slid through the strip behind a half-transparent button (timebank, Lh 2026-10-01: "ตัวหนังสือลอดใต้ปุ่ม"). */
683
+ :is(.sheet--peek, .sheet--away) > .card__foot { translate: 0 0; visibility: hidden; transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--ease-linear) var(--motion-sheet); }
684
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
685
+ /* a WALK (Pages of Page) — Lh 2026-09-23, third call: no top; the picture and the words are ONE run that scrolls up together
686
+ (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
687
+ padding above AND below — so the words begin near the middle and, when a big text size runs them long, scroll up clear.
688
+ The walk shows no scrollbar. */
689
+ .step > .pages { touch-action: pan-y; } /* pan-y: the walk scrolls itself up and down; a sideways swipe is the Walkthrough's (it turns the page) */ /* the scrolling box is the whole content: from the step's top edge, edge to edge — no margin, the default (Lh 2026-09-23) */
690
+ .step > .pages > .page > :is(img, .ph):first-child {
691
+ width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
692
+ max-width: none;
693
+ margin-inline: calc(-1 * var(--screen-pad));
694
+ height: calc(var(--_lead, 0rem) + var(--page-media-h, calc(var(--tap-base) * 14 / 3)));
695
+ max-height: none;
696
+ border-radius: 0;
697
+ border-inline: 0;
698
+ border-top: 0;
699
+ object-fit: cover;
700
+ }
701
+ /* ---- where it sits in what numbers.css draws (docs/reference.md rule 15) ---- */
702
+ .band-stack > .card--band { border: 0; border-radius: 0; box-shadow: none; }
703
+ .band-stack > .card--band + .card--band { border-top: var(--border-w) solid var(--hairline); }
704
+ /* ---- where it sits in what sticky.css draws (docs/reference.md rule 15) ---- */
705
+ .sticky__head > .ph { flex: 1 1 0; min-width: 0; } /* a picture in the head takes the note's width */
706
+ .sticky__head:has(> .sticky-date) > .ph { flex: none; width: var(--sticky-pic-size, calc(var(--tap) * 1.5)); min-height: 0; aspect-ratio: 1; } /* beside a date the picture is a small square at the start */
707
+
708
+ /* ---- where it sits in what media.css draws: a postcard's picture (docs/reference.md rule 15) ---- */
709
+ .postcard__front .postcard__turn > .ph { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border: 0; border-radius: 0; min-height: 0; }
710
+
711
+ /* ---- where it sits in what stack.css draws: a sheet pulled to refresh holds still (docs/reference.md rule 15) ---- */
712
+ .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
713
+
714
+ /* ---- where it sits in what step.css draws: the reading block, a walk of pages (docs/reference.md rule 15) ---- */
715
+ .step:has(> .sheet--peek) > .pages { padding-bottom: var(--sheet-peek); }
716
+ .step > .pages {
717
+ flex: 0 1 auto;
718
+ min-height: 0;
719
+ overflow-y: auto;
720
+ overscroll-behavior: contain;
721
+ }