@usableapp/cardds 0.7.15 → 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 (77) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +29 -28
  3. package/cardds.css +3 -3
  4. package/css/actions.css +67 -22
  5. package/css/base.css +44 -18
  6. package/css/card.css +25 -26
  7. package/css/choice.css +21 -33
  8. package/css/forms.css +0 -3
  9. package/css/journey.css +1 -3
  10. package/css/layover.css +19 -41
  11. package/css/lists.css +1 -6
  12. package/css/media.css +9 -18
  13. package/css/numbers.css +6 -21
  14. package/css/people.css +3 -9
  15. package/css/sheet.css +15 -19
  16. package/css/stack.css +21 -29
  17. package/css/step.css +7 -12
  18. package/css/theme.template.css +2 -1
  19. package/css/tokens.css +22 -4
  20. package/dist/actions/Btn.d.ts +1 -1
  21. package/dist/actions/Dropdown.js +3 -9
  22. package/dist/actions/Fab.d.ts +1 -1
  23. package/dist/actions/Fab.js +3 -2
  24. package/dist/cardds.css +264 -267
  25. package/dist/cards/Card.d.ts +24 -8
  26. package/dist/cards/Card.js +23 -8
  27. package/dist/cards/CardHead.d.ts +0 -1
  28. package/dist/cards/CardHead.js +1 -1
  29. package/dist/choice/Calendar.js +2 -5
  30. package/dist/choice/Mood.d.ts +10 -5
  31. package/dist/choice/Mood.js +4 -2
  32. package/dist/forms/Field.js +2 -3
  33. package/dist/forms/FileBtn.js +1 -1
  34. package/dist/forms/Pin.js +7 -12
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +3 -3
  37. package/dist/layover/Banner.d.ts +10 -3
  38. package/dist/layover/Banner.js +6 -3
  39. package/dist/lists/Row.d.ts +2 -1
  40. package/dist/lists/Row.js +5 -5
  41. package/dist/media/Postcard.js +3 -5
  42. package/dist/numbers/Band.d.ts +3 -1
  43. package/dist/numbers/Picker.js +3 -10
  44. package/dist/numbers/Track.d.ts +4 -2
  45. package/dist/numbers/Track.js +3 -3
  46. package/dist/people/AvatarPick.d.ts +3 -1
  47. package/dist/people/AvatarPick.js +2 -2
  48. package/dist/scaffold/ActionBar.d.ts +3 -3
  49. package/dist/scaffold/ActionBar.js +2 -2
  50. package/dist/scaffold/Centre.d.ts +2 -2
  51. package/dist/scaffold/Centre.js +2 -2
  52. package/dist/scaffold/PagerAt.d.ts +1 -1
  53. package/dist/scaffold/PagerAt.js +1 -1
  54. package/dist/scaffold/Screen.js +4 -7
  55. package/dist/scaffold/TopBar.js +3 -5
  56. package/dist/sheets/Drawer.js +3 -4
  57. package/dist/sheets/Sheet.js +9 -22
  58. package/dist/sheets/SheetStack.d.ts +2 -2
  59. package/dist/sheets/SheetStack.js +20 -13
  60. package/dist/sheets/SheetSteps.js +8 -11
  61. package/dist/sheets/StackBack.js +2 -1
  62. package/dist/sheets/StackSheet.d.ts +3 -2
  63. package/dist/sheets/StackSheet.js +7 -11
  64. package/dist/state.d.ts +24 -0
  65. package/dist/state.js +57 -0
  66. package/dist/templates/CallSheet.js +2 -4
  67. package/dist/templates/Home.d.ts +3 -2
  68. package/dist/templates/Home.js +2 -4
  69. package/dist/templates/Splash.js +3 -3
  70. package/dist/templates/Walkthrough.js +4 -5
  71. package/dist/type/Icon.js +3 -5
  72. package/dist/type/Text.d.ts +1 -1
  73. package/dist/type/textScale.d.ts +6 -0
  74. package/dist/type/textScale.js +12 -0
  75. package/package.json +7 -4
  76. package/dist/layover/Deck.d.ts +0 -9
  77. package/dist/layover/Deck.js +0 -10
