@usableapp/cardds 0.7.105 → 0.7.107

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 (100) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/bin/cardds.mjs +6 -2
  3. package/cardds.css +1 -0
  4. package/css/actions.css +16 -9
  5. package/css/base.css +6 -9
  6. package/css/card.css +21 -173
  7. package/css/finish.css +4 -4
  8. package/css/journey.css +4 -1
  9. package/css/layover.css +4 -14
  10. package/css/lists.css +3 -18
  11. package/css/media.css +1 -7
  12. package/css/parts.css +380 -0
  13. package/css/sheet.css +21 -140
  14. package/css/stack.css +15 -26
  15. package/css/stamp.css +1 -1
  16. package/css/step.css +3 -3
  17. package/css/theme.template.css +1 -1
  18. package/css/tokens.css +5 -4
  19. package/dist/actions/BarNote.d.ts +2 -2
  20. package/dist/actions/BarNote.js +2 -2
  21. package/dist/actions/Btn.d.ts +2 -2
  22. package/dist/actions/Btn.js +1 -1
  23. package/dist/actions/BtnRow.d.ts +1 -1
  24. package/dist/actions/BtnRow.js +1 -1
  25. package/dist/actions/Chip.d.ts +7 -3
  26. package/dist/actions/Chip.js +5 -5
  27. package/dist/actions/IconBtn.d.ts +1 -1
  28. package/dist/actions/IconBtn.js +1 -1
  29. package/dist/actions/IconBtnGroup.d.ts +1 -1
  30. package/dist/cardds.css +486 -410
  31. package/dist/cards/Card.d.ts +6 -22
  32. package/dist/cards/Card.js +11 -22
  33. package/dist/cards/CardFeed.d.ts +1 -1
  34. package/dist/cards/CardFeed.js +5 -5
  35. package/dist/cards/CardParts.d.ts +0 -12
  36. package/dist/cards/CardParts.js +0 -12
  37. package/dist/cards/cardGo.js +1 -1
  38. package/dist/cards/cardGrounds.js +2 -2
  39. package/dist/catalog.json +1 -1
  40. package/dist/forms/Field.d.ts +1 -1
  41. package/dist/forms/Field.js +1 -1
  42. package/dist/index.d.ts +6 -5
  43. package/dist/index.js +6 -5
  44. package/dist/lists/Row.d.ts +7 -22
  45. package/dist/lists/Row.js +6 -4
  46. package/dist/media/MapArea.d.ts +7 -3
  47. package/dist/media/MapArea.js +4 -3
  48. package/dist/media/Tile.d.ts +3 -7
  49. package/dist/media/Tile.js +5 -5
  50. package/dist/numbers/Spinner.d.ts +1 -1
  51. package/dist/numbers/Spinner.js +1 -1
  52. package/dist/parts/Body.d.ts +7 -0
  53. package/dist/parts/Body.js +11 -0
  54. package/dist/parts/End.d.ts +10 -0
  55. package/dist/parts/End.js +7 -0
  56. package/dist/parts/Foot.d.ts +17 -0
  57. package/dist/parts/Foot.js +11 -0
  58. package/dist/parts/Head.d.ts +15 -0
  59. package/dist/parts/Head.js +34 -0
  60. package/dist/parts/Lead.d.ts +10 -0
  61. package/dist/parts/Lead.js +9 -0
  62. package/dist/parts/Words.d.ts +13 -0
  63. package/dist/parts/Words.js +20 -0
  64. package/dist/parts/headCtx.d.ts +7 -0
  65. package/dist/parts/headCtx.js +9 -0
  66. package/dist/scaffold/EmptyState.d.ts +1 -1
  67. package/dist/scaffold/EmptyState.js +1 -1
  68. package/dist/scaffold/Screen.js +1 -1
  69. package/dist/sheets/Modal.d.ts +3 -7
  70. package/dist/sheets/Modal.js +3 -7
  71. package/dist/sheets/Sheet.d.ts +2 -2
  72. package/dist/sheets/Sheet.js +8 -9
  73. package/dist/sheets/SheetStack.d.ts +3 -3
  74. package/dist/sheets/SheetStack.js +4 -4
  75. package/dist/sheets/SheetSteps.d.ts +1 -1
  76. package/dist/sheets/SheetSteps.js +2 -2
  77. package/dist/sheets/StackBack.d.ts +1 -1
  78. package/dist/sheets/StackBack.js +1 -1
  79. package/dist/sheets/StackSheet.js +7 -7
  80. package/dist/sheets/sheetMeasure.js +3 -3
  81. package/dist/sheets/sheetStepsPile.js +2 -2
  82. package/dist/sheets/stackCard.js +6 -6
  83. package/dist/templates/CallSheet.js +6 -5
  84. package/dist/templates/Walkthrough.js +3 -2
  85. package/docs/reference.md +75 -36
  86. package/lib/catalog.mjs +2 -0
  87. package/lib/mcp.mjs +1 -1
  88. package/lint/rules.mjs +22 -8
  89. package/package.json +2 -1
  90. package/scripts/codemod-parts.mjs +324 -0
  91. package/dist/cards/CardFoot.d.ts +0 -15
  92. package/dist/cards/CardFoot.js +0 -9
  93. package/dist/cards/CardHead.d.ts +0 -21
  94. package/dist/cards/CardHead.js +0 -39
  95. package/dist/cards/Stat.d.ts +0 -19
  96. package/dist/cards/Stat.js +0 -14
  97. package/dist/sheets/SheetBody.d.ts +0 -5
  98. package/dist/sheets/SheetBody.js +0 -9
  99. package/dist/sheets/SheetHead.d.ts +0 -15
  100. package/dist/sheets/SheetHead.js +0 -6
