@usableapp/cardds 0.7.106 → 0.7.108

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 (110) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/bin/cardds.mjs +5 -3
  3. package/cardds.css +1 -0
  4. package/css/actions.css +9 -9
  5. package/css/base.css +6 -9
  6. package/css/card.css +19 -186
  7. package/css/choice.css +26 -1
  8. package/css/finish.css +4 -4
  9. package/css/journey.css +7 -3
  10. package/css/layover.css +4 -14
  11. package/css/lists.css +6 -18
  12. package/css/media.css +1 -7
  13. package/css/palette.css +1 -1
  14. package/css/parts.css +380 -0
  15. package/css/sheet.css +12 -139
  16. package/css/stack.css +15 -26
  17. package/css/stamp.css +1 -1
  18. package/css/step.css +3 -3
  19. package/css/theme.template.css +1 -1
  20. package/css/tokens.css +4 -4
  21. package/dist/actions/BarNote.d.ts +2 -2
  22. package/dist/actions/BarNote.js +2 -2
  23. package/dist/actions/Btn.d.ts +2 -2
  24. package/dist/actions/Btn.js +1 -1
  25. package/dist/actions/BtnRow.d.ts +1 -1
  26. package/dist/actions/BtnRow.js +1 -1
  27. package/dist/actions/IconBtn.d.ts +1 -1
  28. package/dist/actions/IconBtn.js +1 -1
  29. package/dist/cardds.css +500 -426
  30. package/dist/cards/Card.d.ts +6 -22
  31. package/dist/cards/Card.js +11 -22
  32. package/dist/cards/CardFeed.d.ts +1 -1
  33. package/dist/cards/CardFeed.js +5 -5
  34. package/dist/cards/CardParts.d.ts +2 -13
  35. package/dist/cards/CardParts.js +2 -13
  36. package/dist/cards/cardGo.js +1 -1
  37. package/dist/cards/cardGrounds.js +2 -2
  38. package/dist/catalog.json +1 -1
  39. package/dist/choice/Calendar.d.ts +12 -3
  40. package/dist/choice/Calendar.js +17 -5
  41. package/dist/choice/CalendarWeeks.d.ts +10 -2
  42. package/dist/choice/CalendarWeeks.js +18 -4
  43. package/dist/choice/HourGrid.d.ts +36 -0
  44. package/dist/choice/HourGrid.js +21 -0
  45. package/dist/choice/calendarDay.d.ts +7 -2
  46. package/dist/choice/calendarDay.js +6 -3
  47. package/dist/choice/span.d.ts +34 -0
  48. package/dist/choice/span.js +80 -0
  49. package/dist/forms/Field.d.ts +1 -1
  50. package/dist/forms/Field.js +1 -1
  51. package/dist/index.d.ts +8 -5
  52. package/dist/index.js +7 -5
  53. package/dist/journey/JourneyBadge.d.ts +2 -1
  54. package/dist/lists/Kv.d.ts +4 -2
  55. package/dist/lists/Kv.js +3 -3
  56. package/dist/lists/Row.d.ts +2 -28
  57. package/dist/lists/Row.js +2 -17
  58. package/dist/media/MapArea.d.ts +2 -2
  59. package/dist/media/MapArea.js +1 -1
  60. package/dist/media/Tile.d.ts +3 -7
  61. package/dist/media/Tile.js +5 -5
  62. package/dist/parts/Body.d.ts +7 -0
  63. package/dist/parts/Body.js +11 -0
  64. package/dist/parts/End.d.ts +10 -0
  65. package/dist/parts/End.js +7 -0
  66. package/dist/parts/Foot.d.ts +17 -0
  67. package/dist/parts/Foot.js +11 -0
  68. package/dist/parts/Head.d.ts +15 -0
  69. package/dist/parts/Head.js +34 -0
  70. package/dist/parts/Lead.d.ts +10 -0
  71. package/dist/parts/Lead.js +9 -0
  72. package/dist/parts/Words.d.ts +13 -0
  73. package/dist/parts/Words.js +20 -0
  74. package/dist/parts/headCtx.d.ts +7 -0
  75. package/dist/parts/headCtx.js +9 -0
  76. package/dist/scaffold/EmptyState.d.ts +1 -1
  77. package/dist/scaffold/EmptyState.js +1 -1
  78. package/dist/scaffold/Screen.js +1 -1
  79. package/dist/sheets/Modal.d.ts +3 -7
  80. package/dist/sheets/Modal.js +3 -7
  81. package/dist/sheets/Sheet.d.ts +2 -2
  82. package/dist/sheets/Sheet.js +8 -9
  83. package/dist/sheets/SheetStack.d.ts +3 -3
  84. package/dist/sheets/SheetStack.js +4 -4
  85. package/dist/sheets/SheetSteps.d.ts +1 -1
  86. package/dist/sheets/SheetSteps.js +2 -2
  87. package/dist/sheets/StackBack.d.ts +1 -1
  88. package/dist/sheets/StackBack.js +1 -1
  89. package/dist/sheets/StackSheet.js +7 -7
  90. package/dist/sheets/sheetMeasure.js +3 -3
  91. package/dist/sheets/sheetStepsPile.js +2 -2
  92. package/dist/sheets/stackCard.js +6 -6
  93. package/dist/templates/CallSheet.js +6 -5
  94. package/dist/templates/Walkthrough.js +3 -2
  95. package/docs/reference.md +76 -37
  96. package/lib/catalog.mjs +1 -1
  97. package/lib/mcp.mjs +1 -1
  98. package/lint/rules.mjs +14 -7
  99. package/package.json +2 -1
  100. package/scripts/codemod-parts.mjs +403 -0
  101. package/dist/cards/CardFoot.d.ts +0 -15
  102. package/dist/cards/CardFoot.js +0 -9
  103. package/dist/cards/CardHead.d.ts +0 -21
  104. package/dist/cards/CardHead.js +0 -39
  105. package/dist/cards/Stat.d.ts +0 -19
  106. package/dist/cards/Stat.js +0 -14
  107. package/dist/sheets/SheetBody.d.ts +0 -5
  108. package/dist/sheets/SheetBody.js +0 -9
  109. package/dist/sheets/SheetHead.d.ts +0 -15
  110. package/dist/sheets/SheetHead.js +0 -6
