@usableapp/cardds 0.7.16 → 0.7.17

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 (63) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +23 -20
  3. package/cardds.css +3 -3
  4. package/css/actions.css +60 -38
  5. package/css/base.css +43 -17
  6. package/css/card.css +18 -13
  7. package/css/choice.css +21 -33
  8. package/css/forms.css +0 -3
  9. package/css/journey.css +0 -2
  10. package/css/layover.css +14 -24
  11. package/css/lists.css +1 -6
  12. package/css/media.css +7 -15
  13. package/css/numbers.css +5 -21
  14. package/css/people.css +1 -8
  15. package/css/sheet.css +12 -14
  16. package/css/stack.css +17 -21
  17. package/css/step.css +5 -8
  18. package/css/tokens.css +19 -4
  19. package/dist/actions/Btn.d.ts +1 -1
  20. package/dist/actions/Dropdown.js +1 -6
  21. package/dist/actions/Fab.d.ts +1 -1
  22. package/dist/actions/Fab.js +3 -2
  23. package/dist/cardds.css +228 -232
  24. package/dist/cards/Card.d.ts +23 -7
  25. package/dist/cards/Card.js +23 -8
  26. package/dist/cards/CardHead.d.ts +0 -1
  27. package/dist/cards/CardHead.js +1 -1
  28. package/dist/choice/Calendar.js +1 -2
  29. package/dist/choice/Mood.js +2 -2
  30. package/dist/forms/Field.js +2 -3
  31. package/dist/forms/FileBtn.js +1 -1
  32. package/dist/forms/Pin.js +6 -10
  33. package/dist/index.js +2 -2
  34. package/dist/layover/Banner.d.ts +10 -3
  35. package/dist/layover/Banner.js +6 -3
  36. package/dist/lists/Row.js +3 -3
  37. package/dist/media/Postcard.js +2 -2
  38. package/dist/numbers/Band.d.ts +3 -1
  39. package/dist/numbers/Picker.js +1 -7
  40. package/dist/scaffold/ActionBar.d.ts +3 -3
  41. package/dist/scaffold/ActionBar.js +2 -2
  42. package/dist/scaffold/Centre.d.ts +1 -1
  43. package/dist/scaffold/Centre.js +1 -1
  44. package/dist/scaffold/PagerAt.d.ts +1 -1
  45. package/dist/scaffold/PagerAt.js +1 -1
  46. package/dist/scaffold/Screen.js +2 -4
  47. package/dist/sheets/Drawer.js +3 -4
  48. package/dist/sheets/Sheet.js +8 -19
  49. package/dist/sheets/SheetStack.d.ts +2 -2
  50. package/dist/sheets/SheetStack.js +18 -10
  51. package/dist/sheets/SheetSteps.js +5 -7
  52. package/dist/sheets/StackBack.js +2 -1
  53. package/dist/sheets/StackSheet.d.ts +3 -2
  54. package/dist/sheets/StackSheet.js +6 -9
  55. package/dist/state.d.ts +19 -2
  56. package/dist/state.js +47 -7
  57. package/dist/templates/CallSheet.js +1 -2
  58. package/dist/templates/Home.d.ts +3 -2
  59. package/dist/templates/Home.js +1 -2
  60. package/dist/templates/Splash.js +2 -3
  61. package/dist/templates/Walkthrough.js +2 -2
  62. package/dist/type/Text.d.ts +1 -1
  63. package/package.json +7 -4
package/css/choice.css CHANGED
@@ -21,7 +21,6 @@
21
21
  background: var(--check-bg, var(--action-bg));
22
22
  display: grid;
23
23
  place-items: center;
24
- cursor: pointer;
25
24
  }
26
25
  .check::after {
27
26
  content: "";
@@ -34,15 +33,14 @@
34
33
  }
35
34
  .check:checked { background: var(--accent); border-color: transparent; }
36
35
  .check:checked::after { opacity: 1; }
37
- .check:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
38
36
  .check--sm { width: var(--check-sm-size, var(--tap-sm)); height: var(--check-sm-size, var(--tap-sm)); }
39
37
 
40
38
  /* ---- toggle: big switch ----
41
39
  <input type="checkbox" role="switch" class="toggle" aria-label="Private"> */
42
40
  .toggle {
43
41
  --_w: var(--bar-h); /* 64 × 36, knob = height − 2 × pad */
44
- --_h: 2.25rem;
45
- --_pad: 0.1875rem;
42
+ --_h: calc(var(--tap-base) * 3 / 4);
43
+ --_pad: calc(var(--tap-base) / 16);
46
44
  appearance: none;
47
45
  width: var(--_w);
48
46
  height: var(--_h);
@@ -52,8 +50,7 @@
52
50
  border: var(--border-w) solid var(--toggle-border, var(--outline));
53
51
  background: var(--toggle-bg, var(--action-bg));
54
52
  position: relative;
55
- cursor: pointer;
56
- transition: background 0.2s ease;
53
+ transition: background-color var(--motion-press) ease-out, border-color var(--motion-press) ease-out;
57
54
  }
58
55
  .toggle::after {
59
56
  content: "";
@@ -65,11 +62,10 @@
65
62
  border-radius: 50%;
66
63
  background: currentColor;
67
64
  opacity: 0.45;
68
- transition: translate 0.2s ease, opacity 0.2s ease;
65
+ transition: translate var(--motion-quick) ease, opacity var(--motion-quick) ease;
69
66
  }