package/css/card.css CHANGED
@@ -42,87 +42,25 @@
42
42
  .card--4 { --card-bg: var(--card-4-bg); --dark: var(--card-4-dark); }
43
43
  .card--5 { --card-bg: var(--card-5-bg); --dark: var(--card-5-dark); }
44
44
 
45
- /* header row: title left, floating action right — centered, since the
46
- trailing control (chip/badge/icon-btn/link) is almost always taller than
47
- a single line of title text; top-aligning them left the control visually
48
- low against the title's optical center. A head with a genuinely tall,
49
- multi-line title can opt back out with .card__head--top. */
50
- .card__head {
51
- display: flex;
52
- flex-wrap: var(--card-head-wrap, wrap); /* the caption takes a line of its own — no wrapper div */
53
- align-items: center;
54
- justify-content: space-between;
55
- column-gap: var(--card-head-col-gap, var(--sp-3));
56
- row-gap: var(--card-head-row-gap, var(--sp-0));
57
- min-height: var(--card-head-min-h, auto);
58
- }
59
- .card__head--top { align-items: flex-start; }
60
45
 
61
46
  .card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
62
- .card__end { text-align: right; display: grid; gap: var(--card-end-gap, var(--sp-0)); justify-items: end; } /* trailing value + caption */
63
47
  .card__media > img { width: 100%; height: auto; object-fit: cover; }
64
48
 
65
- /* ---- the card skeleton: head · content · foot ----
66
- Every card is a head (the heading left, hints / actions / a more-menu at
67
- the far right), its content, and an optional foot of action buttons.
68
- The head's leading child — the heading, or the div that wraps heading and
69
- caption — takes the room, so a heading with nothing at the far right runs
70
- all the way across; whatever trails it hugs its own width. Only the
71
- heading (or the div that wraps it) is that leading child: a control that
72
- leads a custom head — a chip, an avatar, a tile badge — keeps its own
73
- width, else the rule stretched a chip into a bar and an avatar into an
74
- ellipse (two agents and the wallet demo caught it, 2026-09-10). */
75
- .card__head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
76
-
77
- /* Skeleton head — the heading in a card's OWN head, and only there.
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 ·
82
- caption is the head's own wrapping row (above) — no wrapper element, no
83
- height, only the row gap. */
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
- }
95
-
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
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,
98
- stamp.css); the head only places it.
99
- <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><span class="stamp">…</span></div> */
100
- .card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
101
- .card__head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
102
-
103
49
  /* 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
104
50
  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,
105
51
  so a theme that sets that knob still wins. */
106
- .card > .card__head:first-child > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); }
52
+ :is(.card > .head:first-child, .card__face > .card__turn + .head) > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); } /* a turning card's face: its head follows the unseen turn button */
107
53
 
