@usableapp/cardds 0.7.102 → 0.7.104

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 (60) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/css/actions.css +102 -36
  3. package/css/base.css +139 -15
  4. package/css/card.css +308 -79
  5. package/css/choice.css +25 -33
  6. package/css/finish.css +28 -27
  7. package/css/forms.css +6 -14
  8. package/css/journey.css +4 -4
  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 +1 -1
  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 +39 -215
  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 +100 -26
  23. package/dist/actions/Chip.d.ts +1 -1
  24. package/dist/actions/Chip.js +3 -2
  25. package/dist/cardds.css +1053 -868
  26. package/dist/cards/Card.d.ts +3 -3
  27. package/dist/cards/Card.js +1 -1
  28. package/dist/cards/CardFeed.js +7 -9
  29. package/dist/catalog.json +1 -1
  30. package/dist/layover/Snackbar.js +2 -2
  31. package/dist/lists/Bubble.d.ts +1 -1
  32. package/dist/lists/Bubble.js +1 -1
  33. package/dist/lists/Row.js +5 -7
  34. package/dist/numbers/Picker.js +11 -18
  35. package/dist/people/Avatar.d.ts +1 -1
  36. package/dist/people/Avatar.js +3 -2
  37. package/dist/post/PostDesk.js +6 -9
  38. package/dist/post/PostLetter.js +4 -4
  39. package/dist/pressable.d.ts +9 -0
  40. package/dist/pressable.js +6 -0
  41. package/dist/room.d.ts +5 -0
  42. package/dist/room.js +9 -0
  43. package/dist/scaffold/Panes.js +8 -14
  44. package/dist/sheets/Sheet.d.ts +1 -1
  45. package/dist/sheets/Sheet.js +2 -3
  46. package/dist/sheets/SheetSteps.js +3 -3
  47. package/dist/sheets/StackSheet.d.ts +1 -1
  48. package/dist/sheets/sheetStepsPile.js +2 -3
  49. package/dist/sheets/stackCard.js +67 -115
  50. package/dist/state.d.ts +21 -0
  51. package/dist/state.js +28 -1
  52. package/dist/sticky/Sticky.js +3 -7
  53. package/dist/templates/Home.d.ts +1 -1
  54. package/dist/templates/Splash.js +2 -2
  55. package/dist/templates/Walkthrough.js +4 -5
  56. package/docs/reference.md +71 -9
  57. package/lib/catalog.mjs +23 -7
  58. package/lint/rules.mjs +11 -6
  59. package/lint/values.mjs +144 -0
  60. 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 */
@@ -93,15 +88,13 @@
93
88
  <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><span class="stamp">…</span></div> */
94
89
  .card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
95
90
  .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
91
 
99
92
  /* 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
93
  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
94
  so a theme that sets that knob still wins. */
102
95
  .card > .card__head:first-child > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); }
103
96
 
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
97
+ /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are paper bands edge to
105
98
  edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
106
99
  clips, so the bands keep its corners. A two-faced card (below) carries the look on each face. */
107
100
  :is(.card--bands, .card--bands > .card__face) {
@@ -138,13 +131,10 @@
138
131
  position: relative;
139
132
  }
140
133
  .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%); }
134
+ .card__middle { display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, calc(var(--tap-base) * 25 / 4)); overflow: hidden; }
135
+ .card__turn { position: absolute; inset: 0; width: var(--px); height: var(--px); padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
143
136
  .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)); }
137
+ .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
138
 
149
139
  /* ---- bleed: a scrolling track that runs to the container's edges ----
150
140
  For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
@@ -165,15 +155,15 @@
165
155
  nothing jumps. Chromium/Safari paint it through the ::-webkit-scrollbar
166
156
  parts; Firefox gets its thin bar in the same ink. */
167
157
  .bleed {
168
- margin-inline: calc(-1 * var(--card-pad, 0px));
169
- padding-inline: var(--card-pad, 0px);
158
+ margin-inline: calc(-1 * var(--card-pad, 0rem));
159
+ padding-inline: var(--card-pad, 0rem);
170
160
  padding-block-end: var(--sp-2);
171
- scroll-padding-inline: var(--card-pad, 0px);
161
+ scroll-padding-inline: var(--card-pad, 0rem);
172
162
  flex-wrap: nowrap;
173
163
  overflow-x: auto;
174
164
  }