package/css/card.css CHANGED
@@ -1,6 +1,7 @@
1
1
  /* ============================================================
2
2
  cardds/card.css — the card boundary + list variants
3
- One card = one boundary. Tone via .card--1/2/3 (role slots).
3
+ One card = one boundary. The base .card is tone 1; further tones
4
+ via .card--2/3/4/5 (role slots).
4
5
  ============================================================ */
5
6
 
6
7
  .card {
@@ -25,14 +26,18 @@
25
26
  .card > * { min-width: 0; }
26
27
 
27
28
  .card--2 { --card-bg: var(--card-2-bg); --card-ink: var(--card-2-ink); --card-muted: var(--card-2-muted); }
28
- .card--3 { --card-bg: var(--card-3-bg); --card-ink: var(--card-3-ink); }
29
- .card--4 { --card-bg: var(--card-4-bg); --card-ink: var(--card-4-ink); --card-muted: var(--card-4-muted); }
30
- .card--5 { --card-bg: var(--card-5-bg); --card-ink: var(--card-5-ink); --card-muted: var(--card-5-muted); }
31
-
32
29
  /* dark/accent slot: accent-toned controls would vanish — the slot re-points
33
30
  the accent tokens, so every accent consumer (btn--primary, chip--on,
34
31
  check, toggle, ring, day…) inverts without per-component rules */
35
- .card--3 { --accent: var(--card-3-ink); --on-accent: var(--card-3-bg); }
32
+ .card--3 { --card-bg: var(--card-3-bg); --card-ink: var(--card-3-ink); --accent: var(--card-3-ink); --on-accent: var(--card-3-bg); }
33
+ .card--4 { --card-bg: var(--card-4-bg); --card-ink: var(--card-4-ink); --card-muted: var(--card-4-muted); }
34
+ .card--5 { --card-bg: var(--card-5-bg); --card-ink: var(--card-5-ink); --card-muted: var(--card-5-muted); }
35
+ /* the dark slot's captions/hints/footnotes: --card-muted is tuned for a light card, so these dim the full ink
36
+ instead (one rule, matching specificity to what it replaces — .card--3 <space> <one class>) — merged from
37
+ base.css/choice.css/media.css/numbers.css/journey.css/people.css (2026-09-27 review). lists.css's .legend
38
+ differs (opacity 0.8, not --dim) and stays there. */
39
+ .card--3 :is(.t-muted, .calendar__wd, .composer__hint, .quote__by, .ring__label, .band__foot, .picker__unit,
40
+ .route__place, .note-row, .avatar-pick__role) { color: inherit; opacity: var(--dim); }
36
41
 
37
42
  /* header row: title left, floating action right — centered, since the
38
43
  trailing control (chip/badge/icon-btn/link) is almost always taller than
@@ -135,7 +140,7 @@
135
140
  linear-gradient(to top left, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px));
136
141
  border: var(--border-w) solid currentColor;
137
142
  border-radius: var(--r-media);
138
- opacity: 0.4;
143
+ opacity: var(--off);
139
144
  min-height: 10rem;
140
145
  }
141
146
  .ph--sm { min-height: 7.5rem; }
@@ -161,8 +166,7 @@
161
166
  one is there, and the screen's gap), or twice the room below it (the
162
167
  bar's reserve) — whichever is less. --_lead is the title bar's height
163
168
  only on a screen that HAS one: without a .topbar/.appbar before it the
164
- block keeps that room (a deck and its two buttons, a card and its pager
165
- under it, need it on a short phone; Lh, 2026-09-11). */
169
+ block keeps that room (a card and its pager under it needs it on a short phone; Lh, 2026-09-11). */
166
170
  --_lead: 0rem;
167
171
  max-height: calc(2 * min(var(--screen-centre) - var(--_lead) - var(--screen-gap), 100% - var(--screen-centre) - var(--bar-reserve)));
168
172
  overflow-y: auto;
@@ -173,9 +177,8 @@
173
177
 
174
178
  /* ---- centred block: the same place, for one thing to read that is not a
175
179
  lone card — an empty state, a card with its quiet second button and the
176
- pager under it, a deck with its two round buttons. The block runs edge to
177
- edge and keeps the screen's inset as padding, so a deck's fanned cards can
178
- overhang and clip at the screen's edges (correct — the fold visual), and
180
+ pager under it. The block runs edge to
181
+ edge and keeps the screen's inset as padding, and
179
182
  its children stack at the screen's own rhythm (Lh, 2026-09-11):
180
183
  <div class="screen screen--fill"><header class="topbar">…</header>
181
184
  <div class="screen__centre"><article class="card">…</article><button class="btn btn--quiet btn--block">…</button></div>
@@ -193,9 +196,9 @@
193
196
  likes when not (Lh, 2026-09-12) ----
194
197
  The member sees one content area — whatever is between the top bar and the
195
198
  bar — and reads it as a whole page: a short card sits at its middle, a long
196
- one simply makes the page longer, runs under the bar (the bar's reserve is
197
- already the screen's bottom padding, actions.css) and scrolls up like any
198
- page — nothing is capped, nothing shrinks, no scroll box inside a box.
199
+ one simply makes the page longer, runs under the bar (a sticky last row,
200
+ actions.css) and scrolls up like any page — nothing is capped, nothing
201
+ shrinks, no scroll box inside a box.
199
202
  So on a .screen that is NOT --fill the group is in flow: the column is at
200
203
  least one phone tall, the group takes the room after the top bar and
201
204
  centres its children in it. The screen's own inset stays the inset.
@@ -213,9 +216,9 @@
213
216
  the React adapter's slideTo() sets data-slide on :root, starts the transition
214
217
  and updates inside it; a browser without them (the floor: Chrome 105 / iOS 16)
215
218
  simply switches. Only the card is named; the rest of the page must not
216
- cross-fade. */
217
- /* the name is set by slideTo() on the moving element for the move only (2026-09-22): a static name on every walk's current
218
- page made two walks on one page — the gallery — share it, and the browser aborts a transition with a duplicate name */
219
+ cross-fade. The name is set by slideTo() on the moving element for the move only (2026-09-22): a static name on
220
+ every walk's current page made two walks on one page — the gallery — share it, and the browser aborts a
221
+ transition with a duplicate name. */
219
222
 
220
223
  /* ---- pages: the cards of a walk, all one height ----
221
224
  A sequence the member walks (an introduction) reads steadier when every card
@@ -248,14 +251,10 @@
248
251
  animation-timing-function: ease-out;
249
252
  mix-blend-mode: normal;
250
253
  }
251
- :root[data-slide="next"]::view-transition-old(centre-card-out) { animation-name: centre-card-out-left; }
252
- :root[data-slide="next"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-right; }
253
- :root[data-slide="back"]::view-transition-old(centre-card-out) { animation-name: centre-card-out-right; }
254
- :root[data-slide="back"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-left; }
255
- :root[data-slide="next"]::view-transition-old(screen-out) { animation-name: centre-card-out-left; }
256
- :root[data-slide="next"]::view-transition-new(screen-in) { animation-name: centre-card-in-right; }
257
- :root[data-slide="back"]::view-transition-old(screen-out) { animation-name: centre-card-out-right; }
258
- :root[data-slide="back"]::view-transition-new(screen-in) { animation-name: centre-card-in-left; }
254
+ :root[data-slide="next"]::view-transition-old(centre-card-out), :root[data-slide="next"]::view-transition-old(screen-out) { animation-name: centre-card-out-left; }
255
+ :root[data-slide="next"]::view-transition-new(centre-card-in), :root[data-slide="next"]::view-transition-new(screen-in) { animation-name: centre-card-in-right; }
256
+ :root[data-slide="back"]::view-transition-old(centre-card-out), :root[data-slide="back"]::view-transition-old(screen-out) { animation-name: centre-card-out-right; }
257
+ :root[data-slide="back"]::view-transition-new(centre-card-in), :root[data-slide="back"]::view-transition-new(screen-in) { animation-name: centre-card-in-left; }
259
258
  @keyframes centre-card-out-left { to { translate: -100% 0; opacity: 0; } }
260
259
  @keyframes centre-card-out-right { to { translate: 100% 0; opacity: 0; } }
261
260
  @keyframes centre-card-in-right { from { translate: 100% 0; opacity: 0; } }
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: 0.7; }
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: 0.7; }
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: 0.7; }
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
 
@@ -40,7 +39,7 @@
40
39
  background: var(--tile-badge-bg, var(--card-3-bg));
41
40
  color: var(--tile-badge-ink, var(--card-3-ink));
42
41
  }
43
- .tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); opacity: 0.7; }
42
+ .tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); opacity: var(--dim); }
44
43
  .tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
45
44
 
46
45
  /* plain: no fill — a date box beside a title (Jun / 18) */
@@ -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: 0.7; }
package/css/layover.css CHANGED
@@ -1,7 +1,7 @@
1
1
  /* ============================================================
2
2
  cardds/layover.css — cards that lay OVER other content:
3
3
  Modal (ask) · popover menu · expanding banner · float bar ·
4
- callout · swipe deck. The sheet (peek / half / full / page)
4
+ callout. The sheet (peek / half / full / page)
5
5
  lives in sheet.css; these compose with .sheet-stage.
6
6
  ============================================================ */