package/css/card.css CHANGED
@@ -42,80 +42,14 @@
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
- /* a head that LEADS with a date (Lh 2026-10-21, timebank's request card): a `CardHead` whose first child is a JourneyBadge and whose second is a
104
- ChipRow. The date stands at the left and the chips beside it, the two as tall as each other; the heading and its caption take the whole
105
- width under them, so the heading sits right over the words of the middle. The date is always as tall as the chips. The gap under the date row is --card-head-lead-gap; the date and the chips place themselves (journey.css, actions.css).
106
- <div class="card__head"><span class="tile-badge tile-badge--stretch">…</span><div class="chip-row">…</div><div><h2 class="t-h1">…</h2>…</div></div> */
107
- .card__head:has(> .tile-badge:first-child + .chip-row) {
108
- display: grid;
109
- grid-template-columns: auto minmax(0, 1fr);
110
- align-items: center;
111
- row-gap: var(--card-head-lead-gap, var(--sp-3));
112
- }
113
- .card__head:has(> .tile-badge:first-child + .chip-row) > :nth-child(n + 3) { grid-column: 1 / -1; }
114
-
115
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
116
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,
117
51
  so a theme that sets that knob still wins. */
118
- :is(.card > .card__head:first-child, .card__face > .card__turn + .card__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 */
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 */
119
53
 
120
54
  /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are paper bands edge to
121
55
  edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
@@ -127,20 +61,9 @@
127
61
  overflow: hidden;
128
62
  }