108
54
  /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are paper bands edge to
109
55
  edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
110
- clips, so the bands keep its corners. A two-faced card (below) carries the look on each face. */
56
+ clips, so the bands keep its corners. A two-faced card (below) carries the look on each face, never on the turning box: a clip
57
+ there cut the face off at the card's edges while it turned, its near edge being nearer the eye than the box (2026-10-21). */
111
58
  :is(.card--bands, .card--bands > .card__face) {
112
59
  background: var(--card-middle-bg, color-mix(in srgb, var(--card-bg) 35%, var(--card-1-bg)));
113
60
  --dark: var(--card-1-dark); /* the middle is mostly paper: its words take the paper's side */
114
61
  overflow: hidden;
115
62
  }
116
- :is(.card--bands, .card--bands > .card__face) > :is(.card__head, .card__foot) {
117
- --card-bg: var(--card-1-bg);
118
- --dark: var(--card-1-dark);
119
- background: var(--card-bg);
120
- color: var(--card-ink);
121
- margin-inline: calc(-1 * var(--card-pad));
122
- padding-inline: var(--card-pad);
123
- }
124
- :is(.card--bands, .card--bands > .card__face) > .card__head { margin-top: calc(-1 * var(--card-pad)); padding-block: var(--card-pad); }
125
- :is(.card--bands, .card--bands > .card__face) > .card__foot { margin-bottom: calc(-1 * var(--card-pad)); padding-block: var(--sp-3); } /* the foot band is slim */
63
+ .card--bands.card--turns { background: none; overflow: visible; } /* the turning box keeps --dark: the faces' ink is computed from it */
126
64
 
127
65
  /* ---- two faces: the WHOLE card turns (`Card back`, Lh 2026-10-19). The article is the turning box (.flip, base.css) and only that;
128
66
  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
@@ -144,10 +82,10 @@
144
82
  position: relative;
145
83
  }
146
84
  .card__face > * { min-width: 0; }
147
- .card__middle { flex: 1 0 auto; display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, auto); overflow: hidden; }
85
+ .card__middle { flex: 1 0 auto; display: flex; flex-direction: column; gap: var(--card-gap); height: var(--card-middle-h, auto); overflow: hidden; } /* a flex column, not a grid: the middle's height is the flexed one, so one thing on a face (a QR, a code) may take the whole middle with height: 100% (a grid's auto row never stretched to it, and the thing stood at the top of an empty band) */
148
86
  .card__turn { position: absolute; inset: 0; width: var(--px); height: var(--px); padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
149
87
  .card__face:has(> .card__turn:focus-visible) { outline: var(--focus); outline-offset: calc(-1 * 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; }
88
+ .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; }
151
89
 
152
90
  /* ---- bleed: a scrolling track that runs to the container's edges ----
153
91
  For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
@@ -168,7 +106,7 @@
168
106
  nothing jumps. Chromium/Safari paint it through the ::-webkit-scrollbar
169
107
  parts; Firefox gets its thin bar in the same ink. */
170
108
  .bleed {
171
- margin-inline: calc(-1 * var(--card-pad, 0rem));
109
+ margin-inline: calc(-1 * var(--card-pad, 0rem)); /* the edge a bleeding child takes back from what holds it; .map--bleed (sheet.css) reads the same --card-pad */
172
110
  padding-inline: var(--card-pad, 0rem);
173
111
  padding-block-end: var(--sp-2);
174
112
  scroll-padding-inline: var(--card-pad, 0rem);
@@ -185,20 +123,7 @@
185
123
  }
186
124
  /* a bleeding child sits over the container's rounded corner, so a strip that
187
125
  is genuinely first or last needs the corner clipped for it */
188
- .card:has(> .bleed:first-child), .card:has(> .bleed:last-child) { overflow: clip; }
189
-
190
- /* foot: the card's action buttons on one line. One button takes the width,
191
- up to four share it in even parts — so the gaps read even too — and from
192
- three on they drop their side padding so the labels keep the room. */
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); }
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
196
- whole short row of its own, centred — it never shares the buttons' line */
197
- .card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
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)); }
126
+ .card:has(> :is(.bleed, .map--bleed):first-child), .card:has(> :is(.bleed, .map--bleed):last-child) { overflow: clip; }
202
127
 
