@usableapp/cardds 0.7.103 → 0.7.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/css/actions.css +102 -36
  3. package/css/base.css +139 -15
  4. package/css/card.css +308 -79
  5. package/css/choice.css +25 -33
  6. package/css/finish.css +28 -27
  7. package/css/forms.css +6 -14
  8. package/css/journey.css +4 -4
  9. package/css/layover.css +26 -24
  10. package/css/lists.css +8 -6
  11. package/css/media.css +13 -74
  12. package/css/numbers.css +37 -32
  13. package/css/palette.css +1 -1
  14. package/css/people.css +3 -8
  15. package/css/post.css +23 -19
  16. package/css/sheet.css +84 -56
  17. package/css/stack.css +39 -215
  18. package/css/stamp.css +41 -29
  19. package/css/step.css +1 -107
  20. package/css/sticky.css +43 -41
  21. package/css/theme.template.css +12 -0
  22. package/css/tokens.css +100 -26
  23. package/dist/actions/Chip.d.ts +1 -1
  24. package/dist/actions/Chip.js +3 -2
  25. package/dist/cardds.css +1053 -868
  26. package/dist/cards/Card.d.ts +3 -3
  27. package/dist/cards/Card.js +1 -1
  28. package/dist/cards/CardFeed.js +7 -9
  29. package/dist/catalog.json +1 -1
  30. package/dist/layover/Snackbar.js +2 -2
  31. package/dist/lists/Bubble.d.ts +1 -1
  32. package/dist/lists/Bubble.js +1 -1
  33. package/dist/lists/Row.js +5 -7
  34. package/dist/numbers/Picker.js +11 -18
  35. package/dist/people/Avatar.d.ts +1 -1
  36. package/dist/people/Avatar.js +3 -2
  37. package/dist/post/PostDesk.js +6 -9
  38. package/dist/post/PostLetter.js +4 -4
  39. package/dist/pressable.d.ts +9 -0
  40. package/dist/pressable.js +6 -0
  41. package/dist/room.d.ts +5 -0
  42. package/dist/room.js +9 -0
  43. package/dist/scaffold/Panes.js +8 -14
  44. package/dist/sheets/Sheet.d.ts +1 -1
  45. package/dist/sheets/Sheet.js +2 -3
  46. package/dist/sheets/SheetSteps.js +3 -3
  47. package/dist/sheets/StackSheet.d.ts +1 -1
  48. package/dist/sheets/sheetStepsPile.js +2 -3
  49. package/dist/sheets/stackCard.js +67 -115
  50. package/dist/state.d.ts +21 -0
  51. package/dist/state.js +28 -1
  52. package/dist/sticky/Sticky.js +3 -7
  53. package/dist/templates/Home.d.ts +1 -1
  54. package/dist/templates/Splash.js +2 -2
  55. package/dist/templates/Walkthrough.js +4 -5
  56. package/docs/reference.md +71 -9
  57. package/lib/catalog.mjs +23 -7
  58. package/lint/rules.mjs +7 -5
  59. package/lint/values.mjs +144 -0
  60. package/package.json +6 -4
package/css/choice.css CHANGED
@@ -52,7 +52,7 @@
52
52
  --dark: var(--action-dark); /* the paper's side: its knob at rest is the paper's muted ink, on the accent the accent's */
53
53
  color: var(--card-ink);
54
54
  position: relative;
55
- transition: background-color var(--motion-press) ease-out, border-color var(--motion-press) ease-out;
55
+ transition: background-color var(--motion-press) var(--ease-out), border-color var(--motion-press) var(--ease-out);
56
56
  }
57
57
  .toggle::after {
58
58
  content: "";
@@ -63,7 +63,7 @@
63
63
  height: calc(var(--_h) - 2 * var(--_pad) - 2 * var(--border-w));
64
64
  border-radius: 50%;
65
65
  background: var(--card-muted); /* a glyph: a shade of its side, never an opacity (0.45 of the ink was 2.4:1 on the paper) */
66
- transition: translate var(--motion-quick) ease;
66
+ transition: translate var(--motion-quick) var(--ease);
67
67
  }
68
68
  .toggle:checked { background: var(--accent); border-color: transparent; }
69
69
  .toggle:checked::after { translate: calc(var(--_w) - var(--_h)) 0; background: var(--on-accent); }
@@ -75,34 +75,18 @@
75
75
  <div class="chip-row"><label class="chip chip--pick chip--sign"><input type="checkbox">Nature</label>…</div> */
76
76
  .chip-grid {
77
77
  display: grid;
78
- grid-template-columns: repeat(auto-fill, minmax(min(5.5rem, 100%), 1fr));
78
+ grid-template-columns: repeat(auto-fill, minmax(min(calc(var(--tap-base) * 11 / 6), 100%), 1fr));
79
79
  gap: var(--chip-grid-gap, var(--sp-2));
80
80
  }
81
81
  /* .chip hugs its text as a loose grid child (justify-self: start); inside a
82
82
  chip grid the whole point is equal, full-width pick targets, so stretch.
83
83
  Matters most in scripts without word spaces (Thai): min-content is the
84
84
  whole string, tracks get wide, and hugging chips leave a ragged grid. */
85
- .chip-grid > .chip { justify-self: stretch; justify-content: center; min-height: var(--btn-h); padding-inline: var(--sp-2); }
86
- .chip--pick { position: relative; } /* the input covers the whole chip, on a row as in a grid */
85
+ .chip-grid > .chip { --chip-self: stretch; --chip-justify: center; --chip-min-h: var(--btn-h); --chip-pad-x: var(--sp-2); }
87
86
  /* the hidden input, the same shape wherever a native radio/checkbox drives a picked look:
88
87
  a chip, a mood dot, a tile (media.css), a sticky's tone swatch (sticky.css), a place on a map (sheet.css), a card that is a label round a radio
89
88
  (a pickable row card: the app gives the picked one its `tone`) — the native input lies over its whole box, unseen */