129
63
  .card--bands.card--turns { background: none; overflow: visible; } /* the turning box keeps --dark: the faces' ink is computed from it */
130
- :is(.card--bands, .card--bands > .card__face) > :is(.card__head, .card__foot) {
131
- --card-bg: var(--card-1-bg);
132
- --dark: var(--card-1-dark);
133
- background: var(--card-bg);
134
- color: var(--card-ink);
135
- margin-inline: calc(-1 * var(--card-pad));
136
- padding-inline: var(--card-pad);
137
- }
138
- :is(.card--bands, .card--bands > .card__face) > .card__head { margin-top: calc(-1 * var(--card-pad)); padding-block: var(--card-pad); }
139
- :is(.card--bands, .card--bands > .card__face) > .card__head { padding-block-end: var(--card-band-seam, var(--sp-2)); }
140
- /* the seam: the heading and the words under it read as ONE paragraph that changes colour at the band's edge (Lh 2026-10-21), so the room on
141
- each side of the head band's lower edge is --card-band-seam, not the card's padding and gap */
142
- :is(.card--bands, .card--bands > .card__face) > .card__head + * { margin-block-start: calc(var(--card-band-seam, var(--sp-2)) - var(--card-gap)); }
143
- :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 */
64
+ /* on a dark tone (3, 5) the lightened middle is a mid grey (#adadad, #bbbbbb in wireframe) that no muted ink reads on (3.2:1 and 3.8:1;
65
+ the ink gate, 2026-10-22, from the caption under a code on a `Card back`): there the middle's secondary words take the full ink */
66
+ :is(.card--bands, .card--bands > .card__face):is(.card--3, .card--5, .card--3 > *, .card--5 > *) { --card-muted: var(--card-ink); }
144
67
 
145
68
  /* ---- two faces: the WHOLE card turns (`Card back`, Lh 2026-10-19). The article is the turning box (.flip, base.css) and only that;
146
69
  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
@@ -162,10 +85,10 @@
162
85
  position: relative;
163
86
  }
164
87
  .card__face > * { min-width: 0; }
165
- .card__middle { flex: 1 0 auto; display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, auto); overflow: hidden; }
88
+ .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) */
166
89
  .card__turn { position: absolute; inset: 0; width: var(--px); height: var(--px); padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
167
90
  .card__face:has(> .card__turn:focus-visible) { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
168
- .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; }
91
+ .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; }
169
92
 
170
93
  /* ---- bleed: a scrolling track that runs to the container's edges ----
171
94
  For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
@@ -205,19 +128,6 @@
205
128
  is genuinely first or last needs the corner clipped for it */
206
129
  .card:has(> :is(.bleed, .map--bleed):first-child), .card:has(> :is(.bleed, .map--bleed):last-child) { overflow: clip; }
207
130
 
208
- /* foot: the card's action buttons on one line. One button takes the width,
209
- up to four share it in even parts — so the gaps read even too — and from
210
- three on they drop their side padding so the labels keep the room. */
211
- .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; color: var(--card-foot-ink, inherit); }
212
- .card__foot:has(> :nth-child(3)) > .btn { --btn-pad-x: var(--sp-2); }
213
- /* a small link under the buttons (CardFoot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
214
- whole short row of its own, centred — it never shares the buttons' line */
215
- .card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
216
- .card__foot-link { flex: 1 0 100%; display: flex; justify-content: center; }
217
- /* 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
218
- 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 */
219
- .card__foot--round { justify-content: center; gap: var(--card-foot-gap, var(--sp-6)); }
220
-
221
131
  /* media placeholder for wireframes — currentColor so it survives any palette */
222
132
  .ph {
223
133
  background:
@@ -334,7 +244,6 @@
334
244
  align-items: center;
335
245
  gap: var(--card-row-gap, var(--sp-3));
336
246
  }
337
- .card--row .card__body { flex: 1; display: grid; gap: var(--sp-0); }
338
247
 