203
128
  /* media placeholder for wireframes — currentColor so it survives any palette */
204
129
  .ph {
@@ -316,7 +241,6 @@
316
241
  align-items: center;
317
242
  gap: var(--card-row-gap, var(--sp-3));
318
243
  }
319
- .card--row .card__body { flex: 1; display: grid; gap: var(--sp-0); }
320
244
 
321
245
  /* ---- a short window of past entries (`CardList show`, Lh 2026-10-19): the list scrolls IN ITSELF, a few rows in sight and the next one cut
322
246
  in half to say there is more. The height is counted in rows by the component (`--_card-list-h`, px: rows wrap to any height); the room
@@ -333,7 +257,7 @@
333
257
  }
334
258
 
335
259
  /* ---- the cross that takes a row off the list (`Card row onRemove`): small, at the row's top right corner, inside it ----
336
- <label class="card card--row"><input type="radio">…<div class="card__body">…</div><button class="card__remove" aria-label="Remove"><svg class="icon">x</svg></button></label>
260
+ <label class="card card--row"><input type="radio">…<span class="words">…</span><button class="card__remove" aria-label="Remove"><svg class="icon">x</svg></button></label>
337
261
  Smaller than any button (a ring half a thing's size, `--tap-base` / 2 = 24) and it grows with the size of things; the touch target is an
338
262
  unseen ring round it, never under --tap-floor (44). It stands in the row's flow, so the title wraps before it, tucked into the corner
339
263
  by the row's own padding; it sits over the row's radio (which covers the whole card), so a touch on it removes and never picks. */
@@ -361,21 +285,10 @@
361
285
  .card__remove > .icon { --icon-size: 62%; }
362
286
  .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
363
287
 
364
- /* leading thumb/icon in a row */
365
- .card__thumb {
366
- width: var(--card-thumb-size, var(--tap-sm)); height: var(--card-thumb-size, var(--tap-sm));
367
- flex: none;
368
- border-radius: var(--card-thumb-radius, var(--r-control));
369
- border: var(--border-w) solid currentColor;
370
- display: grid;
371
- place-items: center;
372
- font: var(--card-thumb-font, var(--type-label));
373
- }
374
-
375
288
  /* ---- card feed: a queue of cards, all in sight — one big, the rest small (Lh 2026-10-09, the whole queue 2026-10-15) ----
376
289
  <div class="card-feed">
377
- <article class="card feed-card"><div class="card__head feed-card__head">thumb or lead · h2 · caption</div>
378
- <div class="feed-card__more"><div class="feed-card__rest">content · <div class="card__foot">…</div></div></div></article>
290
+ <article class="card feed-card"><div class="head">lead · h2 · caption</div>
291
+ <div class="feed-card__more"><div class="feed-card__rest">content · <div class="foot">…</div></div></div></article>
379
292
  <article class="card feed-card feed-card--small" role="button" tabindex="0">… <div class="feed-card__more" inert>…</div></article>
380
293
  <p class="t-caption card-feed__end" role="note">the end</p>
381
294
  </div>
@@ -386,9 +299,7 @@
386
299
  shut content in the scrollable overflow of whatever holds the feed — and hidden once the row has shut. */
387
300
  .card-feed { display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; gap: var(--card-feed-gap, var(--card-gap-rest)); }
388
301
  .feed-card { row-gap: 0; }
389
- .feed-card__head { display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; min-height: var(--feed-card-head-h, var(--tap)); }
390
- .feed-card__head:has(> :is(.card__thumb, .feed-card__lead)) { grid-template-columns: auto minmax(0, 1fr); }
391
- .feed-card__head:has(> .t-caption) > :is(.card__thumb, .feed-card__lead) { grid-row: span 2; }
302
+ .feed-card > .head:has(> .t-caption) > .feed-card__lead { grid-row: span 2; }
392
303
  .feed-card__lead { display: grid; align-self: center; } /* a Stamp here is the small one, 48 × 60 (stamp.css) */
393
304
  .card-feed__end { text-align: center; }
394
305
  .feed-card__more { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: 1fr; }
@@ -405,22 +316,6 @@
405
316
  .feed-card--small .feed-card__rest { transition: visibility 0s var(--motion-sheet); }
406
317
  }
407
318
 