90
89
  :is(.chip--pick, .mood__opt, .tile--pick, .sticky-pick__swatch, .map-pick) > input, label.card > input[type="radio"] { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
91
- :is(.chip--pick, .tile--pick):has(:checked) { background: var(--accent); --dark: var(--accent-dark); color: var(--card-ink); border-color: transparent; }
92
- /* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
93
- .chip--sign { padding-right: var(--sp-2); }
94
- .chip--sign::after {
95
- content: "";
96
- width: var(--icon-xs);
97
- height: var(--icon-xs);
98
- flex: none;
99
- background: currentColor;
100
- mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
101
- -webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
102
- }
103
- .chip--sign:has(:checked)::after { rotate: 45deg; }
104
- @media (prefers-reduced-motion: no-preference) { .chip--sign::after { transition: rotate var(--motion-slide); } }
105
-
106
90
  /* ---- day strip: a week, one day selected, today a light tone ----
107
91
  <div class="day-strip" role="tablist"><button class="day" role="tab" aria-selected="true"><span class="day__name">Thu</span><span class="day__num">16</span></button>…</div> */
108
92
  .day-strip { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--day-strip-gap, var(--sp-1)); }
@@ -125,7 +109,7 @@
125
109
  color: var(--day-rest-ink, var(--card-muted)); /* an unpicked day at rest: the muted words of the ground it stands on (an opacity faded it under AA) */
126
110
  }
127
111
  .day__name { font: var(--day-name-font, var(--type-caption)); }
128
- .day__num { font: 700 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
112
+ .day__num { font: var(--fw-bold) var(--fs-lg) / var(--lh-tight) var(--font-display); font-variant-numeric: tabular-nums; }
129
113
  /* today: a light tone, no louder than a rest; the selected pill below wins when it is both (it comes after and weighs more) */
130
114
  .day--today { background: var(--day-today-bg, var(--ink-tint)); color: var(--day-today-ink, var(--card-ink)); }
131
115
  .day[aria-selected="true"] { background: var(--day-selected-bg, var(--accent)); color: var(--day-selected-ink, var(--on-accent)); }
@@ -144,7 +128,7 @@
144
128
  display: grid;
145
129
  place-items: center;
146
130
  border-radius: 50%;
147
- font: 500 var(--fs-title)/1 var(--font-ui);
131
+ font: var(--fw-medium) var(--fs-title) / var(--lh-solid) var(--font-ui);
148
132
  font-variant-numeric: tabular-nums;
149
133
  grid-column-start: var(--start, auto); /* --start on the 1st: weekday column 1–7 */
150
134
  }
@@ -152,8 +136,8 @@
152
136
  strong mark adds a ring of the accent. A stronger fill sat at the middle, where neither the ink nor --on-accent holds 4.5:1 */
153
137
  .calendar__day--mark { background: var(--calendar-day-mark-bg, color-mix(in srgb, var(--accent) 12%, transparent)); }
154
138
  .calendar__day--mark2 { background: var(--calendar-day-mark2-bg, color-mix(in srgb, var(--accent) 12%, transparent)); color: var(--calendar-day-mark2-ink, inherit); box-shadow: var(--calendar-day-mark2-shadow, inset 0 0 0 var(--border-w) var(--accent)); }
155
- .calendar__day[aria-selected="true"] { background: var(--calendar-day-selected-bg, var(--accent)); color: var(--calendar-day-selected-ink, var(--on-accent)); font-weight: 700; }
156
- .calendar__day:disabled { opacity: 0.3; }
139
+ .calendar__day[aria-selected="true"] { background: var(--calendar-day-selected-bg, var(--accent)); color: var(--calendar-day-selected-ink, var(--on-accent)); font-weight: var(--fw-bold); }
140
+ .calendar__day:disabled { opacity: var(--fade-dim); }
157
141
  .calendar__day[aria-current="date"] { text-decoration: underline; text-underline-offset: 0.2em; }
158
142
 
159
143
  /* ---- 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
@@ -166,12 +150,12 @@
166
150
  overflow-y: auto;
167
151
  overscroll-behavior: contain;
168
152
  scroll-snap-type: y mandatory;
169
- scroll-padding-block-start: var(--_weeks-lead, 0px);
153
+ scroll-padding-block-start: var(--_weeks-lead, 0rem);
170
154
  overflow-anchor: none; /* a pick rerenders the rows; the browser must not shift the scroll to keep an anchor */
171
155
  scrollbar-width: none;
172
156
  }
173
157
  .calendar--weeks::-webkit-scrollbar { display: none; }
174
- .calendar__weeks-head { position: sticky; inset-block-start: 0; z-index: 1; display: grid; gap: var(--calendar-head-gap, var(--sp-2)); background: var(--calendar-weeks-head-bg, var(--card-bg)); }
158
+ .calendar__weeks-head { position: sticky; inset-block-start: 0; z-index: var(--z-over); display: grid; gap: var(--calendar-head-gap, var(--sp-2)); background: var(--calendar-weeks-head-bg, var(--card-bg)); }
175
159
  .calendar__label { font: var(--calendar-label-font, var(--type-label)); color: var(--calendar-label-ink, inherit); justify-self: start; }
176
160
  button.calendar__label { appearance: none; border: 0; padding: 0; background: none; cursor: pointer; border-radius: var(--sp-1); }
177
161
  button.calendar__label:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
@@ -196,14 +180,14 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
196
180
  height: var(--mood-dot-size, var(--icon-xs));
197
181
  border-radius: 50%;
198
182
  background: var(--_mood, currentColor);
199
- transition: scale 0.2s ease, box-shadow 0.2s ease;
183
+ transition: scale var(--motion-quick) var(--ease), box-shadow var(--motion-quick) var(--ease);
200
184
  }
201
185
  .mood__opt:nth-child(1) { --_mood: var(--neg); }
202
186
  .mood__opt:nth-child(2) { --_mood: color-mix(in srgb, var(--neg) 50%, var(--dot-b)); }
203
187
  .mood__opt:nth-child(3) { --_mood: var(--dot-b); }
204
188
  .mood__opt:nth-child(4) { --_mood: color-mix(in srgb, var(--pos) 50%, var(--dot-b)); }
205
189
  .mood__opt:nth-child(5) { --_mood: var(--pos); }
206
- .mood__opt:has(:checked) > .mood__dot { scale: 1.7; box-shadow: 0 0 0 var(--sp-2) color-mix(in srgb, var(--_mood, currentColor) 25%, transparent); }
190
+ .mood__opt:has(:checked) > .mood__dot { scale: var(--scale-pick); box-shadow: 0 0 0 var(--sp-2) color-mix(in srgb, var(--_mood, currentColor) 25%, transparent); }
207
191
  .mood__opt:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-0); border-radius: 50%; }
208
192
 