70
67
  .toggle:checked { background: var(--accent); border-color: transparent; }
71
68
  .toggle:checked::after { translate: calc(var(--_w) - var(--_h)) 0; background: var(--on-accent); opacity: 1; }
72
- .toggle:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
73
69
 
74
70
  /* ---- chip pick: a chip that is a choice — a native radio/checkbox inside the label, the picked one filled with accent.
75
71
  Full-width in a .chip-grid (one slot / many options), or hugging its word on a .chip-row with --sign (an interest
@@ -85,11 +81,12 @@
85
81
  chip grid the whole point is equal, full-width pick targets, so stretch.
86
82
  Matters most in scripts without word spaces (Thai): min-content is the
87
83
  whole string, tracks get wide, and hugging chips leave a ragged grid. */
88
- .chip-grid > .chip { justify-self: stretch; justify-content: center; min-height: var(--btn-h); padding-inline: var(--sp-2); position: relative; }
89
- .chip--pick { cursor: pointer; position: relative; } /* the input covers the whole chip, on a row as in a grid */
90
- .chip--pick > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
91
- .chip--pick:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
92
- .chip--pick:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
84
+ .chip-grid > .chip { justify-self: stretch; justify-content: center; min-height: var(--btn-h); padding-inline: var(--sp-2); }
85
+ .chip--pick { position: relative; } /* the input covers the whole chip, on a row as in a grid */
86
+ /* the hidden input, the same shape wherever a native radio/checkbox drives a picked look:
87
+ a chip, a mood dot, a tile (media.css) */
88
+ :is(.chip--pick, .mood__opt, .tile--pick) > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
89
+ :is(.chip--pick, .tile--pick):has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
93
90
  /* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
94
91
  .chip--sign { padding-right: var(--sp-2); }
95
92
  .chip--sign::after {
@@ -122,11 +119,8 @@
122
119
  padding-block: var(--day-pad-y, var(--sp-3)); /* equal air top and bottom, so the selected pill
123
120
  is the same shape on every day */
124
121
  padding-inline: 0;
125
- border: 0;
126
122
  border-radius: var(--day-radius, var(--r-chip));
127
- background: transparent;
128
- color: inherit;
129
- opacity: 0.6;
123
+ opacity: var(--rest);
130
124
  }
131
125
  .day__name { font: var(--day-name-font, var(--type-caption)); }
132
126
  .day__num { font: 700 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
@@ -139,17 +133,13 @@
139
133
  .calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--calendar-head-gap, var(--sp-2)); }
140
134
  .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; }
141
135
  .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)); }
142
- .card--3 .calendar__wd { color: inherit; opacity: var(--dim); }
143
136
  .calendar__day {
144
137
  width: 100%;
145
138
  max-width: var(--calendar-day-max-w, var(--tap));
146
139
  aspect-ratio: 1;
147
140
  display: grid;
148
141
  place-items: center;
149
- border: 0;
150
142
  border-radius: 50%;
151
- background: transparent;
152
- color: inherit;
153
143
  font: 500 var(--fs-title)/1 var(--font-ui);
154
144
  font-variant-numeric: tabular-nums;
155
145
  grid-column-start: var(--start, auto); /* --start on the 1st: weekday column 1–7 */
@@ -172,8 +162,7 @@
172
162
  display: flex;
173
163
  justify-content: space-between;
174
164
  }