339
248
  /* ---- 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
340
249
  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
@@ -351,7 +260,7 @@
351
260
  }
352
261
 
353
262
  /* ---- the cross that takes a row off the list (`Card row onRemove`): small, at the row's top right corner, inside it ----
354
- <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>
263
+ <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>
355
264
  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
356
265
  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
357
266
  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. */
@@ -379,21 +288,10 @@
379
288
  .card__remove > .icon { --icon-size: 62%; }
380
289
  .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
381
290
 
382
- /* leading thumb/icon in a row */
383
- .card__thumb {
384
- width: var(--card-thumb-size, var(--tap-sm)); height: var(--card-thumb-size, var(--tap-sm));
385
- flex: none;
386
- border-radius: var(--card-thumb-radius, var(--r-control));
387
- border: var(--border-w) solid currentColor;
388
- display: grid;
389
- place-items: center;
390
- font: var(--card-thumb-font, var(--type-label));
391
- }
392
-
393
291
  /* ---- card feed: a queue of cards, all in sight — one big, the rest small (Lh 2026-10-09, the whole queue 2026-10-15) ----
394
292
  <div class="card-feed">
395
- <article class="card feed-card"><div class="card__head feed-card__head">thumb or lead · h2 · caption</div>
396
- <div class="feed-card__more"><div class="feed-card__rest">content · <div class="card__foot">…</div></div></div></article>
293
+ <article class="card feed-card"><div class="head">lead · h2 · caption</div>
294
+ <div class="feed-card__more"><div class="feed-card__rest">content · <div class="foot">…</div></div></div></article>
397
295
  <article class="card feed-card feed-card--small" role="button" tabindex="0">… <div class="feed-card__more" inert>…</div></article>
398
296
  <p class="t-caption card-feed__end" role="note">the end</p>
399
297
  </div>
@@ -404,9 +302,7 @@
404
302
  shut content in the scrollable overflow of whatever holds the feed — and hidden once the row has shut. */
405
303
  .card-feed { display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; gap: var(--card-feed-gap, var(--card-gap-rest)); }
406
304
  .feed-card { row-gap: 0; }
407
- .feed-card__head { display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; min-height: var(--feed-card-head-h, var(--tap)); }
408
- .feed-card__head:has(> :is(.card__thumb, .feed-card__lead)) { grid-template-columns: auto minmax(0, 1fr); }
409
- .feed-card__head:has(> .t-caption) > :is(.card__thumb, .feed-card__lead) { grid-row: span 2; }
305
+ .feed-card > .head:has(> .t-caption) > .feed-card__lead { grid-row: span 2; }
410
306
  .feed-card__lead { display: grid; align-self: center; } /* a Stamp here is the small one, 48 × 60 (stamp.css) */
411
307
  .card-feed__end { text-align: center; }
412
308
  .feed-card__more { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: 1fr; }
@@ -423,22 +319,6 @@
423
319
  .feed-card--small .feed-card__rest { transition: visibility 0s var(--motion-sheet); }
424
320
  }
425
321
 
426
- /* ---- inline stat block (revenue-style) ---- */
427
- .stat { display: grid; gap: var(--stat-gap, var(--sp-1)); }
428
- .stat--centre { text-align: center; justify-items: center; } /* the one number of a sheet or a stage, under a centred avatar */
429
- .stat__row { display: flex; align-items: center; justify-content: space-between; gap: var(--stat-row-gap, var(--sp-3)); }
430
-
431
- .delta {
432
- font: var(--delta-font, var(--type-caption));
433
- font-weight: var(--fw-bold);
434
- padding: var(--delta-pad-y, var(--sp-1)) var(--delta-pad-x, var(--sp-2));
435
- border-radius: var(--delta-radius, var(--r-chip));
436
- background: var(--delta-bg, var(--card-3-bg));
437
- --dark: var(--card-3-dark);
438
- color: var(--delta-ink, var(--card-ink));
439
- }
440
- .delta--neg { background: var(--delta-neg-bg, var(--neg)); --dark: var(--neg-dark); color: var(--delta-neg-ink, var(--card-ink)); }
441
-
442
322
  /* ---- panes: a few panes side by side inside a card or a sheet, one in sight (Lh 2026-10-02) ----
443
323
  <div class="panes" role="group"><div class="panes__rail"><div class="panes__pane">…</div>…</div><span class="pager__at">…</span></div>
444
324
  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
@@ -462,7 +342,7 @@
462
342
 
463
343
  /* ---- cover: the image IS the card; text over a scrim ----
464
344
  <article class="card card--cover"><div class="ph" role="img"></div>
465
- <div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
345
+ <div class="head"><button class="icon-btn icon-btn--sm">♥</button></div>
466
346
  <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
467
347
  <div class="row">…meta…</div></article> */