209
193
  /* ---- pin: big code entry, one digit per pill ----
@@ -243,7 +227,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
243
227
  background-position: center;
244
228
  background-repeat: no-repeat;
245
229
  }
246
- .pin--secret .pin__cell.is-fresh:not(:placeholder-shown) { animation: pin-flash var(--pin-flash-time, 0.5s) steps(1); }
230
+ .pin--secret .pin__cell.is-fresh:not(:placeholder-shown) { animation: pin-flash var(--pin-flash-time, var(--motion-flash)) var(--ease-step); }
247
231
  @keyframes pin-flash { 0% { -webkit-text-fill-color: currentColor; background-size: 0 0; } }
248
232
  .pin--secret .pin__cell::selection { background: transparent; } /* its digit is selected when the cell takes the cursor: nothing to show of it */
249
233
  .pin__cell:focus { outline: none; }
@@ -254,12 +238,12 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
254
238
  .pin--asleep .pin__cell:focus:placeholder-shown { background-image: none; animation: none; }
255
239
  .pin__cell:is(:focus, [data-cursor]):placeholder-shown {
256
240
  background-image: linear-gradient(currentColor, currentColor);
257
- background-size: var(--pin-caret-w, 2px) var(--pin-caret-h, 1em);
241
+ background-size: var(--pin-caret-w, var(--hairline-w)) var(--pin-caret-h, 1em);
258
242
  background-position: center;
259
243
  background-repeat: no-repeat;
260
244
  }
261
245
  @media (prefers-reduced-motion: no-preference) {
262
- .pin__cell:is(:focus, [data-cursor]):placeholder-shown { animation: pin-caret 1.1s steps(1) infinite; }
246
+ .pin__cell:is(:focus, [data-cursor]):placeholder-shown { animation: pin-caret var(--motion-blink) var(--ease-step) infinite; }
263
247
  @keyframes pin-caret { 50% { background-size: 0 0; } }
264
248
  }
265
249
 
@@ -271,13 +255,13 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
271
255
  .composer { display: grid; gap: var(--composer-gap, var(--sp-3)); }
272
256
  .composer__input {
273
257
  width: 100%;
274
- min-height: var(--composer-input-min-h, 11rem);
258
+ min-height: var(--composer-input-min-h, calc(var(--tap-base) * 11 / 3));
275
259
  resize: none;
276
260
  border: 0;
277
261
  padding: 0;
278
262
  background: transparent;
279
263
  color: inherit;
280
- font: var(--composer-input-font, 700 var(--fs-2xl)/1.25 var(--font-display));
264
+ font: var(--composer-input-font, var(--fw-bold) var(--fs-2xl) / var(--lh-h2) var(--font-display));
281
265
  letter-spacing: var(--track-h2);
282
266
  outline: none;
283
267
  }
@@ -363,3 +347,11 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
363
347
  -webkit-user-select: none; user-select: none;
364
348
  flex: none;
365
349
  }
350
+
351
+ /* ==== the pin's wrong code: red cells and one shake (#62: a block is written in its own file) ==== */
352
+ .pin.pin--error .pin__cell, .pin.pin--error .pin__cell:focus { border-color: var(--pin-error-border, var(--signal-error)); color: var(--pin-error-ink, var(--signal-error)); } /* ALL the cells, edge and digit — the one the cursor waits in too (Lh 2026-09-24); the wrong digits stay in red until the member types again */
353
+ /* a code that does not match (Pin match): the row shakes once, sideways, then the Pin empties itself for a new try (Lh 2026-09-25) */
354
+ @media (prefers-reduced-motion: no-preference) {
355
+ .pin--shake { animation: pin-shake var(--pin-shake-time, var(--motion-shake)) var(--ease-in-out); }
356
+ }
357
+ @keyframes pin-shake { 0%, 100% { translate: 0; } 20%, 60% { translate: calc(-1 * var(--pin-shake-x, var(--sp-2))); } 40%, 80% { translate: var(--pin-shake-x, var(--sp-2)); } }
package/css/finish.css CHANGED
@@ -33,28 +33,28 @@
33
33
  ============================================================ */
34
34
  [data-finish="lit"] {
35
35
  --_shade: var(--on-surface);
36
- --finish-edge: inset 0 1px 0 rgb(255 255 255 / 0.55); /* the lit top edge */
37
- --finish-ring: 0 0 0 1px color-mix(in srgb, var(--_shade) 7%, transparent); /* the hairline round a raised thing */
36
+ --finish-edge: inset 0 0.0625rem 0 rgb(255 255 255 / 0.55); /* the lit top edge */
37
+ --finish-ring: 0 0 0 0.0625rem color-mix(in srgb, var(--_shade) 7%, transparent); /* the hairline round a raised thing */
38
38
 
39
39
  /* the three shadows every component already reads — layered: one close and crisp, one far and soft */
40
40
  --shadow-card: var(--finish-edge), var(--finish-ring),
41
- 0 1px 2px color-mix(in srgb, var(--_shade) 8%, transparent),
41
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 8%, transparent),
42
42
  0 0.375rem 0.75rem -0.125rem color-mix(in srgb, var(--_shade) 8%, transparent),
43
43
  0 1.25rem 2.5rem -0.75rem color-mix(in srgb, var(--_shade) 14%, transparent);
44
44
  --shadow-float: var(--finish-edge), var(--finish-ring),
45
- 0 1px 2px color-mix(in srgb, var(--_shade) 12%, transparent),
45
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 12%, transparent),
46
46
  0 0.5rem 1.25rem -0.25rem color-mix(in srgb, var(--_shade) 22%, transparent);
47
47
  --shadow-sheet: var(--finish-edge), var(--finish-ring),
48
- 0 -1px 2px color-mix(in srgb, var(--_shade) 6%, transparent),
48
+ 0 -0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 6%, transparent),
49
49
  0 -0.75rem 2.5rem color-mix(in srgb, var(--_shade) 18%, transparent);
50
50
 
51
51
  /* a control: its own small lift, and the sheen that runs down its face */
52
52
  --finish-control: var(--finish-edge), var(--finish-ring),
53
- 0 1px 2px color-mix(in srgb, var(--_shade) 12%, transparent),
53
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 12%, transparent),
54
54
  0 0.25rem 0.5rem -0.125rem color-mix(in srgb, var(--_shade) 12%, transparent);
55
- --finish-control-down: var(--finish-ring), inset 0 1px 2px color-mix(in srgb, var(--_shade) 18%, transparent);
55
+ --finish-control-down: var(--finish-ring), inset 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 18%, transparent);
56
56
  --finish-sheen: linear-gradient(to bottom, rgb(255 255 255 / 0.16), rgb(255 255 255 / 0) 60%);