175
- .mood__opt { flex: 1 1 0; max-width: var(--mood-opt-max-w, var(--tap)); min-width: 0; height: var(--mood-opt-h, var(--tap)); display: grid; place-items: center; position: relative; cursor: pointer; }
176
- .mood__opt > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
165
+ .mood__opt { flex: 1 1 0; max-width: var(--mood-opt-max-w, var(--tap)); min-width: 0; height: var(--mood-opt-h, var(--tap)); display: grid; place-items: center; position: relative; }
177
166
  .mood__dot {
178
167
  width: var(--mood-dot-size, var(--icon-xs));
179
168
  height: var(--mood-dot-size, var(--icon-xs));
@@ -204,12 +193,11 @@
204
193
  text-align: center;
205
194
  font: var(--pin-cell-font, var(--type-stat));
206
195
  font-variant-numeric: tabular-nums;
196
+ caret-color: transparent; /* the cell the cursor waits in (Lh 2026-09-22): NO outline ring — it sat outside the pill and got cropped by the sheet's
197
+ edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
198
+ is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
207
199
  }
208
200
  .pin__cell:not(:placeholder-shown) { color: var(--pin-cell-filled-ink, var(--accent)); } /* a typed digit: coloured words on the cell's own ground — no fill (Lh 2026-09-24; was an accent-filled pill) */
209
- /* the cell the cursor waits in (Lh 2026-09-22): NO outline ring — it sat outside the pill and got cropped by the sheet's
210
- edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
211
- is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
212
- .pin__cell { caret-color: transparent; }
213
201
  .pin--secret .pin__cell { -webkit-text-security: disc; text-security: disc; } /* a PIN: a dot per digit, the number pad kept (Lh 2026-09-25) */
214
202
  .pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
215
203
  .pin__cell:focus:placeholder-shown {
@@ -239,10 +227,9 @@
239
227
  letter-spacing: var(--track-h2);
240
228
  outline: none;
241
229
  }
242
- .composer__input::placeholder { color: inherit; opacity: 0.4; }
230
+ .composer__input::placeholder { color: inherit; opacity: var(--off); }
243
231
  .composer__bar { display: flex; align-items: center; justify-content: space-between; gap: var(--composer-bar-gap, var(--sp-3)); }
244
232
  .composer__hint { font: var(--composer-hint-font, var(--type-caption)); color: var(--composer-hint-ink, var(--card-muted)); }
245
- .card--3 .composer__hint { color: inherit; opacity: var(--dim); }
246
233
 
247
234
  /* ---- slider: one value along a track, a thumb-size knob ----
248
235
  <label class="slider"><span class="slider__label">ขนาดตัวหนังสือ</span><input class="slider__range" type="range" min="100" max="160" step="15" aria-label="ขนาดตัวหนังสือ"></label>
@@ -253,8 +240,8 @@
253
240
  .slider { display: grid; gap: var(--slider-gap, var(--sp-1)); flex: 1; min-width: 0; position: relative; } /* relative: the glyph rides the knob */
254
241
  /* ---- the sample glyph: one letter IN the knob, on-accent, at the type size
255
242
  the slider sets — so a text-size slider shows the size before anything is
256
- read (Lh, 2026-09-12). cardds.js publishes the knob's place on the .slider
257
- as --slider-at (0..1) from the input's value; the glyph sits over the knob
243
+ read (Lh, 2026-09-12). The Slider component publishes the knob's place on
244
+ the .slider as --slider-at (0..1) from the input's value; the glyph sits over the knob
258
245
  and never takes the pointer. <span class="slider__glyph" aria-hidden="true">ก</span>
259
246
  before the input. */
260
247
  .slider__glyph {
@@ -304,6 +291,8 @@
304
291
  box-shadow: var(--shadow-float);
305
292
  }
306
293
  .slider__range:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
294
+ .slider__range:active::-webkit-slider-thumb { scale: var(--press-scale); } /* the knob gives under the finger like any control */
295
+ .slider__range:active::-moz-range-thumb { scale: var(--press-scale); }
307
296
 
308
297
  /* ---- a switch that names itself (Toggle text, Lh 2026-09-23 — "อยู่บ้าน" on the home's top): the words, then the switch,
309
298
  one label — the whole of it is the tap target ---- */
@@ -311,7 +300,6 @@
311
300
  display: inline-flex; align-items: center; gap: var(--toggle-field-gap, var(--sp-2));
312
301
  min-height: var(--tap);
313
302
  font: var(--toggle-field-font, var(--type-label));
314
- cursor: pointer;
315
- -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent;
303
+ -webkit-user-select: none; user-select: none;
316
304
  flex: none;
317
305
  }
package/css/forms.css CHANGED
@@ -44,9 +44,6 @@
44
44
  display: inline-flex;
45
45
  align-items: center;
46
46
  gap: var(--add-row-gap, var(--sp-2));
47
- border: 0;
48
- background: none;
49
- color: inherit;
50
47
  font: var(--add-row-font, var(--type-label));
51
48
  font-size: var(--add-row-font-size, var(--fs-body));
52
49
  padding: var(--add-row-pad-y, var(--sp-2)) var(--add-row-pad-x, 0);
package/css/journey.css CHANGED
@@ -26,7 +26,6 @@
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)); }
29
- .card--3 .route__place { color: inherit; opacity: var(--dim); }
30
29
 
31
30
  .route__via { grid-column: 2; grid-row: 2; align-self: center; display: flex; align-items: center; opacity: 0.65; font-size: var(--route-via-font-size, var(--fs-body)); }
32
31
 
@@ -58,4 +57,3 @@
58
57
  font: var(--note-row-font, var(--type-caption));
59
58
  color: var(--note-row-ink, var(--card-muted));
60
59
  }
61
- .card--3 .note-row { color: inherit; opacity: var(--dim); }
package/css/layover.css CHANGED
@@ -24,10 +24,11 @@
24
24
  2. the drawer comes out of the card's bottom edge while the lift moves
25
25
  up, both at once: the card grows from its centre, which never moves
26
26
  (the modal centres itself — absolute, margin auto; a growing card keeps its middle where it is)