175
165
  .bleed::-webkit-scrollbar { height: var(--scroll-w); }
176
- .bleed::-webkit-scrollbar-track { background: transparent; margin-inline: var(--card-pad, 0px); }
166
+ .bleed::-webkit-scrollbar-track { background: transparent; margin-inline: var(--card-pad, 0rem); }
177
167
  .bleed::-webkit-scrollbar-thumb { background: transparent; }
178
168
  .bleed:is(:hover, .is-scrolling)::-webkit-scrollbar-thumb { background: currentColor; }
179
169
  @supports not selector(::-webkit-scrollbar) {
@@ -187,12 +177,8 @@
187
177
  /* foot: the card's action buttons on one line. One button takes the width,
188
178
  up to four share it in even parts — so the gaps read even too — and from
189
179
  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); }
180
+ .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; color: var(--card-foot-ink, inherit); }
181
+ .card__foot:has(> :nth-child(3)) > .btn { --btn-pad-x: var(--sp-2); }
196
182
  /* 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
183
  whole short row of its own, centred — it never shares the buttons' line */
198
184
  .card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
@@ -201,15 +187,15 @@
201
187
  /* media placeholder for wireframes — currentColor so it survives any palette */
202
188
  .ph {
203
189
  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));
190
+ 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))),
191
+ 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
192
  border: var(--border-w) solid currentColor;
207
193
  border-radius: var(--r-media);
208
194
  opacity: var(--off);
209
- min-height: 10rem;
195
+ min-height: calc(var(--tap-base) * 10 / 3);
210
196
  }
211
- .ph--sm { min-height: 7.5rem; }
212
- .ph--md { min-height: 9rem; }
197
+ .ph--sm { min-height: calc(var(--tap-base) * 5 / 2); }
198
+ .ph--md { min-height: calc(var(--tap-base) * 3); }
213
199
  /* ---- centred card: one thing to read, at the human centre of a fill screen ----
214
200
  (.screen__centre below is the same placement for a small GROUP of things)
215
201
  Where the centre of a phone IS, to the person holding it: the bottom fifth is
@@ -222,8 +208,7 @@
222
208
  The card hugs its words and keeps the screen's inset. It is capped so that it
223
209
  never reaches the title bar above it nor the bar's room below it — past that it
224
210
  scrolls inside — from the same tokens those bars reserve their room with. */
225
- .card--centre,
226
- .screen--fill > .screen__centre {
211
+ .card--centre {
227
212
  position: absolute;
228
213
  top: var(--screen-centre);
229
214
  translate: 0 -50%;
@@ -240,38 +225,6 @@
240
225
  :is(.topbar, .appbar) ~ :is(.card--centre, .screen__centre) { --_lead: var(--header-h); }
241
226
  .card--centre { inset-inline: var(--screen-pad); }
242
227
 
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
228
  /* ---- the card that changes slides: out one side, the next in from the other ----
276
229
  A .screen__centre group that walks pages (data-slide="next" | "back" on the
277
230
  root while a step changes) turns its card like a page: the old card slides
@@ -300,7 +253,7 @@
300
253
  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
254
  touch-action: pan-y; /* leaves vertical scroll to the browser; horizontal is the app's own drag, never a native pan */
302
255
  }
303
- .pages > :is(.card, .page) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
256
+ .pages > .card { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
304
257
  .pages > [inert] { visibility: hidden; }
305
258
  /* pages of WORDS (Lh, 2026-09-20): the same walk with .page pages, on the base — in a .step the
306
259
  .pages is the reading block (step.css), the round pager in the bar turns it, the same slideTo().
@@ -312,7 +265,7 @@
312
265
  ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
313
266
  ::view-transition-old(screen-out), ::view-transition-new(screen-in) { /* screen-*: slideScreen — one whole screen to the next (Lh 2026-09-25) */
314
267
  animation-duration: var(--motion-slide);
315
- animation-timing-function: ease-out;
268
+ animation-timing-function: var(--ease-out);
316
269
  mix-blend-mode: normal;
317
270
  }
318
271
  :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 +280,6 @@
327
280
  ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
328
281
  ::view-transition-old(screen-out), ::view-transition-new(screen-in) { animation: none; }
329
282
  }
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
283
 