57
- --finish-sunk: inset 0 1px 2px color-mix(in srgb, var(--_shade) 14%, transparent), 0 1px 0 rgb(255 255 255 / 0.5);
57
+ --finish-sunk: inset 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 14%, transparent), 0 0.0625rem 0 rgb(255 255 255 / 0.5);
58
58
 
59
59
  --outline: color-mix(in srgb, currentColor 14%, transparent);
60
60
  }
@@ -78,7 +78,7 @@
78
78
  [data-finish="lit"] .btn--primary,
79
79
  [data-finish="lit"] .icon-btn--invert:not(.icon-btn-group > *) {
80
80
  box-shadow: var(--finish-edge),
81
- 0 1px 2px color-mix(in srgb, var(--_shade) 20%, transparent),
81
+ 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 20%, transparent),
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) */
@@ -92,7 +92,7 @@
92
92
  }
93
93
  [data-finish="lit"] .icon-btn-group { box-shadow: var(--finish-control); }
94
94
  [data-finish="lit"] .toggle { box-shadow: var(--finish-sunk); }
95
- [data-finish="lit"] .toggle::after { box-shadow: 0 1px 2px color-mix(in srgb, var(--_shade) 35%, transparent); }
95
+ [data-finish="lit"] .toggle::after { box-shadow: 0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 35%, transparent); }
96
96
 
97
97
  /* ---- sunk: what the member types into ---- */
98
98
  [data-finish="lit"] :is(.field, .pin__cell) { box-shadow: var(--finish-sunk); }
@@ -110,12 +110,12 @@
110
110
  }
111
111
  [data-finish="print"] .card {
112
112
  border: var(--border-w) solid var(--_ink);
113
- box-shadow: var(--print-off, 3px) var(--print-off, 3px) 0 0 color-mix(in srgb, var(--card-ink) 28%, transparent);
113
+ box-shadow: var(--print-off, 0.1875rem) var(--print-off, 0.1875rem) 0 0 color-mix(in srgb, var(--card-ink) 28%, transparent);
114
114
  isolation: isolate; /* the grain's blend stays inside the card */
115
115
  }
116
116
  /* the grain: a ::after, not ::before — .card--cover's scrim is its ::before. Over everything in the card (z-index 1, the foot's own level), touches pass through it */
117
117
  [data-finish="print"] .card::after {
118
- content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: 1;
118
+ content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: var(--z-over);
119
119
  opacity: var(--print-grain, 0.32); mix-blend-mode: multiply;
120
120
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.3 0 0 0 0 0.25 0 0 0 0 0.2 0 0 0 0.9 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
121
121
  }
@@ -127,10 +127,11 @@
127
127
  }
128
128
  [data-finish="print"] :is(.btn, .chip) {
129
129
  border-color: var(--_ink);
130
- box-shadow: 2px 2px 0 0 color-mix(in srgb, currentColor 35%, transparent);
130
+ box-shadow: 0.125rem 0.125rem 0 0 color-mix(in srgb, currentColor 35%, transparent);
131
131
  }
132
- [data-finish="print"] .btn--primary { box-shadow: 2px 2px 0 0 color-mix(in srgb, var(--card-ink) 60%, transparent); }
133
- [data-finish="print"] :is(.btn, .chip):active { box-shadow: none; translate: 2px 2px; }
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
+ [data-finish="print"] .btn--primary { box-shadow: 0.125rem 0.125rem 0 0 color-mix(in srgb, var(--card-ink) 60%, transparent); }
134
+ [data-finish="print"] :is(.btn, .chip):active { box-shadow: none; translate: 0.125rem 0.125rem; }
134
135
  [data-finish="print"] .card__thumb { border-style: dashed; }
135
136
 
136
137
  /* ============================================================
@@ -142,28 +143,28 @@
142
143
  [data-finish="sticker"] { --outline: color-mix(in srgb, currentColor 18%, transparent); }
143
144
  [data-finish="sticker"] .card {
144
145
  box-shadow:
145
- 0 0 0 var(--sticker-cut, 5px) #fff,
146
- 0 calc(var(--sticker-cut, 5px) + 4px) 0 0 color-mix(in srgb, var(--on-surface) 16%, transparent),
147
- 0 calc(var(--sticker-cut, 5px) + 12px) 20px -8px color-mix(in srgb, var(--on-surface) 22%, transparent);
146
+ 0 0 0 var(--sticker-cut, 0.3125rem) #fff,
147
+ 0 calc(var(--sticker-cut, 0.3125rem) + 0.25rem) 0 0 color-mix(in srgb, var(--on-surface) 16%, transparent),
148
+ 0 calc(var(--sticker-cut, 0.3125rem) + 0.75rem) 1.25rem -0.5rem color-mix(in srgb, var(--on-surface) 22%, transparent);
148
149
  }
149
150
  [data-finish="sticker"] .card:not(.sheet, .modal, .card--away, .sheet-stack *, .card-list > *, .card-feed > *) { rotate: var(--sticker-tilt, 0.8deg); }
150
151
  [data-finish="sticker"] .card:not(.sheet, .modal, .card--away, .sheet-stack *, .card-list > *, .card-feed > *):nth-of-type(even) { rotate: calc(-1 * var(--sticker-tilt, 0.8deg)); }
151
152
  [data-finish="sticker"] .card-list { gap: var(--sp-5); }
152
153
  [data-finish="sticker"] .btn {
153
- box-shadow: 0 3px 0 0 color-mix(in srgb, currentColor 30%, transparent);
154
+ box-shadow: 0 0.1875rem 0 0 color-mix(in srgb, currentColor 30%, transparent);
154
155
  border-color: color-mix(in srgb, currentColor 25%, transparent);
155
156
  }
156
- [data-finish="sticker"] .btn--primary { box-shadow: 0 3px 0 0 color-mix(in srgb, var(--btn-primary-bg, var(--accent)) 55%, var(--on-surface)); }
157
- [data-finish="sticker"] .btn:active { box-shadow: none; translate: 0 3px; }
158
- [data-finish="sticker"] .chip { box-shadow: 0 0 0 3px #fff; }
157
+ [data-finish="sticker"] .btn--primary { box-shadow: 0 0.1875rem 0 0 color-mix(in srgb, var(--btn-primary-bg, var(--accent)) 55%, var(--on-surface)); }
158
+ [data-finish="sticker"] .btn:active { box-shadow: none; translate: 0 0.1875rem; }
159
+ [data-finish="sticker"] .chip { box-shadow: 0 0 0 0.1875rem #fff; }
159
160
 
160
161
  /* ============================================================
161
162
  ink — a poster / comic print: --ink-w of the surface's ink round every boundary and control, and a hard
162
163
  offset of --ink-off in the same ink. The two wireframe borders come back as that line.
163
164
  ============================================================ */