468
348
  .card--cover {
@@ -552,14 +432,6 @@
552
432
  .sheet-stack > .card:nth-child(3) { margin-top: calc(2 * var(--stack-step)); }
553
433
  .sheet-stack > .card:nth-child(4) { margin-top: calc(3 * var(--stack-step)); }
554
434
  .sheet-stack > .card:nth-child(5) { margin-top: calc(4 * var(--stack-step)); }
555
- /* stack headers: uniform — [back?] title left, icons right, one height,
556
- same gap to content on every card */
557
- .sheet-stack > .card > .card__head {
558
- align-items: center;
559
- justify-content: flex-start;
560
- min-height: var(--tap);
561
- }
562
- .sheet-stack--display > .card > .card__head { align-items: start; min-height: 0; }
563
435
  /* ============================================================
564
436
  .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
565
437
 
@@ -612,8 +484,7 @@
612
484
  overscroll-behavior: contain;
613
485
  }
614
486
  .sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
615
- .sheet-stack--tap > .card > .card__head { touch-action: none; }
616
- .sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
487
+ .sheet-stack--tap > .card.is-open:not(:has(> .body)) {
617
488
  padding-top: calc(var(--card-pad) + var(--_refresh, 0rem));
618
489
  transition: translate var(--motion-sheet) var(--ease), padding-top var(--motion-sheet) var(--ease);
619
490
  }
@@ -649,56 +520,18 @@
649
520
  only, the sheet's size is untouched. A pull on the head keeps them square until the finger lets go. */
650
521
  .sheet-stack--flood > .card { transition: translate var(--motion-sheet) var(--ease), border-radius var(--motion-sheet) var(--ease); }
651
522
  .sheet-stack--flood > .card.is-open { border-top-left-radius: 0; border-top-right-radius: 0; }
652
- :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head {
653
- position: relative;
654
- padding-left: 0;
655
- transition: padding-left var(--motion-sheet) var(--ease);
656
- }
657
- /* ---- a stacked sheet with a SheetBody has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
658
- pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a CardFoot, the CTA) at the bottom.
659
- The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no SheetBody
523
+ /* ---- a stacked sheet with a Body has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
524
+ pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a Foot, the CTA) at the bottom.
525
+ The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no Body
660
526
  keeps the old way: the whole open card scrolls. ---- */
661
- .sheet-stack--tap > .card:has(> .sheet__body) { display: flex; flex-direction: column; gap: var(--card-gap); }
662
- .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
663
- /* a paper stack's foot is paper to the card's edges, and it covers the tinted flex gap above it so the
664
- button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
665
- where it was — the padding gives back what the bleed takes. */
666
- .sheet-stack > .card.sheet--paper > .card__foot {
667
- background: var(--action-bg);
668
- color: var(--card-ink);
669
- margin-top: calc(-1 * var(--card-gap));
670
- margin-inline: calc(-1 * var(--card-pad));
671
- margin-bottom: calc(-1 * var(--card-pad));
672
- padding-top: var(--card-gap);
673
- padding-inline: var(--card-pad);
674
- padding-bottom: var(--card-pad);
675
- }
676
- .stack-sheet > .card__head { flex-wrap: nowrap; }
527
+ .sheet-stack--tap > .card:has(> .body) { display: flex; flex-direction: column; gap: var(--card-gap); }
528
+ .sheet-stack--tap > .card:has(> .body).is-open { overflow: clip; }
677
529
  /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
678
530
  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
679
531
  (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
680
- .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))); }
681
- :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); }
682
- .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))); }
683
532
  /* 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),
684
533
  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) */