27
- Closing: a click on any button in the drawer plays the same animations in
28
- reverse (cardds.js: Animation.reverse() on each — nothing duplicated), then
29
- fires "cardds:modal" on the .sheet-stage (bubbles, detail.answer from the
30
- button's data-answer) for the app to remove the stage.
27
+ Closing: <Modal open onClosed> — set open={false} and the Modal component plays the
28
+ same animations in reverse (Animation.reverse() on each — nothing duplicated), then
29
+ calls onClosed for the app to unmount the stage. Modal no longer knows its own
30
+ buttons (data-answer / the "cardds:modal" event are gone; the app's own onClick
31
+ sets open={false}).
31
32
  Reduced motion: the finished state, no beats; closing fires at once. */
32
33
  .modal {
33
34
  --drawer-h: 14rem;
@@ -106,8 +107,8 @@
106
107
  element the button belongs to (a card, a sheet), opened by an .icon-btn:
107
108
  <button class="icon-btn icon-btn--sm" popovertarget="m1" aria-label="More">…</button>
108
109
  <div class="menu menu--actions menu--end" id="m1" popover><button class="menu__item"><svg class="icon"/>Edit</button>…</div>
109
- No item stays current there (cardds.js skips the mark). .menu--end = the
110
- trigger sits at the far right, so the panel grows leftwards. */
110
+ No item stays current there — an actions menu's items carry no aria-current, unlike
111
+ the dropdown's. .menu--end = the trigger sits at the far right, so the panel grows leftwards. */
111
112
  .menu {
112
113
  display: none; /* explicit closed state — never display on the base rule */
113
114
  margin: 0;
@@ -133,15 +134,11 @@
133
134
  min-height: var(--menu-item-min-h, var(--tap));
134
135
  width: 100%;
135
136
  padding: var(--menu-item-pad-y, 0) var(--menu-item-pad-x, var(--sp-3));
136
- border: 0;
137
137
  /* concentric with the panel: the card radius minus the panel's padding, so the
138
138
  highlight's curve follows the outer corner (Lh 2026-09-16) */
139
139
  border-radius: calc(var(--r-card) - var(--menu-pad, var(--sp-2)));
140
- background: transparent;
141
- color: inherit;
142
140
  font: var(--menu-item-font, var(--type-title));
143
141
  text-align: left;
144
- cursor: pointer;
145
142
  }
146
143
  .menu__item:hover, .menu__item:focus-visible { background: var(--ink-tint); outline: none; }
147
144
  .menu__item[aria-current] { background: var(--accent); color: var(--on-accent); }
@@ -158,7 +155,7 @@
158
155
  .menu[popover] {
159
156
  opacity: 0;
160
157
  translate: 0 calc(-1 * var(--sp-2));
161
- transition: display 0.2s allow-discrete, overlay 0.2s allow-discrete, opacity 0.2s, translate 0.2s;
158
+ transition: display var(--motion-quick) allow-discrete, overlay var(--motion-quick) allow-discrete, opacity var(--motion-quick), translate var(--motion-quick);
162
159
  }
163
160
  .menu:popover-open { opacity: 1; translate: 0 0; }
164
161
  .menu.\:popover-open { opacity: 1; translate: 0 0; }
@@ -179,19 +176,19 @@
179
176
  padding-inline: var(--card-pad, var(--screen-pad));
180
177
  }
181
178
  .banner__summary {
182
- list-style: none; cursor: pointer;
179
+ list-style: none;
183
180
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
184
181
  padding-block: var(--banner-summary-pad-y, calc(var(--sp-base) * 1.5)); font: var(--banner-summary-font, var(--type-caption)); font-weight: 400;
185
182
  }
186
183
  .banner__summary::-webkit-details-marker { display: none; }
187
- .banner__summary .icon { transition: rotate 0.2s ease; }
184
+ .banner__summary .icon { transition: rotate var(--motion-quick) ease; }
188
185
  .banner[open] .banner__summary .icon { rotate: 180deg; }
189
186
  .banner__body { display: grid; gap: var(--banner-body-gap, var(--sp-1)); padding-bottom: var(--sp-4); }
190
187
  .banner::details-content { width: 100%; }
191
188
  @supports (interpolate-size: allow-keywords) {
192
189
  @media (prefers-reduced-motion: no-preference) {
193
190
  .banner { interpolate-size: allow-keywords; }
194
- .banner::details-content { block-size: 0; overflow: clip; transition: block-size 0.25s ease, content-visibility 0.25s allow-discrete; }
191
+ .banner::details-content { block-size: 0; overflow: clip; transition: block-size var(--motion-quick) ease, content-visibility var(--motion-quick) allow-discrete; }
195
192
  .banner[open]::details-content { block-size: auto; }
196
193
  }
197
194
  }
@@ -208,16 +205,12 @@
208
205
  display: flex;
209
206
  gap: var(--float-bar-gap, var(--sp-2));
210
207
  align-items: center;
211
- z-index: 30;
208
+ z-index: var(--z-bar);
212
209
  }
213
210
  /* in a fill screen (its `bottom`) the float bar floats over the SCREEN's foot, not the window's — it lies over the content
214
211
  (a map, a stack), so it is not a row of the column; the screen is its box (Lh 2026-09-23) */
215
212
  .screen--fill > .float-bar { position: absolute; }