164
165
  [data-finish="ink"] { --card-border: var(--on-surface); --action-border: var(--on-surface); --outline: currentColor; }
165
- [data-finish="ink"] .card { border: var(--ink-w, 2.5px) solid var(--on-surface); box-shadow: var(--ink-off, 5px) var(--ink-off, 5px) 0 0 var(--on-surface); }
166
- [data-finish="ink"] :is(.btn, .chip, .icon-btn:not(.icon-btn-group > *)) { border: var(--ink-w, 2.5px) solid currentColor; box-shadow: 3px 3px 0 0 currentColor; }
167
- [data-finish="ink"] .btn--primary { border-color: var(--on-surface); box-shadow: 3px 3px 0 0 var(--on-surface); }
168
- [data-finish="ink"] .toggle { box-shadow: 2px 2px 0 0 var(--on-surface); }
169
- [data-finish="ink"] .card__thumb { border-width: var(--ink-w, 2.5px); }
166
+ [data-finish="ink"] .card { border: var(--ink-w, 0.15625rem) solid var(--on-surface); box-shadow: var(--ink-off, 0.3125rem) var(--ink-off, 0.3125rem) 0 0 var(--on-surface); }
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
+ [data-finish="ink"] .btn--primary { border-color: var(--on-surface); box-shadow: 0.1875rem 0.1875rem 0 0 var(--on-surface); }
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); }
package/css/forms.css CHANGED
@@ -23,7 +23,7 @@
23
23
  .field select,
24
24
  .field textarea,
25
25
  .field--rows {
26
- font: var(--field-font, 400 var(--fs-lg) / 1.4 var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does. .field--rows too, so 1lh is the textarea's line for its icon */
26
+ font: var(--field-font, var(--fw-regular) var(--fs-lg) / var(--lh-field) var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does. .field--rows too, so 1lh is the textarea's line for its icon */
27
27
  }
28
28
  .field input,
29
29
  .field select,
@@ -37,17 +37,16 @@
37
37
  caret-color: currentColor;
38
38
  }
39
39
  .field input::placeholder,
40
- .field textarea::placeholder { color: inherit; opacity: 0.55; font-size: calc(1em * var(--_ph-fit, 1)); } /* --_ph-fit: Field's measure — a placeholder longer than the box shrinks to show whole (Lh 2026-09-25) */
40
+ .field textarea::placeholder { color: inherit; opacity: var(--fade-hint); font-size: calc(1em * var(--_ph-fit, 1)); } /* --_ph-fit: Field's measure — a placeholder longer than the box shrinks to show whole (Lh 2026-09-25) */
41
41
  /* several lines in the same frame (Field rows): the textarea is as tall as its `rows` × line, the frame grows round it, the icon and the action stay at the top (Lh 2026-10-19) */
42
42
  .field--rows { align-items: flex-start; padding-block: var(--field-rows-pad-y, var(--sp-3)); }
43
43
  .field textarea { flex: 1; resize: none; display: block; }
44
- .field--rows > .icon { margin-top: calc((1lh - var(--icon-sm)) / 2); } /* centred on the first line */
44
+ .field--rows > .icon { --icon-margin: calc((1lh - var(--icon-sm)) / 2) 0 0; } /* centred on the first line */
45
45
  /* the field the cursor is in: its own border goes full ink, in every palette (the coloured ones fade it to 25% at rest) */
46
46
  .field:has(:focus, [data-cursor]) { border-color: var(--field-active-border, currentColor); } /* [data-cursor]: SheetSteps holds the phone's keyboard for this field */
47
47
 
48
48
  /* field with its action inside: date pick, send, go — the button sits flush right */
49
49
  .field--action { padding-right: var(--sp-1); min-height: var(--field-action-min-h, var(--tap-lg)); }
50
- .field--action > .icon-btn { margin-left: auto; }
51
50
 
52
51
  /* colored palettes drop the visible outline elsewhere — fields keep one */
53
52
  [data-palette] .field:not(:has(:focus)) { border-color: color-mix(in srgb, currentColor 25%, transparent); }
@@ -72,22 +71,15 @@
72
71
  display: grid;
73
72
  place-items: center;
74
73
  font-size: var(--add-row-plus-font-size, var(--fs-md));
75
- line-height: 1;
74
+ line-height: var(--lh-solid);
76
75
  }
77
- .add-row__plus .icon { width: var(--icon-xs); height: var(--icon-xs); }
76
+ .add-row__plus .icon { --icon-size: var(--icon-xs); }
78
77
 
79
78
  /* ---- a wrong value, and the line that says so (Lh 2026-09-24) ----
80
79
  --signal-error is the one colour the wireframe has; it never speaks alone — the line carries an icon and words.
81
80
  <p class="form-note form-note--error" role="alert"><svg class="icon icon--xs">…</svg>Not a phone number</p>
82
81
  <p class="form-note form-note--busy" role="status"><svg …/>Sending the code…</p> (the ring turns) */
83
82
  .field.field--error, [data-palette] .field.field--error { border-color: var(--field-error-border, var(--signal-error)); }
84
- .pin.pin--error .pin__cell, .pin.pin--error .pin__cell:focus { border-color: var(--pin-error-border, var(--signal-error)); color: var(--pin-error-ink, var(--signal-error)); } /* ALL the cells, edge and digit — the one the cursor waits in too (Lh 2026-09-24); the wrong digits stay in red until the member types again */
85
- /* a code that does not match (Pin match): the row shakes once, sideways, then the Pin empties itself for a new try (Lh 2026-09-25) */
86
- @media (prefers-reduced-motion: no-preference) {
87
- .pin--shake { animation: pin-shake var(--pin-shake-time, 0.4s) ease-in-out; }
88
- }
89
- @keyframes pin-shake { 0%, 100% { translate: 0; } 20%, 60% { translate: calc(-1 * var(--pin-shake-x, var(--sp-2))); } 40%, 80% { translate: var(--pin-shake-x, var(--sp-2)); } }
90
83
  .form-note { display: flex; align-items: center; justify-content: center; gap: var(--sp-2); margin: 0; font: var(--form-note-font, var(--type-caption)); color: var(--form-note-ink, var(--card-muted)); text-align: center; }
91
- .form-note > .icon { flex: none; }
92
84
  .form-note--error { color: var(--form-note-error-ink, var(--signal-error)); }