408
- /* ---- inline stat block (revenue-style) ---- */
409
- .stat { display: grid; gap: var(--stat-gap, var(--sp-1)); }
410
- .stat--centre { text-align: center; justify-items: center; } /* the one number of a sheet or a stage, under a centred avatar */
411
- .stat__row { display: flex; align-items: center; justify-content: space-between; gap: var(--stat-row-gap, var(--sp-3)); }
412
-
413
- .delta {
414
- font: var(--delta-font, var(--type-caption));
415
- font-weight: var(--fw-bold);
416
- padding: var(--delta-pad-y, var(--sp-1)) var(--delta-pad-x, var(--sp-2));
417
- border-radius: var(--delta-radius, var(--r-chip));
418
- background: var(--delta-bg, var(--card-3-bg));
419
- --dark: var(--card-3-dark);
420
- color: var(--delta-ink, var(--card-ink));
421
- }
422
- .delta--neg { background: var(--delta-neg-bg, var(--neg)); --dark: var(--neg-dark); color: var(--delta-neg-ink, var(--card-ink)); }
423
-
424
319
  /* ---- panes: a few panes side by side inside a card or a sheet, one in sight (Lh 2026-10-02) ----
425
320
  <div class="panes" role="group"><div class="panes__rail"><div class="panes__pane">…</div>…</div><span class="pager__at">…</span></div>
426
321
  The rail runs to the card's edges and snaps a pane at a time; each pane takes the inset back, so its content stands on the
@@ -444,7 +339,7 @@
444
339
 
445
340
  /* ---- cover: the image IS the card; text over a scrim ----
446
341
  <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>
342
+ <div class="head"><button class="icon-btn icon-btn--sm">♥</button></div>
448
343
  <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
449
344
  <div class="row">…meta…</div></article> */
450
345
  .card--cover {
@@ -534,14 +429,6 @@
534
429
  .sheet-stack > .card:nth-child(3) { margin-top: calc(2 * var(--stack-step)); }
535
430
  .sheet-stack > .card:nth-child(4) { margin-top: calc(3 * var(--stack-step)); }
536
431
  .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
432
  /* ============================================================
546
433
  .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
547
434
 
@@ -594,8 +481,7 @@
594
481
  overscroll-behavior: contain;
595
482
  }
596
483
  .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)) {
484
+ .sheet-stack--tap > .card.is-open:not(:has(> .body)) {
599
485
  padding-top: calc(var(--card-pad) + var(--_refresh, 0rem));
600
486
  transition: translate var(--motion-sheet) var(--ease), padding-top var(--motion-sheet) var(--ease);
601
487
  }
@@ -631,56 +517,18 @@
631
517
  only, the sheet's size is untouched. A pull on the head keeps them square until the finger lets go. */
632
518
  .sheet-stack--flood > .card { transition: translate var(--motion-sheet) var(--ease), border-radius var(--motion-sheet) var(--ease); }
633
519
  .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
520
+ /* ---- a stacked sheet with a Body has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
521
+ pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a Foot, the CTA) at the bottom.
522
+ The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no Body
642
523
  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; }
524
+ .sheet-stack--tap > .card:has(> .body) { display: flex; flex-direction: column; gap: var(--card-gap); }
525
+ .sheet-stack--tap > .card:has(> .body).is-open { overflow: clip; }
659
526
  /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
660
527
  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
528
  (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
529
  /* 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
530
  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); }
531
+ .sheet-stack--tap > .card { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
684
532
  /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
685
533
  /* 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
534
  (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
@@ -709,7 +557,7 @@
709
557
  .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
558
 
711
559
  /* ---- 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; }
560
+ .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .body)) { transition: none; }
713
561
 
714
562
  /* ---- where it sits in what step.css draws: the reading block, a walk of pages (docs/reference.md rule 15) ---- */
715
563
  .step:has(> .sheet--peek) > .pages { padding-bottom: var(--sheet-peek); }
package/css/finish.css CHANGED
@@ -82,7 +82,7 @@
82
82
  0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
83
83
  }
84
84
  /* a peeked stack tab is the icon alone: no sheen and no shadow until its sheet is open (the fill fades with the sheet) */