216
- .sheet-stage > .float-bar { position: absolute; padding-inline: var(--sp-4); }
217
- /* .screen's default bottom padding clears an .action-bar; a .float-bar is
218
- taller (--bar-h + its own bottom offset), so reserve the difference. Any
219
- parent that hosts the bar counts (see .action-bar's reserve, actions.css) */
220
- :has(> .float-bar) .screen:not(.screen--fill) { padding-bottom: calc(var(--bar-h) + var(--sp-5) + var(--sp-6)); }
213
+ .sheet-stage > .float-bar { position: absolute; } /* padding-inline removed 2026-09-27: it hard-coded --sp-4 over the --float-bar-pad-x knob, so the knob had no effect here */
221
214
  .float-bar__pill {
222
215
  flex: 1;
223
216
  min-width: 0;
@@ -242,13 +235,10 @@
242
235
  gap: var(--float-bar-item-gap, var(--sp-0));
243
236
  min-height: var(--float-bar-item-min-h, var(--tap-lg));
244
237
  padding: var(--float-bar-item-pad, var(--sp-1));
245
- border: 0;
246
238
  border-radius: var(--float-bar-item-radius, var(--r-chip));
247
- background: transparent;
248
- color: inherit;
249
239
  font: var(--float-bar-item-font, var(--type-caption));
250
240
  text-decoration: none;
251
- opacity: 0.6;
241
+ opacity: var(--rest);
252
242
  overflow: hidden;
253
243
  white-space: nowrap;
254
244
  text-overflow: ellipsis;
package/css/lists.css CHANGED
@@ -40,7 +40,7 @@
40
40
  who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
41
41
  <details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
42
42
  .row-expand { display: block; }
43
- .row-expand > summary { list-style: none; cursor: pointer; }
43
+ .row-expand > summary { list-style: none; }
44
44
  .row-expand > summary::-webkit-details-marker { display: none; }
45
45
  .row-expand > summary > .icon:last-child { transition: rotate var(--motion-slide); }
46
46
  .row-expand[open] > summary > .icon:last-child { rotate: 180deg; }
@@ -192,14 +192,9 @@
192
192
  align-items: center;
193
193
  gap: var(--link-gap, var(--sp-1));
194
194
  min-height: var(--link-min-h, var(--tap-sm));
195
- color: inherit;
196
195
  text-decoration: none;
197
196
  font: var(--link-font, var(--type-label));
198
197
  font-size: var(--link-font-size, var(--fs-body));
199
- border: 0;
200
- background: transparent;
201
- padding: 0;
202
- cursor: pointer;
203
198
  }
204
199
  /* a SMALL link (Lh 2026-09-23): a thing tapped rarely — "details", a ctaLink — at the minor-link size (15) and 28px of height;
205
200
  its 44px tap target stays, an invisible ::after around it */
package/css/media.css CHANGED
@@ -13,7 +13,7 @@
13
13
  --card-ink: var(--card-3-ink);
14
14
  --accent: var(--card-3-ink);
15
15
  --on-accent: var(--card-3-bg);
16
- min-height: 22.5rem;
16
+ min-height: var(--_cover-min-h);
17
17
  align-content: space-between;
18
18
  overflow: hidden;
19
19
  isolation: isolate;
@@ -53,7 +53,6 @@
53
53
  .quote__text { margin: 0; font: var(--quote-text-font, var(--type-h1)); letter-spacing: var(--track-h1); text-wrap: balance; }
54
54
  .quote--sm .quote__text { font: var(--type-h2); letter-spacing: var(--track-h2); }
55
55
  .quote__by { font: var(--quote-by-font, var(--type-caption)); color: var(--quote-by-ink, var(--card-muted)); }
56
- .card--3 .quote__by { color: inherit; opacity: var(--dim); }
57
56
 
58
57
  /* ---- tile grid: big square tiles — icon top, label bottom ----
59
58
  <div class="tile-grid"><button class="tile"><svg class="icon"/><span class="tile__label">Light</span></button>
@@ -85,13 +84,10 @@
85
84
  .tile__sub { font: var(--tile-sub-font, var(--type-caption)); opacity: var(--dim); }
86
85
  .tile__text { display: grid; gap: var(--tile-text-gap, var(--sp-0)); }
87
86
  .tile--on { background: var(--tile-on-bg, var(--accent)); color: var(--tile-on-ink, var(--on-accent)); border-color: transparent; }
88
- .tile--pick > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
89
- .tile--pick:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
90
- .tile--pick:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
87
+ /* .tile--pick's hidden input and :has(:checked) fill are declared once in choice.css, alongside .chip--pick and .mood__opt */
91
88
  .tile--wide { grid-column: 1 / -1; }
92
89
  /* 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 */
93
- .tile--stat { border-color: transparent; background: none; padding-inline: 0; }
94
- .tile--stat { 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; }
90
+ .tile--stat { 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; }
95
91
  /* upload: dashed, centred plus + label */
96
92
  .tile--upload {
97
93
  border-style: dashed;
@@ -168,8 +164,8 @@
168
164
  overflow: hidden;
169
165
  }
170
166
  .mosaic > :first-child { grid-row: span 2; }
171
- .mosaic > * { min-height: 0; border-radius: 0; opacity: 1; border-width: 0; }
172
- .mosaic > .ph { opacity: 0.4; border-width: var(--border-w); }
167
+ .mosaic > * { min-height: 0; border-radius: 0; } /* opacity/border-width here were no-ops: .ph restates its own below, and nothing else in a mosaic carries either by default */
168
+ .mosaic > .ph { opacity: var(--off); border-width: var(--border-w); }
173
169
  .mosaic > img { width: 100%; height: 100%; object-fit: cover; }
174
170
 
175
171
  /* ---- postcard: a member's introduction in the shape of a cover card — the photo fills it,
@@ -198,7 +194,7 @@
198
194
  liked it, a voice note, a reply live OUTSIDE the postcard, in a Row under it. */
199
195
  .postcard {
200
196
  position: relative;
201
- min-height: var(--postcard-min-h, 22.5rem);
197
+ min-height: var(--postcard-min-h, var(--_cover-min-h));
202
198
  perspective: 60rem;
203
199
  border-radius: var(--postcard-radius, var(--r-card));
204
200
  isolation: isolate;
@@ -224,14 +220,10 @@
224
220
  display: grid;
225
221
  align-content: start;
226
222
  padding: var(--postcard-face-pad, var(--card-pad-rest));
227
- border: 0;
228
223
  border-radius: inherit;
229
- background: transparent;
230
- color: inherit;
231
224
  text-align: left;
232
- cursor: pointer;
233
225
  }