93
- @media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { animation: turn 0.9s linear infinite; } } /* `turn`: tokens.css */
85
+ @media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { --icon-spin: turn var(--motion-spin) var(--ease-linear) infinite; } } /* `turn`: tokens.css */
package/css/journey.css CHANGED
@@ -22,7 +22,7 @@
22
22
  .route__time { grid-row: 3; align-self: start; }
23
23
 
24
24
  .route__code { font: var(--route-code-font, var(--type-h2)); letter-spacing: var(--track-h2); display: flex; align-items: center; gap: var(--route-code-gap, var(--sp-3)); }
25
- .route__code::after, .route__point--end .route__code::before { content: ""; flex: 1; border-top: var(--rule-w) dotted currentColor; opacity: 0.65; }
25
+ .route__code::after, .route__point--end .route__code::before { content: ""; flex: 1; border-top: var(--rule-w) dotted currentColor; opacity: var(--fade-mild); }
26
26
  .route__point--end .route__code::after { content: none; }
27
27
  .route__time { font: var(--route-time-font, var(--type-body)); font-variant-numeric: tabular-nums; }
28
28
  .route__place { font: var(--route-place-font, var(--type-caption)); color: var(--route-place-ink, var(--card-muted)); }
@@ -43,11 +43,11 @@
43
43
  /* the label is the badge's own ink, smaller: a badge is a small fill of any colour (a palette paints it yellow), and only its own ink is
44
44
  sure to hold 4.5:1 on it; --dim faded it to 2.4:1 */
45
45
  .tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); color: var(--tile-badge-label-ink, inherit); }
46
- .tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
46
+ .tile-badge__value { font: var(--fw-heavy) var(--fs-lg) / var(--lh-tight) var(--font-display); font-variant-numeric: tabular-nums; }
47
47
 
48
48
  /* plain: no fill — a date box beside a title (Jun / 18) */
49
49
  .tile-badge--plain { background: transparent; --dark: inherit; color: inherit; padding: 0; min-width: var(--tile-badge-plain-min-w, var(--tap-sm)); }
50
- .tile-badge--plain .tile-badge__value { font: 800 var(--fs-h1)/1 var(--font-display); }
50
+ .tile-badge--plain .tile-badge__value { font: var(--fw-heavy) var(--fs-h1) / var(--lh-solid) var(--font-display); }
51
51
 
52
52
  /* stretch: a date in a head (Lh 2026-10-19) — as tall as the title block beside it and no taller (the head stretches it; size
53
53
  containment keeps its own words from adding height). Its height picks the form: short (a one-line title) = two lines, the day name
@@ -63,7 +63,7 @@
63
63
  column-gap: var(--sp-1);
64
64
  row-gap: 0;
65
65
  padding-block: var(--sp-3);
66
- line-height: var(--tile-badge-line, 0.95);
66
+ line-height: var(--tile-badge-line, var(--lh-badge));
67
67
  }
68
68
  .tile-badge--stretch > * { line-height: inherit; }
69
69
  .tile-badge--stretch > :first-child { grid-column: 1 / -1; }
package/css/layover.css CHANGED
@@ -31,14 +31,14 @@
31
31
  sets open={false}).
32
32
  Reduced motion: the finished state, no beats; closing fires at once. */
33
33
  .modal {
34
- --drawer-h: 14rem;
34
+ --drawer-h: calc(var(--tap-base) * 14 / 3);
35
35
  position: absolute; /* at the stage's centre, and kept there as it grows: its middle is pinned at 50% and */
36
36
  top: 50%; /* translate -50% of its OWN height re-centres it on every height. NOT inset 0 + margin auto + */
37
37
  inset-inline: 0; /* align-self: center — Safari 18 ignores align-self on an absolute box in a block container, */
38
38
  margin-inline: auto; /* so the card stretched to the whole stage (iOS 18.6, Lh 2026-09-24); nor height: fit-content */
39
39
  translate: 0 -50%;
40
40
  width: calc(100% - 2 * var(--screen-pad));
41
- z-index: 2; /* over the dim stage's scrim (z 1) */
41
+ z-index: var(--z-over-2); /* over the dim stage's scrim (z 1) */
42
42
  max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) */
43
43
  padding: 0; /* the parts pad themselves */
44
44
  border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
@@ -83,18 +83,17 @@
83
83
  }