685
- .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
686
- /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
687
- .sheet > .card__foot {
688
- 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 */
689
- align-items: end;
690
- align-content: end;
691
- padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
692
- background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
693
- pointer-events: none;
694
- translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
695
- transition: translate var(--motion-sheet) var(--ease), visibility 0s;
696
- }
697
- .sheet.is-dragging > .card__foot { transition: none; }
698
- /* 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
699
- LEAVES AND ARRIVES WITH ITS SHEET — solid the whole way, hidden only once the slide is over. It used to stay in place and
700
- fade: the body slid through the strip behind a half-transparent button (timebank, Lh 2026-10-01: "ตัวหนังสือลอดใต้ปุ่ม"). */
701
- :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); }
534
+ .sheet-stack--tap > .card { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
702
535
  /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
703
536
  /* 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
704
537
  (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
@@ -727,7 +560,7 @@
727
560
  .postcard__front .postcard__turn > .ph { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border: 0; border-radius: 0; min-height: 0; }
728
561
 
729
562
  /* ---- where it sits in what stack.css draws: a sheet pulled to refresh holds still (docs/reference.md rule 15) ---- */
730
- .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
563
+ .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .body)) { transition: none; }
731
564
 
732
565
  /* ---- where it sits in what step.css draws: the reading block, a walk of pages (docs/reference.md rule 15) ---- */
733
566
  .step:has(> .sheet--peek) > .pages { padding-bottom: var(--sheet-peek); }
package/css/choice.css CHANGED
@@ -119,7 +119,7 @@
119
119
  <div class="calendar"><div class="calendar__head">…</div><div class="calendar__grid"><span class="calendar__wd">Mo</span>×7 <button class="calendar__day" style="--start: 3">1</button>…</div></div> */
120
120
  .calendar { display: grid; gap: var(--calendar-gap, var(--sp-3)); }
121
121
  .calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--calendar-head-gap, var(--sp-2)); }