234
- .postcard__turn:focus-visible { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
226
+ .postcard__turn:focus-visible { outline-offset: calc(-1 * var(--sp-1)); } /* the ring inside the card's edge */
235
227
  /* the front: the photo (or the wireframe box) fills it; the band reads on a scrim */
236
228
  .postcard__front { background: var(--postcard-front-bg, var(--card-3-bg)); color: var(--postcard-front-ink, var(--card-3-ink)); }
237
229
  .postcard__front .postcard__turn > img,
package/css/numbers.css CHANGED
@@ -33,7 +33,6 @@
33
33
  }
34
34
  .ring__value { font: var(--ring-value-font, var(--type-stat)); letter-spacing: var(--track-display); }
35
35
  .ring__label { font: var(--ring-label-font, var(--type-caption)); color: var(--ring-label-ink, var(--card-muted)); }
36
- .card--3 .ring__label { color: inherit; opacity: var(--dim); }
37
36
  .ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
38
37
  .ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
39
38
  .ring--xs { --_size: var(--tap-sm); --_w: var(--dot-xs); } /* inline counter (step 3 of 5) */
@@ -72,7 +71,7 @@
72
71
  content: "";
73
72
  flex: 1;
74
73
  height: var(--sp-3);
75
- background: color-mix(in srgb, var(--_ink) 30%, transparent);
74
+ background: var(--ink-rail); /* the track's own ink at 30% — identical to color-mix(in srgb, var(--_ink) 30%, transparent): --ink-rail already reads currentColor, and .track sets color to --_ink */
76
75
  }
77
76
  .track__step:first-child::before { display: none; }
78
77
  .track__step.is-done::before { background: var(--accent); }
@@ -83,7 +82,7 @@
83
82
  flex: none;
84
83
  padding: calc((var(--tap-lg) - var(--icon)) / 2);
85
84
  border-radius: 50%;
86
- background: color-mix(in srgb, var(--_ink) 30%, transparent);
85
+ background: var(--ink-rail);
87
86
  color: var(--_ink);
88
87
  }
89
88
  .track__step > .track__dot { display: grid; place-items: center; padding: 0; font: var(--track-dot-font, var(--type-title)); font-variant-numeric: tabular-nums; } /* a number in the round (TrackStep num) */
@@ -108,7 +107,6 @@
108
107
  }
109
108
  .band__num small { font-size: 0.5em; font-weight: 700; margin-right: 0.1em; } /* unit or % */
110
109
  .band__foot { grid-area: foot; font: var(--band-foot-font, var(--type-caption)); color: var(--band-foot-ink, var(--card-muted)); }
111
- .card--3 .band__foot { color: inherit; opacity: var(--dim); }
112
110
  /* band stack: ONE card boundary; bands are frameless inside it, a hairline
113
111
  between each. Bands may still carry slot tints (card--2 / card--3). */
114
112
  .band-stack {
@@ -155,14 +153,6 @@
155
153
  @supports not (width: round(up, 1px, 1px)) {
156
154
  .dotgrid::before { display: none; }
157
155
  }
158
- .dotgrid--stars {
159
- background-image: none;
160
- }
161
- .dotgrid--stars,
162
- .dotgrid--stars::before {
163
- --_star: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%);
164
- }
165
-
166
156
  /* ---- picker: horizontal snap picker, centre value big ----
167
157
  <div class="picker"><div class="picker__rail"><button class="picker__item">100</button>…</div><span class="picker__unit">ml</span></div> */
168
158
  .picker { display: grid; gap: var(--picker-gap, var(--sp-1)); justify-items: center; position: relative; }
@@ -177,20 +167,15 @@
177
167
  grid-auto-columns: 100%;
178
168
  overflow-x: auto;
179
169
  scroll-snap-type: x mandatory;
180
- scrollbar-width: none;
181
170
  position: relative;
182
171
  }
183
- .picker__rail::-webkit-scrollbar { display: none; }
184
172
  .picker__item {
185
173
  scroll-snap-align: center;
186
- border: 0;
187
- background: transparent;
188
- color: inherit;
189
174
  padding: var(--picker-item-pad-y, var(--sp-2)) var(--picker-item-pad-x, 0);
190
175
  font: var(--picker-item-font, var(--type-stat));
191
176
  letter-spacing: var(--track-display);
192
177
  font-variant-numeric: tabular-nums;
193
- opacity: 0.3;
178
+ opacity: var(--faint);
194
179
  text-align: center;
195
180
  }
196
181
  .picker__item[aria-selected="true"] { opacity: 1; }
@@ -207,12 +192,11 @@
207
192
  }
208
193
  .picker::after { left: auto; right: calc(100% / 3); }
209
194
  .picker__unit { font: var(--picker-unit-font, var(--type-caption)); color: var(--picker-unit-ink, var(--card-muted)); }
210
- .card--3 .picker__unit { color: inherit; opacity: var(--dim); }
211
195
  /* the scrolled-to item lights up by position — no JS */