85
- [data-finish="lit"] .sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab {
85
+ [data-finish="lit"] .sheet-stack--tap > .card:not(.is-open) > .head .stack-sheet__tab {
86
86
  background-image: none;
87
87
  box-shadow: none;
88
88
  }
@@ -121,7 +121,7 @@
121
121
  }
122
122
  /* a dark card (the brand tone, a cover): multiply would only blacken it, so the grain lightens, fainter */
123
123
  [data-finish="print"] :is(.card--3, .card--cover)::after { mix-blend-mode: screen; opacity: calc(var(--print-grain, 0.32) * 0.56); }
124
- [data-finish="print"] .card:not(.card--row, .card--cover, .feed-card--small, .sheet-stack > *, .sheet-stack > :not(.sheet-stack__base) > *) > .card__head {
124
+ [data-finish="print"] .card:not(.card--row, .card--cover, .feed-card--small, .sheet-stack > *, .sheet-stack > :not(.sheet-stack__base) > *) > .head {
125
125
  padding-bottom: var(--sp-2);
126
126
  border-bottom: var(--rule-w) dotted color-mix(in srgb, currentColor 45%, transparent);
127
127
  }
@@ -132,7 +132,7 @@
132
132
  [data-finish="print"] .chip { border-color: var(--chip-border, var(--_ink)); } /* a holder that clears a chip's edge (a note's chips) clears it in print too */
133
133
  [data-finish="print"] .btn--primary { box-shadow: 0.125rem 0.125rem 0 0 color-mix(in srgb, var(--card-ink) 60%, transparent); }
134
134
  [data-finish="print"] :is(.btn, .chip):active { box-shadow: none; translate: 0.125rem 0.125rem; }
135
- [data-finish="print"] .card__thumb { border-style: dashed; }
135
+ [data-finish="print"] .card--row > .lead { border-style: dashed; }
136
136
 
137
137
  /* ============================================================
138
138
  sticker — a die-cut sticker on a sheet: --sticker-cut of white paper left round the print, a short hard
@@ -167,4 +167,4 @@
167
167
  [data-finish="ink"] :is(.btn, .chip, .icon-btn:not(.icon-btn-group > *)) { border: var(--ink-w, 0.15625rem) solid currentColor; box-shadow: 0.1875rem 0.1875rem 0 0 currentColor; }
168
168
  [data-finish="ink"] .btn--primary { border-color: var(--on-surface); box-shadow: 0.1875rem 0.1875rem 0 0 var(--on-surface); }
169
169
  [data-finish="ink"] .toggle { box-shadow: 0.125rem 0.125rem 0 0 var(--on-surface); }
170
- [data-finish="ink"] .card__thumb { border-width: var(--ink-w, 0.15625rem); }
170
+ [data-finish="ink"] .card--row > .lead { border-width: var(--ink-w, 0.15625rem); }
package/css/journey.css CHANGED
@@ -54,6 +54,7 @@
54
54
  over the number and the month on one line; tall (a two-line title) = three lines, day · number · month. It grows no taller than
55
55
  its three lines (2026-10-21, timebank: beside a four-line title it was a tall empty block); held there, it stands at the head's top. */
56
56
  .tile-badge--stretch {
57
+ line-height: var(--tile-badge-line, var(--lh-badge)); /* the knob's own property; the cap below reads it too */
57
58
  align-self: stretch;
58
59
  max-block-size: calc((2 * var(--fs-caption) + var(--fs-lg)) * var(--tile-badge-line, var(--lh-badge)) + 2 * var(--sp-3));
59
60
  container-type: size;
@@ -65,7 +66,6 @@
65
66
  column-gap: var(--sp-1);
66
67
  row-gap: 0;
67
68
  padding-block: var(--sp-3);
68
- line-height: var(--tile-badge-line, var(--lh-badge));
69
69
  }
70
70
  .tile-badge--stretch > * { line-height: inherit; }
71
71
  .tile-badge--stretch > :first-child { grid-column: 1 / -1; }
@@ -73,6 +73,9 @@
73
73
  .tile-badge--stretch > * { grid-column: 1 / -1; }
74
74
  }
75
75
 
76
+ /* where it sits in a head that leads with the date (card.css): the first cell, as tall as the chips beside it however many rows, so no cap */
77
+ .head:has(> .tile-badge:first-child + .chip-row) > .tile-badge--stretch:first-child { grid-column: 1; grid-row: 1; max-block-size: none; }
78
+
76
79
  /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */
77
80
  .tile-badge--corner { position: absolute; right: var(--sp-3); bottom: var(--sp-3); }
78
81
 
package/css/layover.css CHANGED
@@ -8,14 +8,14 @@
8
8
  /* ---- the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
9
9
  <div class="sheet-stage sheet-stage--dim"> (dim = the page under a scrim; a Modal on a clean ground is the stage without it — Lh 2026-09-22, `--ask` retired: the stage does the ground, the modal centres itself)
10
10
  <article class="card modal">
11
- <div class="modal__lift">…the card's content (a head, rows, a timeline)…</div>
11
+ <div class="body">…the card's content (a head, rows, a timeline)…</div>
12
12
  <div class="modal__drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
13
13
  <div class="btn-trio">…</div>
14
14
  </div>
15
15
  </article>
16
16
  </div>
17
17
  .modal is the card — no border (the shadow is its edge on the scrim);
18
- .modal__lift the light part that holds the thing being asked about (it
18
+ .body the light part that holds the thing being asked about (it
19
19
  scrolls when tall); .modal__drawer the dark end that asks. The ground is
20
20
  whatever the stage shows (dim it or not). Two beats (2 × --motion-sheet) when
21
21
  the stage appears, and the same two backwards when it closes:
@@ -47,16 +47,6 @@
47
47
  overflow: clip;
48
48
  box-shadow: var(--modal-shadow, var(--shadow-sheet));
49
49
  }
50
- .modal__lift {
51
- padding: var(--card-pad);
52
- display: grid;
53
- gap: var(--card-gap);
54
- align-content: start;
55
- flex: 0 1 auto;
56
- min-height: 0;
57
- overflow-y: auto; /* tall content scrolls inside; the drawer stays */
58
- overscroll-behavior: contain;
59
- }
60
50
  .modal__drawer {
61
51
  flex-shrink: 0;
62
52
  height: var(--drawer-h);
@@ -81,8 +71,8 @@
81
71
  --accent: var(--card-ink);
82
72
  --accent-dark: calc(1 - var(--dark));
83
73
  }