7
7
 
@@ -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;
@@ -72,7 +73,7 @@
72
73
  --accent: var(--card-3-ink); /* the one solid button inverts on the dark drawer */
73
74
  --on-accent: var(--card-3-bg);
74
75
  }
75
- .modal__drawer .t-caption { opacity: 0.7; }
76
+ .modal__drawer .t-caption { opacity: var(--dim); }
76
77
  .modal__drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
77
78
  .modal__drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
78
79
  @media (prefers-reduced-motion: no-preference) {
@@ -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,18 +134,14 @@
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
- .menu__item[aria-current], .menu__item.is-on { background: var(--accent); color: var(--on-accent); }
144
+ .menu__item[aria-current] { background: var(--accent); color: var(--on-accent); }
148
145
  @supports (anchor-name: --a) {
149
146
  .menu[popover] {
150
147
  position-area: block-end span-inline-end;
@@ -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,21 +235,17 @@
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;
255
245
  }
256
246
  /* aria-current="false" is the correct way to mark the OTHER tabs, so the
257
247
  bare attribute selector lights every one of them up */
258
- .float-bar__item[aria-current]:not([aria-current="false"]),
259
- .float-bar__item.is-on { background: var(--accent); color: var(--on-accent); opacity: 1; }
248
+ .float-bar__item[aria-current]:not([aria-current="false"]) { background: var(--accent); color: var(--on-accent); opacity: 1; }
260
249
  .float-bar > .icon-btn { width: var(--bar-h); height: var(--bar-h); box-shadow: var(--shadow-float); }
261
250
 
262
251
  /* ---- callout: a small card with a tail — over a map, a media, a mascot ----
@@ -290,18 +279,7 @@
290
279
  border-left: 0;
291
280
  }
292
281
  .callout__title { font: var(--callout-title-font, var(--type-title)); }
293
- .callout__sub { font: var(--callout-sub-font, var(--type-caption)); opacity: 0.7; }
294
- /* ---- deck: swipe cards; the last child is on top ----
295
- <div class="deck"><article class="card">…</article><article class="card">…</article><article class="card card--fold">front</article></div>
296
- <div class="deck__actions"><button class="icon-btn">×</button><button class="icon-btn icon-btn--invert">✓</button></div> */
297
- .deck { display: grid; place-items: center; padding: var(--deck-pad-top, var(--sp-4)) var(--deck-pad-x, var(--sp-4)) var(--deck-pad-bottom, var(--sp-6)); }
298
- .deck > .card { grid-area: 1 / 1; width: min(100%, 20rem); min-height: 12.5rem; transition: rotate 0.3s ease, translate 0.3s ease; }
299
- .deck > .card:nth-last-child(2) { rotate: -4deg; translate: calc(-1 * var(--sp-2)) var(--sp-2); opacity: 0.85; }
300
- .deck > .card:nth-last-child(3) { rotate: 5deg; translate: var(--sp-3) var(--sp-4); opacity: 0.6; }
301
- .deck > .card:nth-last-child(n + 4) { display: none; }
302
- .deck__actions { display: flex; justify-content: center; gap: var(--deck-actions-gap, var(--sp-8)); }
303
- .deck__actions > .icon-btn { width: var(--bar-h); height: var(--bar-h); }
304
-
282
+ .callout__sub { font: var(--callout-sub-font, var(--type-caption)); opacity: var(--dim); }
305
283
  /* ---- splash: the app's mark at the human centre while it boots (Lh 2026-09-24) ----
306
284
  <section class="screen screen--fill splash"><div class="splash__group"><div class="splash__mark">{the app's lottie/img}</div>
307
285
  <p class="splash__text">…</p></div></section>
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: 0.7; }
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>
@@ -79,20 +78,16 @@
79
78
  font: inherit;
80
79
  text-align: left;
81
80
  position: relative;
82
- cursor: pointer;
83
81
  }
84
82
  .tile__label { font: var(--tile-label-font, var(--type-title)); }
85
83
  .tile__value { font: var(--tile-value-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
86
- .tile__sub { font: var(--tile-sub-font, var(--type-caption)); opacity: 0.7; }
84
+ .tile__sub { font: var(--tile-sub-font, var(--type-caption)); opacity: var(--dim); }
87
85
  .tile__text { display: grid; gap: var(--tile-text-gap, var(--sp-0)); }
88
86
  .tile--on { background: var(--tile-on-bg, var(--accent)); color: var(--tile-on-ink, var(--on-accent)); border-color: transparent; }
89
- .tile--pick > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
90
- .tile--pick:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
91
- .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 */
92
88
  .tile--wide { grid-column: 1 / -1; }
93
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 */
94
- .tile--stat { border-color: transparent; background: none; padding-inline: 0; cursor: default; }
95
- .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; }
96
91
  /* upload: dashed, centred plus + label */
97
92
  .tile--upload {
98
93
  border-style: dashed;
@@ -115,7 +110,7 @@
115
110
  the corner read as a notch with a line through it rather than a lifted flap.
116
111
  The COLOUR goes transparent rather than the border going to 0, so the box
117
112
  keeps its geometry and a fold card still measures the same as the cards
118
- beside it in a deck. */
113
+ beside it. */
119
114
  .card--fold {
120
115
  border-color: transparent;
121
116
  border-bottom-right-radius: 0;
@@ -169,8 +164,8 @@
169
164
  overflow: hidden;
170
165
  }
171
166
  .mosaic > :first-child { grid-row: span 2; }
172
- .mosaic > * { min-height: 0; border-radius: 0; opacity: 1; border-width: 0; }
173
- .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); }
174
169
  .mosaic > img { width: 100%; height: 100%; object-fit: cover; }
175
170
 
176
171
  /* ---- postcard: a member's introduction in the shape of a cover card — the photo fills it,
@@ -199,7 +194,7 @@
199
194
  liked it, a voice note, a reply live OUTSIDE the postcard, in a Row under it. */
200
195
  .postcard {
201
196
  position: relative;
202
- min-height: var(--postcard-min-h, 22.5rem);
197
+ min-height: var(--postcard-min-h, var(--_cover-min-h));
203
198
  perspective: 60rem;
204
199
  border-radius: var(--postcard-radius, var(--r-card));
205
200
  isolation: isolate;
@@ -225,14 +220,10 @@
225
220
  display: grid;
226
221
  align-content: start;
227
222
  padding: var(--postcard-face-pad, var(--card-pad-rest));
228
- border: 0;
229
223
  border-radius: inherit;
230
- background: transparent;
231
- color: inherit;
232
224
  text-align: left;
233
- cursor: pointer;
234
225
  }
235
- .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 */
236
227
  /* the front: the photo (or the wireframe box) fills it; the band reads on a scrim */
237
228
  .postcard__front { background: var(--postcard-front-bg, var(--card-3-bg)); color: var(--postcard-front-ink, var(--card-3-ink)); }
238
229
  .postcard__front .postcard__turn > img,