84
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
85
  .modal:not(:has(> .modal__lift)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
86
- .modal__drawer .t-caption { color: var(--modal-drawer-caption-ink, var(--card-muted)); }
86
+ .modal__drawer .t-caption { --t-caption-ink: var(--modal-drawer-caption-ink, var(--card-muted)); }
87
87
  .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
88
  /* 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
89
89
  line — left to its words' width the row squeezed two buttons into round blobs, their words on two lines. A quiet button is an outline only,
90
90
  so on the ask its edge is the ask's ink at full strength (light on the dark end, dark on --light). The BtnTrio of round buttons is not touched. */
91
- .modal__drawer > .btn-row { justify-self: stretch; margin-top: var(--modal-drawer-answers-gap, var(--sp-5)); white-space: nowrap; }
92
- .modal__drawer .btn--quiet { border-color: var(--modal-drawer-quiet-border, currentColor); }
93
- .modal__drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
94
- .modal__drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
91
+ .modal__drawer .btn--quiet { --btn-quiet-border: var(--modal-drawer-quiet-border, currentColor); }
92
+ .modal__drawer .icon-btn { --icon-btn-bg: transparent; --icon-btn-ink: currentColor; --icon-btn-border: var(--ink-rail); }
93
+ .modal__drawer .icon-btn--invert { --icon-btn-size: var(--tap-lg); }
95
94
  @media (prefers-reduced-motion: no-preference) {
96
- .modal { animation: modal-drop calc(2 * var(--motion-sheet)) ease both; }
97
- .modal__drawer { animation: modal-open calc(2 * var(--motion-sheet)) ease both; }
95
+ .modal { animation: modal-drop calc(2 * var(--motion-sheet)) var(--ease) both; }
96
+ .modal__drawer { animation: modal-open calc(2 * var(--motion-sheet)) var(--ease) both; }
98
97
  @keyframes modal-drop {
99
98
  0% { translate: 0 calc(-50% - 100cqh); } /* above the stage */
100
99
  50%, 100% { translate: 0 -50%; } /* 1. at the centre — and it stays: beat 2 is the drawer's */
@@ -109,8 +108,8 @@
109
108
  .btn-trio__dots {
110
109
  width: var(--btn-trio-dots-w, var(--sp-5));
111
110
  height: var(--btn-trio-dots-h, var(--sp-1));
112
- background: radial-gradient(circle, currentColor var(--sp-0), transparent calc(var(--sp-0) + 0.5px)) 0 0 / calc(var(--sp-5) / 2) 100% repeat-x;
113
- opacity: 0.4;
111
+ background: radial-gradient(circle, currentColor var(--sp-0), transparent calc(var(--sp-0) + var(--px) / 2)) 0 0 / calc(var(--sp-5) / 2) 100% repeat-x;
112
+ opacity: var(--fade-soft);
114
113
  }
115
114
 
116
115
  /* ---- popover menu: a card anchored to its button, rows of icon + label ----
@@ -133,7 +132,7 @@
133
132
  border-radius: var(--menu-radius, var(--r-card));
134
133
  --menu-pad: var(--sp-2); /* published: a row's radius is concentric with the panel's */
135
134
  padding: var(--menu-pad);
136
- min-width: 12rem;
135
+ min-width: calc(var(--tap-base) * 4);
137
136
  align-content: start; /* rows keep their own height: WebKit stretches a popover grid's rows to the whole room under its anchor (iOS 26.5, 2026-10-08) */
138
137
  --_menu-max: min(60dvh, calc(100dvh - 2 * var(--sp-5)));
139
138
  max-height: var(--menu-max-h, var(--_menu-max)); /* a long list (twenty neighbours) scrolls inside the panel — it ran off the screen's foot, the last names out of reach (timebank, 2026-10-08) */
@@ -167,7 +166,7 @@
167
166
  .menu[popover] {
168
167
  position-area: block-end span-inline-end;
169
168
  margin-top: var(--sp-2);
170
- min-width: max(12rem, anchor-size(width)); /* at least as wide as its trigger */
169
+ min-width: max(calc(var(--tap-base) * 4), anchor-size(width)); /* at least as wide as its trigger */
171
170
  position-try-fallbacks: flip-block, flip-inline;
172
171
  }
173
172
  .menu--end[popover] { position-area: block-end span-inline-start; }
@@ -200,11 +199,11 @@
200
199
  .banner__summary {
201
200
  list-style: none;
202
201
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
203
- padding-block: var(--banner-summary-pad-y, calc(var(--sp-base) * 1.5)); font: var(--banner-summary-font, var(--type-caption)); font-weight: 400;
202
+ padding-block: var(--banner-summary-pad-y, calc(var(--sp-base) * 1.5)); font: var(--banner-summary-font, var(--type-caption)); font-weight: var(--fw-regular);
204
203
  }
205
204
  .banner__summary::-webkit-details-marker { display: none; }
206
- .banner__summary .icon { transition: rotate var(--motion-quick) ease; }
207
- .banner[open] .banner__summary .icon { rotate: 180deg; }
205
+ .banner__summary .icon { --icon-motion: rotate var(--motion-quick) var(--ease); }
206
+ .banner[open] .banner__summary .icon { --icon-turn: 180deg; }
208
207
  /* the band's words never run past it: one column that cannot be wider than the band (a grid track is as wide as its longest
209
208
  word otherwise), and a word with no break in it — a link — breaks where it must (mailpool, Lh 2026-10-01) */
210
209
  .banner__body { display: grid; grid-template-columns: minmax(0, 1fr); overflow-wrap: anywhere; gap: var(--banner-body-gap, var(--sp-1)); padding-bottom: var(--sp-4); }
@@ -212,7 +211,7 @@
212
211
  @supports (interpolate-size: allow-keywords) {
213
212
  @media (prefers-reduced-motion: no-preference) {
214
213
  .banner { interpolate-size: allow-keywords; }
215
- .banner::details-content { block-size: 0; overflow: clip; transition: block-size var(--motion-quick) ease, content-visibility var(--motion-quick) allow-discrete; }
214
+ .banner::details-content { block-size: 0; overflow: clip; transition: block-size var(--motion-quick) var(--ease), content-visibility var(--motion-quick) allow-discrete; }
216
215
  .banner[open]::details-content { block-size: auto; }
217
216
  }
218
217
  }
@@ -271,7 +270,7 @@
271
270
  /* aria-current="false" is the correct way to mark the OTHER tabs, so the
272
271
  bare attribute selector lights every one of them up */
273
272
  .float-bar__item[aria-current]:not([aria-current="false"]) { background: var(--accent); color: var(--on-accent); }
274
- .float-bar > .icon-btn { width: var(--bar-h); height: var(--bar-h); box-shadow: var(--shadow-float); }
273
+ .float-bar > .icon-btn { --icon-btn-size: var(--bar-h); --icon-btn-shadow: var(--shadow-float); }
275
274
 
276
275
  /* ---- callout: a small card with a tail — over a map, a media, a mascot ----
277
276
  <div class="callout" style="top: 30%; left: 40%"><span class="callout__title">Telok Ayer Park</span><span class="callout__sub">1.2 km</span></div> */
@@ -280,7 +279,7 @@
280
279
  display: grid;
281
280
  gap: var(--callout-gap, var(--sp-0));
282
281
  width: max-content;
283
- max-width: 15rem;
282
+ max-width: calc(var(--tap-base) * 5);
284
283
  padding: var(--callout-pad-y, var(--sp-3)) var(--callout-pad-x, var(--sp-4));
285
284
  border: var(--border-w) solid var(--callout-border, var(--action-border));
286
285
  border-radius: var(--callout-radius, var(--r-card));
@@ -313,7 +312,7 @@
313
312
  <p class="splash__text">…</p></div></section>
314
313
  The group's MIDDLE is on --screen-centre (40% down), the same line a centred card uses. The mark is the app's own content —
315
314
  a lottie box often carries empty room around its drawing — the app sets the knobs below, cardds does not guess it. */
316
- .splash { position: relative; background: var(--splash-bg, var(--surface)); transition: opacity var(--motion-sheet) ease; }
315
+ .splash { position: relative; background: var(--splash-bg, var(--surface)); transition: opacity var(--motion-sheet) var(--ease); }
317
316
  .splash--leaving { opacity: 0; }
318
317
  .splash__group {
319
318
  position: absolute; inset-inline: var(--screen-pad); top: var(--splash-at, var(--screen-centre)); translate: 0 -50%;
@@ -335,13 +334,13 @@
335
334
  position: fixed; inset-inline: 0; top: calc(var(--sp-3) + var(--safe-top));
336
335
  max-width: var(--screen-w); margin-inline: auto; padding-inline: var(--screen-pad, var(--sp-4));
337
336
  display: grid; justify-items: center;
338
- z-index: calc(var(--z-top) + 1);
337
+ z-index: calc(var(--z-top) + var(--z-over));
339
338
  pointer-events: none; visibility: hidden;
340
339
  translate: 0 calc(var(--_out) * (100% + var(--sp-8) + var(--safe-top)));
341
- transition: translate var(--motion-sheet) ease, visibility 0s var(--motion-sheet);
340
+ transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--motion-sheet);
342
341
  }
343
342
  .screen--fill > .snackbar, .sheet-stage > .snackbar { position: absolute; }
344
- .snackbar--bottom { --_out: 1; top: auto; bottom: calc(var(--sp-3) + var(--bottom-h, 0px) + max(var(--safe-bottom), var(--kb, 0px))); }
343
+ .snackbar--bottom { --_out: 1; top: auto; bottom: calc(var(--sp-3) + var(--bottom-h, 0rem) + max(var(--safe-bottom), var(--kb, 0rem))); }
345
344
  .snackbar--open { translate: 0; visibility: visible; transition-delay: 0s; }
346
345
  .snackbar__body {
347
346
  display: flex; align-items: center; gap: var(--snackbar-gap, var(--sp-3));
@@ -356,8 +355,11 @@
356
355
  font: inherit; text-align: start;
357
356
  pointer-events: auto;
358
357
  }
359
- .snackbar__body > .icon { flex: none; color: var(--snackbar-icon-ink, var(--signal-ok)); }
358
+ .snackbar__body > .icon { --icon-ink: var(--snackbar-icon-ink, var(--signal-ok)); }
360
359
  .snackbar__words { display: grid; min-width: 0; overflow-wrap: anywhere; }
361
360
  .snackbar__text { font: var(--snackbar-text-font, var(--type-body)); }
362
361
  .snackbar__caption { font: var(--snackbar-caption-font, var(--type-caption)); color: var(--snackbar-caption-ink, inherit); } /* on the signal's soft ground the full ink: a muted shade there fell under AA */
363
362
  @media (prefers-reduced-motion: reduce) { .snackbar { transition: none; } }
363
+
364
+ /* ---- where it sits in what actions.css draws (docs/reference.md rule 15) ---- */
365
+ .dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards (= .menu--end, layover.css) */
package/css/lists.css CHANGED
@@ -34,15 +34,15 @@
34
34
  .row__meta { font: var(--row-meta-font, var(--type-caption)); color: var(--row-meta-ink, var(--card-muted)); display: flex; align-items: center; gap: var(--row-meta-gap, var(--sp-1)); }
35
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
36
  .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: 700 var(--fs-lg)/1.2 var(--font-display); }
37
+ .row--lg .row__title { font: var(--fw-bold) var(--fs-lg) / var(--lh-h1) var(--font-display); }
38
38
  /* a row that EXPANDS (Lh 2026-09-22): the row is a <details>' summary, what it shows comes out under it —
39
39
  who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
40
40
  <details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
41
41
  .row-expand { display: block; }
42
42
  .row-expand > summary { list-style: none; }
43
43
  .row-expand > summary::-webkit-details-marker { display: none; }
44
- .row-expand > summary > .icon:last-child { transition: rotate var(--motion-slide); }
45
- .row-expand[open] > summary > .icon:last-child { rotate: 180deg; }
44
+ .row-expand > summary > .icon:last-child { --icon-motion: rotate var(--motion-slide); }
45
+ .row-expand[open] > summary > .icon:last-child { --icon-turn: 180deg; }
46
46
  .row-expand__more { padding-top: var(--row-expand-gap, var(--sp-3)); }
47
47
  /* a finished item in a checklist (a to-do list, an onboarding "complete your introduction" list):
48
48
  the words go muted, a filled tick sits at the end, and the row is no longer a tap target —
@@ -91,7 +91,7 @@
91
91
  .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
92
  .kv-grid > .kv-cell + .kv-cell { border-left: var(--border-w) solid var(--hairline); padding-left: var(--sp-4); }
93
93
  .kv-cell { display: grid; gap: var(--kv-cell-gap, var(--sp-0)); align-content: start; min-width: 0; }
94
- .kv-cell > .icon { margin-bottom: var(--sp-1); }
94
+ .kv-cell > .icon { --icon-margin: 0 0 var(--sp-1); }
95
95
  .kv-cell__val { font: var(--kv-cell-val-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
96
96
  .kv-cell__key { font: var(--kv-cell-key-font, var(--type-caption)); color: var(--kv-cell-key-ink, var(--card-muted)); }
97
97
 
@@ -136,7 +136,7 @@
136
136
  }
137
137
  .bubble__meta { display: flex; align-items: center; gap: var(--bubble-meta-gap, var(--sp-1)); font: var(--bubble-meta-font, var(--type-caption)); color: var(--bubble-meta-ink, var(--card-muted)); }
138
138
  .bubble--right > .bubble__meta { justify-self: end; }
139
- .bubble__meta > .icon { color: var(--bubble-meta-icon-ink, var(--accent-ink)); }
139
+ .bubble__meta > .icon { --icon-ink: var(--bubble-meta-icon-ink, var(--accent-ink)); }
140
140
 
141
141
  /* ---- timeline: a hairline with date chips, entries to the right ----
142
142
  <ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
@@ -207,5 +207,7 @@
207
207
  .bullets { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--bullets-gap, var(--sp-3)); font: var(--bullets-font, var(--type-body)); }
208
208
  .bullets__item { display: flex; align-items: start; gap: var(--bullets-pic-gap, var(--sp-3)); min-width: 0; }
209
209
  .bullets__pic { flex: none; inline-size: var(--bullets-pic-size, 1lh); block-size: var(--bullets-pic-size, 1lh); min-height: 0; object-fit: contain; border-radius: var(--bullets-pic-radius, 0rem); display: grid; place-items: center; }
210
- .bullets__pic.ph { border-radius: var(--bullets-pic-radius, 0rem); }
211
210
  .bullets__text { flex: 1; min-width: 0; }
211
+
212
+ /* ---- where it sits in what sheet.css draws (docs/reference.md rule 15) ---- */
213
+ .sheet-step__inner > .link { justify-self: center; } /* a link of its own row in a step (skip, "change number", "set PIN again"): centred under the step, as the skip (Lh 2026-09-25) */