334
284
  /* compact card inside a card (nested note, promo) */
335
285
  .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 +288,7 @@
338
288
  stack.css; a stacked sheet is a plain .card, the material */
339
289
 
340
290
  /* 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; }
291
+ .icon-row { display: flex; align-items: center; gap: var(--icon-row-gap, var(--sp-4)); opacity: var(--fade-near); }
342
292
 
343
293
  /* ---- card list: rows, each row its own boundary ---- */
344
294
  .card-list { display: grid; gap: var(--card-list-gap, var(--sp-3)); }
@@ -374,7 +324,7 @@
374
324
  .card__remove {
375
325
  --_size: var(--card-remove-size, calc(var(--tap-base) / 2));
376
326
  position: relative;
377
- z-index: 1;
327
+ z-index: var(--z-over);
378
328
  flex: none;
379
329
  align-self: flex-start;
380
330
  display: grid;
@@ -392,7 +342,7 @@
392
342
  cursor: pointer;
393
343
  }
394
344
  .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%; }
345
+ .card__remove > .icon { --icon-size: 62%; }
396
346
  .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
397
347
 
398
348
  /* leading thumb/icon in a row */
@@ -435,7 +385,7 @@
435
385
  .feed-card__rest > :first-child { margin-top: var(--card-gap); }
436
386
  .feed-card--small .feed-card__rest { overflow: clip; visibility: hidden; }
437
387
  @media (prefers-reduced-motion: no-preference) {
438
- .feed-card__more { transition: grid-template-rows var(--motion-sheet) ease; }
388
+ .feed-card__more { transition: grid-template-rows var(--motion-sheet) var(--ease); }
439
389
  .feed-card--small .feed-card__rest { transition: visibility 0s var(--motion-sheet); }
440
390
  }
441
391
 
@@ -446,7 +396,7 @@
446
396
 
447
397
  .delta {
448
398
  font: var(--delta-font, var(--type-caption));
449
- font-weight: 700;
399
+ font-weight: var(--fw-bold);
450
400
  padding: var(--delta-pad-y, var(--sp-1)) var(--delta-pad-x, var(--sp-2));
451
401
  border-radius: var(--delta-radius, var(--r-chip));
452
402
  background: var(--delta-bg, var(--card-3-bg));
@@ -473,4 +423,283 @@
473
423
  scroll-snap-align: start; scroll-snap-stop: always;
474
424
  display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--card-gap); align-content: start;
475
425
  }