122
- .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; }
122
+ .calendar__grid { display: grid; grid-template-columns: repeat(var(--_cols, 7), 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; container-type: inline-size; } /* a container: the span band below is measured in its width */
123
123
  .calendar__wd { font: var(--calendar-wd-font, var(--type-caption)); color: var(--calendar-wd-ink, var(--card-muted)); padding-block: var(--calendar-wd-pad-y, var(--sp-2)); }
124
124
  .calendar__day {
125
125
  width: 100%;
@@ -140,6 +140,31 @@
140
140
  .calendar__day:disabled { opacity: var(--fade-dim); }
141
141
  .calendar__day[aria-current="date"] { text-decoration: underline; text-underline-offset: 0.2em; }
142
142
 
143
+ /* ---- span: a run of cells covered by one drag or two taps (Calendar, CalendarWeeks, HourGrid; src/choice/span.ts is the mechanic). The whole
144
+ run is ONE highlight, as a finger drags over words (Lh 2026-10-21: no picked first or last day, the same colour throughout): each covered
145
+ cell's own ::before is a slice of the band, a column and a gap wide between so the slices meet across the grid's gaps, the slice at an end
146
+ stops at its column's edge with the pill's corner; a one-cell run is a pill the column wide. The cells keep their ink. A grid that is a
147
+ span takes the touch (no scroll starts on it); the weeks' box keeps its vertical scroll, so there a drag covers a row and two taps cover across rows */
148
+ .calendar--span .calendar__grid { touch-action: none; -webkit-user-select: none; user-select: none; }
149
+ .calendar--weeks.calendar--span .calendar__grid { touch-action: pan-y; }
150
+ .calendar__grid { --_col: calc((100cqw - (var(--_cols, 7) - 1) * var(--calendar-grid-gap, var(--sp-1))) / var(--_cols, 7)); --_span-w: calc(var(--_col) + var(--calendar-grid-gap, var(--sp-1))); }
151
+ :is(.calendar__day--span-start, .calendar__day--span-in, .calendar__day--span-end, .calendar__day--span-one) { position: relative; isolation: isolate; }
152
+ .calendar__day--span-start::before, .calendar__day--span-in::before, .calendar__day--span-end::before, .calendar__day--span-one::before { /* flat, not :is(): the knob walk reads each alternative's element */
153
+ content: "";
154
+ position: absolute;
155
+ inset-block: 0;
156
+ inset-inline-start: calc(50% - var(--_span-w) / 2);
157
+ width: var(--_span-w);
158
+ z-index: var(--z-under);
159
+ background: var(--calendar-span-bg, color-mix(in srgb, var(--accent) 25%, transparent));
160
+ }
161
+ .calendar__day--span-start::before { inset-inline-start: calc(50% - var(--_col) / 2); width: calc(var(--_span-w) - var(--calendar-grid-gap, var(--sp-1)) / 2); border-start-start-radius: var(--r-chip); border-end-start-radius: var(--r-chip); }
162
+ .calendar__day--span-end::before { width: calc(var(--_span-w) - var(--calendar-grid-gap, var(--sp-1)) / 2); border-start-end-radius: var(--r-chip); border-end-end-radius: var(--r-chip); }
163
+ .calendar__day--span-one::before { inset-inline-start: calc(50% - var(--_col) / 2); width: var(--_col); border-radius: var(--r-chip); }
164
+ /* ---- hour grid: the hours of a day in Calendar's cells, `--hour-grid-cols` to a row, a label over them
165
+ <div class="calendar hour-grid"><span class="calendar__label">น.</span><div class="calendar__grid"><button class="calendar__day">9</button>…</div></div> */
166
+ .hour-grid > .calendar__grid { --_cols: var(--hour-grid-cols, 6); }
167
+
143
168
  /* ---- calendar weeks: a run of week rows that scrolls week by week, `show` rows in sight; the head sticks at the top of the box
144
169
  <div class="calendar calendar--weeks"><div class="calendar__weeks-head"><span class="calendar__label">June 2026</span><div class="calendar__grid">…wd×7</div></div>
145
170
  <div class="calendar__grid calendar__week"><button class="calendar__day">…</button>×7</div>…</div>
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
@@ -58,7 +58,7 @@
58
58
  align-self: stretch;
59
59
  max-block-size: calc((2 * var(--fs-caption) + var(--fs-lg)) * var(--tile-badge-line, var(--lh-badge)) + 2 * var(--sp-3));
60
60
  container-type: size;
61
- inline-size: calc(var(--fs-lg) * 3.25);
61
+ inline-size: var(--tile-badge-stretch-w, calc(var(--fs-lg) * 3.25)); /* a run of days ("18–21") needs more: the app sets the knob on that badge */
62
62
  grid-template-columns: auto auto;
63
63
  align-content: center;
64
64
  justify-content: center;
@@ -67,17 +67,21 @@
67
67
  row-gap: 0;
68
68
  padding-block: var(--sp-3);
69
69
  }
70
- .tile-badge--stretch > * { line-height: inherit; }
70
+ .tile-badge--stretch > * { line-height: inherit; white-space: nowrap; } /* a value never breaks at its dash */
71
71
  .tile-badge--stretch > :first-child { grid-column: 1 / -1; }
72
72
  @container (min-height: 3.5rem) { /* just under the three lines' own height, the most the badge grows to */
73
73
  .tile-badge--stretch > * { grid-column: 1 / -1; }
74
74
  }
75
75
 
76
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
- .card__head:has(> .tile-badge:first-child + .chip-row) > .tile-badge--stretch:first-child { grid-column: 1; grid-row: 1; max-block-size: none; }
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
78
 
79
79
  /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */
80
80
  .tile-badge--corner { position: absolute; right: var(--sp-3); bottom: var(--sp-3); }
81
+ /* on a CardMedia (ruled Lh 2026-10-21, three screenshots side by side): the card's paper, its corner concentric with the media's — the media's
82
+ corner less the inset above. Not the theme's highlight: the distance on a request card is information, and a highlight fill reads as a
83
+ control. Both knobs still win (palette.css leaves this badge out of orchard's highlight rule for the same reason) */
84
+ .card__media > .tile-badge--corner { --_badge-radius: calc(var(--r-media) - var(--sp-3)); background: var(--tile-badge-bg, var(--card-1-bg)); --dark: var(--card-1-dark); }
81
85
 
82
86
  /* ---- note row: icon + caption (transit hints, fine print) ---- */
83
87
  .note-row {
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));
@@ -90,6 +75,9 @@
90
75
  <div class="kv-grid"><div class="kv-cell"><svg class="icon"/><span class="kv-cell__val">15.6 kg</span><span class="kv-cell__key">Weight</span></div>…</div> */