84
- /* the CONFIRM: a Modal with no ModalBody: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
85
- .modal:not(:has(> .modal__lift)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
74
+ /* the CONFIRM: a Modal with no Body: the ModalAsk IS the card, as tall as its words (no fixed band, no second beat) */
75
+ .modal:not(:has(> .body)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
86
76
  .modal__drawer .t-caption { --t-caption-ink: var(--modal-drawer-caption-ink, var(--card-muted)); }
87
77
  .modal__drawer > h2:focus { outline: none; } /* the question takes the cursor when the modal arrives (for a screen reader, the keyboard's way in) — it is not a control: no ring */
88
78
  /* the answers as WORD buttons (a BtnRow of Btns; Lh 2026-10-11): the row fills the ask with room over it and each button keeps its words on one
package/css/lists.css CHANGED
@@ -1,13 +1,13 @@
1
1
  /* ============================================================
2
2
  cardds/lists.css — frameless rows and pairs INSIDE a card:
3
- row (lead · body · end) · key-value rows · kv grid ·
4
- timeline · legend · link
3
+ row (its parts, lead · words · end, are parts.css) · key-value
4
+ rows · kv grid · timeline · legend · link
5
5
  A row inside a card has no boundary of its own; .rows draws
6
6
  hairlines between rows. A row that IS a card = .card--row.
7
7
  ============================================================ */
8
8
 
9
9
  /* ---- row: leading visual · title + meta · trailing control ----
10
- <div class="row"><span class="avatar">A</span><div class="row__body"><span class="row__title">Alice Kau</span><span class="row__meta">Owner</span></div><button class="icon-btn icon-btn--sm">›</button></div> */
10
+ <div class="row"><span class="avatar">A</span><span class="words"><span class="words__title">Alice Kau</span><span class="words__meta">Owner</span></span><button class="icon-btn icon-btn--sm">›</button></div> */
11
11
  .row {
12
12
  display: flex;
13
13
  align-items: center;
@@ -20,21 +20,7 @@
20
20
  /* no padding on a row (Lh 2026-09-16): padding is for room, not for spreading.
21
21
  Space BETWEEN rows is .rows' gap; a lone row as card content takes the card's
22
22
  gap. (.row--flush, which zeroed the old padding, is gone — every row is flush.) */
23
- .row__lead {
24
- width: var(--row-lead-size, var(--tap));
25
- height: var(--row-lead-size, var(--tap));
26
- flex: none;
27
- border-radius: var(--row-lead-radius, var(--r-control));
28
- border: var(--border-w) solid var(--row-lead-border, var(--outline));
29
- display: grid;
30
- place-items: center;
31
- }
32
- .row__body { flex: 1; min-width: 0; display: grid; gap: var(--row-body-gap, var(--sp-0)); }
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
- .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
- .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
36
23
  .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 */
37
- .row--lg .row__title { font: var(--fw-bold) var(--fs-lg) / var(--lh-h1) var(--font-display); }
38
24
  /* a row that EXPANDS (Lh 2026-09-22): the row is a <details>' summary, what it shows comes out under it —
39
25
  who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
40
26
  <details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
@@ -49,7 +35,6 @@
49
35
  pointer-events off here, aria-disabled from the markup. The tick is the .check's checked look
50
36
  at --tap-sm, but not an input: nothing to press. */
51
37
  .row--done { pointer-events: none; cursor: default; }
52
- .row--done .row__title { color: var(--card-muted); }
53
38
  .row__done {
54
39
  width: var(--row-done-size, var(--tap-sm));
55
40
  height: var(--row-done-size, var(--tap-sm));
package/css/media.css CHANGED
@@ -43,17 +43,11 @@
43
43
  position: relative;
44
44
  }
45
45
  .tile__label { font: var(--tile-label-font, var(--type-title)); }
46
- .tile__value { font: var(--tile-value-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
47
46
  .tile__sub { font: var(--tile-sub-font, var(--type-caption)); color: var(--tile-sub-ink, var(--card-muted)); } /* the muted words of the tile's ground; --dim faded the full ink to 2.4–3.4:1 */
48
47
  .tile__text { display: grid; gap: var(--tile-text-gap, var(--sp-0)); }
49
48
  .tile--on { background: var(--tile-on-bg, var(--accent)); --dark: var(--accent-dark); color: var(--tile-on-ink, var(--card-ink)); border-color: transparent; }
50
49
  /* .tile--pick's hidden input and :has(:checked) fill are declared once in choice.css, alongside .chip--pick and .mood__opt */
51
50
  .tile--wide { grid-column: 1 / -1; }
52
- /* stat: READ, not pressed — no frame of its own inside a card (a frame says "tap me"): icon · value · words on the card's ground */
53
- /* a stat tile has no fill of its own, so it has no ink of its own: it takes the ink of what it stands on (currentColor on `color`
54
- = the inherited colour), as the card's heading does. It kept the plain tile's --action-ink, dark on a dark card (timebank, tone 3,
55
- Lh 2026-10-18). --tile-ink still overrides; a toned stat tile (.tile--N, below) takes its fill and its ink back */
56
- .tile--stat { --dark: inherit; color: var(--tile-ink, currentColor); border-color: transparent; background: none; padding-inline: 0; min-height: var(--tile-stat-min-h, var(--avatar-xl)); gap: var(--tile-stat-gap, var(--sp-3)); grid-template-columns: auto 1fr; align-items: center; }
57
51
  /* upload: dashed, centred plus + label */
58
52
  .tile--upload {
59
53
  border-style: dashed;
@@ -72,7 +66,7 @@
72
66
  .tile--3 { background: var(--tile-3-bg, var(--card-3-bg)); --dark: var(--card-3-dark); color: var(--tile-3-ink, var(--card-ink)); border-color: transparent; }
73
67
 
74
68
  /* ---- wave: audio waveform stand-in for voice rows ----
75
- <span class="wave"></span> — sits in a .row__body, fills the width */
69
+ <span class="wave"></span> — sits in a .words, fills the width */
76
70
  .wave {
77
71
  /* only WHOLE bars show (Lh 2026-09-23 — a clipped last bar read thinner): the columns are as many whole bar-widths as fit
78
72
  (auto-fill), spread edge to edge; bars that do not fit go to rows of no height, clipped away */