476
- .panes > .pager__at { justify-self: center; }
426
+
427
+ /* ==== the card's variants another family draws: Cover and the fold (media), Band (numbers) (#62: a block is written in its own file) ==== */
428
+
429
+ /* ---- cover: the image IS the card; text over a scrim ----
430
+ <article class="card card--cover"><div class="ph" role="img"></div>
431
+ <div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
432
+ <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
433
+ <div class="row">…meta…</div></article> */
434
+ .card--cover {
435
+ --card-bg: var(--card-3-bg);
436
+ --dark: var(--card-3-dark);
437
+ --accent: var(--card-ink);
438
+ --accent-dark: calc(1 - var(--dark));
439
+ min-height: var(--_cover-min-h);
440
+ align-content: space-between;
441
+ overflow: hidden;
442
+ isolation: isolate;
443
+ }
444
+ .card--cover > img,
445
+ .card--cover > .ph {
446
+ position: absolute;
447
+ inset: 0;
448
+ width: 100%;
449
+ height: 100%;
450
+ object-fit: cover;
451
+ z-index: var(--z-under-2);
452
+ border: 0;
453
+ border-radius: 0;
454
+ min-height: 0;
455
+ }
456
+ /* scrim: dark at both ends so the meta stays readable */
457
+ .card--cover::before {
458
+ content: "";
459
+ position: absolute;
460
+ inset: 0;
461
+ z-index: var(--z-under);
462
+ background: linear-gradient(to bottom, var(--scrim), transparent 40% 60%, var(--scrim));
463
+ }
464
+
465
+ /* ---- fold: a sticky-note corner on any card (decorative) ----
466
+ No border. The card's outline follows its border box, so it ran straight
467
+ across the squared bottom-right corner and cut through the fold's diagonal —
468
+ the corner read as a notch with a line through it rather than a lifted flap.
469
+ The COLOUR goes transparent rather than the border going to 0, so the box
470
+ keeps its geometry and a fold card still measures the same as the cards
471
+ beside it. */
472
+ .card--fold {
473
+ border-color: transparent;
474
+ border-bottom-right-radius: 0;
475
+ background-image: linear-gradient(to top left,
476
+ 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 */
477
+ color-mix(in srgb, currentColor 18%, var(--card-bg)) 50% 100%);
478
+ background-repeat: no-repeat;
479
+ background-position: bottom right;
480
+ background-size: var(--sp-8) var(--sp-8);
481
+ background-origin: border-box; /* the flap sits on the true corner, not inside the (transparent) border */
482
+ /* the corner is CUT: the triangle under the flap shows what is behind the card. Painted in the card's own colour it
483
+ read as the square corner of a second card underneath (Lh 2026-09-23). The polygon reaches --sp-12 past the other
484
+ edges so a coloured palette's shadow is not clipped there. */
485
+ 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)),
486
+ calc(100% - var(--sp-8) - var(--sp-12)) calc(100% + var(--sp-12)), calc(-1 * var(--sp-12)) calc(100% + var(--sp-12)));
487
+ }
488
+
489
+ /* ---- band: data card; numbers.css draws its parts and the band stack ---- */
490
+ .card--band {
491
+ grid-template-columns: 1fr auto;
492
+ grid-template-areas: "label num" "foot num";
493
+ align-items: end;
494
+ gap: var(--sp-2) var(--sp-4);
495
+ min-height: var(--card-band-min-h, var(--tile-h));
496
+ }
497
+
498
+ /* ---- where it sits in what base.css draws (docs/reference.md rule 15) ---- */
499
+ .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 */
500
+ .page > .ph { width: 100%; height: var(--page-media-h, calc(var(--tap-base) * 14 / 3)); }
501
+ /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
502
+ .sheet-stack > .card {
503
+ grid-area: 1 / 1; /* every card shares the stack's cell; later ones offset down */
504
+ border-bottom-left-radius: 0;
505
+ border-bottom-right-radius: 0;
506
+ border-bottom: 0;
507
+ align-content: start;
508
+ align-self: start; /* own natural height (padding + head) by default */
509
+ z-index: var(--level, var(--z-over)); /* explicit paint order, 1 = back of the stack */
510
+ }
511
+ /* THE FRONT SHEET is the last .card, not the last child: a --closed stack
512
+ may carry a .sheet-stack__base after its sheets (below). Written once here
513
+ as "a card no card follows" so every rule about the front sheet agrees. */
514
+ .sheet-stack > .card:not(:has(~ .card)) {
515
+ align-self: stretch; /* the front card fills the remaining stack height */
516
+ }
517
+ .sheet-stack > .card:nth-child(2) { margin-top: var(--stack-step); }
518
+ .sheet-stack > .card:nth-child(3) { margin-top: calc(2 * var(--stack-step)); }
519
+ .sheet-stack > .card:nth-child(4) { margin-top: calc(3 * var(--stack-step)); }
520
+ .sheet-stack > .card:nth-child(5) { margin-top: calc(4 * var(--stack-step)); }
521
+ /* stack headers: uniform — [back?] title left, icons right, one height,
522
+ same gap to content on every card */
523
+ .sheet-stack > .card > .card__head {
524
+ align-items: center;
525
+ justify-content: flex-start;
526
+ min-height: var(--tap);
527
+ }
528
+ .sheet-stack--display > .card > .card__head { align-items: start; min-height: 0; }
529
+ /* ============================================================
530
+ .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
531
+
532
+ A sheet (a .card — the material, sheet.css's single sheet is the same
533
+ stuff) is rigid: it can move in, move out, or be covered — it can
534
+ never change shape. So every sheet here is the SAME full height,
535
+ always, set once and never touched again by anything. A "peek" is
536
+ not a short sheet; it's a full sheet the next one happens to cover,
537
+ showing only the strip above it, exactly like a sheet of paper part
538
+ way out of a real stack.
539
+
540
+ Because of that, the whole animation is one property: translate.
541
+ Nothing resizes, nothing re-stacks — z-index is the plain resting
542
+ --level order and is never touched, since the order already says
543
+ what has to happen. Cards after the tapped one sit IN FRONT of it,
544
+ so they're what's covering it, so they're what must leave. Cards
545
+ before it are already BEHIND it, so they need do nothing at all
546
+ except let themselves be covered as it rises over them.
547
+
548
+ .is-open — the tapped card, moved up to the top of the stack. Its
549
+ own resting offset is margin-top (below); opening just
550
+ translates that offset back off, landing it flush at
551
+ the stack's top with its full height exactly filling
552
+ to the bottom.
553
+ .is-before — an earlier card, nudged one step down so it slips
554
+ under the next card's resting strip and is covered
555
+ there. Each carries its own --stagger (set in JS: 0
556
+ for the one nearest the tapped card, counting up
557
+ further back) so they tuck away, and later un-tuck,
558
+ one at a time.
559
+ .is-after — a later card, moved out below, together, no stagger.
560
+ ============================================================ */
561
+ .sheet-stack--tap > .card {
562
+ height: 100%; /* every card identical and full length, always — the one
563
+ size in this whole component, never toggled, never transitioned. At a
564
+ resting margin-top of n steps it simply overhangs the stack's bottom
565
+ by n steps (clipped by .screen--fill), so translating it up by exactly
566
+ that much lands it filling the stack precisely. */
567
+ overflow: clip;
568
+ transition: translate var(--motion-sheet) var(--ease); /* the only animated property here */
569
+ transition-delay: calc(var(--stagger, 0) * var(--motion-stagger)); /* 0 for everything
570
+ except a staggered is-before card: the card renders --stagger as it tucks or un-tucks */
571
+ }
572
+ .sheet-stack--tap > .card.is-open {
573
+ /* up by its own resting offset — pure movement, nothing resizes */
574
+ translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step) + var(--_pull, 0rem));
575
+ cursor: default;
576
+ overflow-y: auto;
577
+ overflow-x: clip;
578
+ overscroll-behavior: contain;
579
+ }
580
+ .sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
581
+ .sheet-stack--tap > .card > .card__head { touch-action: none; }
582
+ .sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
583
+ padding-top: calc(var(--card-pad) + var(--_refresh, 0rem));
584
+ transition: translate var(--motion-sheet) var(--ease), padding-top var(--motion-sheet) var(--ease);
585
+ }
586
+ /* the front sheet — the one open on arrival — never travels up (it is not
587
+ tappable), so its resting offset would leave its last n steps below the
588
+ screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
589
+ alone is cut to the room it actually has, and it scrolls like an opened
590
+ sheet does. Every other sheet keeps the one full height. */
591
+ .sheet-stack--tap:not(.sheet-stack--closed) > .card:not(:has(~ .card)) {
592
+ height: calc(100% - var(--slot, 0) * var(--stack-step));
593
+ overflow-y: auto;
594
+ overflow-x: clip;
595
+ overscroll-behavior: contain;
596
+ }
597
+ /* a scrolling sheet shows no scrollbar (a desktop with a mouse would paint one
598
+ down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself
599
+ (the hiding rule is base.css's, shared with every other scrolling track) */
600
+ .sheet-stack--tap > .card.is-before { translate: 0 var(--stack-step); }
601
+ .sheet-stack--tap > .card.is-after { translate: 0 100vh; }
602
+ .sheet-stack--closed > .card { translate: 0 calc(var(--_headroom) - var(--_lift, 0rem)); }
603
+ .sheet-stack--closed > .card.is-dragging { transition: none; }
604
+ .sheet-stack--tap:not(.sheet-stack--closed) > .card.is-dragging:not(.is-open) {
605
+ translate: 0 calc(-1 * var(--_lift, 0rem));
606
+ transition: none;
607
+ }
608
+ .sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
609
+ .sheet-stack--closed > .card.is-after { translate: 0 100vh; }
610
+ /* 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
611
+ line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
612
+ :is([data-palette], [data-finish]) .sheet-stack__base .card { box-shadow: none; }
613
+ /* 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
614
+ 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
615
+ only, the sheet's size is untouched. A pull on the head keeps them square until the finger lets go. */
616
+ .sheet-stack--flood > .card { transition: translate var(--motion-sheet) var(--ease), border-radius var(--motion-sheet) var(--ease); }
617
+ .sheet-stack--flood > .card.is-open { border-top-left-radius: 0; border-top-right-radius: 0; }
618
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head {
619
+ position: relative;
620
+ padding-left: 0;
621
+ transition: padding-left var(--motion-sheet) var(--ease);
622
+ }
623
+ /* ---- a stacked sheet with a SheetBody has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
624
+ pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a CardFoot, the CTA) at the bottom.
625
+ The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no SheetBody
626
+ keeps the old way: the whole open card scrolls. ---- */
627
+ .sheet-stack--tap > .card:has(> .sheet__body) { display: flex; flex-direction: column; gap: var(--card-gap); }
628
+ .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
629
+ /* a paper stack's foot is paper to the card's edges, and it covers the tinted flex gap above it so the
630
+ button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
631
+ where it was — the padding gives back what the bleed takes. */
632
+ .sheet-stack > .card.sheet--paper > .card__foot {
633
+ background: var(--action-bg);
634
+ color: var(--card-ink);
635
+ margin-top: calc(-1 * var(--card-gap));
636
+ margin-inline: calc(-1 * var(--card-pad));
637
+ margin-bottom: calc(-1 * var(--card-pad));
638
+ padding-top: var(--card-gap);
639
+ padding-inline: var(--card-pad);
640
+ padding-bottom: var(--card-pad);
641
+ }
642
+ .stack-sheet > .card__head { flex-wrap: nowrap; }
643
+ /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
644
+ 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
645
+ (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
646
+ .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))); }
647
+ :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); }
648
+ .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))); }
649
+ /* 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),
650
+ 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) */
651
+ .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
652
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
653
+ .sheet > .card__foot {
654
+ 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 */
655
+ align-items: end;
656
+ align-content: end;
657
+ padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
658
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
659
+ pointer-events: none;
660
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
661
+ transition: translate var(--motion-sheet) var(--ease), visibility 0s;
662
+ }
663
+ .sheet.is-dragging > .card__foot { transition: none; }
664
+ /* 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
665
+ LEAVES AND ARRIVES WITH ITS SHEET — solid the whole way, hidden only once the slide is over. It used to stay in place and
666
+ fade: the body slid through the strip behind a half-transparent button (timebank, Lh 2026-10-01: "ตัวหนังสือลอดใต้ปุ่ม"). */
667
+ :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); }
668
+ /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
669
+ /* 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
670
+ (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
671
+ padding above AND below — so the words begin near the middle and, when a big text size runs them long, scroll up clear.
672
+ The walk shows no scrollbar. */
673
+ .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) */
674
+ .step > .pages > .page > :is(img, .ph):first-child {
675
+ width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
676
+ max-width: none;
677
+ margin-inline: calc(-1 * var(--screen-pad));
678
+ height: calc(var(--_lead, 0rem) + var(--page-media-h, calc(var(--tap-base) * 14 / 3)));
679
+ max-height: none;
680
+ border-radius: 0;
681
+ border-inline: 0;
682
+ border-top: 0;
683
+ object-fit: cover;
684
+ }
685
+ /* ---- where it sits in what numbers.css draws (docs/reference.md rule 15) ---- */
686
+ .band-stack > .card--band { border: 0; border-radius: 0; box-shadow: none; }
687
+ .band-stack > .card--band + .card--band { border-top: var(--border-w) solid var(--hairline); }
688
+ /* ---- where it sits in what sticky.css draws (docs/reference.md rule 15) ---- */
689
+ .sticky__head > .ph { flex: 1 1 0; min-width: 0; } /* a picture in the head takes the note's width */
690
+ .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 */
691
+
692
+ /* ---- where it sits in what media.css draws: a postcard's picture (docs/reference.md rule 15) ---- */
693
+ .postcard__front .postcard__turn > .ph { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border: 0; border-radius: 0; min-height: 0; }
694
+
695
+ /* ---- where it sits in what stack.css draws: a sheet pulled to refresh holds still (docs/reference.md rule 15) ---- */
696
+ .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
697
+
698
+ /* ---- where it sits in what step.css draws: the reading block, a walk of pages (docs/reference.md rule 15) ---- */
699
+ .step:has(> .sheet--peek) > .pages { padding-bottom: var(--sheet-peek); }
700
+ .step > .pages {
701
+ flex: 0 1 auto;
702
+ min-height: 0;
703
+ overflow-y: auto;
704
+ overscroll-behavior: contain;
705
+ }