212
196
  @supports (animation-timeline: view(x)) {
213
- .picker__item { opacity: 0.3; animation: picker-focus linear both; animation-timeline: view(x); }
197
+ .picker__item { animation: picker-focus linear both; animation-timeline: view(x); } /* opacity: var(--faint) already set above — the animation alone is new here */
214
198
  @keyframes picker-focus {
215
- 0%, 100% { opacity: 0.3; }
199
+ 0%, 100% { opacity: var(--faint); }
216
200
  45%, 55% { opacity: 1; }
217
201
  }
218
202
  }
package/css/people.css CHANGED
@@ -57,10 +57,8 @@
57
57
  display: flex;
58
58
  gap: var(--avatar-pick-gap, var(--sp-4));
59
59
  overflow-x: auto;
60
- scroll-snap-type: x proximity;
61
- scrollbar-width: none; /* starts at the card's padding edge, like every row */
60
+ scroll-snap-type: x proximity; /* starts at the card's padding edge, like every row */
62
61
  }
63
- .avatar-pick::-webkit-scrollbar { display: none; }
64
62
  .avatar-pick__item {
65
63
  scroll-snap-align: start;
66
64
  flex: none;
@@ -68,13 +66,8 @@
68
66
  display: grid;
69
67
  justify-items: center;
70
68
  gap: var(--avatar-pick-item-gap, var(--sp-2));
71
- border: 0;
72
- background: transparent;
73
- color: inherit;
74
- padding: 0;
75
69
  text-align: center;
76
70
  }
77
71
  .avatar-pick__name { font: var(--avatar-pick-name-font, var(--type-label)); font-size: var(--avatar-pick-name-font-size, var(--fs-sm)); }
78
72
  .avatar-pick__role { font: var(--avatar-pick-role-font, var(--type-caption)); color: var(--avatar-pick-role-ink, var(--card-muted)); margin-top: calc(-1 * var(--sp-1)); }
79
73
  .avatar-pick__item[aria-pressed="true"] > .avatar { background: var(--avatar-pick-on-bg, var(--accent)); color: var(--avatar-pick-on-ink, var(--on-accent)); border-color: transparent; } /* the picked person */
80
- .card--3 .avatar-pick__role { color: inherit; opacity: var(--dim); }
package/css/sheet.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /* ============================================================
2
2
  cardds/sheet.css — sheet single: one card as a bottom sheet.
3
- Four states: .sheet--peek · .sheet--half · .sheet--3q · .sheet--full
3
+ Five states: .sheet--peek · .sheet--half · .sheet--3q · .sheet--full · .sheet--away
4
4
 
5
5
  A sheet is rigid material, exactly like a sheet in .sheet-stack--tap
6
6
  (stack.css): it is ALWAYS the full height of the box that holds it —
@@ -41,8 +41,8 @@
41
41
  the body. The handle and the head stay where they are at every
42
42
  state; the body is what scrolls — a sheet is a card whose head is
43
43
  pinned, not a scrolling column with a title somewhere in it.
44
- cardds.js adds the handle's click / drag / keyboard and the focus
45
- raise; without it every state still renders.
44
+ The Sheet component adds the handle's click / drag / keyboard and the
45
+ focus raise; without it every state still renders.
46
46
  ============================================================ */
47
47
 