91
76
  .kv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(var(--tile-min), 100%), 1fr)); gap: var(--kv-grid-gap, var(--sp-4)); }
92
77
  .kv-grid > .kv-cell + .kv-cell { border-left: var(--border-w) solid var(--hairline); padding-left: var(--sp-4); }
78
+ /* `centre` (Lh 2026-10-21, the code on timebank's request card, as `Stat centre` and `Head centre` were): the grid is as tall as its holder, its cells stand in the middle of it, each value and caption centred */
79
+ .kv-grid--centre { min-height: 100%; align-content: center; text-align: center; } /* min-height, not height: in a plain card (a grid of auto rows) a percent height resolved 8px short and the cell overflowed it; a percent min-height is nothing there, and the holder's height on a turning card's face */
80
+ .kv-grid--centre > .kv-cell { justify-items: center; }
93
81
  .kv-cell { display: grid; gap: var(--kv-cell-gap, var(--sp-0)); align-content: start; min-width: 0; }
94
82
  .kv-cell > .icon { --icon-margin: 0 0 var(--sp-1); }
95
83
  .kv-cell__val { font: var(--kv-cell-val-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
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 */
package/css/palette.css CHANGED
@@ -92,7 +92,7 @@
92
92
  /* where the highlights land — written as the component's own rule, its knob still first, so a project's knob wins (a palette never DECLARES a
93
93
  knob). Each fill says its side; the words on it follow */
94
94
  [data-palette="orchard"] .badge:not(.badge--soft) { background: var(--badge-bg, var(--highlight-1)); color: var(--badge-ink, var(--ink-ondark)); }
95
- [data-palette="orchard"] .tile-badge:not(.tile-badge--plain) { background: var(--tile-badge-bg, var(--highlight-2)); --dark: 0; }
95
+ [data-palette="orchard"] .tile-badge:not(.tile-badge--plain, .card__media > .tile-badge--corner) { background: var(--tile-badge-bg, var(--highlight-2)); --dark: 0; } /* a badge on a media stays the paper (journey.css) */
96
96
  [data-palette="orchard"] .calendar__day--mark { background: var(--calendar-day-mark-bg, color-mix(in srgb, var(--highlight-2) 15%, transparent)); }
97
97
  /* fields are WHITE boxes on every ground (Lh 2026-09-25): a light ground, so the words typed in them are onlight, even on the moss card */
98
98
  [data-palette="orchard"] .field { background: var(--field-bg, #ffffff); color: var(--ink-onlight); }