48
48
  .sheet {
@@ -70,7 +70,7 @@
70
70
  position: absolute;
71
71
  inset: 0;
72
72
  top: var(--_top, 0); /* a stage keeps its top bar's line free — see .sheet-stage */
73
- translate: 0 calc(var(--_y) + var(--_drag, 0rem)); /* --_drag: cardds.js, only while the handle is held */
73
+ translate: 0 calc(var(--_y) + var(--_drag, 0rem)); /* --_drag: the Sheet component, only while the handle is held */
74
74
  transition: translate var(--motion-sheet) ease;
75
75
 
76
76
  /* handle · head · body: a column, the body takes what is left and scrolls
@@ -81,6 +81,7 @@
81
81
  border-radius: var(--r-card) var(--r-card) 0 0;
82
82
  border-bottom: 0;
83
83
  box-shadow: var(--sheet-shadow, var(--shadow-sheet));
84
+ --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
84
85
  }
85
86
  .sheet--full { --_y: 0%; --_cover: 0rem; }
86
87
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
@@ -146,9 +147,8 @@
146
147
  takes no taps, its buttons do; the strip behind them is the sheet's ground, --_foot tall, and the body
147
148
  stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
148
149
  One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
149
- .sheet { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); }
150
150
  /* …and the link row under the button, when there is one (its line + the gap over it) */
151
- .sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + calc(var(--tap-base) * 7 / 12) + var(--card-pad)); }
151
+ .sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad)); }
152
152
  .sheet > .card__foot {
153
153
  position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: 1; /* the sheet's border box: the same height the sheet's own % reads */
154
154
  align-items: end;
@@ -174,7 +174,7 @@
174
174
  /* grab handle — first child of the sheet. Drawn as a small bar, but it is
175
175
  the thumb's grab area for the whole strip above the title: a pseudo widens
176
176
  its hit box to --tap tall and wider than the bar without changing the
177
- picture. touch-action: none hands a touch drag to cardds.js instead of to
177
+ picture. touch-action: none hands a touch drag to the Sheet component instead of to
178
178
  the scroller behind it. */
179
179
  .sheet__handle {
180
180
  position: relative;
@@ -248,7 +248,7 @@
248
248
  .screen > .sheet-stage { margin-inline: calc(-1 * var(--screen-pad)); width: auto; } /* auto: 100% would keep the inset width */
249
249
  /* a stage laid OVER its screen (a Modal a link opened, Lh 2026-09-23): the page shows through, the stage covers it and its bar */
250
250
  .sheet-stage--over, .screen > .sheet-stage--over {
251
- position: absolute; inset: 0; z-index: 40;
251
+ position: absolute; inset: 0; z-index: var(--z-top);
252
252
  width: auto; height: auto; max-width: none; margin: 0;
253
253
  background: transparent;
254
254
  --_top: 0rem;
@@ -305,7 +305,7 @@
305
305
  translate: -50% -50%;
306
306
  border-radius: 50%;
307
307
  border: var(--border-w) solid var(--map-ring-border, var(--accent));
308
- background: var(--map-ring-bg, color-mix(in srgb, var(--accent) 22%, transparent));
308
+ background: var(--map-ring-bg, var(--accent-soft));
309
309
  box-shadow: var(--map-ring-shadow, var(--shadow-float));
310
310
  pointer-events: none;
311
311
  z-index: 1; /* above the engine's panes (Leaflet's tiles sit at 200, but inside their own stacking context) */
@@ -339,9 +339,6 @@
339
339
  z-index: 1;
340
340
  }
341
341
 
342
- /* raised: the sheet floats above a footnote strip on the scrim. Its bottom
343
- edge shows, so it gets its corners and border back; full still stops at
344
- the top bar's line, because the top edge never moved. */
345
342
  /* EVERY sheet has one shape (Lh, 2026-09-21): the handle and the head stay at the top and never scroll · the
346
343
  foot's button stands at the bottom, and ABOVE the keyboard when there is one · everything between them
347
344
  scrolls, as far as it needs to. In a .step the keyboard's room is the step's (--kb, step.css); on a stage
@@ -389,6 +386,7 @@
389
386
  display: grid; grid-template-rows: auto 0fr; align-content: start; /* mid-fold the rows sum to less than the box: the spare room stays BELOW, never spread into the title row (Lh: it looked like a spring) */
390
387
  flex: 0 0 auto;
391
388
  transition: grid-template-rows var(--motion-sheet) ease, flex-grow var(--motion-sheet) ease;
389
+ --_row-h: var(--sheet-steps-row-h, calc(var(--tap-base) * 2 / 3)); /* the bar's own floor, read once (__head and __go below share it) */
392
390
  }
393
391
  /* the open step GROWS into the room left, so the steps not done yet wait in a pile at the bottom — and a move is an ACCORDION:
394
392
  in one beat the step that was open folds while the new one unfolds (flex-grow and the 1fr row both ease), the piles slide
@@ -396,7 +394,7 @@
396
394
  .sheet-step--now { grid-template-rows: auto 1fr; flex-grow: 1; }
397
395
  .sheet-step + .sheet-step { margin-top: var(--sheet-steps-gap, var(--sp-1)); } /* the folded bars read as a pile, a little ground between them (Lh 2026-09-25) */
398
396
  .sheet-step__head { /* every step's title line is a quiet BAR (a tint of the ink, not the brand — a brand bar read as a button, Lh 2026-09-25), the same height open or folded — opening shows the white room under it, never a taller bar (Lh 2026-09-25; was a thin rule on top) */
399
- display: flex; align-items: center; gap: var(--sp-3); min-height: var(--sheet-steps-row-h, calc(var(--tap-base) * 2 / 3)); /* THIN: 32px, a summary line, not a button (Lh 2026-09-25) — the tap stays 44, see __go */
397
+ display: flex; align-items: center; gap: var(--sp-3); min-height: var(--_row-h); /* THIN: 32px, a summary line, not a button (Lh 2026-09-25) — the tap stays 44, see __go */
400
398
  padding-inline: var(--sheet-step-bar-pad-x, var(--sp-3)); border-radius: var(--sheet-step-bar-radius, var(--r-chip));
401
399
  background: var(--sheet-step-bar-bg, var(--ink-tint)); color: var(--sheet-step-bar-ink, inherit);
402
400
  border-top: var(--sheet-step-border-w, 0rem) solid var(--sheet-step-border, currentColor); /* a rule instead of the bar: set this, the bar's bg transparent, radius 0 */
@@ -414,7 +412,7 @@
414
412
  .sheet-step__inner > * { min-width: 0; }
415
413
  .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) */
416
414
  .sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s linear var(--motion-sheet); }
417
- .sheet-step__go { position: absolute; inset: min(0rem, (var(--sheet-steps-row-h, calc(var(--tap-base) * 2 / 3)) - var(--tap-sm)) / 2) 0 auto; min-height: max(100%, var(--tap-sm)); /* a thin bar still takes a 44px tap */ margin: 0; padding: 0; border: 0; background: none; cursor: pointer; -webkit-tap-highlight-color: transparent; }
415
+ .sheet-step__go { position: absolute; inset: min(0rem, (var(--_row-h) - var(--tap-sm)) / 2) 0 auto; min-height: max(100%, var(--tap-sm)); /* a thin bar still takes a 44px tap */ margin: 0; cursor: pointer; }
418
416
  /* the last step's finish, when the app gives one: under the steps */
419
417
  .sheet-steps__finish { display: grid; padding-top: var(--card-gap); }
420
418
  @media (prefers-reduced-motion: reduce) { .sheet-step { transition: none; } }