@usableapp/cardds 0.1.0

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 (170) hide show
  1. package/README.md +562 -0
  2. package/cardds.css +18 -0
  3. package/cardds.js +473 -0
  4. package/css/actions.css +292 -0
  5. package/css/base.css +128 -0
  6. package/css/card.css +283 -0
  7. package/css/choice.css +297 -0
  8. package/css/fonts.css +37 -0
  9. package/css/forms.css +64 -0
  10. package/css/journey.css +61 -0
  11. package/css/layover.css +243 -0
  12. package/css/lists.css +142 -0
  13. package/css/media.css +232 -0
  14. package/css/numbers.css +258 -0
  15. package/css/palette.css +112 -0
  16. package/css/people.css +79 -0
  17. package/css/sheet.css +295 -0
  18. package/css/stack.css +262 -0
  19. package/css/step.css +71 -0
  20. package/css/tokens.css +177 -0
  21. package/dist/actions/Btn.d.ts +311 -0
  22. package/dist/actions/Btn.js +14 -0
  23. package/dist/actions/BtnRow.d.ts +5 -0
  24. package/dist/actions/BtnRow.js +6 -0
  25. package/dist/actions/Chip.d.ts +328 -0
  26. package/dist/actions/Chip.js +25 -0
  27. package/dist/actions/Dot.d.ts +7 -0
  28. package/dist/actions/Dot.js +6 -0
  29. package/dist/actions/Dropdown.d.ts +34 -0
  30. package/dist/actions/Dropdown.js +19 -0
  31. package/dist/actions/Fab.d.ts +9 -0
  32. package/dist/actions/Fab.js +7 -0
  33. package/dist/actions/IconBtn.d.ts +308 -0
  34. package/dist/actions/IconBtn.js +8 -0
  35. package/dist/actions/Link.d.ts +301 -0
  36. package/dist/actions/Link.js +8 -0
  37. package/dist/actions/Segment.d.ts +11 -0
  38. package/dist/actions/Segment.js +10 -0
  39. package/dist/cardds.css +3068 -0
  40. package/dist/cards/Card.d.ts +310 -0
  41. package/dist/cards/Card.js +14 -0
  42. package/dist/cards/CardFoot.d.ts +5 -0
  43. package/dist/cards/CardFoot.js +6 -0
  44. package/dist/cards/CardHead.d.ts +21 -0
  45. package/dist/cards/CardHead.js +11 -0
  46. package/dist/cards/CardParts.d.ts +25 -0
  47. package/dist/cards/CardParts.js +26 -0
  48. package/dist/cards/Placeholder.d.ts +9 -0
  49. package/dist/cards/Placeholder.js +6 -0
  50. package/dist/cards/Stat.d.ts +17 -0
  51. package/dist/cards/Stat.js +14 -0
  52. package/dist/choice/Calendar.d.ts +23 -0
  53. package/dist/choice/Calendar.js +7 -0
  54. package/dist/choice/Check.d.ts +14 -0
  55. package/dist/choice/Check.js +10 -0
  56. package/dist/choice/DayStrip.d.ts +16 -0
  57. package/dist/choice/DayStrip.js +10 -0
  58. package/dist/choice/Mood.d.ts +11 -0
  59. package/dist/choice/Mood.js +6 -0
  60. package/dist/choice/Slider.d.ts +11 -0
  61. package/dist/choice/Slider.js +14 -0
  62. package/dist/cx.d.ts +2 -0
  63. package/dist/cx.js +2 -0
  64. package/dist/forms/AddRow.d.ts +5 -0
  65. package/dist/forms/AddRow.js +7 -0
  66. package/dist/forms/Composer.d.ts +13 -0
  67. package/dist/forms/Composer.js +6 -0
  68. package/dist/forms/Field.d.ts +17 -0
  69. package/dist/forms/Field.js +10 -0
  70. package/dist/forms/FileBtn.d.ts +18 -0
  71. package/dist/forms/FileBtn.js +10 -0
  72. package/dist/forms/Pin.d.ts +19 -0
  73. package/dist/forms/Pin.js +27 -0
  74. package/dist/index.d.ts +72 -0
  75. package/dist/index.js +75 -0
  76. package/dist/journey/NoteRow.d.ts +7 -0
  77. package/dist/journey/NoteRow.js +7 -0
  78. package/dist/journey/Route.d.ts +18 -0
  79. package/dist/journey/Route.js +8 -0
  80. package/dist/journey/TileBadge.d.ts +15 -0
  81. package/dist/journey/TileBadge.js +6 -0
  82. package/dist/layover/Banner.d.ts +9 -0
  83. package/dist/layover/Banner.js +6 -0
  84. package/dist/layover/Callout.d.ts +10 -0
  85. package/dist/layover/Callout.js +6 -0
  86. package/dist/layover/Deck.d.ts +9 -0
  87. package/dist/layover/Deck.js +10 -0
  88. package/dist/lists/Kv.d.ts +27 -0
  89. package/dist/lists/Kv.js +19 -0
  90. package/dist/lists/Legend.d.ts +11 -0
  91. package/dist/lists/Legend.js +11 -0
  92. package/dist/lists/Row.d.ts +319 -0
  93. package/dist/lists/Row.js +17 -0
  94. package/dist/lists/Timeline.d.ts +11 -0
  95. package/dist/lists/Timeline.js +10 -0
  96. package/dist/media/MapArea.d.ts +20 -0
  97. package/dist/media/MapArea.js +11 -0
  98. package/dist/media/Mosaic.d.ts +11 -0
  99. package/dist/media/Mosaic.js +10 -0
  100. package/dist/media/Postcard.d.ts +20 -0
  101. package/dist/media/Postcard.js +10 -0
  102. package/dist/media/Quote.d.ts +11 -0
  103. package/dist/media/Quote.js +6 -0
  104. package/dist/media/Tile.d.ts +320 -0
  105. package/dist/media/Tile.js +16 -0
  106. package/dist/numbers/Badge.d.ts +7 -0
  107. package/dist/numbers/Badge.js +6 -0
  108. package/dist/numbers/Band.d.ts +16 -0
  109. package/dist/numbers/Band.js +10 -0
  110. package/dist/numbers/Bars.d.ts +14 -0
  111. package/dist/numbers/Bars.js +10 -0
  112. package/dist/numbers/DotGrid.d.ts +12 -0
  113. package/dist/numbers/DotGrid.js +6 -0
  114. package/dist/numbers/Picker.d.ts +13 -0
  115. package/dist/numbers/Picker.js +18 -0
  116. package/dist/numbers/Ring.d.ts +15 -0
  117. package/dist/numbers/Ring.js +6 -0
  118. package/dist/numbers/Track.d.ts +12 -0
  119. package/dist/numbers/Track.js +10 -0
  120. package/dist/people/Avatar.d.ts +314 -0
  121. package/dist/people/Avatar.js +13 -0
  122. package/dist/people/AvatarPick.d.ts +18 -0
  123. package/dist/people/AvatarPick.js +10 -0
  124. package/dist/scaffold/ActionBar.d.ts +21 -0
  125. package/dist/scaffold/ActionBar.js +16 -0
  126. package/dist/scaffold/AppBar.d.ts +18 -0
  127. package/dist/scaffold/AppBar.js +10 -0
  128. package/dist/scaffold/Centre.d.ts +30 -0
  129. package/dist/scaffold/Centre.js +41 -0
  130. package/dist/scaffold/EmptyState.d.ts +14 -0
  131. package/dist/scaffold/EmptyState.js +8 -0
  132. package/dist/scaffold/FilterRow.d.ts +5 -0
  133. package/dist/scaffold/FilterRow.js +6 -0
  134. package/dist/scaffold/FloatBar.d.ts +310 -0
  135. package/dist/scaffold/FloatBar.js +15 -0
  136. package/dist/scaffold/PagerAt.d.ts +11 -0
  137. package/dist/scaffold/PagerAt.js +6 -0
  138. package/dist/scaffold/Screen.d.ts +299 -0
  139. package/dist/scaffold/Screen.js +11 -0
  140. package/dist/scaffold/Section.d.ts +12 -0
  141. package/dist/scaffold/Section.js +13 -0
  142. package/dist/scaffold/TopBar.d.ts +15 -0
  143. package/dist/scaffold/TopBar.js +9 -0
  144. package/dist/sheets/CardBack.d.ts +7 -0
  145. package/dist/sheets/CardBack.js +7 -0
  146. package/dist/sheets/Drawer.d.ts +17 -0
  147. package/dist/sheets/Drawer.js +16 -0
  148. package/dist/sheets/Sheet.d.ts +21 -0
  149. package/dist/sheets/Sheet.js +13 -0
  150. package/dist/sheets/SheetBody.d.ts +5 -0
  151. package/dist/sheets/SheetBody.js +6 -0
  152. package/dist/sheets/SheetHead.d.ts +13 -0
  153. package/dist/sheets/SheetHead.js +6 -0
  154. package/dist/sheets/SheetStack.d.ts +21 -0
  155. package/dist/sheets/SheetStack.js +14 -0
  156. package/dist/sheets/SheetStage.d.ts +20 -0
  157. package/dist/sheets/SheetStage.js +11 -0
  158. package/dist/sheets/Step.d.ts +15 -0
  159. package/dist/sheets/Step.js +15 -0
  160. package/dist/type/Icon.d.ts +16 -0
  161. package/dist/type/Icon.js +12 -0
  162. package/dist/type/Text.d.ts +302 -0
  163. package/dist/type/Text.js +16 -0
  164. package/dist/type/icons.d.ts +6 -0
  165. package/dist/type/icons.js +2 -0
  166. package/fonts/LICENSES.md +44 -0
  167. package/fonts/fc-pride-medium.otf +0 -0
  168. package/fonts/pk-nonthaburi-demo.ttf +0 -0
  169. package/icons.svg +61 -0
  170. package/package.json +62 -0
@@ -0,0 +1,3068 @@
1
+ /* cardds — card-first mobile design system. One import. */
2
+ /* ---- css/fonts.css — the two faces: PK Nonthaburi (body), FC Pride (display) ---- */
3
+ /* ============================================================
4
+ cardds/fonts.css — the two faces the system ships (Lh, 2026-09-10).
5
+ Body: PK Nonthaburi (Pocket Fonts, demo weight — one face; the browser
6
+ synthesises bold for labels and titles, so the 400/700 contrast holds).
7
+ Display: FC Pride (Fontcraft Studio, Medium — non-commercial licence);
8
+ declared across every weight so headings render the real Medium, never
9
+ a smeared faux-bold. Both cover Thai + Latin + digits. Licences: fonts/LICENSES.md.
10
+ tokens.css points --font-ui and --font-display at these; a consumer who
11
+ vendors the CSS copies fonts/ beside it (the url()s are ../fonts/).
12
+ ============================================================ */
13
+ @font-face {
14
+ font-family: "PK Nonthaburi";
15
+ src: url("../fonts/pk-nonthaburi-demo.ttf") format("truetype");
16
+ font-weight: 400 500;
17
+ font-style: normal;
18
+ font-display: swap;
19
+ /* the box the line-height centres = the ink: tone marks reach 1.02em up,
20
+ lower vowels 0.30em down (measured; the face declares 0.92 / 0.30 + gap) */
21
+ ascent-override: 102%;
22
+ descent-override: 30%;
23
+ line-gap-override: 0%;
24
+ }
25
+ @font-face {
26
+ font-family: "FC Pride";
27
+ src: url("../fonts/fc-pride-medium.otf") format("opentype");
28
+ font-weight: 100 900;
29
+ font-style: normal;
30
+ font-display: swap;
31
+ /* the face declares 1.20 up / 0.18 down, but a stacked vowel + tone mark
32
+ reaches 1.10em and สระอู 0.29em — override so the line box centres the
33
+ ink and a clipped heading loses the same hair top and bottom, not the
34
+ whole lower vowel. With the leading in tokens.css this is 0.075em a side
35
+ at h2; the clipped containers pad that back (card.css, actions.css) */
36
+ ascent-override: 110%;
37
+ descent-override: 30%;
38
+ line-gap-override: 0%;
39
+ }
40
+
41
+ /* ---- css/tokens.css — structure + wireframe defaults ---- */
42
+ /* ============================================================
43
+ cardds/tokens.css — structural tokens + WIREFRAME color defaults
44
+ Colors here are grayscale on purpose. Real color = palette.css.
45
+ Components reference ONLY these role tokens, never raw hex.
46
+
47
+ Every length is rem so the whole system scales with the root
48
+ font size (base.css steps it down on narrow phones and it follows
49
+ the user's text-size setting). Only borders, rules and focus
50
+ rings stay in px — they must stay crisp, not scale.
51
+ 4pt grid: 1rem = 16px → --sp-1 = 4px.
52
+ ============================================================ */
53
+
54
+ :root {
55
+ /* ---- layout ---- */
56
+ --screen-w: 25rem; /* 400: mobile column — .screen, .action-bar, .sheet-stage */
57
+ --screen-centre: 40%; /* the human centre of a screen: the middle of its top four fifths — the bottom fifth is the thumb's (Lh, 2026-09-10). .card--centre sits here */
58
+
59
+ /* ---- spacing (4pt grid) ---- */
60
+ --sp-0: 0.125rem; /* 2 hairline gaps (title→meta) */
61
+ --sp-1: 0.25rem; /* 4 */
62
+ --sp-2: 0.5rem; /* 8 */
63
+ --sp-3: 0.75rem; /* 12 */
64
+ --sp-4: 1rem; /* 16 */
65
+ --sp-5: 1.25rem; /* 20 */
66
+ --sp-6: 1.5rem; /* 24 */
67
+ --sp-8: 2rem; /* 32 */
68
+ --sp-10: 2.5rem; /* 40 */
69
+ --sp-12: 3rem; /* 48 */
70
+
71
+ /* ---- touch & control sizes ---- */
72
+ --tap: 3rem; /* 48 primary touch target */
73
+ --tap-sm: 2.75rem; /* 44 minimum touch target (Apple HIG floor) */
74
+ --tap-lg: 3.5rem; /* 56 hero controls: track steps, send, field with action */
75
+ --btn-h: 3.25rem; /* 52 button height */
76
+ --btn-xl-h: 5.5rem; /* 88 the one giant CTA */
77
+ --chip-h: 2.375rem; /* 38 chip height */
78
+ --bar-h: 4rem; /* 64 float bar, day cell, deck actions */
79
+ --header-h: 3.75rem;/* 60 top bar — .topbar and .appbar alike */
80
+ --screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
81
+ --map-ring: 10rem; /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
82
+ --fab: 4.5rem; /* 72 square floating action */
83
+ --badge: 1.625rem; /* 26 count badge */
84
+ --dot: 0.625rem; /* 10 signal dot */
85
+ --dot-xs: 0.375rem; /* 6 marker dot */
86
+
87
+ /* ---- icons (icons.svg sprite, Lucide) ---- */
88
+ --icon: 1.75rem; /* 28 default */
89
+ --icon-sm: 1.75rem; /* one size everywhere; retune if small contexts appear */
90
+ --icon-lg: 2rem; /* 32 empty states */
91
+ --icon-xs: 1rem; /* 16 inline with captions/chips only (row meta, link arrows) */
92
+
93
+ /* ---- people & big elements ---- */
94
+ --avatar-sm: 2.5rem; /* 40 row lead */
95
+ --avatar: 3.5rem; /* 56 card head, person row */
96
+ --avatar-lg: 5rem; /* 80 chooser row */
97
+ --avatar-xl: 6rem; /* 96 sheet head icon ring */
98
+ --ring: 11rem; /* 176 .ring gauge diameter */
99
+ --ring-w: 1.375rem; /* 22 .ring stroke */
100
+ --tile-min: 7.5rem; /* 120 .tile-grid column floor */
101
+ --tile-h: 8.25rem; /* 132 tile / band height */
102
+
103
+ /* ---- radius ---- */
104
+ --r-chip: 999px; /* pills, chips, buttons — always fully round */
105
+ --r-card: 2rem; /* 32 the one card radius: every card, row, tile, menu, sheet, fab */
106
+ --r-media: 1rem; /* 16 images inside cards */
107
+ --r-control: 0.875rem; /* 14 fields, thumbs, menu rows */
108
+ --r-badge: 0.625rem; /* 10 tile badge, bars */
109
+
110
+ /* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
111
+ --border-w: 1.5px;
112
+ --rule-w: 2px; /* dotted connectors, picker rules */
113
+ --focus: 2px solid currentColor; /* :focus-visible ring */
114
+ --shadow-card: 0 1px 2px rgb(0 0 0 / 0.06), 0 0.5rem 1.5rem rgb(0 0 0 / 0.08);
115
+ --shadow-float: 0 0.25rem 0.75rem rgb(0 0 0 / 0.14);
116
+ --shadow-sheet: 0 -0.5rem 2rem rgb(0 0 0 / 0.16); /* sheets, lifted cards */
117
+
118
+ /* ---- motion ---- */
119
+ --motion-sheet: 0.35s;
120
+ --motion-slide: 0.22s; /* a centred card sliding out and the next in (card.css) — quick, a page turn (Lh, 2026-09-12) */
121
+ --motion-flip: 0.45s; /* a postcard turning over (media.css) — slow enough to read as a card in the hand */
122
+
123
+ /* ---- typography: one scale, rem so it follows the root ---- */
124
+ --font-ui: "PK Nonthaburi", -apple-system, system-ui, sans-serif; /* fonts.css ships it; the rest is the fallback while it loads */
125
+ --font-display: "FC Pride", var(--font-ui); /* the character face for headings and big numbers (fonts.css) */
126
+ --font-mono: "SF Mono", "JetBrains Mono", ui-monospace, monospace;
127
+
128
+ --fs-xs: 0.6875rem; /* 11 overline */
129
+ --fs-caption: 0.75rem; /* 12 */
130
+ --fs-label: 0.8125rem; /* 13 */
131
+ --fs-sm: 0.875rem; /* 14 chips, segments */
132
+ --fs-body: 0.9375rem; /* 15 */
133
+ --fs-md: 1rem; /* 16 buttons */
134
+ --fs-title: 1.0625rem; /* 17 */
135
+ --fs-lg: 1.25rem; /* 20 day numbers, tile values */
136
+ --fs-h2: 1.375rem; /* 22 */
137
+ --fs-2xl: 1.5rem; /* 24 composer, small ring */
138
+ --fs-h1: 1.75rem; /* 28 */
139
+ --fs-3xl: 2rem; /* 32 xl button */
140
+ --fs-display: clamp(2.125rem, 9vw, 2.625rem); /* 34–42 */
141
+ --fs-stat: clamp(1.875rem, 8vw, 2.375rem); /* 30–38 big numbers */
142
+ --fs-hero: 3.5rem; /* 56 the one number on a sheet */
143
+ --fs-giant: clamp(3rem, 15vw, 4rem); /* 48–64 data bands */
144
+
145
+ /* level weight size / line-height family */
146
+ --type-display: 800 var(--fs-display) / 1.15 var(--font-display); /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
147
+ --track-display: -0.02em;
148
+ --type-h1: 900 var(--fs-h1) / 1.2 var(--font-display);
149
+ --track-h1: -0.015em;
150
+ --type-h2: 700 var(--fs-h2) / 1.25 var(--font-display);
151
+ --track-h2: -0.01em;
152
+ --type-title: 650 var(--fs-title) / 1.3 var(--font-ui);
153
+ --type-body: 400 var(--fs-body) / 1.5 var(--font-ui);
154
+ --type-label: 600 var(--fs-label) / 1.2 var(--font-ui); /* chips, buttons */
155
+ --type-caption: 500 var(--fs-caption) / 1.35 var(--font-ui);
156
+ --type-overline:700 var(--fs-xs) / 1.2 var(--font-mono); /* uppercase micro-labels */
157
+ --track-overline: 0.08em;
158
+ --type-stat: 800 var(--fs-stat) / 1.05 var(--font-display); /* big numbers */
159
+
160
+ /* ============================================================
161
+ COLOR ROLES — wireframe defaults (grayscale).
162
+ palette.css overrides these under [data-palette="…"].
163
+ ============================================================ */
164
+
165
+ /* app background ("earth-toned surface or shiny color, up to the app") */
166
+ --surface: #f2f2f0;
167
+ --on-surface: #141414;
168
+ --on-surface-muted: #6b6b6b;
169
+
170
+ /* card boundaries — 3 slots so stacked/adjacent cards can differ */
171
+ --card-1-bg: #ffffff; --card-1-ink: #141414;
172
+ --card-2-bg: #e6e6e3; --card-2-ink: #141414;
173
+ --card-3-bg: #141414; --card-3-ink: #ffffff;
174
+
175
+ --card-border: #141414; /* wireframe: visible outline; palettes may set transparent */
176
+ --card-muted: #6b6b6b; /* secondary text inside cards (the default, tone-1 card) */
177
+ --card-2-muted: #5f5f5f; /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
178
+ a palette whose tone-2 ground is strong
179
+ sets this to its tone-2 ink — a muted shade cannot reach AA there (clay) */
180
+
181
+ /* actions */
182
+ --accent: #141414; /* primary CTA fill */
183
+ --on-accent: #ffffff;
184
+ --action-bg: #ffffff; /* neutral buttons/chips/inputs */
185
+ --action-ink: #141414;
186
+ --action-border: #141414;
187
+
188
+ /* signals (chip dots, deltas) */
189
+ --pos: #8a8a8a;
190
+ --neg: #3d3d3d;
191
+ --dot-a: #141414;
192
+ --dot-b: #8a8a8a;
193
+
194
+ /* derived tints — read the element's own ink, so they work on any slot */
195
+ --outline: var(--action-border); /* control edges (palette.css softens) */
196
+ --hairline: color-mix(in srgb, currentColor 18%, transparent); /* dividers inside cards */
197
+ --ink-tint: color-mix(in srgb, currentColor 12%, transparent); /* hover, rails, rests */
198
+ --ink-rail: color-mix(in srgb, currentColor 30%, transparent); /* todo steps, ghost edges */
199
+ --accent-soft: color-mix(in srgb, var(--accent) 22%, transparent); /* calendar marks */
200
+ --accent-mid: color-mix(in srgb, var(--accent) 50%, transparent);
201
+ --scrim: color-mix(in srgb, var(--card-3-bg) 50%, transparent); /* dim stage, cover gradient */
202
+ }
203
+
204
+ /* ============================================================
205
+ REGISTERED TOKENS — the two sheet lengths are typed, so that
206
+ cardds.js can read them back RESOLVED: getComputedStyle returns a
207
+ registered <length> in px and a <number> as a number, where an
208
+ unregistered custom property only ever reads back as its text
209
+ ("calc(1.25rem + …)"). Drag-to-snap needs to know where the states
210
+ are; nothing else in the system reads a token from JS.
211
+ --sheet-half is a share of the sheet's own height (0.5 = half), a
212
+ number rather than a percentage so one token can serve both the
213
+ sheet's translate (× 100%) and its body's scroll-padding (× 100cqh).
214
+ sheet.css sets the values; these initial values are what a sheet
215
+ falls back to when its own declaration cannot resolve.
216
+ ============================================================ */
217
+ @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
218
+ @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
219
+
220
+ /* ---- css/palette.css — color settings — the file you tune ---- */
221
+ /* ============================================================
222
+ cardds/palette.css — COLOR SETTINGS. The only file you tune.
223
+ Apply with <html data-palette="clay"> (or <body>).
224
+ No attribute = wireframe grayscale from tokens.css.
225
+ Each palette overrides role tokens only — components never change.
226
+ ============================================================ */
227
+
228
+ /* ---- CLAY — earth-toned: cream surface, terracotta / mustard / ink cards ---- */
229
+ [data-palette="clay"] {
230
+ --surface: #ece5d8;
231
+ --on-surface: #191713;
232
+ --on-surface-muted: #6f675a;
233
+
234
+ --card-1-bg: #e8b647; --card-1-ink: #191713; /* mustard hero */
235
+ --card-2-bg: #e05c3a; --card-2-ink: #191713; /* terracotta */
236
+ --card-3-bg: #1c1a17; --card-3-ink: #f4efe4; /* ink */
237
+
238
+ --card-border: transparent;
239
+ --card-muted: #45412f; /* 5.5:1 on the mustard card — AA for captions and metas (#55503f was 4.3:1) */
240
+ --card-2-muted: var(--card-2-ink); /* terracotta holds 5:1 for its ink alone; nothing lighter passes AA, so tone 2 has no muted */
241
+
242
+ --accent: #1c1a17; --on-accent: #f4efe4;
243
+ --action-bg: #f7f3e9; --action-ink: #191713; --action-border: transparent;
244
+
245
+ --pos: #3e7d4f; --neg: #c23c1d;
246
+ --dot-a: #e05c3a; --dot-b: #6f6be0;
247
+ }
248
+
249
+ /* ---- SHINE — shiny: periwinkle surface, coral / black / gold cards ---- */
250
+ [data-palette="shine"] {
251
+ --surface: #6d6ae4;
252
+ --on-surface: #ffffff;
253
+ --on-surface-muted: #d3d2f7;
254
+
255
+ --card-1-bg: #eec85a; --card-1-ink: #16161a;
256
+ --card-2-bg: #e8624a; --card-2-ink: #16161a;
257
+ --card-3-bg: #1b1b1f; --card-3-ink: #ffffff;
258
+
259
+ --card-border: transparent;
260
+ --card-muted: #4c4a44;
261
+
262
+ --accent: #1b1b1f; --on-accent: #ffffff;
263
+ --action-bg: #ffffff; --action-ink: #16161a; --action-border: transparent;
264
+
265
+ --pos: #2e9e5b; --neg: #d6402a;
266
+ --dot-a: #e8624a; --dot-b: #6d6ae4;
267
+ }
268
+
269
+ /* ---- FIELD — warm peach editorial: cream surface, orange accent ---- */
270
+ [data-palette="field"] {
271
+ --surface: #f9e8dc;
272
+ --on-surface: #241f1b;
273
+ --on-surface-muted: #8a7d72;
274
+
275
+ --card-1-bg: #ffffff; --card-1-ink: #241f1b;
276
+ --card-2-bg: #f6d9c5; --card-2-ink: #241f1b;
277
+ --card-3-bg: #ef4e0b; --card-3-ink: #fff4ec;
278
+
279
+ --card-border: transparent;
280
+ --card-muted: #8a7d72;
281
+
282
+ --accent: #ef4e0b; --on-accent: #fff4ec;
283
+ --action-bg: #ffffff; --action-ink: #241f1b; --action-border: transparent;
284
+
285
+ --pos: #2e8f52; --neg: #d13b1a;
286
+ --dot-a: #ef4e0b; --dot-b: #241f1b;
287
+ }
288
+
289
+ /* ---- ROAST — coffee: warm cream surface, caramel accent ---- */
290
+ [data-palette="roast"] {
291
+ --surface: #f6f1e7;
292
+ --on-surface: #2b211a;
293
+ --on-surface-muted: #8c7f70;
294
+
295
+ --card-1-bg: #ffffff; --card-1-ink: #2b211a;
296
+ --card-2-bg: #e9dcc6; --card-2-ink: #2b211a;
297
+ --card-3-bg: #b97e3c; --card-3-ink: #fdf9f1;
298
+
299
+ --card-border: transparent;
300
+ --card-muted: #8c7f70;
301
+
302
+ --accent: #b97e3c; --on-accent: #fdf9f1;
303
+ --action-bg: #ffffff; --action-ink: #2b211a; --action-border: transparent;
304
+
305
+ --pos: #3e7d4f; --neg: #bf4028;
306
+ --dot-a: #b97e3c; --dot-b: #2b211a;
307
+ }
308
+
309
+ /* ---- HYDRO — cool shiny blue: airy surface, saturated blue CTA ---- */
310
+ [data-palette="hydro"] {
311
+ --surface: #e9f0fb;
312
+ --on-surface: #17203a;
313
+ --on-surface-muted: #64729a;
314
+
315
+ --card-1-bg: #ffffff; --card-1-ink: #17203a;
316
+ --card-2-bg: #cddcf7; --card-2-ink: #17203a;
317
+ --card-3-bg: #3d63e0; --card-3-ink: #f2f6ff;
318
+
319
+ --card-border: transparent;
320
+ --card-muted: #64729a;
321
+
322
+ --accent: #3d63e0; --on-accent: #f2f6ff;
323
+ --action-bg: #ffffff; --action-ink: #17203a; --action-border: transparent;
324
+
325
+ --pos: #2e8f52; --neg: #d13b1a;
326
+ --dot-a: #3d63e0; --dot-b: #eec85a;
327
+ }
328
+
329
+ /* Colored palettes trade wireframe outlines for soft elevation. */
330
+ [data-palette] :is(.card, .band-stack) { box-shadow: var(--shadow-card); }
331
+ /* …but controls keep a faint edge so they still read as controls */
332
+ [data-palette] { --outline: color-mix(in srgb, currentColor 25%, transparent); }
333
+
334
+ /* ---- css/base.css — reset, surface, type levels, section ---- */
335
+ /* ============================================================
336
+ cardds/base.css — reset, surface, typography levels, section
337
+ ============================================================ */
338
+
339
+ *, *::before, *::after { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
340
+
341
+ /* the scale: everything is rem, so this one number resizes the whole system.
342
+ 100% = the user's text size (16px by default); narrow phones step to 15/16 so
343
+ the 25rem column and its 48px taps keep their proportions on 320–360 screens.
344
+ --text-scale is an app's OWN text-size setting — a unitless number the app
345
+ writes on <html> (1 = as the browser has it; 1.1875 = larger). It multiplies
346
+ in here so it composes with the narrow-phone step instead of overwriting the
347
+ root, and so the app never writes a length from script. */
348
+ html { font-size: calc(100% * var(--text-scale, 1)); scrollbar-width: none; }
349
+ @media (max-width: 22.5em) { html { font-size: calc(93.75% * var(--text-scale, 1)); } }
350
+ html::-webkit-scrollbar { display: none; } /* Chromium/Safari */
351
+
352
+ body {
353
+ margin: 0;
354
+ background: var(--surface);
355
+ color: var(--on-surface);
356
+ font: var(--type-body);
357
+ -webkit-font-smoothing: antialiased;
358
+ }
359
+
360
+ /* components here set display (grid/flex), which outranks the UA's
361
+ [hidden] { display: none } — so the attribute silently does nothing on a
362
+ .btn, .icon-btn, .chip or .row. Restore it once, for all of them. */
363
+ [hidden] { display: none !important; }
364
+
365
+ img, svg { display: block; max-width: 100%; }
366
+ button { font: inherit; cursor: pointer; }
367
+
368
+ /* icon from icons.svg sprite: <svg class="icon"><use href="icons.svg#bell"/></svg> */
369
+ .icon { width: var(--icon); height: var(--icon); flex: none; }
370
+ .icon--sm { width: var(--icon-sm); height: var(--icon-sm); }
371
+ .icon--xs { width: var(--icon-xs); height: var(--icon-xs); } /* inside captions, chips, links */
372
+ .icon--lg { width: var(--icon-lg); height: var(--icon-lg); } /* empty states */
373
+
374
+ /* mobile screen column — the standard horizontal inset, and nothing vertical.
375
+ Top and bottom are zero on purpose: vertical room is only ever a RESERVE for
376
+ something fixed parked over the screen, so each bar adds its own and a page
377
+ without bars needs no override. .appbar reserves the top (actions.css),
378
+ .action-bar (actions.css) and .float-bar (layover.css) the bottom. */
379
+ .screen {
380
+ --screen-gap: var(--sp-5); /* published: a .step cancels it to touch the bar it follows (step.css) */
381
+ max-width: var(--screen-w);
382
+ margin-inline: auto;
383
+ padding-inline: var(--screen-pad);
384
+ display: grid;
385
+ gap: var(--screen-gap);
386
+ }
387
+ .screen > * { min-width: 0; } /* content never widens the column past the viewport */
388
+ /* a screen that carries its own palette (data-palette on the element, not on <html>)
389
+ paints its own surface, so a design tool can show one screen per palette */
390
+ .screen[data-palette] { background: var(--surface); color: var(--on-surface); }
391
+
392
+ /* app screen: exactly one phone height — a height, not a minimum, so that a
393
+ .step (step.css) has a bound to fill and its card and sheet scroll INSIDE
394
+ it instead of growing the page; and clip, not hidden, because a clipped
395
+ box is not a scroll container: nothing — not even the browser revealing a
396
+ focused field — can shift the screen up. A trailing .sheet-stack (stack.css) stretches
397
+ and bleeds off the bottom edge. */
398
+ .screen--fill {
399
+ display: flex;
400
+ flex-direction: column;
401
+ height: 100dvh;
402
+ overflow: clip;
403
+ position: relative; /* the box a .card--centre or a .screen__centre floats in (card.css) */
404
+ }
405
+ /* min-height: 0 — a flex item's floor is its content, and the stack's content
406
+ is its own full-height sheets, so without it the stack grew to the whole
407
+ screen and its open sheet ran past the bottom edge by the bars' height,
408
+ unreachable by scrolling (timebank home, 2026-09-13). The stack IS the room
409
+ left under the bars; every sheet fills exactly that. */
410
+ .screen--fill > .sheet-stack { flex: 1; min-height: 0; }
411
+
412
+ /* ---- typography levels ---- */
413
+ /* --fs-self: each level publishes its own size, so a consumer that scales a
414
+ heading (card.css --squeeze) can multiply it without knowing the level */
415
+ .t-display { font: var(--type-display); letter-spacing: var(--track-display); margin: 0; text-wrap: balance; --fs-self: var(--fs-display); }
416
+ .t-h1 { font: var(--type-h1); letter-spacing: var(--track-h1); margin: 0; --fs-self: var(--fs-h1); }
417
+ .t-h2 { font: var(--type-h2); letter-spacing: var(--track-h2); margin: 0; --fs-self: var(--fs-h2); }
418
+ .t-title { font: var(--type-title); margin: 0; --fs-self: var(--fs-title); }
419
+ .t-body { font: var(--type-body); margin: 0; }
420
+ .t-label { font: var(--type-label); }
421
+ .t-caption { font: var(--type-caption); }
422
+ .t-overline { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; }
423
+ .t-stat { font: var(--type-stat); letter-spacing: var(--track-display); margin: 0; font-variant-numeric: tabular-nums; }
424
+ .t-stat--mono { font-family: var(--font-mono); letter-spacing: 0; } /* clocks, ETAs, codes */
425
+ .t-stat--xl { font-size: var(--fs-hero); } /* the one number on a sheet */
426
+
427
+ .t-muted { color: var(--on-surface-muted); }
428
+ .card .t-muted { color: var(--card-muted); } /* muted follows the card's ink, not the surface's */
429
+ .card--3 .t-muted { color: inherit; opacity: 0.7; } /* card-muted is tuned for light cards; dim instead on the dark slot */
430
+ .t-caps { text-transform: uppercase; }
431
+ .grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
432
+
433
+ /* ---- section: frameless on-surface content + CTA (never a card) ---- */
434
+ .section {
435
+ display: grid;
436
+ gap: var(--sp-3);
437
+ padding-block: var(--sp-4);
438
+ }
439
+ .section > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
440
+ .section .cta-pack {
441
+ display: flex;
442
+ gap: var(--sp-2);
443
+ margin-top: var(--sp-2);
444
+ flex-wrap: wrap;
445
+ }
446
+
447
+ /* ---- empty state: on-surface, centered, optional CTA below ---- */
448
+ .empty-state {
449
+ display: grid;
450
+ justify-items: center;
451
+ text-align: center;
452
+ gap: var(--sp-2);
453
+ padding: var(--sp-12) var(--sp-6);
454
+ color: var(--on-surface-muted);
455
+ }
456
+ .empty-state > :is(h1, h2, h3) { color: var(--on-surface); } /* the heading in full ink, the words muted */
457
+ .empty-state__action { margin-top: var(--sp-2); }
458
+ /* inside a card (a section's "nothing yet"): the card's ink, the way .t-muted and .row__meta follow it */
459
+ .card .empty-state { color: var(--card-muted); padding-block: var(--sp-6); }
460
+ .card .empty-state > :is(h1, h2, h3) { color: var(--card-ink); }
461
+ .card--3 .empty-state { color: inherit; }
462
+ .card--3 .empty-state > :not(:is(h1, h2, h3)) { opacity: 0.7; }
463
+
464
+ /* ---- css/card.css — card boundary + list variants ---- */
465
+ /* ============================================================
466
+ cardds/card.css — the card boundary + list variants
467
+ One card = one boundary. Tone via .card--1/2/3 (role slots).
468
+ ============================================================ */
469
+
470
+ .card {
471
+ --card-bg: var(--card-1-bg); /* the slot, readable by children (avatar rings, fold) */
472
+ --card-ink: var(--card-1-ink);
473
+ --card-pad: var(--sp-5); /* published so .bleed can cancel it exactly */
474
+ --card-gap: var(--sp-4); /* published so a sheet's peek strip (sheet.css) is exact */
475
+ background: var(--card-bg);
476
+ color: var(--card-ink);
477
+ border: var(--border-w) solid var(--card-border);
478
+ border-radius: var(--r-card);
479
+ padding: var(--card-pad);
480
+ display: grid;
481
+ gap: var(--card-gap);
482
+ position: relative;
483
+ min-width: 0;
484
+ text-decoration: none; /* .card works as <a> too */
485
+ }
486
+ /* the card is a grid: a child with a wide min-content (inputs, fixed-width
487
+ rows) would otherwise widen the track past the card's padding on narrow
488
+ screens. Children shrink to the track; they never size the card. */
489
+ .card > * { min-width: 0; }
490
+
491
+ .card--2 { --card-bg: var(--card-2-bg); --card-ink: var(--card-2-ink); --card-muted: var(--card-2-muted); }
492
+ .card--3 { --card-bg: var(--card-3-bg); --card-ink: var(--card-3-ink); }
493
+
494
+ /* dark/accent slot: accent-toned controls would vanish — the slot re-points
495
+ the accent tokens, so every accent consumer (btn--primary, chip--on,
496
+ check, toggle, ring, day…) inverts without per-component rules */
497
+ .card--3 { --accent: var(--card-3-ink); --on-accent: var(--card-3-bg); }
498
+
499
+ /* header row: title left, floating action right — centered, since the
500
+ trailing control (chip/badge/icon-btn/link) is almost always taller than
501
+ a single line of title text; top-aligning them left the control visually
502
+ low against the title's optical center. A head with a genuinely tall,
503
+ multi-line title can opt back out with .card__head--top. */
504
+ .card__head {
505
+ display: flex;
506
+ align-items: center;
507
+ justify-content: space-between;
508
+ gap: var(--sp-3);
509
+ }
510
+ .card__head--top { align-items: flex-start; }
511
+
512
+ .card__media { border-radius: var(--r-media); overflow: hidden; position: relative; } /* relative: corner badges */
513
+ .card__end { text-align: right; display: grid; gap: var(--sp-0); justify-items: end; } /* trailing value + caption */
514
+ .card__media > img { width: 100%; height: auto; object-fit: cover; }
515
+
516
+ /* ---- the card skeleton: head · content · foot ----
517
+ Every card is a head (the heading left, hints / actions / a more-menu at
518
+ the far right), its content, and an optional foot of action buttons.
519
+ The head's leading child — the heading, or the div that wraps heading and
520
+ caption — takes the room, so a heading with nothing at the far right runs
521
+ all the way across; whatever trails it hugs its own width. Only the
522
+ heading (or the div that wraps it) is that leading child: a control that
523
+ leads a custom head — a chip, an avatar, a tile badge — keeps its own
524
+ width, else the rule stretched a chip into a bar and an avatar into an
525
+ ellipse (two agents and the wallet demo caught it, 2026-09-10). */
526
+ .card__head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
527
+
528
+ /* Skeleton head — the heading in a card's OWN head, and only there.
529
+ Two lines are the budget: cardds.js measures and steps --squeeze down so the
530
+ type shrinks to fit; the clamp holds a heading that still overflows to three
531
+ lines, never more. The level's own size comes through --fs-self (base.css),
532
+ so the multiplier is level-agnostic and a heading without --squeeze renders
533
+ exactly as before.
534
+ A STACK head is not a skeleton head — it is a one-line peek at its own size,
535
+ and "stack headers" below opts it out of the budget, the squeeze and the
536
+ clamp. cardds.js skips those same cards, so CSS and JS agree on what counts
537
+ as a skeleton head. Change one, change the other. */
538
+ .card__head :is(h1, h2, h3) {
539
+ font-size: calc(var(--fs-self, var(--fs-h2)) * var(--squeeze, 1));
540
+ overflow-wrap: anywhere;
541
+ display: -webkit-box;
542
+ -webkit-box-orient: vertical;
543
+ -webkit-line-clamp: 3;
544
+ overflow: hidden;
545
+ /* the clip would take the hair the tight leading leaves outside the line
546
+ box — a stacked tone mark on top, สระอู below (fonts.css). Padding is
547
+ inside the clip, the negative margin gives the room back, so the head
548
+ measures exactly as before. */
549
+ padding-block: 0.15em;
550
+ margin-block: -0.15em;
551
+ }
552
+
553
+ /* ---- bleed: a scrolling track that runs to the container's edges ----
554
+ For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
555
+ a tile grid: the TRACK reaches the edges so it reads as scrollable, while
556
+ the first and last item stay on the same line as everything else.
557
+ Reads --card-pad, so it is exact by construction and follows any container
558
+ that publishes one (.card today, a sheet or a screen the same way).
559
+ <div class="avatar-pick bleed">…</div>
560
+ Padding, not item margins: padding is inside the scrollable area, so the
561
+ last item scrolls fully clear of the edge instead of stopping against it.
562
+ scroll-padding keeps snap points on the content line, not the border edge. */
563
+ .bleed {
564
+ margin-inline: calc(-1 * var(--card-pad, 0px));
565
+ padding-inline: var(--card-pad, 0px);
566
+ scroll-padding-inline: var(--card-pad, 0px);
567
+ }
568
+ /* a bleeding child sits over the container's rounded corner, so a strip that
569
+ is genuinely first or last needs the corner clipped for it */
570
+ .card:has(> .bleed:first-child), .card:has(> .bleed:last-child) { overflow: clip; }
571
+
572
+ /* foot: the card's action buttons on one line. One button takes the width,
573
+ up to four share it in even parts — so the gaps read even too — and from
574
+ three on they drop their side padding so the labels keep the room. */
575
+ .card__foot { display: flex; gap: var(--sp-2); align-items: center; }
576
+ .card__foot > .btn { flex: 1 1 0; min-width: 0; }
577
+ .card__foot:has(> :nth-child(3)) > .btn { padding-inline: var(--sp-2); }
578
+
579
+ /* media placeholder for wireframes — currentColor so it survives any palette */
580
+ .ph {
581
+ background:
582
+ linear-gradient(to top right, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px)),
583
+ linear-gradient(to top left, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px));
584
+ border: var(--border-w) solid currentColor;
585
+ border-radius: var(--r-media);
586
+ opacity: 0.4;
587
+ min-height: 10rem;
588
+ }
589
+ .ph--sm { min-height: 7.5rem; }
590
+ .ph--md { min-height: 9rem; }
591
+ /* ---- centred card: one thing to read, at the human centre of a fill screen ----
592
+ (.screen__centre below is the same placement for a small GROUP of things)
593
+ Where the centre of a phone IS, to the person holding it: the bottom fifth is
594
+ the thumb's — the bar, the controls — so what is meant to be read sits at the
595
+ middle of the four fifths above it, --screen-centre (40%) down the screen, not
596
+ at 50%. (Lh, 2026-09-10.) Any screen with one
597
+ card and nothing else to read may.
598
+ <div class="screen screen--fill"><header class="topbar">…</header><article class="card card--centre">…</article></div>
599
+ <div class="action-bar">…</div>
600
+ The card hugs its words and keeps the screen's inset. It is capped so that it
601
+ never reaches the title bar above it nor the bar's room below it — past that it
602
+ scrolls inside — from the same tokens those bars reserve their room with. */
603
+ .card--centre,
604
+ .screen--fill > .screen__centre {
605
+ position: absolute;
606
+ top: var(--screen-centre);
607
+ translate: 0 -50%;
608
+ /* the cap: twice the room above the centre (the title bar's height, when
609
+ one is there, and the screen's gap), or twice the room below it (the
610
+ bar's reserve) — whichever is less. --_lead is the title bar's height
611
+ only on a screen that HAS one: without a .topbar/.appbar before it the
612
+ block keeps that room (a deck and its two buttons, a card and its pager
613
+ under it, need it on a short phone; Lh, 2026-09-11). */
614
+ --_lead: 0rem;
615
+ max-height: calc(2 * min(var(--screen-centre) - var(--_lead) - var(--screen-gap), 100% - var(--screen-centre) - var(--bar-reserve)));
616
+ overflow-y: auto;
617
+ overscroll-behavior: contain;
618
+ }
619
+ :is(.topbar, .appbar) ~ :is(.card--centre, .screen__centre) { --_lead: var(--header-h); }
620
+ .card--centre { inset-inline: var(--screen-pad); }
621
+
622
+ /* ---- centred block: the same place, for one thing to read that is not a
623
+ lone card — an empty state, a card with its quiet second button and the
624
+ pager under it, a deck with its two round buttons. The block runs edge to
625
+ edge and keeps the screen's inset as padding, so a deck's fanned cards can
626
+ overhang and clip at the screen's edges (correct — the fold visual), and
627
+ its children stack at the screen's own rhythm (Lh, 2026-09-11):
628
+ <div class="screen screen--fill"><header class="topbar">…</header>
629
+ <div class="screen__centre"><article class="card">…</article><button class="btn btn--quiet btn--block">…</button></div>
630
+ </div> */
631
+ .screen__centre {
632
+ inset-inline: 0;
633
+ padding-inline: var(--screen-pad);
634
+ display: grid;
635
+ gap: var(--screen-gap);
636
+ align-content: start;
637
+ }
638
+ .screen__centre > * { min-width: 0; }
639
+
640
+ /* ---- the same group on a plain .screen: centred when short, as long as it
641
+ likes when not (Lh, 2026-09-12) ----
642
+ The member sees one content area — whatever is between the top bar and the
643
+ bar — and reads it as a whole page: a short card sits at its middle, a long
644
+ one simply makes the page longer, runs under the bar (the bar's reserve is
645
+ already the screen's bottom padding, actions.css) and scrolls up like any
646
+ page — nothing is capped, nothing shrinks, no scroll box inside a box.
647
+ So on a .screen that is NOT --fill the group is in flow: the column is at
648
+ least one phone tall, the group takes the room after the top bar and
649
+ centres its children in it. The screen's own inset stays the inset.
650
+ <div class="screen"><header class="topbar">…</header>
651
+ <div class="screen__centre"><article class="card">…</article><div class="pager__at">…</div></div>
652
+ </div><div class="action-bar">…</div> */
653
+ .screen:not(.screen--fill):has(> .screen__centre) { min-height: 100dvh; display: flex; flex-direction: column; }
654
+ .screen:not(.screen--fill) > .screen__centre { flex: 1; padding-inline: 0; align-content: center; }
655
+
656
+ /* ---- the card that changes slides: out one side, the next in from the other ----
657
+ A .screen__centre group that walks pages (data-slide="next" | "back" on the
658
+ root while a step changes) turns its card like a page: the old card slides
659
+ out, the new one slides in from the side the move came from — quick, the
660
+ heading and the dots stay still (Lh, 2026-09-12). Built on view transitions:
661
+ the React adapter's slideTo() sets data-slide on :root, starts the transition
662
+ and updates inside it; a browser without them (the floor: Chrome 105 / iOS 16)
663
+ simply switches. Only the card is named; the rest of the page must not
664
+ cross-fade. */
665
+ .screen__centre > .card, .pages > .card[aria-current] { view-transition-name: centre-card; }
666
+
667
+ /* ---- pages: the cards of a walk, all one height ----
668
+ A sequence the member walks (an introduction) reads steadier when every card
669
+ is the same height (Lh, 2026-09-12): so all its cards are on the page at
670
+ once, stacked in one grid cell, and the cell is as tall as the tallest — at
671
+ any text size. The current card carries aria-current="step"; every other
672
+ one is inert (out of the tab order and the accessibility tree) and hidden
673
+ in place, keeping its height. A page turn is the same slideTo() as a lone card.
674
+ <div class="screen__centre"><div class="pages">
675
+ <article class="card" aria-current="step">…</article>
676
+ <article class="card" inert>…</article>
677
+ </div><div class="pager__at">…</div></div> */
678
+ .pages { display: grid; }
679
+ .pages > .card { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
680
+ .pages > .card[inert] { visibility: hidden; }
681
+ ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
682
+ ::view-transition-old(centre-card), ::view-transition-new(centre-card) {
683
+ animation-duration: var(--motion-slide);
684
+ animation-timing-function: ease-out;
685
+ mix-blend-mode: normal;
686
+ }
687
+ :root[data-slide="next"]::view-transition-old(centre-card) { animation-name: centre-card-out-left; }
688
+ :root[data-slide="next"]::view-transition-new(centre-card) { animation-name: centre-card-in-right; }
689
+ :root[data-slide="back"]::view-transition-old(centre-card) { animation-name: centre-card-out-right; }
690
+ :root[data-slide="back"]::view-transition-new(centre-card) { animation-name: centre-card-in-left; }
691
+ @keyframes centre-card-out-left { to { translate: -100% 0; opacity: 0; } }
692
+ @keyframes centre-card-out-right { to { translate: 100% 0; opacity: 0; } }
693
+ @keyframes centre-card-in-right { from { translate: 100% 0; opacity: 0; } }
694
+ @keyframes centre-card-in-left { from { translate: -100% 0; opacity: 0; } }
695
+ @media (prefers-reduced-motion: reduce) {
696
+ ::view-transition-old(centre-card), ::view-transition-new(centre-card) { animation: none; }
697
+ }
698
+ /* the page dots under the card sit centred under it — the indicator belongs
699
+ with the page that changes, never in the bar (Lh, 2026-09-12) */
700
+ .screen__centre > .pager__at { justify-self: center; }
701
+
702
+ /* compact card inside a card (nested note, promo) */
703
+ .card--sm { padding: var(--sp-4); --card-gap: var(--sp-2); border-radius: var(--r-card); }
704
+
705
+ /* the sheet stack (.sheet-stack) — sheets stacked, heads peeking — lives in
706
+ stack.css; a stacked sheet is a plain .card, the material */
707
+
708
+ /* trailing icon cluster in a card header */
709
+ .icon-row { display: flex; align-items: center; gap: var(--sp-4); opacity: 0.85; }
710
+
711
+ /* ---- card list: rows, each row its own boundary ---- */
712
+ .card-list { display: grid; gap: var(--sp-3); }
713
+
714
+ .card--row {
715
+ border-radius: var(--r-card);
716
+ padding: var(--sp-4);
717
+ display: flex;
718
+ align-items: center;
719
+ gap: var(--sp-3);
720
+ }
721
+ .card--row .card__body { flex: 1; display: grid; gap: var(--sp-0); }
722
+
723
+ /* leading thumb/icon in a row */
724
+ .card__thumb {
725
+ width: var(--tap-sm); height: var(--tap-sm);
726
+ flex: none;
727
+ border-radius: var(--r-control);
728
+ border: var(--border-w) solid currentColor;
729
+ display: grid;
730
+ place-items: center;
731
+ font: var(--type-label);
732
+ opacity: 0.85;
733
+ }
734
+
735
+ /* ---- inline stat block (revenue-style) ---- */
736
+ .stat { display: grid; gap: var(--sp-1); }
737
+ .stat__row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
738
+
739
+ .delta {
740
+ font: var(--type-caption);
741
+ font-weight: 700;
742
+ padding: var(--sp-1) var(--sp-2);
743
+ border-radius: var(--r-chip);
744
+ background: var(--card-3-bg);
745
+ color: var(--card-3-ink);
746
+ }
747
+ .delta--neg { background: var(--neg); color: var(--card-3-ink); } /* light ink on the signal color in every slot */
748
+
749
+ /* ---- css/stack.css — sheet stack: .sheet-stack — the menu, sheets stacked, heads peek, tap opens ---- */
750
+ /* ============================================================
751
+ cardds/stack.css — .sheet-stack: the menu.
752
+ Sheets stacked in one column like paper, every one a main section
753
+ of the app, their heads peeking, the front one open. Tap a peeked
754
+ head and that sheet comes to the front; its back button returns it.
755
+
756
+ cardds has two sheet systems and one material:
757
+ - sheet single (sheet.css): ONE sheet the member acts in — a short
758
+ entry in front of a .step's card, or an overlay in a .sheet-stage.
759
+ Its state is how far it slid: peek / half / full.
760
+ - sheet stack (this file): MANY sheets, each a section, stacked at
761
+ the middle of the home screen. A peek is the strip the next sheet
762
+ does not cover; open is one sheet brought to the front.
763
+ A stacked sheet is written as a .card — the material — directly
764
+ inside .sheet-stack; .sheet stays the single sheet's class. This
765
+ component was first named "card stack": wrong, and retired — the
766
+ members are sheets and the stack is the menu, not a list of cards.
767
+
768
+ <div class="sheet-stack sheet-stack--tap"> (drop --tap for a static stack)
769
+ <article class="card card--2">
770
+ <div class="card__head">
771
+ <button class="icon-btn icon-btn--sm card__back" aria-label="Back">…</button>
772
+ <h2 class="t-h2 t-caps">Shopping</h2>
773
+ <span class="icon-row">…</span> (optional trailing icons)
774
+ </div>
775
+ …the sheet's own content…
776
+ </article>
777
+ <article class="card">…</article> (last = the front sheet)
778
+ <div class="sheet-stack__base">…</div> (--closed only, optional: what the base shows above the peeks)
779
+ </div>
780
+ --closed (with --tap): nothing open on arrival — every sheet peeks at the
781
+ bottom, the headroom above is the base's (block below).
782
+ Direct children only, five at most (slots are :nth-child below):
783
+ first = the back of the stack, last = the front. README "Sheet stack
784
+ markup". cardds.js opens and closes a --tap stack; without it the
785
+ stack still renders, it just won't open.
786
+ ============================================================ */
787
+
788
+ /* ---- the stack: cascading peek sheets; heads peek, the set bleeds to
789
+ the screen's bottom edge, so bottom corners are square ---- */
790
+ /* step = card padding (20) + head (48) + 20 breathing below the title,
791
+ so every peeked strip shows the title with even space above and below */
792
+ /* one track, exactly the stack's own height: an auto track would size itself
793
+ to the tallest sheet's CONTENT (a sheet's `height: 100%` is cyclic while the
794
+ track is being sized), so a long sheet grew the track past the screen and
795
+ every sheet's 100% then resolved against that — the bottom of every open
796
+ sheet sat below the screen, unreachable (timebank home, 2026-09-13). */
797
+ .sheet-stack { display: grid; grid-template-rows: minmax(0, 1fr); --stack-step: 5.5rem; }
798
+
799
+ /* the stack breaks out of the screen inset, edge to edge, always: the peek
800
+ strips and the opened sheet span the screen's full width whatever the
801
+ viewport (it used to bleed only under a 27em media query — a stack in a
802
+ phone frame on a desktop kept the inset, and read as a narrower card;
803
+ Lh, 2026-09-11) */
804
+ .sheet-stack { margin-inline: calc(-1 * var(--screen-pad)); }
805
+ .sheet-stack > .card {
806
+ grid-area: 1 / 1; /* every card shares the stack's cell; later ones offset down */
807
+ border-bottom-left-radius: 0;
808
+ border-bottom-right-radius: 0;
809
+ border-bottom: 0;
810
+ align-content: start;
811
+ align-self: start; /* own natural height (padding + head) by default */
812
+ --card-gap: var(--sp-5); /* head→content gap = the strip's 20px rhythm */
813
+ z-index: var(--level, 1); /* explicit paint order, 1 = back of the stack */
814
+ }
815
+ /* THE FRONT SHEET is the last .card, not the last child: a --closed stack
816
+ may carry a .sheet-stack__base after its sheets (below). Written once here
817
+ as "a card no card follows" so every rule about the front sheet agrees. */
818
+ .sheet-stack > .card:not(:has(~ .card)) {
819
+ align-self: stretch; /* the front card fills the remaining stack height */
820
+ }
821
+ /* --_n: how many sheets the stack holds, read from the DOM by :has so the
822
+ closed geometry needs no script and no per-stack declaration */
823
+ .sheet-stack { --_n: 1; }
824
+ .sheet-stack:has(> .card:nth-child(2)) { --_n: 2; }
825
+ .sheet-stack:has(> .card:nth-child(3)) { --_n: 3; }
826
+ .sheet-stack:has(> .card:nth-child(4)) { --_n: 4; }
827
+ .sheet-stack:has(> .card:nth-child(5)) { --_n: 5; }
828
+ /* a card's place in the deck, declared here rather than measured in JS, so
829
+ a stack renders correctly — right offsets, right paint order — with no
830
+ script at all. --slot is that place in steps; the tap animation reads it
831
+ to know how far up to travel. Five is the practical peek depth. */
832
+ .sheet-stack > .card:nth-child(1) { --slot: 0; --level: 1; }
833
+ .sheet-stack > .card:nth-child(2) { --slot: 1; --level: 2; margin-top: var(--stack-step); }
834
+ .sheet-stack > .card:nth-child(3) { --slot: 2; --level: 3; margin-top: calc(2 * var(--stack-step)); }
835
+ .sheet-stack > .card:nth-child(4) { --slot: 3; --level: 4; margin-top: calc(3 * var(--stack-step)); }
836
+ .sheet-stack > .card:nth-child(5) { --slot: 4; --level: 5; margin-top: calc(4 * var(--stack-step)); }
837
+
838
+ /* stack headers: uniform — [back?] title left, icons right, one height,
839
+ same gap to content on every card */
840
+ .sheet-stack > .card > .card__head {
841
+ align-items: center;
842
+ justify-content: flex-start;
843
+ min-height: var(--tap);
844
+ }
845
+ /* NOT a skeleton head: a peek is one line at its own size, and the display
846
+ stack deliberately runs a second line under the next card. So it opts out of
847
+ the whole skeleton-head treatment — budget, squeeze and clamp. cardds.js
848
+ skips the same cards. */
849
+ .sheet-stack > .card > .card__head :is(h1, h2, h3) {
850
+ font-size: var(--fs-h1);
851
+ line-height: 1;
852
+ letter-spacing: -0.01em;
853
+ margin-right: auto; /* pins the icon cluster right */
854
+ display: block;
855
+ -webkit-line-clamp: none;
856
+ overflow: visible;
857
+ overflow-wrap: normal;
858
+ }
859
+
860
+ /* display stack: the giant title IS the peek — line one shows, line two is
861
+ clipped by the next card (recents / library lists) */
862
+ .sheet-stack--display { --stack-step: 6.5rem; }
863
+ .sheet-stack--display > .card > .card__head { align-items: start; min-height: 0; }
864
+ .sheet-stack--display > .card > .card__head :is(h1, h2, h3) {
865
+ font: var(--type-display);
866
+ letter-spacing: var(--track-display);
867
+ /* the FIRST line sits on the centre of the peek strip (--stack-step tall,
868
+ measured from the card's top edge); line two runs under the next card */
869
+ margin-top: calc((var(--stack-step) - 1lh) / 2 - var(--sp-5));
870
+ }
871
+ /* fanned stack: a toggle opens the stack wider so every peek shows more */
872
+ .sheet-stack--fanned { --stack-step: 9.5rem; }
873
+
874
+ .sheet-stack > .card:has(~ .card) { cursor: pointer; }
875
+ /* a peeked card in a plain stack hides its own body. A --tap stack does
876
+ NOT: there, every card is full height and full content always, and a
877
+ peek is simply the strip of it the next card doesn't cover — the way a
878
+ card in a real deck is never a shorter card, just a mostly hidden one. */
879
+ .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.card__head) { display: none; }
880
+
881
+ /* ============================================================
882
+ .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
883
+
884
+ A sheet (a .card — the material, sheet.css's single sheet is the same
885
+ stuff) is rigid: it can move in, move out, or be covered — it can
886
+ never change shape. So every sheet here is the SAME full height,
887
+ always, set once and never touched again by anything. A "peek" is
888
+ not a short sheet; it's a full sheet the next one happens to cover,
889
+ showing only the strip above it, exactly like a sheet of paper part
890
+ way out of a real stack.
891
+
892
+ Because of that, the whole animation is one property: translate.
893
+ Nothing resizes, nothing re-stacks — z-index is the plain resting
894
+ --level order and is never touched, since the order already says
895
+ what has to happen. Cards after the tapped one sit IN FRONT of it,
896
+ so they're what's covering it, so they're what must leave. Cards
897
+ before it are already BEHIND it, so they need do nothing at all
898
+ except let themselves be covered as it rises over them.
899
+
900
+ .is-open — the tapped card, moved up to the top of the stack. Its
901
+ own resting offset is margin-top (below); opening just
902
+ translates that offset back off, landing it flush at
903
+ the stack's top with its full height exactly filling
904
+ to the bottom.
905
+ .is-before — an earlier card, nudged one step down so it slips
906
+ under the next card's resting strip and is covered
907
+ there. Each carries its own --stagger (set in JS: 0
908
+ for the one nearest the tapped card, counting up
909
+ further back) so they tuck away, and later un-tuck,
910
+ one at a time.
911
+ .is-after — a later card, moved out below, together, no stagger.
912
+ ============================================================ */
913
+ .sheet-stack--tap > .card {
914
+ height: 100%; /* every card identical and full length, always — the one
915
+ size in this whole component, never toggled, never transitioned. At a
916
+ resting margin-top of n steps it simply overhangs the stack's bottom
917
+ by n steps (clipped by .screen--fill), so translating it up by exactly
918
+ that much lands it filling the stack precisely. */
919
+ overflow: clip;
920
+ transition: translate 0.35s ease; /* the only animated property here */
921
+ transition-delay: calc(var(--stagger, 0) * 70ms); /* 0 for everything
922
+ except a staggered is-before card, set in JS as it tucks or un-tucks */
923
+ }
924
+ .sheet-stack--tap > .card.is-open {
925
+ /* up by its own resting offset — pure movement, nothing resizes */
926
+ translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step));
927
+ cursor: default;
928
+ overflow-y: auto;
929
+ overflow-x: clip;
930
+ overscroll-behavior: contain;
931
+ }
932
+ /* the front sheet — the one open on arrival — never travels up (it is not
933
+ tappable), so its resting offset would leave its last n steps below the
934
+ screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
935
+ alone is cut to the room it actually has, and it scrolls like an opened
936
+ sheet does. Every other sheet keeps the one full height. */
937
+ .sheet-stack--tap:not(.sheet-stack--closed) > .card:not(:has(~ .card)) {
938
+ height: calc(100% - var(--slot, 0) * var(--stack-step));
939
+ overflow-y: auto;
940
+ overflow-x: clip;
941
+ overscroll-behavior: contain;
942
+ }
943
+ /* a scrolling sheet shows no scrollbar (a desktop with a mouse would paint one
944
+ down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself */
945
+ .sheet-stack--tap > .card { scrollbar-width: none; }
946
+ .sheet-stack--tap > .card::-webkit-scrollbar { display: none; }
947
+ .sheet-stack--tap > .card.is-before { translate: 0 var(--stack-step); }
948
+ .sheet-stack--tap > .card.is-after { translate: 0 100vh; }
949
+
950
+ /* ============================================================
951
+ .sheet-stack--closed — nothing open on arrival. Every sheet, the front
952
+ one too, is a peek at the BOTTOM of the stack; the room above the peeks
953
+ is the screen's base, free for whatever the screen wants there (a
954
+ picture, a greeting — timebank home, Lh 2026-09-15). Tap any head and
955
+ that sheet rises over the base to fill the stack; back drops it home.
956
+
957
+ Same rigid material, same one animated property. The sheets keep their
958
+ full height and their margin-top slots; what changes is where they REST:
959
+ translated down by the headroom, 100% − n × step — a card's 100% is the
960
+ stack's height, so the last peek strip ends exactly at the stack's
961
+ bottom edge. Open is translate to −slot × step, as before: the sheet
962
+ lands flush at the top. is-before tucks one step below its resting place.
963
+ ============================================================ */
964
+ .sheet-stack--closed { --_headroom: calc(100% - var(--_n) * var(--stack-step)); }
965
+ .sheet-stack--closed > .card { translate: 0 var(--_headroom); cursor: pointer; }
966
+ .sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
967
+ .sheet-stack--closed > .card.is-open { translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step)); }
968
+ .sheet-stack--closed > .card.is-after { translate: 0 100vh; }
969
+
970
+ /* .sheet-stack__base — the base's content, in the headroom of a closed stack:
971
+ behind every sheet (level 0), the stack's inset put back (the stack bleeds),
972
+ exactly the headroom tall so it can lay its content out against the
973
+ peeks (align-content: end puts it right above them). Written LAST, after
974
+ the sheets, so the sheets' :nth-child slots stay what they are. */
975
+ .sheet-stack > .sheet-stack__base {
976
+ grid-area: 1 / 1;
977
+ align-self: start;
978
+ z-index: 0;
979
+ height: var(--_headroom, 0px);
980
+ padding-inline: var(--screen-pad);
981
+ display: grid;
982
+ align-content: end;
983
+ gap: var(--card-gap, var(--sp-5));
984
+ overflow: clip;
985
+ }
986
+
987
+ /* back button: lives in a slot the header reserves once open, and DISSOLVES
988
+ into it — an opacity fade in place, never a slide in from the left (the
989
+ slide read as a second thing moving while the sheet itself moved; Lh,
990
+ 2026-09-11). Only the sheet travels; the button appears where it is. */
991
+ .sheet-stack--tap .card__head {
992
+ position: relative;
993
+ padding-left: 0;
994
+ transition: padding-left 0.35s ease;
995
+ }
996
+ .sheet-stack--tap > .card.is-open > .card__head {
997
+ padding-left: calc(var(--tap-sm) + var(--sp-3));
998
+ }
999
+ .sheet-stack--tap .card__back {
1000
+ position: absolute;
1001
+ left: 0;
1002
+ top: 50%;
1003
+ translate: 0 -50%;
1004
+ opacity: 0;
1005
+ visibility: hidden;
1006
+ transition: opacity 0.35s ease, visibility 0.35s;
1007
+ }
1008
+ .sheet-stack--tap > .card.is-open .card__back {
1009
+ opacity: 1;
1010
+ visibility: visible;
1011
+ }
1012
+
1013
+ /* ---- css/sheet.css — sheet single: .sheet — one card as a bottom sheet, peek / half / full ---- */
1014
+ /* ============================================================
1015
+ cardds/sheet.css — sheet single: one card as a bottom sheet.
1016
+ Three states: .sheet--peek · .sheet--half · .sheet--full
1017
+
1018
+ A sheet is rigid material, exactly like a sheet in .sheet-stack--tap
1019
+ (stack.css): it is ALWAYS the full height of the box that holds it —
1020
+ set once by inset: 0, never written again, never transitioned — and
1021
+ a state is nothing but how far that full card has slid up. Peek is
1022
+ a full sheet that slid down until only its handle and title show;
1023
+ half is one that slid down by half of itself; full is one that has
1024
+ not slid at all. The box clips whatever slid below its bottom edge,
1025
+ so a sheet at any height is still a card: rounded top, a straight
1026
+ cut at the box's edge, its content scrolling inside.
1027
+
1028
+ So translate is the only animated property. Nothing here toggles
1029
+ height, display or z-index for a state — every past bug in the
1030
+ stack's animation came from exactly that, and the sheet inherits
1031
+ the rule.
1032
+
1033
+ Two boxes hold sheets, and the state classes mean the same in both:
1034
+ - .step (step.css) — an app screen where the member acts: the
1035
+ reading card behind, the sheet in front, .action-bar outside.
1036
+ - .sheet-stage (below) — a page-level overlay stage: a sheet over
1037
+ a map, the dim page sheet, the ask drawer (layover.css).
1038
+
1039
+ This is the SINGLE sheet: one sheet the member acts in, whatever it
1040
+ holds — a short entry in a step, a place on a map, a payment on a dim
1041
+ page. Many sheets stacked as the app's menu are the other system,
1042
+ .sheet-stack (stack.css): the same material, its own class and file.
1043
+
1044
+ <article class="card sheet sheet--half" data-sheet-states="peek half full">
1045
+ <button class="sheet__handle" type="button" aria-label="…"></button>
1046
+ <div class="card__head"><h2 class="t-h2">…</h2>…</div>
1047
+ <div class="sheet__body">…controls, or small content cards…</div>
1048
+ </article>
1049
+ Written top-down: the handle, the title row (what peek shows), then
1050
+ the body. The handle and the head stay where they are at every
1051
+ state; the body is what scrolls — a sheet is a card whose head is
1052
+ pinned, not a scrolling column with a title somewhere in it.
1053
+ cardds.js adds the handle's click / drag / keyboard and the focus
1054
+ raise; without it every state still renders.
1055
+ ============================================================ */
1056
+
1057
+ .sheet {
1058
+ --_handle: var(--sp-1);
1059
+ /* the strip a peeked sheet shows: the card's own top padding, the handle,
1060
+ the gap, one title row (--tap — the head rule below makes every head at
1061
+ least that tall) and the gap under it, so the cut lands exactly where
1062
+ the first control starts and nothing of it leaks through. Published
1063
+ (and registered in tokens.css) so a sheet whose head is taller can
1064
+ retune it: .my-sheet { --sheet-peek: … } */
1065
+ --sheet-peek: calc(var(--card-pad) + var(--_handle) + 2 * var(--card-gap) + var(--tap));
1066
+ --sheet-half: 0.5; /* a share of the sheet's own height */
1067
+ /* the sheet's own height, seen from inside it: the box it slides in is the
1068
+ size container (.step, .sheet-stage), so 100cqh is that box, less what
1069
+ the sheet gives up at its top (--_top) and bottom (--_lift). The sheet
1070
+ cannot be its own container — cq units measure a content box, and the
1071
+ sheet's is its border box less the card's padding. */
1072
+ --_box: calc(100cqh - var(--_top, 0rem) - var(--_lift, 0rem));
1073
+ /* the resting offset of the current state, said twice: as a share of the
1074
+ sheet's own height for its translate (--_y), and as a length for the
1075
+ body's scroll-padding (--_cover) — a percentage down there would be of
1076
+ the body, not of the sheet. Half by default. */
1077
+ --_y: calc(var(--sheet-half) * 100%);
1078
+ --_cover: calc(var(--sheet-half) * var(--_box));
1079
+
1080
+ position: absolute;
1081
+ inset: 0;
1082
+ top: var(--_top, 0); /* a stage keeps its top bar's line free — see .sheet-stage */
1083
+ translate: 0 calc(var(--_y) + var(--_drag, 0rem)); /* --_drag: cardds.js, only while the handle is held */
1084
+ transition: translate var(--motion-sheet) ease;
1085
+
1086
+ /* handle · head · body: a column, the body takes what is left and scrolls
1087
+ (below) — so the handle and the head never move, whatever the content */
1088
+ display: flex;
1089
+ flex-direction: column;
1090
+ overflow: clip;
1091
+ border-radius: var(--r-card) var(--r-card) 0 0;
1092
+ border-bottom: 0;
1093
+ box-shadow: var(--shadow-sheet);
1094
+ }
1095
+ .sheet--full { --_y: 0%; --_cover: 0rem; }
1096
+ .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
1097
+ .sheet--peek { --_y: calc(100% - var(--sheet-peek)); --_cover: calc(var(--_box) - var(--sheet-peek)); }
1098
+ .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
1099
+
1100
+ /* the body — everything under the head, the one thing in a sheet that
1101
+ scrolls. Its scrollport runs to the card's side and bottom edges (reading
1102
+ --card-pad, the way .bleed does) so content scrolls clear of the edge
1103
+ rather than stopping inside the padding, and a .bleed strip inside it
1104
+ still reaches the edges. The card's own grid rhythm continues inside. */
1105
+ .sheet__body {
1106
+ flex: 1;
1107
+ min-height: 0;
1108
+ display: grid;
1109
+ gap: var(--card-gap);
1110
+ align-content: start;
1111
+ margin-inline: calc(-1 * var(--card-pad));
1112
+ padding-inline: var(--card-pad);
1113
+ overflow-y: auto;
1114
+ overscroll-behavior: contain;
1115
+ /* the part of the sheet that slid under the box's bottom edge cannot be
1116
+ seen, and the scroller has to know that: scroll-padding is exactly "the
1117
+ obscured part of my scrollport", so a focused field, scrollIntoView and
1118
+ anchor navigation all land a control in the visible part, never under
1119
+ the cut. A length from the box's container units (--_box), since the
1120
+ cut is a share of the SHEET's height, not of the body's. */
1121
+ scroll-padding-bottom: var(--_cover);
1122
+ }
1123
+ .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
1124
+ .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: var(--card-pad); }
1125
+
1126
+ /* grab handle — first child of the sheet. Drawn as a small bar, but it is
1127
+ the thumb's grab area for the whole strip above the title: a pseudo widens
1128
+ its hit box to --tap tall and wider than the bar without changing the
1129
+ picture. touch-action: none hands a touch drag to cardds.js instead of to
1130
+ the scroller behind it. */
1131
+ .sheet__handle {
1132
+ position: relative;
1133
+ border: 0;
1134
+ color: inherit; /* a <button> has its own UA colour; the bar is the card's ink */
1135
+ background: currentColor;
1136
+ opacity: 0.3;
1137
+ width: var(--tap-sm);
1138
+ height: var(--_handle);
1139
+ border-radius: var(--r-chip);
1140
+ margin: 0 auto;
1141
+ padding: 0;
1142
+ touch-action: none;
1143
+ cursor: grab;
1144
+ }
1145
+ .sheet__handle::before {
1146
+ content: "";
1147
+ position: absolute;
1148
+ inset: calc((var(--_handle) - var(--tap)) / 2) calc(-1 * var(--sp-8));
1149
+ }
1150
+ .sheet__handle:focus-visible { outline: var(--focus); outline-offset: var(--sp-2); }
1151
+ .sheet.is-dragging .sheet__handle { cursor: grabbing; }
1152
+
1153
+ /* the head — a title row at least --tap tall, either kind, so the peek strip
1154
+ (--sheet-peek) is the same for every sheet:
1155
+ - in a step it is a plain card head (card.css): the heading left, the
1156
+ contextual thing at the far right, the two-line budget and the squeeze
1157
+ - .sheet__head is the stage sheet's three-column head: a round button,
1158
+ the centred title and sub, a round button
1159
+ - a label .row with its .check / .toggle may be the head when the sheet's
1160
+ whole ask is that switch: peek shows it (a row is --tap tall plus its own
1161
+ padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
1162
+ .sheet > .card__head { min-height: var(--tap); }
1163
+ .sheet__head {
1164
+ display: grid;
1165
+ grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
1166
+ align-items: center;
1167
+ gap: var(--sp-3);
1168
+ min-height: var(--tap);
1169
+ text-align: center;
1170
+ }
1171
+ .sheet__title { font: var(--type-h2); letter-spacing: var(--track-h2); margin: 0; }
1172
+ .sheet__sub { font: var(--type-caption); color: var(--card-muted); }
1173
+
1174
+ /* ============================================================
1175
+ .sheet-stage — the overlay stage: a page-height box the sheet
1176
+ slides in, over a map, a page, previous cards. It is its own
1177
+ 100dvh page, never a section of a scrolling screen — an app
1178
+ screen where the member acts is a .step (step.css).
1179
+ ============================================================ */
1180
+ .sheet-stage {
1181
+ /* the centre of the band a half sheet leaves open — from the bar's line to the sheet's
1182
+ top edge at half ((100% + --_top) / 2): where a hero or a map's ring sits */
1183
+ --_band-centre: calc((100% + 3 * var(--_top, 0rem)) / 4);
1184
+ --_top: var(--header-h); /* full stops UNDER the line a top bar takes rather than
1185
+ covering it — the same reserve .screen makes for a fixed .appbar — and
1186
+ keeps its corners; the sheet reads it as its top edge */
1187
+ position: relative;
1188
+ width: 100%; /* also as a grid/flex child: children are absolute, so it must size itself */
1189
+ height: 100dvh;
1190
+ max-width: var(--screen-w);
1191
+ margin-inline: auto;
1192
+ overflow: clip; /* not hidden: a clipped box is not a scroll container, so revealing a focused field never shifts the stage */
1193
+ container-type: size; /* the sheet's body reads this box's height as 100cqh (--_box above) */
1194
+ background: var(--surface);
1195
+ }
1196
+ /* on an app screen the stage is the page: it takes the screen's inset back (edge to
1197
+ edge, like a step) and, after a fixed .appbar, the room the screen reserved for
1198
+ that bar — the stage keeps the bar's line free itself (--_top), so the reserve
1199
+ would be paid twice and the page would scroll by it */
1200
+ .screen > .sheet-stage { margin-inline: calc(-1 * var(--screen-pad)); width: auto; } /* auto: 100% would keep the inset width */
1201
+ .appbar + .sheet-stage { margin-top: calc(-1 * (var(--header-h) + var(--header-gap, 0rem))); }
1202
+ .sheet-stage__bg { position: absolute; inset: 0; } /* map / previous cards behind */
1203
+ /* the hero: ONE thing shown in the band a half sheet leaves open (a postcard being made,
1204
+ a photo being chosen) — centred in that band like a map's ring, on the screen's inset;
1205
+ the sheet slides over it at full */
1206
+ .sheet-stage__hero {
1207
+ position: absolute;
1208
+ inset-inline: var(--screen-pad);
1209
+ top: var(--_band-centre);
1210
+ translate: 0 -50%;
1211
+ /* no z-index: it follows the background in DOM order and the sheet, later, covers it at full */
1212
+ }
1213
+
1214
+ /* ---- map: the box a map engine draws in, behind a stage sheet ----
1215
+ <div class="sheet-stage__bg"><div class="map"><div>…the engine's canvas…</div>
1216
+ <span class="map__ring" role="img" aria-label="ย่านของคุณโดยประมาณ"></span>
1217
+ <span class="map__hint">ลากแผนที่ให้วงครอบย่านที่คุณอยู่</span>
1218
+ <span class="map__credit">© OpenStreetMap</span></div></div>
1219
+ The first child is the engine's mount (the app hands it to Leaflet or the
1220
+ like); it fills the box. Before tiles arrive — or offline — the box shows the
1221
+ wireframe's street pattern, so a map is never a hole. The ring marks the
1222
+ rough area the member is choosing: fixed at the box's centre, so dragging
1223
+ the map underneath is what moves it (the centre IS the choice), never a
1224
+ pin to place. The hint rides at the top, under the bar's line. */
1225
+ .map {
1226
+ position: absolute;
1227
+ inset: 0;
1228
+ isolation: isolate; /* the ring and the hint stack inside the map only — a sheet over the map covers them */
1229
+ color: var(--on-surface);
1230
+ background:
1231
+ repeating-linear-gradient(75deg, transparent 0 5.5rem, var(--hairline) 5.5rem 6.25rem),
1232
+ repeating-linear-gradient(-18deg, transparent 0 8.75rem, var(--ink-tint) 8.75rem 9.25rem),
1233
+ var(--card-2-bg);
1234
+ }
1235
+ .map > :first-child { position: absolute; inset: 0; z-index: 0; isolation: isolate; } /* its own stacking context: an engine's layered panes stay under the ring */
1236
+ .sheet-stage__bg .map > :first-child { background: transparent; } /* an engine's own grey ground would hide the pattern while tiles load */
1237
+ .map__ring {
1238
+ position: absolute;
1239
+ left: 50%;
1240
+ /* centred in the band a half sheet leaves open (the stage's --_band-centre) — the
1241
+ member sees the whole ring while the sheet asks; the engine reads the point under it */
1242
+ top: var(--_band-centre);
1243
+ width: var(--map-ring);
1244
+ height: var(--map-ring);
1245
+ translate: -50% -50%;
1246
+ border-radius: 50%;
1247
+ border: var(--border-w) solid var(--accent);
1248
+ background: color-mix(in srgb, var(--accent) 22%, transparent);
1249
+ box-shadow: var(--shadow-float);
1250
+ pointer-events: none;
1251
+ z-index: 1; /* above the engine's panes (Leaflet's tiles sit at 200, but inside their own stacking context) */
1252
+ }
1253
+ .map__credit { /* the tile provider's credit — required by OSM; a caption, not the engine's own control */
1254
+ position: absolute;
1255
+ right: var(--sp-2);
1256
+ bottom: var(--sp-2);
1257
+ padding: var(--sp-0) var(--sp-2);
1258
+ border-radius: var(--r-chip);
1259
+ background: color-mix(in srgb, var(--surface) 80%, transparent);
1260
+ color: var(--on-surface-muted);
1261
+ font: var(--type-caption);
1262
+ pointer-events: none;
1263
+ z-index: 1;
1264
+ }
1265
+ .map__hint {
1266
+ position: absolute;
1267
+ top: calc(var(--_top, 0rem) + var(--sp-3));
1268
+ left: 50%;
1269
+ translate: -50% 0;
1270
+ max-width: calc(100% - 2 * var(--sp-4));
1271
+ padding: var(--sp-2) var(--sp-4);
1272
+ border-radius: var(--r-chip);
1273
+ background: var(--card-3-bg);
1274
+ color: var(--card-3-ink);
1275
+ font: var(--type-label);
1276
+ text-align: center;
1277
+ box-shadow: var(--shadow-float);
1278
+ pointer-events: none;
1279
+ z-index: 1;
1280
+ }
1281
+
1282
+ /* raised: the sheet floats above a footnote strip on the scrim. Its bottom
1283
+ edge shows, so it gets its corners and border back; full still stops at
1284
+ the top bar's line, because the top edge never moved. */
1285
+ .sheet--raised { --_lift: var(--tap-lg); bottom: var(--_lift); border-radius: var(--r-card); border-bottom: var(--border-w) solid var(--card-border); }
1286
+ .sheet-stage__foot {
1287
+ position: absolute;
1288
+ inset-inline: 0;
1289
+ bottom: 0;
1290
+ height: var(--tap-lg);
1291
+ margin: 0;
1292
+ display: grid;
1293
+ place-items: center;
1294
+ font: var(--type-caption);
1295
+ color: var(--card-3-ink);
1296
+ z-index: 2;
1297
+ }
1298
+
1299
+ /* dim stage: blur + darken whatever sits behind the sheet */
1300
+ .sheet-stage--dim .sheet-stage__bg { filter: blur(var(--sp-2)); }
1301
+ .sheet-stage--dim::after {
1302
+ content: "";
1303
+ position: absolute;
1304
+ inset: 0;
1305
+ background: var(--scrim);
1306
+ z-index: 1;
1307
+ }
1308
+ .sheet-stage--dim .sheet { z-index: 2; }
1309
+
1310
+ /* ---- css/step.css — .step: one screen, one task — card, optional sheet, bar outside ---- */
1311
+ /* ============================================================
1312
+ cardds/step.css — .step: one screen, one task.
1313
+ What the member reads (a card), what the member does when the
1314
+ step asks for it (a sheet, sheet.css), and the screen's one
1315
+ move (.action-bar, actions.css) OUTSIDE the step, below it.
1316
+
1317
+ <section class="step">
1318
+ <article class="card step__card">…the words…</article> ← first
1319
+ <article class="card sheet sheet--half">…the controls…</article> ← last, optional
1320
+ </section>
1321
+ <div class="action-bar">…</div>
1322
+
1323
+ The step is the box the sheet slides in. It fills the screen
1324
+ column between the top bar's reserve (.screen already pads for
1325
+ .appbar) and the bar's, and clips at its own bottom edge — so the
1326
+ sheet, at every state, stops above the bar and nothing ever sits
1327
+ behind it. The card hugs its words and scrolls inside itself when
1328
+ the column runs out. The sheet is full-width, like the bar: the
1329
+ step bleeds to the screen's edges and the card takes the inset
1330
+ back, so the card and the bar's buttons stand on the inset line
1331
+ while the sheet runs edge to edge.
1332
+
1333
+ It needs a bounded column to fill: .screen--fill (base.css), or
1334
+ an app section of a definite height. Reads → card only. Acts →
1335
+ card + sheet. Never a hand-laid step, never a sheet height.
1336
+ ============================================================ */
1337
+ .step {
1338
+ position: relative;
1339
+ display: flex;
1340
+ flex-direction: column;
1341
+ flex: 1;
1342
+ min-height: 0;
1343
+ overflow: clip;
1344
+ margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge, reading the inset the screen publishes (as .sheet-stack does) */
1345
+ container-type: size; /* the sheet's body reads the step's height as 100cqh — sheet.css, --_box.
1346
+ (size containment also makes the step the containing block for fixed
1347
+ descendants: the bar is outside the step, and a popover is in the top layer) */
1348
+ /* --kb: the on-screen keyboard's height, published on :root by cardds.js
1349
+ from the visual viewport — 0 without one, and on a platform that shrinks
1350
+ the layout viewport itself. It grows the room below the step the same
1351
+ way it lifts the bar (actions.css), so the control being typed into and
1352
+ the bar both stay above the keyboard. */
1353
+ margin-bottom: var(--kb, 0);
1354
+ }
1355
+ /* the step touches the top bar it follows, so that the sheet at full reaches
1356
+ the bar's bottom edge and nothing else: the room the screen would have
1357
+ left between them moves INSIDE the step, above the card (--_lead), where
1358
+ the sheet can rise over it. After an in-flow .topbar that room is the
1359
+ screen's gap; after a fixed .appbar it is the reserve's breathing — both
1360
+ read from the tokens the screen publishes, never recomputed. A step that
1361
+ follows anything else keeps the screen's gap and the sheet stops under
1362
+ that thing. */
1363
+ .topbar + .step { margin-top: calc(-1 * var(--screen-gap)); --_lead: var(--screen-gap); }
1364
+ .appbar + .step { margin-top: calc(-1 * var(--header-gap)); --_lead: var(--screen-gap); }
1365
+
1366
+ /* the bar's room — read from actions.css, never recomputed. Margin, not
1367
+ padding, so the clip cuts at the box edge and nothing shows behind the
1368
+ bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
1369
+ frame — the bar is fixed to the viewport wherever it sits. */
1370
+ :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
1371
+
1372
+ /* the reading card: a plain card with a flex rule — it hugs its content,
1373
+ and when the column runs out (a short phone, large text) it shrinks and
1374
+ scrolls inside itself instead of pushing the bar or the sheet */
1375
+ .step__card {
1376
+ flex: 0 1 auto;
1377
+ min-height: 0;
1378
+ margin: var(--_lead, 0) var(--screen-pad) 0; /* the screen's gap, moved in here from above the step; the inset, taken back from the bleed */
1379
+ overflow-y: auto;
1380
+ overscroll-behavior: contain;
1381
+ }
1382
+
1383
+ /* ---- css/forms.css — outlined fields, add-row ---- */
1384
+ /* ============================================================
1385
+ cardds/forms.css — inputs + inline form actions
1386
+ .field: flat outlined input, the label IS the placeholder.
1387
+ Markup: <label class="field"><svg?/><input placeholder="Passport Number"></label>
1388
+ ============================================================ */
1389
+
1390
+ .field {
1391
+ display: flex;
1392
+ align-items: center;
1393
+ gap: var(--sp-2);
1394
+ border: var(--border-w) solid var(--action-border);
1395
+ border-radius: var(--r-control);
1396
+ background: var(--action-bg);
1397
+ color: var(--action-ink);
1398
+ padding-inline: var(--sp-4);
1399
+ min-height: var(--tap);
1400
+ min-width: 0;
1401
+ }
1402
+
1403
+ .field input,
1404
+ .field select {
1405
+ border: 0;
1406
+ background: transparent;
1407
+ color: inherit;
1408
+ font: var(--type-body);
1409
+ width: 100%;
1410
+ min-width: 0;
1411
+ outline-offset: var(--sp-1);
1412
+ }
1413
+ .field input::placeholder { color: inherit; opacity: 0.55; }
1414
+
1415
+ /* field with its action inside: date pick, send, go — the button sits flush right */
1416
+ .field--action { padding-right: var(--sp-1); min-height: var(--tap-lg); }
1417
+ .field--action > .icon-btn { margin-left: auto; }
1418
+
1419
+ /* colored palettes drop the visible outline elsewhere — fields keep one */
1420
+ [data-palette] .field { border-color: color-mix(in srgb, currentColor 25%, transparent); }
1421
+
1422
+ /* ---- add-row: inline "+ add another" action ---- */
1423
+ .add-row {
1424
+ display: inline-flex;
1425
+ align-items: center;
1426
+ gap: var(--sp-2);
1427
+ border: 0;
1428
+ background: none;
1429
+ color: inherit;
1430
+ font: var(--type-label);
1431
+ font-size: var(--fs-body);
1432
+ padding: var(--sp-2) 0;
1433
+ min-height: var(--tap-sm);
1434
+ }
1435
+ .add-row__plus {
1436
+ width: var(--icon);
1437
+ height: var(--icon);
1438
+ flex: none;
1439
+ border-radius: 50%;
1440
+ background: var(--accent);
1441
+ color: var(--on-accent);
1442
+ display: grid;
1443
+ place-items: center;
1444
+ font-size: var(--fs-md);
1445
+ line-height: 1;
1446
+ }
1447
+ .add-row__plus .icon { width: var(--icon-xs); height: var(--icon-xs); }
1448
+
1449
+ /* ---- css/journey.css — route, tile badge, note row ---- */
1450
+ /* ============================================================
1451
+ cardds/journey.css — trip/status primitives inside cards:
1452
+ route row, tile badge, note row. Generic: flights, buses,
1453
+ deliveries — anything with endpoints and a status.
1454
+ ============================================================ */
1455
+
1456
+ /* ---- route: endpoint · dotted connector (icon in middle) · endpoint ---- */
1457
+ .route { display: flex; align-items: center; gap: var(--sp-3); }
1458
+
1459
+ .route__point { display: grid; gap: var(--sp-0); min-width: 0; }
1460
+ .route__point--end { text-align: right; }
1461
+
1462
+ .route__code { font: var(--type-h2); letter-spacing: var(--track-h2); }
1463
+ .route__time { font: var(--type-body); font-variant-numeric: tabular-nums; }
1464
+ .route__place { font: var(--type-caption); color: var(--card-muted); }
1465
+ .card--3 .route__place { color: inherit; opacity: 0.7; }
1466
+
1467
+ .route__via {
1468
+ flex: 1;
1469
+ display: flex;
1470
+ align-items: center;
1471
+ gap: var(--sp-1);
1472
+ opacity: 0.65;
1473
+ font-size: var(--fs-body);
1474
+ }
1475
+ .route__via::before,
1476
+ .route__via::after {
1477
+ content: "";
1478
+ flex: 1;
1479
+ border-top: var(--rule-w) dotted currentColor;
1480
+ }
1481
+
1482
+ /* ---- tile badge: tiny label over a big value (gate, seat, zone) ---- */
1483
+ .tile-badge {
1484
+ display: inline-grid;
1485
+ justify-items: center;
1486
+ gap: var(--sp-0);
1487
+ padding: var(--sp-1) var(--sp-2);
1488
+ border-radius: var(--r-badge);
1489
+ background: var(--card-3-bg);
1490
+ color: var(--card-3-ink);
1491
+ }
1492
+ .tile-badge__label { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; opacity: 0.7; }
1493
+ .tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
1494
+
1495
+ /* plain: no fill — a date box beside a title (Jun / 18) */
1496
+ .tile-badge--plain { background: transparent; color: inherit; padding: 0; min-width: var(--tap-sm); }
1497
+ .tile-badge--plain .tile-badge__value { font: 800 var(--fs-h1)/1 var(--font-display); }
1498
+
1499
+ /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */
1500
+ .tile-badge--corner { position: absolute; right: var(--sp-3); bottom: var(--sp-3); }
1501
+
1502
+ /* ---- note row: icon + caption (transit hints, fine print) ---- */
1503
+ .note-row {
1504
+ display: flex;
1505
+ align-items: center;
1506
+ gap: var(--sp-2);
1507
+ font: var(--type-caption);
1508
+ color: var(--card-muted);
1509
+ }
1510
+ .card--3 .note-row { color: inherit; opacity: 0.7; }
1511
+
1512
+ /* ---- css/actions.css — topbar, chips, buttons, CTA pack ---- */
1513
+ /* ============================================================
1514
+ cardds/actions.css — contextual commands around/inside cards:
1515
+ topbar, chips, buttons, selects, bottom CTA pack
1516
+ ============================================================ */
1517
+
1518
+ /* ---- the bars across the top of a screen: one height, whatever they carry ----
1519
+ .topbar (screen title) and .appbar (app context) are the same bar with
1520
+ different contents, so the height is declared here once rather than left to
1521
+ whichever control happens to be tallest. Measured before this rule existed:
1522
+ a title-only topbar 46px, one with an .icon-btn--sm 60px, with a full-size
1523
+ .icon-btn 64px, with a .btn 70px. --header-h is a floor, not a cap — a bar
1524
+ given a deliberately taller control still grows instead of clipping it. */
1525
+ .topbar, .appbar {
1526
+ display: flex;
1527
+ align-items: center;
1528
+ gap: var(--sp-3);
1529
+ padding-block: var(--sp-2);
1530
+ min-height: var(--header-h);
1531
+ }
1532
+
1533
+ /* ---- top bar: back + screen title ---- */
1534
+ .topbar .t-h1 {
1535
+ text-transform: uppercase;
1536
+ flex: 1;
1537
+ min-width: 0;
1538
+ overflow: hidden;
1539
+ text-overflow: ellipsis;
1540
+ white-space: nowrap;
1541
+ padding-block: 0.15em; /* Thai marks survive the clip — see card.css, the head rule */
1542
+ margin-block: -0.15em;
1543
+ }
1544
+ .topbar__actions { margin-left: auto; display: flex; gap: var(--sp-2); }
1545
+
1546
+ /* a screen whose one big move sits at the top right (home's "ขอความช่วยเหลือ" — Lh 2026-09-09:
1547
+ no bottom bar on the home screen) puts a primary .btn in the top bar's actions; it stands at
1548
+ the hero height so it is the easiest thing on the screen to hit */
1549
+ .topbar__actions > .btn--primary { min-height: var(--tap-lg); }
1550
+
1551
+ /* ---- appbar: app context/brand switcher (logo · two-line label · chevron) ---- */
1552
+ .appbar {
1553
+ position: fixed;
1554
+ inset-inline: 0;
1555
+ top: 0;
1556
+ max-width: var(--screen-w);
1557
+ margin-inline: auto;
1558
+ padding-inline: var(--screen-pad); /* fixed + full-bleed: the screen's own inset, so the bar's contents line up with the title bar under it (Lh 2026-09-15) */
1559
+ z-index: 40; /* topmost — card animations never cover it */
1560
+ background: var(--surface); /* solid: sliding cards never show through */
1561
+ }
1562
+ /* fixed, so it covers the top of the screen it sits in — reserve its height
1563
+ there, from the same token, the way the bottom bars reserve theirs. The
1564
+ breathing under it is published: a .step cancels it to touch the bar. */
1565
+ .screen:has(> .appbar) { --header-gap: var(--sp-2); padding-top: calc(var(--header-h) + var(--header-gap)); }
1566
+ .appbar__brand { flex: 1; display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
1567
+ .appbar__text { display: grid; min-width: 0; flex: 1; } /* pushes trailing button to the right edge */
1568
+ .appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.35; } /* clipped lines: room for Thai marks */
1569
+ .appbar__title { font: var(--type-label); }
1570
+ .appbar__sub { font: var(--type-caption); color: var(--on-surface-muted); }
1571
+ /* the bar's trailing controls (one or two: a labelled switch chip, a wallet button — home,
1572
+ Lh 2026-09-13) stand on the tap floor: a chip that is a switch grows to --tap here */
1573
+ .appbar .chip--toggle { min-height: var(--tap); }
1574
+
1575
+ /* ---- icon button (round) ---- */
1576
+ .icon-btn {
1577
+ width: var(--tap); height: var(--tap);
1578
+ flex: none;
1579
+ border-radius: var(--r-chip);
1580
+ border: var(--border-w) solid var(--outline); /* see .btn — same reason */
1581
+ background: var(--action-bg);
1582
+ color: var(--action-ink);
1583
+ display: grid;
1584
+ place-items: center;
1585
+ font-size: var(--fs-lg);
1586
+ line-height: 1;
1587
+ }
1588
+ .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; }
1589
+ .icon-btn--sm { width: var(--tap-sm); height: var(--tap-sm); font-size: var(--fs-title); }
1590
+ /* fab: the square floating action, bottom-right of a stage or screen */
1591
+ .fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3)); box-shadow: var(--shadow-float); z-index: 31; }
1592
+
1593
+ /* ---- buttons ---- */
1594
+ .btn {
1595
+ /* --outline, not --action-border: same reason as .chip — on a palette where
1596
+ --card-1-bg equals --action-bg a transparent border makes a plain button
1597
+ invisible on a white card. --primary and --quiet set their own edge. */
1598
+ border: var(--border-w) solid var(--outline);
1599
+ background: var(--action-bg);
1600
+ color: var(--action-ink);
1601
+ border-radius: var(--r-chip);
1602
+ padding: var(--sp-4) var(--sp-6);
1603
+ font: var(--type-label);
1604
+ font-size: var(--fs-md);
1605
+ display: inline-flex;
1606
+ align-items: center;
1607
+ justify-content: center;
1608
+ gap: var(--sp-2);
1609
+ min-height: var(--btn-h);
1610
+ text-decoration: none; /* .btn works as <a> too */
1611
+ }
1612
+ .btn--primary { background: var(--accent); color: var(--on-accent); border-color: transparent; }
1613
+ /* quiet keeps a faint outline so it still reads as a button on colored palettes;
1614
+ it is ink on whatever it sits on — inherit, or on the dark slot (card--3) it
1615
+ would take the light-card action ink and vanish */
1616
+ .btn--quiet { background: transparent; color: inherit; border-color: color-mix(in srgb, currentColor 35%, transparent); }
1617
+ .btn--block { width: 100%; }
1618
+ /* xl: the one giant call to action in a tinted card ("Taxi!") */
1619
+ .btn--xl { min-height: var(--btn-xl-h); font: 800 var(--fs-3xl)/1 var(--font-display); letter-spacing: var(--track-h1); border-radius: var(--r-card); padding-inline: var(--sp-8); }
1620
+ /* disabled keeps the slot — a control that vanishes moves everything else,
1621
+ and a pager whose "previous" disappeared on page one would shift the bar
1622
+ under the thumb on every page */
1623
+ :is(.btn, .icon-btn):disabled { opacity: 0.4; cursor: default; }
1624
+ /* a row of buttons sharing the width (twin CTAs inside a card) */
1625
+ .btn-row { display: flex; gap: var(--sp-2); }
1626
+ .btn-row > .btn { flex: 1; min-width: 0; }
1627
+
1628
+ /* ---- chip: status/attribute tag, optional signal dot ---- */
1629
+ .chip {
1630
+ display: inline-flex;
1631
+ align-items: center;
1632
+ gap: var(--sp-2);
1633
+ border-radius: var(--r-chip);
1634
+ /* --outline, not --action-border: a chip is a control, and on a palette
1635
+ where --card-1-bg == --action-bg (field, roast, hydro) a transparent
1636
+ border makes it invisible on a white card. Same edge fields, checks,
1637
+ toggles, tiles and the dropdown already use. */
1638
+ border: var(--border-w) solid var(--outline);
1639
+ background: var(--action-bg);
1640
+ color: var(--action-ink);
1641
+ padding: var(--sp-2) var(--sp-3);
1642
+ font: var(--type-label);
1643
+ font-size: var(--fs-sm);
1644
+ min-height: var(--chip-h);
1645
+ text-decoration: none;
1646
+ justify-self: start; /* as a grid child it hugs its text, never stretches */
1647
+ }
1648
+ .dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); } /* signal dot: chips, legends */
1649
+ .dot--b { background: var(--dot-b); }
1650
+ .chip--on { background: var(--accent); color: var(--on-accent); border-color: transparent; }
1651
+ /* dark-slot inversion lives in card.css (.card--3 :is(...)) */
1652
+
1653
+ .chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
1654
+
1655
+ /* ---- dropdown: the standard for every select — a pill trigger + .menu popover ----
1656
+ <div class="dropdown"><button class="dropdown__btn" popovertarget="f1"><span class="dropdown__label">this week</span><svg class="icon icon--xs"><use href="icons.svg#chevron-down"/></svg></button>
1657
+ <div class="menu" id="f1" popover><button class="menu__item" aria-current="true" data-value="week">this week</button>…</div></div>
1658
+ cardds.js copies the picked item into the label and fires "change" on .dropdown.
1659
+ .menu (layover.css) draws the panel; anchor positioning pins it under the trigger. */
1660
+ .dropdown { position: relative; min-width: 0; display: inline-grid; }
1661
+ .dropdown__btn {
1662
+ display: flex;
1663
+ align-items: center;
1664
+ justify-content: space-between;
1665
+ gap: var(--sp-2);
1666
+ width: 100%;
1667
+ min-width: 0;
1668
+ min-height: var(--tap-sm);
1669
+ padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-4);
1670
+ border: var(--border-w) solid var(--outline); /* --outline: keeps a faint edge on colored palettes, like fields */
1671
+ border-radius: var(--r-chip);
1672
+ background: var(--action-bg);
1673
+ color: var(--action-ink);
1674
+ font: var(--type-label);
1675
+ font-size: var(--fs-body);
1676
+ text-align: left;
1677
+ cursor: pointer;
1678
+ }
1679
+ .dropdown__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1680
+ .dropdown__btn > .icon { transition: rotate 0.2s ease; }
1681
+ .dropdown:has(.menu:popover-open) .dropdown__btn > .icon { rotate: 180deg; }
1682
+ .dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards */
1683
+ .filter-row { display: flex; gap: var(--sp-2); }
1684
+ .filter-row > .dropdown { flex: 1; min-width: 0; }
1685
+
1686
+ /* ---- segment (day picker / tab strip) — items share width, shrink on small screens ---- */
1687
+ .segment { display: flex; gap: var(--sp-1); }
1688
+ .segment__item {
1689
+ border: 0;
1690
+ background: transparent;
1691
+ color: inherit;
1692
+ opacity: 0.6;
1693
+ border-radius: var(--r-chip);
1694
+ padding: var(--sp-2) var(--sp-1);
1695
+ font: var(--type-label);
1696
+ font-size: var(--fs-sm);
1697
+ min-height: calc(var(--tap-sm) - var(--sp-0));
1698
+ flex: 1;
1699
+ min-width: 0;
1700
+ }
1701
+ .segment__item[aria-selected="true"] {
1702
+ background: var(--accent);
1703
+ color: var(--on-accent);
1704
+ opacity: 1;
1705
+ }
1706
+
1707
+ /* ---- bottom CTA pack: fixed action zone under the content ----
1708
+ --bar-reserve is the room the bar needs — its own height plus a breathing
1709
+ gap above it — published here so anything else that must stop clear of it
1710
+ reads this instead of recomputing it. .screen's own reserve below reads
1711
+ it; so does .step (step.css), the box a sheet slides in — one number, so
1712
+ the two can't drift apart.
1713
+ --kb is the on-screen keyboard's height, published on :root by cardds.js
1714
+ from the visual viewport (0 without one, and on a platform that shrinks
1715
+ the layout viewport itself): the bar rides above the keyboard on iOS the
1716
+ way it already does on Android, and .step grows its room by the same. */
1717
+ :root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
1718
+ .action-bar {
1719
+ position: fixed;
1720
+ inset-inline: 0;
1721
+ bottom: var(--kb, 0);
1722
+ z-index: 30; /* above .sheet-stack sheets, which paint at --level 1-5 */
1723
+ max-width: var(--screen-w);
1724
+ margin-inline: auto;
1725
+ padding: var(--sp-3) var(--sp-4) var(--sp-5);
1726
+ display: flex;
1727
+ gap: var(--sp-2);
1728
+ }
1729
+ .action-bar .btn { flex: 1; box-shadow: var(--shadow-float); }
1730
+ .action-bar .icon-btn { width: var(--btn-h); height: var(--btn-h); box-shadow: var(--shadow-float); }
1731
+
1732
+ /* ---- pager: previous / next at the two edges of the bar ----
1733
+ For a sequence you can walk BOTH ways — a three-card feature intro, a step
1734
+ flow. Use it instead of the default pack whenever going back is as ordinary
1735
+ as going on: a wide CTA beside a small round icon reads as one action plus
1736
+ an afterthought, which is wrong when the two are peers.
1737
+
1738
+ Both buttons are the same shape and the same height, so neither wins by
1739
+ being physically bigger; the fill alone leans forward. The bar carries the
1740
+ buttons only: where you are is said by the .pager__at dots UNDER the card
1741
+ that changes (a .screen__centre group, card.css), never in the bar — the
1742
+ dots move with what moves (Lh, 2026-09-12). Between the buttons goes a
1743
+ "skip" link or nothing. Keep the last step of a flow on the plain
1744
+ .action-bar: a step that COMMITS deserves one wide button, not a pair.
1745
+
1746
+ <div class="action-bar action-bar--pager">
1747
+ <button class="btn" disabled><svg class="icon icon--xs"><use href="icons.svg#arrow-left"/></svg>Back</button>
1748
+ <button class="btn btn--primary">Next<svg class="icon icon--xs"><use href="icons.svg#arrow-right"/></svg></button>
1749
+ </div> */
1750
+ .action-bar--pager { align-items: center; }
1751
+ /* equal share of the bar, and tighter than a lone CTA so two labels plus the
1752
+ indicator still fit a 320px screen */
1753
+ .action-bar--pager > .btn {
1754
+ flex: 1 1 0;
1755
+ min-width: 0;
1756
+ padding-inline: var(--sp-3);
1757
+ gap: var(--sp-1);
1758
+ white-space: nowrap;
1759
+ overflow: hidden;
1760
+ }
1761
+ /* the page dots: under the card in a .screen__centre group; tight gaps */
1762
+ .pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
1763
+ .pager__at > * {
1764
+ width: var(--dot);
1765
+ height: var(--dot);
1766
+ border-radius: 50%;
1767
+ background: currentColor;
1768
+ opacity: 0.3;
1769
+ }
1770
+ .pager__at > .is-at { background: var(--accent); opacity: 1; }
1771
+ /* ---- tiers: a control row above the buttons ----
1772
+ A screen whose one move comes with a control — the introduction's text-size
1773
+ slider above its pager. Each tier is a row; the bar stacks them, and any
1774
+ parent hosting the bar publishes the room for both, the way the plain
1775
+ reserve is published above, so a .screen or a .card--centre stops clear.
1776
+ <div class="action-bar action-bar--tiers">
1777
+ <div class="action-bar__tier"><label class="slider">…</label></div>
1778
+ <div class="action-bar__tier action-bar__tier--pager">…the round pager…</div>
1779
+ </div> */
1780
+ .action-bar--tiers { flex-direction: column; align-items: stretch; gap: var(--sp-3); }
1781
+ /* the tiers bar is as transparent as every other bar: the content scrolls under
1782
+ it and shows through — the rail and the round buttons carry their own float
1783
+ shadow and read on their own (Lh, 2026-09-12; replaces the 09-11 fade) */
1784
+ .action-bar__tier { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--tap); }
1785
+ .action-bar__tier > .btn { flex: 1; }
1786
+ :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
1787
+
1788
+ /* ---- round pager: previous / next as two round buttons at the two edges ----
1789
+ The pager with nothing to say between its buttons — no dots, no label — as
1790
+ two equal round icon buttons, one at each edge, in thumb reach from either
1791
+ side. Rule 3 still holds: the step that commits goes back to one wide button.
1792
+ <div class="action-bar action-bar--pager">
1793
+ <button class="icon-btn" aria-label="ย้อนกลับ">…</button>
1794
+ <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
1795
+ </div> — or the same two in an .action-bar__tier--pager under a control */
1796
+ :is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
1797
+
1798
+ /* .screen's default bottom padding is a constant; derive the reserve from the
1799
+ bar instead, so a taller button (a longer label, a script with more leading)
1800
+ can never end up underneath it. Any parent that hosts the bar counts —
1801
+ <body>, or an app shell (#app > .screen + .action-bar) — since the bar is
1802
+ fixed to the viewport wherever it sits. (.screen--fill reserves nothing:
1803
+ its .step carries the bar's room itself, step.css.) */
1804
+ :has(> .action-bar) .screen:not(.screen--fill) { padding-bottom: var(--bar-reserve); }
1805
+
1806
+ /* ---- css/numbers.css — ring, track, band, dot grid, picker, badge, bars ---- */
1807
+ /* ============================================================
1808
+ cardds/numbers.css — big numbers inside cards:
1809
+ ring gauge · step track · data band · dot grid · value picker
1810
+ Data goes in as unitless custom properties on the element
1811
+ (--value, --done, --total); the math lives here.
1812
+ ============================================================ */
1813
+
1814
+ /* ---- ring: conic gauge, --value 0–100, content centred ----
1815
+ <div class="ring" style="--value: 60"><span class="ring__value">60%</span><span class="ring__label">done</span></div> */
1816
+ .ring {
1817
+ --value: 0;
1818
+ --_size: var(--ring);
1819
+ --_w: var(--ring-w);
1820
+ width: var(--_size);
1821
+ height: var(--_size);
1822
+ flex: none;
1823
+ border-radius: 50%;
1824
+ display: grid;
1825
+ place-content: center;
1826
+ text-align: center;
1827
+ position: relative;
1828
+ font-variant-numeric: tabular-nums;
1829
+ }
1830
+ .ring::before {
1831
+ content: "";
1832
+ position: absolute;
1833
+ inset: 0;
1834
+ border-radius: 50%;
1835
+ background: conic-gradient(var(--accent) calc(var(--value) * 1%), var(--ink-tint) 0);
1836
+ /* masks only read alpha: currentColor = "opaque" without naming a color */
1837
+ mask: radial-gradient(farthest-side, transparent calc(100% - var(--_w) - 1px), currentColor calc(100% - var(--_w)));
1838
+ -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--_w) - 1px), currentColor calc(100% - var(--_w)));
1839
+ }
1840
+ .ring__value { font: var(--type-stat); letter-spacing: var(--track-display); }
1841
+ .ring__label { font: var(--type-caption); color: var(--card-muted); }
1842
+ .card--3 .ring__label { color: inherit; opacity: 0.7; }
1843
+ .ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
1844
+ .ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
1845
+ .ring--xs { --_size: var(--tap-sm); --_w: var(--dot-xs); } /* inline counter (step 3 of 5) */
1846
+ .ring--xs .ring__value { font: var(--type-overline); }
1847
+
1848
+ /* ---- track: step progress in a pill — done · done · todo ----
1849
+ <ol class="track"><li class="track__step is-done">icon</li> …</ol> */
1850
+ .track {
1851
+ --_bg: var(--card-3-bg);
1852
+ --_ink: var(--card-3-ink);
1853
+ /* the track lays down its own dark ground, so like the dark card slot it
1854
+ re-points the accent to its own ink (README rule 10). Without this, any
1855
+ palette where --accent equals --card-3-bg — field, hydro, and the
1856
+ wireframe default — paints done steps invisibly on the track. */
1857
+ --accent: var(--_ink);
1858
+ --on-accent: var(--_bg);
1859
+ list-style: none;
1860
+ margin: 0;
1861
+ padding: var(--sp-2);
1862
+ display: flex;
1863
+ align-items: center;
1864
+ border-radius: var(--r-chip);
1865
+ background: var(--_bg);
1866
+ color: var(--_ink);
1867
+ }
1868
+ .card--3 .track { --_bg: var(--card-3-ink); --_ink: var(--card-3-bg); }
1869
+ .track__step {
1870
+ flex: 1;
1871
+ display: flex;
1872
+ align-items: center;
1873
+ min-width: 0;
1874
+ }
1875
+ .track__step:first-child { flex: none; }
1876
+ /* the connector runs INTO each step; it lights up when that step is done */
1877
+ .track__step::before {
1878
+ content: "";
1879
+ flex: 1;
1880
+ height: var(--sp-3);
1881
+ background: color-mix(in srgb, var(--_ink) 30%, transparent);
1882
+ }
1883
+ .track__step:first-child::before { display: none; }
1884
+ .track__step.is-done::before { background: var(--accent); }
1885
+ .track__step > .icon,
1886
+ .track__step > .track__dot {
1887
+ width: var(--tap-lg);
1888
+ height: var(--tap-lg);
1889
+ flex: none;
1890
+ padding: calc((var(--tap-lg) - var(--icon)) / 2);
1891
+ border-radius: 50%;
1892
+ background: color-mix(in srgb, var(--_ink) 30%, transparent);
1893
+ color: var(--_ink);
1894
+ }
1895
+ .track__step.is-done > .icon,
1896
+ .track__step.is-done > .track__dot { background: var(--accent); color: var(--on-accent); }
1897
+
1898
+ /* ---- band: data card — 2-line label · giant number · footnote ----
1899
+ <article class="card card--band"><span class="band__label">Hours<br>banked</span><span class="band__num">18</span><span class="band__foot">this week</span></article> */
1900
+ .card--band {
1901
+ grid-template-columns: 1fr auto;
1902
+ grid-template-areas: "label num" "foot num";
1903
+ align-items: end;
1904
+ gap: var(--sp-2) var(--sp-4);
1905
+ min-height: var(--tile-h);
1906
+ }
1907
+ .band__label { grid-area: label; align-self: start; font: var(--type-label); font-size: var(--fs-body); line-height: 1.2; }
1908
+ .band__num {
1909
+ grid-area: num;
1910
+ font: 800 var(--fs-giant) / 0.9 var(--font-display);
1911
+ letter-spacing: var(--track-display);
1912
+ font-variant-numeric: tabular-nums;
1913
+ }
1914
+ .band__num small { font-size: 0.5em; font-weight: 700; margin-right: 0.1em; } /* unit or % */
1915
+ .band__foot { grid-area: foot; font: var(--type-caption); color: var(--card-muted); }
1916
+ .card--3 .band__foot { color: inherit; opacity: 0.7; }
1917
+ /* band stack: ONE card boundary; bands are frameless inside it, a hairline
1918
+ between each. Bands may still carry slot tints (card--2 / card--3). */
1919
+ .band-stack {
1920
+ display: grid;
1921
+ border: var(--border-w) solid var(--card-border);
1922
+ border-radius: var(--r-card);
1923
+ overflow: hidden;
1924
+ }
1925
+ .band-stack > .card--band { border: 0; border-radius: 0; box-shadow: none; }
1926
+ .band-stack > .card--band + .card--band { border-top: var(--border-w) solid var(--hairline); }
1927
+
1928
+ /* ---- dotgrid: progress over time as dots — --total, --done, --cols ----
1929
+ one element, no children:
1930
+ <div class="dotgrid" style="--total: 365; --done: 128" role="img" aria-label="128 of 365 days"></div> */
1931
+ .dotgrid {
1932
+ --cols: 12;
1933
+ --total: 100;
1934
+ --done: 0;
1935
+ --_rows: round(up, var(--total) / var(--cols));
1936
+ --_full: round(down, var(--done) / var(--cols)); /* complete rows */
1937
+ --_part: calc(var(--done) - var(--_full) * var(--cols)); /* dots in the partial row */
1938
+ --_cell-x: calc(100% / var(--cols));
1939
+ --_cell-y: calc(100% / var(--_rows));
1940
+ width: 100%;
1941
+ aspect-ratio: calc(var(--cols) / var(--_rows));
1942
+ position: relative;
1943
+ background: radial-gradient(circle, var(--hairline) 0 34%, transparent 37%) 0 0 / var(--_cell-x) var(--_cell-y);
1944
+ }
1945
+ .dotgrid::before {
1946
+ content: "";
1947
+ position: absolute;
1948
+ inset: 0;
1949
+ background: radial-gradient(circle, var(--accent) 0 34%, transparent 37%) 0 0 / var(--_cell-x) var(--_cell-y);
1950
+ clip-path: polygon(
1951
+ 0 0,
1952
+ 100% 0,
1953
+ 100% calc(var(--_full) * var(--_cell-y)),
1954
+ calc(var(--_part) * var(--_cell-x)) calc(var(--_full) * var(--_cell-y)),
1955
+ calc(var(--_part) * var(--_cell-x)) calc((var(--_full) + 1) * var(--_cell-y)),
1956
+ 0 calc((var(--_full) + 1) * var(--_cell-y))
1957
+ );
1958
+ }
1959
+ /* no round(): show the grid without the done fill rather than a broken shape */
1960
+ @supports not (width: round(up, 1px, 1px)) {
1961
+ .dotgrid::before { display: none; }
1962
+ }
1963
+ .dotgrid--stars {
1964
+ background-image: none;
1965
+ }
1966
+ .dotgrid--stars,
1967
+ .dotgrid--stars::before {
1968
+ --_star: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%);
1969
+ }
1970
+
1971
+ /* ---- picker: horizontal snap picker, centre value big ----
1972
+ <div class="picker"><div class="picker__rail"><button class="picker__item">100</button>…</div><span class="picker__unit">ml</span></div> */
1973
+ .picker { display: grid; gap: var(--sp-1); justify-items: center; position: relative; }
1974
+ .picker__rail {
1975
+ width: 100%;
1976
+ display: grid;
1977
+ grid-auto-flow: column;
1978
+ /* side padding = one third of the rail, so the first and last value can sit
1979
+ in the centre; the content box is then one third wide, and each column
1980
+ fills it (percent columns resolve against the content box) */
1981
+ padding-inline: calc(100% / 3);
1982
+ grid-auto-columns: 100%;
1983
+ overflow-x: auto;
1984
+ scroll-snap-type: x mandatory;
1985
+ scrollbar-width: none;
1986
+ position: relative;
1987
+ }
1988
+ .picker__rail::-webkit-scrollbar { display: none; }
1989
+ .picker__item {
1990
+ scroll-snap-align: center;
1991
+ border: 0;
1992
+ background: transparent;
1993
+ color: inherit;
1994
+ padding: var(--sp-2) 0;
1995
+ font: var(--type-stat);
1996
+ letter-spacing: var(--track-display);
1997
+ font-variant-numeric: tabular-nums;
1998
+ opacity: 0.3;
1999
+ text-align: center;
2000
+ }
2001
+ .picker__item[aria-selected="true"] { opacity: 1; }
2002
+ /* centre rules either side of the selected value */
2003
+ .picker::before,
2004
+ .picker::after {
2005
+ content: "";
2006
+ position: absolute;
2007
+ top: var(--sp-2);
2008
+ height: calc(var(--sp-10) + var(--sp-3));
2009
+ width: var(--rule-w);
2010
+ background: var(--accent);
2011
+ left: calc(100% / 3);
2012
+ }
2013
+ .picker::after { left: auto; right: calc(100% / 3); }
2014
+ .picker__unit { font: var(--type-caption); color: var(--card-muted); }
2015
+ .card--3 .picker__unit { color: inherit; opacity: 0.7; }
2016
+ /* the scrolled-to item lights up by position — no JS */
2017
+ @supports (animation-timeline: view(x)) {
2018
+ .picker__item { opacity: 0.3; animation: picker-focus linear both; animation-timeline: view(x); }
2019
+ @keyframes picker-focus {
2020
+ 0%, 100% { opacity: 0.3; }
2021
+ 45%, 55% { opacity: 1; }
2022
+ }
2023
+ }
2024
+
2025
+ /* ---- badge: tiny count next to a title or tab ("Leads +9", "All 23") ----
2026
+ <h3 class="t-h2">Leads <span class="badge">+9</span></h3> */
2027
+ .badge {
2028
+ display: inline-grid;
2029
+ place-items: center;
2030
+ min-width: var(--badge);
2031
+ height: var(--badge);
2032
+ padding: 0 var(--sp-2);
2033
+ border-radius: var(--r-chip);
2034
+ background: var(--accent);
2035
+ color: var(--on-accent);
2036
+ font: var(--type-label);
2037
+ font-variant-numeric: tabular-nums;
2038
+ vertical-align: middle;
2039
+ }
2040
+ .badge--soft { background: var(--ink-tint); color: inherit; }
2041
+
2042
+ /* ---- bars: three or four bars with a count on top — the glance chart ----
2043
+ <div class="bars"><div class="bar" style="--v: 16"><span>+16</span></div>… --max on the parent */
2044
+ .bars { --max: 20; --_h: 5rem; display: flex; align-items: end; gap: var(--sp-2); height: calc(var(--_h) + var(--sp-8)); }
2045
+ .bar {
2046
+ flex: 1;
2047
+ min-width: 0;
2048
+ display: grid;
2049
+ align-content: end;
2050
+ justify-items: center;
2051
+ gap: var(--sp-1);
2052
+ height: 100%;
2053
+ font: var(--type-caption);
2054
+ font-weight: 700;
2055
+ }
2056
+ .bar::after {
2057
+ content: "";
2058
+ width: 100%;
2059
+ height: max(var(--sp-2), calc(var(--v) / var(--max) * var(--_h)));
2060
+ border-radius: var(--r-badge);
2061
+ background: var(--ink-tint);
2062
+ }
2063
+ .bar--on::after { background: var(--accent); }
2064
+ .bar--on > span { background: var(--accent); color: var(--on-accent); padding: var(--sp-0) var(--sp-2); border-radius: var(--r-chip); }
2065
+
2066
+ /* ---- css/people.css — avatar, avatar stack, chooser row ---- */
2067
+ /* ============================================================
2068
+ cardds/people.css — avatars: single, stacked, chooser row.
2069
+ A person row = .row with an .avatar as lead (see lists.css).
2070
+ ============================================================ */
2071
+
2072
+ .avatar--center { margin-inline: auto; } /* centred in a sheet head */
2073
+
2074
+ /* ---- avatar: initials by default, <img> when there is a photo ----
2075
+ <span class="avatar">LH</span> · <span class="avatar"><img src alt></span> */
2076
+ .avatar {
2077
+ --_size: var(--avatar);
2078
+ width: var(--_size);
2079
+ height: var(--_size);
2080
+ flex: none;
2081
+ border-radius: 50%;
2082
+ display: grid;
2083
+ place-items: center;
2084
+ background: var(--card-2-bg);
2085
+ color: var(--card-2-ink);
2086
+ font: var(--type-label);
2087
+ font-size: calc(var(--_size) * 0.3);
2088
+ text-transform: uppercase;
2089
+ letter-spacing: 0.02em;
2090
+ position: relative;
2091
+ text-decoration: none; /* .avatar works as <a> too */
2092
+ }
2093
+ .avatar > img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
2094
+ .avatar > .icon { width: 50%; height: 50%; }
2095
+ .avatar--sm { --_size: var(--avatar-sm); }
2096
+ .avatar--lg { --_size: var(--avatar-lg); }
2097
+ .avatar--xl { --_size: var(--avatar-xl); } /* sheet head icon ring */
2098
+ /* outline avatar: no fill, an edge — icons in a ring, "add" slots */
2099
+ .avatar--outline { background: transparent; color: inherit; border: var(--border-w) solid var(--outline); }
2100
+ .avatar--add { background: transparent; color: inherit; border: var(--border-w) dashed var(--outline); cursor: pointer; }
2101
+ /* accent avatar: the "+7" tail, the selected person */
2102
+ .avatar--on { background: var(--accent); color: var(--on-accent); }
2103
+ /* halo: faint concentric rings behind a hero avatar (decorative) */
2104
+ .avatar--halo::before {
2105
+ content: "";
2106
+ position: absolute;
2107
+ inset: calc(-0.55 * var(--_size));
2108
+ border-radius: 50%;
2109
+ background: repeating-radial-gradient(circle, transparent 0 calc(var(--_size) * 0.5), currentColor calc(var(--_size) * 0.5) calc(var(--_size) * 0.5 + var(--border-w)), transparent calc(var(--_size) * 0.5 + var(--border-w)) calc(var(--_size) * 0.68));
2110
+ opacity: 0.25;
2111
+ pointer-events: none;
2112
+ }
2113
+
2114
+ /* ---- avatar stack: overlapping heads, optional +N tail ----
2115
+ <span class="avatar-stack"><span class="avatar avatar--sm">A</span>…<span class="avatar avatar--sm avatar--on">+7</span></span> */
2116
+ .avatar-stack { display: inline-flex; align-items: center; }
2117
+ .avatar-stack > .avatar { box-shadow: 0 0 0 var(--sp-1) var(--card-bg, var(--surface)); }
2118
+ .avatar-stack > .avatar + .avatar { margin-left: calc(-1 * var(--sp-3)); }
2119
+
2120
+ /* ---- avatar chooser: big heads with name + role under, scrolls sideways ----
2121
+ <div class="avatar-pick"><button class="avatar-pick__item"><span class="avatar avatar--lg avatar--add">+</span><span class="avatar-pick__name">Add</span><span class="avatar-pick__role">member</span></button>…</div> */
2122
+ .avatar-pick {
2123
+ display: flex;
2124
+ gap: var(--sp-4);
2125
+ overflow-x: auto;
2126
+ scroll-snap-type: x proximity;
2127
+ scrollbar-width: none; /* starts at the card's padding edge, like every row */
2128
+ }
2129
+ .avatar-pick::-webkit-scrollbar { display: none; }
2130
+ .avatar-pick__item {
2131
+ scroll-snap-align: start;
2132
+ flex: none;
2133
+ width: var(--avatar-lg);
2134
+ display: grid;
2135
+ justify-items: center;
2136
+ gap: var(--sp-2);
2137
+ border: 0;
2138
+ background: transparent;
2139
+ color: inherit;
2140
+ padding: 0;
2141
+ text-align: center;
2142
+ }
2143
+ .avatar-pick__name { font: var(--type-label); font-size: var(--fs-sm); }
2144
+ .avatar-pick__role { font: var(--type-caption); color: var(--card-muted); margin-top: calc(-1 * var(--sp-1)); }
2145
+ .card--3 .avatar-pick__role { color: inherit; opacity: 0.7; }
2146
+
2147
+ /* ---- css/choice.css — check, toggle, toggle chip, chip grid, days, calendar, mood, pin, composer ---- */
2148
+ /* ============================================================
2149
+ cardds/choice.css — thumb-size choice controls inside cards:
2150
+ check circle · toggle · toggle chip · chip grid · day strip ·
2151
+ calendar · mood picker · pin field · composer · upload tile
2152
+ Native inputs carry the state; CSS reads :checked. No JS.
2153
+ ============================================================ */
2154
+
2155
+ /* the data-URI icons below are masks: only their alpha is read, so the
2156
+ "black" stroke inside them is shape, not color — the fill is the token. */
2157
+
2158
+ /* ---- check: round check circle at a row's end ----
2159
+ <input type="checkbox" class="check" aria-label="Done"> */
2160
+ .check {
2161
+ appearance: none;
2162
+ width: var(--tap);
2163
+ height: var(--tap);
2164
+ flex: none;
2165
+ margin: 0;
2166
+ border-radius: 50%;
2167
+ border: var(--border-w) solid var(--outline);
2168
+ background: var(--action-bg);
2169
+ display: grid;
2170
+ place-items: center;
2171
+ cursor: pointer;
2172
+ }
2173
+ .check::after {
2174
+ content: "";
2175
+ width: calc(var(--tap) / 2);
2176
+ height: calc(var(--tap) / 2);
2177
+ background: var(--on-accent);
2178
+ 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" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>') center / contain no-repeat;
2179
+ -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" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>') center / contain no-repeat;
2180
+ opacity: 0;
2181
+ }
2182
+ .check:checked { background: var(--accent); border-color: transparent; }
2183
+ .check:checked::after { opacity: 1; }
2184
+ .check:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
2185
+ .check--sm { width: var(--tap-sm); height: var(--tap-sm); }
2186
+
2187
+ /* ---- toggle: big switch ----
2188
+ <input type="checkbox" role="switch" class="toggle" aria-label="Private"> */
2189
+ .toggle {
2190
+ --_w: var(--bar-h); /* 64 × 36, knob = height − 2 × pad */
2191
+ --_h: 2.25rem;
2192
+ --_pad: 0.1875rem;
2193
+ appearance: none;
2194
+ width: var(--_w);
2195
+ height: var(--_h);
2196
+ flex: none;
2197
+ margin: 0;
2198
+ border-radius: var(--r-chip);
2199
+ border: var(--border-w) solid var(--outline);
2200
+ background: var(--action-bg);
2201
+ position: relative;
2202
+ cursor: pointer;
2203
+ transition: background 0.2s ease;
2204
+ }
2205
+ .toggle::after {
2206
+ content: "";
2207
+ position: absolute;
2208
+ top: var(--_pad);
2209
+ left: var(--_pad);
2210
+ width: calc(var(--_h) - 2 * var(--_pad) - 2 * var(--border-w));
2211
+ height: calc(var(--_h) - 2 * var(--_pad) - 2 * var(--border-w));
2212
+ border-radius: 50%;
2213
+ background: currentColor;
2214
+ opacity: 0.45;
2215
+ transition: translate 0.2s ease, opacity 0.2s ease;
2216
+ }
2217
+ .toggle:checked { background: var(--accent); border-color: transparent; }
2218
+ .toggle:checked::after { translate: calc(var(--_w) - var(--_h)) 0; background: var(--on-accent); opacity: 1; }
2219
+ .toggle:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
2220
+
2221
+ /* ---- toggle chip: interest / filter picker — shows + off, × on ----
2222
+ <label class="chip chip--toggle"><input type="checkbox">Nature</label> */
2223
+ .chip--toggle { cursor: pointer; padding-right: var(--sp-2); }
2224
+ .chip--toggle > input { appearance: none; position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
2225
+ .chip--toggle::after {
2226
+ content: "";
2227
+ width: var(--icon-xs);
2228
+ height: var(--icon-xs);
2229
+ flex: none;
2230
+ background: currentColor;
2231
+ 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;
2232
+ -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;
2233
+ }
2234
+ .chip--toggle:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
2235
+ .chip--toggle:has(:checked)::after {
2236
+ mask-image: 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="M18 6 6 18"/><path d="m6 6 12 12"/></svg>');
2237
+ -webkit-mask-image: 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="M18 6 6 18"/><path d="m6 6 12 12"/></svg>');
2238
+ }
2239
+ .chip--toggle:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
2240
+ .chip--toggle:has(:checked) { font-style: italic; } /* the interest-picker flourish; drop if unwanted */
2241
+
2242
+ /* ---- chip grid: pick one slot / many options in a grid ----
2243
+ <div class="chip-grid"><label class="chip chip--pick"><input type="radio" name="t">9:00</label>…</div> */
2244
+ .chip-grid {
2245
+ display: grid;
2246
+ grid-template-columns: repeat(auto-fill, minmax(min(5.5rem, 100%), 1fr));
2247
+ gap: var(--sp-2);
2248
+ }
2249
+ /* .chip hugs its text as a loose grid child (justify-self: start); inside a
2250
+ chip grid the whole point is equal, full-width pick targets, so stretch.
2251
+ Matters most in scripts without word spaces (Thai): min-content is the
2252
+ whole string, tracks get wide, and hugging chips leave a ragged grid. */
2253
+ .chip-grid > .chip { justify-self: stretch; justify-content: center; min-height: var(--btn-h); padding-inline: var(--sp-2); position: relative; }
2254
+ .chip--pick { cursor: pointer; }
2255
+ .chip--pick > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
2256
+ .chip--pick:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
2257
+ .chip--pick:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
2258
+
2259
+ /* ---- day strip: a week, one day selected ----
2260
+ <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> */
2261
+ .day-strip { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--sp-1); }
2262
+ .day {
2263
+ display: grid;
2264
+ /* three rows, always: name · number · dot. The dot's row is reserved even on
2265
+ a day that has no dot, so every number sits on the same line and every dot
2266
+ sits on the same line — otherwise a marked day's rows share the height
2267
+ differently and its number rides up. */
2268
+ grid-template-rows: auto auto var(--dot-xs);
2269
+ align-content: center; /* the group sits centred in the cell, not spread out */
2270
+ justify-items: center;
2271
+ gap: var(--sp-0);
2272
+ min-width: 0;
2273
+ min-height: var(--bar-h);
2274
+ padding-block: var(--sp-3); /* equal air top and bottom, so the selected pill
2275
+ is the same shape on every day */
2276
+ padding-inline: 0;
2277
+ border: 0;
2278
+ border-radius: var(--r-chip);
2279
+ background: transparent;
2280
+ color: inherit;
2281
+ opacity: 0.6;
2282
+ }
2283
+ .day__name { font: var(--type-caption); }
2284
+ .day__num { font: 700 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
2285
+ .day[aria-selected="true"] { background: var(--accent); color: var(--on-accent); opacity: 1; }
2286
+ .day__dot { grid-row: 3; align-self: center; width: var(--dot-xs); height: var(--dot-xs); border-radius: 50%; background: currentColor; }
2287
+
2288
+ /* ---- calendar: month grid, 44px cells, one accent day, optional marks ----
2289
+ <div class="calendar"><div class="calendar__head">…</div><div class="calendar__grid"><span class="calendar__wd">Mo</span>×7 <button class="calendar__day" style="--start: 3">1</button>…</div></div> */
2290
+ .calendar { display: grid; gap: var(--sp-3); }
2291
+ .calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
2292
+ .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--sp-1); justify-items: center; }
2293
+ .calendar__wd { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; color: var(--card-muted); padding-block: var(--sp-2); }
2294
+ .card--3 .calendar__wd { color: inherit; opacity: 0.7; }
2295
+ .calendar__day {
2296
+ width: 100%;
2297
+ max-width: var(--tap);
2298
+ aspect-ratio: 1;
2299
+ display: grid;
2300
+ place-items: center;
2301
+ border: 0;
2302
+ border-radius: 50%;
2303
+ background: transparent;
2304
+ color: inherit;
2305
+ font: 500 var(--fs-title)/1 var(--font-ui);
2306
+ font-variant-numeric: tabular-nums;
2307
+ grid-column-start: var(--start, auto); /* --start on the 1st: weekday column 1–7 */
2308
+ }
2309
+ .calendar__day--mark { background: var(--accent-soft); }
2310
+ .calendar__day--mark2 { background: var(--accent-mid); color: var(--on-accent); }
2311
+ .calendar__day[aria-selected="true"] { background: var(--accent); color: var(--on-accent); font-weight: 700; }
2312
+ .calendar__day:disabled { opacity: 0.3; }
2313
+
2314
+ /* ---- mood: five dots in a pill, the picked one grows ----
2315
+ <fieldset class="mood"><label class="mood__opt"><input type="radio" name="mood"><span class="mood__dot"></span></label>×5</fieldset> */
2316
+ .mood {
2317
+ margin: 0;
2318
+ min-inline-size: 0; /* fieldset UA default is min-content — it must shrink with the card */
2319
+ padding: var(--sp-1);
2320
+ border: var(--border-w) solid var(--outline);
2321
+ border-radius: var(--r-chip);
2322
+ background: var(--action-bg);
2323
+ color: var(--action-ink);
2324
+ display: flex;
2325
+ justify-content: space-between;
2326
+ }
2327
+ .mood__opt { flex: 1 1 0; max-width: var(--tap); min-width: 0; height: var(--tap); display: grid; place-items: center; position: relative; cursor: pointer; }
2328
+ .mood__opt > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
2329
+ .mood__dot {
2330
+ width: var(--icon-xs);
2331
+ height: var(--icon-xs);
2332
+ border-radius: 50%;
2333
+ background: var(--_mood, currentColor);
2334
+ transition: scale 0.2s ease, box-shadow 0.2s ease;
2335
+ }
2336
+ .mood__opt:nth-child(1) { --_mood: var(--neg); }
2337
+ .mood__opt:nth-child(2) { --_mood: color-mix(in srgb, var(--neg) 50%, var(--dot-b)); }
2338
+ .mood__opt:nth-child(3) { --_mood: var(--dot-b); }
2339
+ .mood__opt:nth-child(4) { --_mood: color-mix(in srgb, var(--pos) 50%, var(--dot-b)); }
2340
+ .mood__opt:nth-child(5) { --_mood: var(--pos); }
2341
+ .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); }
2342
+ .mood__opt:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-0); border-radius: 50%; }
2343
+
2344
+ /* ---- pin: big code entry, one digit per pill ----
2345
+ <div class="pin"><input class="pin__cell" inputmode="numeric" maxlength="1" placeholder=" ">×4</div> */
2346
+ .pin { display: flex; gap: var(--sp-3); }
2347
+ .pin__cell {
2348
+ flex: 1;
2349
+ min-width: 0;
2350
+ max-width: var(--fab);
2351
+ aspect-ratio: 2 / 3;
2352
+ border: var(--border-w) solid var(--outline);
2353
+ border-radius: var(--r-chip);
2354
+ background: var(--action-bg);
2355
+ color: var(--action-ink);
2356
+ text-align: center;
2357
+ font: var(--type-stat);
2358
+ font-variant-numeric: tabular-nums;
2359
+ caret-color: currentColor;
2360
+ }
2361
+ .pin__cell:not(:placeholder-shown) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
2362
+ .pin__cell:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
2363
+
2364
+ /* ---- composer: big text area with a bottom bar (icon · hint · send) ----
2365
+ <div class="composer"><textarea class="composer__input" placeholder="What do you need?"></textarea>
2366
+ <div class="composer__bar"><button class="icon-btn">…</button><span class="composer__hint">Your idea is born</span><button class="icon-btn icon-btn--invert">↑</button></div></div> */
2367
+ .composer { display: grid; gap: var(--sp-3); }
2368
+ .composer__input {
2369
+ width: 100%;
2370
+ min-height: 11rem;
2371
+ resize: none;
2372
+ border: 0;
2373
+ padding: 0;
2374
+ background: transparent;
2375
+ color: inherit;
2376
+ font: 700 var(--fs-2xl)/1.25 var(--font-display);
2377
+ letter-spacing: var(--track-h2);
2378
+ outline: none;
2379
+ }
2380
+ .composer__input::placeholder { color: inherit; opacity: 0.4; }
2381
+ .composer__bar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
2382
+ .composer__hint { font: var(--type-caption); color: var(--card-muted); }
2383
+ .card--3 .composer__hint { color: inherit; opacity: 0.7; }
2384
+
2385
+ /* ---- slider: one value along a track, a thumb-size knob ----
2386
+ <label class="slider"><span class="slider__label">ขนาดตัวหนังสือ</span><input class="slider__range" type="range" min="100" max="160" step="15" aria-label="ขนาดตัวหนังสือ"></label>
2387
+ The label row is optional: in a bar under a card that already says what the
2388
+ slider does, the input's aria-label is enough. The track is a hairline rail;
2389
+ the knob is a full --tap, and the whole range is --tap tall, so the thumb has
2390
+ a real target anywhere along it. The value is the input's own. */
2391
+ .slider { display: grid; gap: var(--sp-1); flex: 1; min-width: 0; position: relative; } /* relative: the glyph rides the knob */
2392
+ /* ---- the sample glyph: one letter IN the knob, on-accent, at the type size
2393
+ the slider sets — so a text-size slider shows the size before anything is
2394
+ read (Lh, 2026-09-12). cardds.js publishes the knob's place on the .slider
2395
+ as --slider-at (0..1) from the input's value; the glyph sits over the knob
2396
+ and never takes the pointer. <span class="slider__glyph" aria-hidden="true">ก</span>
2397
+ before the input. */
2398
+ .slider__glyph {
2399
+ position: absolute;
2400
+ bottom: 0;
2401
+ left: calc(var(--tap) / 2 + (100% - var(--tap)) * var(--slider-at, 0));
2402
+ width: var(--tap);
2403
+ height: var(--tap);
2404
+ /* the eye centres the INK, not the line box: a letter that stands on the
2405
+ baseline (ก) has no descender, while the face's box keeps 0.30em under it
2406
+ (fonts.css) — so the ink lands low when the box is centred; lifted 0.15em
2407
+ (set by eye with Lh on FC Pride at h2, 2026-09-12) */
2408
+ translate: -50% -0.15em;
2409
+ display: grid;
2410
+ place-items: center;
2411
+ font: var(--type-h2);
2412
+ color: var(--on-accent);
2413
+ pointer-events: none;
2414
+ }
2415
+ .slider__label { font: var(--type-label); }
2416
+ .slider__range {
2417
+ appearance: none;
2418
+ width: 100%;
2419
+ height: var(--tap);
2420
+ margin: 0;
2421
+ background: transparent;
2422
+ cursor: pointer;
2423
+ }
2424
+ .slider__range::-webkit-slider-runnable-track { height: var(--sp-1); border-radius: var(--r-chip); background: var(--ink-rail); }
2425
+ .slider__range::-webkit-slider-thumb {
2426
+ appearance: none;
2427
+ width: var(--tap);
2428
+ height: var(--tap);
2429
+ margin-top: calc((var(--sp-1) - var(--tap)) / 2);
2430
+ border-radius: 50%;
2431
+ background: var(--accent);
2432
+ border: var(--border-w) solid var(--on-accent);
2433
+ box-shadow: var(--shadow-float);
2434
+ }
2435
+ .slider__range::-moz-range-track { height: var(--sp-1); border-radius: var(--r-chip); background: var(--ink-rail); }
2436
+ .slider__range::-moz-range-thumb {
2437
+ width: var(--tap);
2438
+ height: var(--tap);
2439
+ border-radius: 50%;
2440
+ background: var(--accent);
2441
+ border: var(--border-w) solid var(--on-accent);
2442
+ box-shadow: var(--shadow-float);
2443
+ }
2444
+ .slider__range:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
2445
+
2446
+ /* ---- css/lists.css — row, rows, key-value, kv grid, timeline, legend, link ---- */
2447
+ /* ============================================================
2448
+ cardds/lists.css — frameless rows and pairs INSIDE a card:
2449
+ row (lead · body · end) · key-value rows · kv grid ·
2450
+ timeline · legend · link
2451
+ A row inside a card has no boundary of its own; .rows draws
2452
+ hairlines between rows. A row that IS a card = .card--row.
2453
+ ============================================================ */
2454
+
2455
+ /* ---- row: leading visual · title + meta · trailing control ----
2456
+ <div class="row"><span class="avatar">A</span><div class="row__body"><span class="row__title">Alice Kau</span><span class="row__meta">Owner</span></div><button class="icon-btn icon-btn--sm">›</button></div> */
2457
+ .row {
2458
+ display: flex;
2459
+ align-items: center;
2460
+ gap: var(--sp-3);
2461
+ min-height: var(--tap);
2462
+ padding-block: var(--sp-2);
2463
+ min-width: 0;
2464
+ color: inherit;
2465
+ text-decoration: none; /* .row works as <a> too — it is a tap target */
2466
+ }
2467
+ .row__lead {
2468
+ width: var(--tap);
2469
+ height: var(--tap);
2470
+ flex: none;
2471
+ border-radius: var(--r-control);
2472
+ border: var(--border-w) solid var(--outline);
2473
+ display: grid;
2474
+ place-items: center;
2475
+ }
2476
+ .row__body { flex: 1; min-width: 0; display: grid; gap: var(--sp-0); }
2477
+ .row__title { font: var(--type-title); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-block: 0.12em; margin-block: -0.12em; } /* the clip keeps Thai marks — see card.css */
2478
+ .row__meta { font: var(--type-caption); color: var(--card-muted); display: flex; align-items: center; gap: var(--sp-1); }
2479
+ .card--3 .row__meta { color: inherit; opacity: 0.7; }
2480
+ .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--sp-0); justify-items: end; flex: none; }
2481
+ .row--lg { min-height: var(--fab); padding-block: var(--sp-3); } /* menu rows: more air, round arrow at the end */
2482
+ .row--lg .row__title { font: 700 var(--fs-lg)/1.2 var(--font-display); }
2483
+ .row--flush { padding-block: 0; } /* a single row as card content: the card's gap is the spacing */
2484
+ /* a finished item in a checklist (a to-do list, an onboarding "complete your introduction" list):
2485
+ the words go muted, a filled tick sits at the end, and the row is no longer a tap target —
2486
+ pointer-events off here, aria-disabled from the markup. The tick is the .check's checked look
2487
+ at --tap-sm, but not an input: nothing to press. */
2488
+ .row--done { pointer-events: none; cursor: default; }
2489
+ .row--done .row__title { color: var(--card-muted); }
2490
+ .card--3 .row--done .row__title { color: inherit; opacity: 0.7; }
2491
+ .row__done {
2492
+ width: var(--tap-sm);
2493
+ height: var(--tap-sm);
2494
+ flex: none;
2495
+ margin-left: auto;
2496
+ border-radius: 50%;
2497
+ background: var(--accent);
2498
+ color: var(--on-accent);
2499
+ display: grid;
2500
+ place-items: center;
2501
+ }
2502
+
2503
+ /* rows: a list of rows separated by hairlines */
2504
+ .rows { display: grid; }
2505
+ .rows > .row + .row { border-top: var(--border-w) solid var(--hairline); }
2506
+
2507
+ /* ---- kv: label left, value right ----
2508
+ <dl class="kv"><div class="kv__row"><dt class="kv__key">Pay with</dt><dd class="kv__val">GHO</dd></div>…</dl> */
2509
+ .kv { margin: 0; display: grid; }
2510
+ .kv__row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); min-height: var(--tap); }
2511
+ .kv--lined > .kv__row + .kv__row { border-top: var(--border-w) solid var(--hairline); }
2512
+ .kv__key { font: var(--type-body); color: var(--card-muted); }
2513
+ .card--3 .kv__key { color: inherit; opacity: 0.7; }
2514
+ .kv__val { margin: 0; font: var(--type-title); display: inline-flex; align-items: center; gap: var(--sp-2); text-align: right; min-width: 0; }
2515
+
2516
+ /* ---- kv grid: value over label, cells split by a rule (Sex | Weight) ----
2517
+ <div class="kv-grid"><div class="kv-cell"><svg class="icon"/><span class="kv-cell__val">15.6 kg</span><span class="kv-cell__key">Weight</span></div>…</div> */
2518
+ .kv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(var(--tile-min), 100%), 1fr)); gap: var(--sp-4); }
2519
+ .kv-grid > .kv-cell + .kv-cell { border-left: var(--border-w) solid var(--hairline); padding-left: var(--sp-4); }
2520
+ .kv-cell { display: grid; gap: var(--sp-0); align-content: start; min-width: 0; }
2521
+ .kv-cell > .icon { margin-bottom: var(--sp-1); }
2522
+ .kv-cell__val { font: var(--type-h2); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
2523
+ .kv-cell__key { font: var(--type-caption); color: var(--card-muted); }
2524
+ .card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
2525
+
2526
+ /* ---- timeline: a hairline with date chips, entries to the right ----
2527
+ <ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
2528
+ .timeline {
2529
+ list-style: none;
2530
+ margin: 0;
2531
+ padding: 0 0 0 var(--sp-6);
2532
+ display: grid;
2533
+ gap: var(--sp-5);
2534
+ position: relative;
2535
+ }
2536
+ .timeline::before {
2537
+ content: "";
2538
+ position: absolute;
2539
+ left: calc((var(--sp-3) - var(--border-w)) / 2); /* through the centre of the dots */
2540
+ top: var(--sp-3);
2541
+ bottom: var(--sp-3);
2542
+ border-left: var(--border-w) solid var(--hairline);
2543
+ }
2544
+ .timeline__item { display: grid; gap: var(--sp-2); position: relative; min-width: 0; }
2545
+ .timeline__item::before {
2546
+ content: "";
2547
+ position: absolute;
2548
+ left: calc(-1 * var(--sp-6));
2549
+ top: var(--sp-2);
2550
+ width: var(--sp-3);
2551
+ height: var(--sp-3);
2552
+ border-radius: 50%;
2553
+ background: var(--accent);
2554
+ }
2555
+ .timeline__mark {
2556
+ justify-self: start;
2557
+ font: var(--type-overline);
2558
+ letter-spacing: var(--track-overline);
2559
+ text-transform: uppercase;
2560
+ padding: var(--sp-1) var(--sp-2);
2561
+ border-radius: var(--r-chip);
2562
+ background: var(--ink-tint);
2563
+ }
2564
+
2565
+ /* ---- legend: signal dots with captions ----
2566
+ <div class="legend"><span class="legend__item"><span class="dot"></span> very high</span>…</div> */
2567
+ .legend { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); font: var(--type-caption); color: var(--card-muted); }
2568
+ .card--3 .legend { color: inherit; opacity: 0.8; }
2569
+ .legend__item { display: inline-flex; align-items: center; gap: var(--sp-2); min-height: var(--icon); }
2570
+
2571
+ /* ---- link: "See all ↗" in a card head, big enough to tap ----
2572
+ <a class="link" href="#">See all <svg class="icon icon--xs"/></a> */
2573
+ .link {
2574
+ display: inline-flex;
2575
+ align-items: center;
2576
+ gap: var(--sp-1);
2577
+ min-height: var(--tap-sm);
2578
+ color: inherit;
2579
+ text-decoration: none;
2580
+ font: var(--type-label);
2581
+ font-size: var(--fs-body);
2582
+ border: 0;
2583
+ background: transparent;
2584
+ padding: 0;
2585
+ cursor: pointer;
2586
+ }
2587
+ .link--muted { color: var(--card-muted); }
2588
+ .card--3 .link--muted { color: inherit; opacity: 0.7; }
2589
+
2590
+ /* ---- css/media.css — cover card, quote, tile grid, fold, mosaic, wave ---- */
2591
+ /* ============================================================
2592
+ cardds/media.css — media & display inside cards:
2593
+ cover card · quote · tile grid · fold · waveform
2594
+ ============================================================ */
2595
+
2596
+ /* ---- cover: the image IS the card; text over a scrim ----
2597
+ <article class="card card--cover"><div class="ph" role="img"></div>
2598
+ <div class="card__head"><span class="t-overline">Featured</span><button class="icon-btn icon-btn--sm">♥</button></div>
2599
+ <div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
2600
+ <div class="row">…meta…</div></article> */
2601
+ .card--cover {
2602
+ --card-bg: var(--card-3-bg);
2603
+ --card-ink: var(--card-3-ink);
2604
+ --accent: var(--card-3-ink);
2605
+ --on-accent: var(--card-3-bg);
2606
+ min-height: 22.5rem;
2607
+ align-content: space-between;
2608
+ overflow: hidden;
2609
+ isolation: isolate;
2610
+ }
2611
+ .card--cover > img,
2612
+ .card--cover > .ph {
2613
+ position: absolute;
2614
+ inset: 0;
2615
+ width: 100%;
2616
+ height: 100%;
2617
+ object-fit: cover;
2618
+ z-index: -2;
2619
+ border: 0;
2620
+ border-radius: 0;
2621
+ min-height: 0;
2622
+ }
2623
+ /* scrim: dark at both ends so overline and meta stay readable */
2624
+ .card--cover::before {
2625
+ content: "";
2626
+ position: absolute;
2627
+ inset: 0;
2628
+ z-index: -1;
2629
+ background: linear-gradient(to bottom, var(--scrim), transparent 40% 60%, var(--scrim));
2630
+ }
2631
+ .card--cover .t-muted { color: inherit; opacity: 0.75; }
2632
+ .card--cover .row__meta { color: inherit; opacity: 0.85; }
2633
+
2634
+ /* ---- quote: giant mark, big text, attribution ----
2635
+ <blockquote class="quote"><p class="quote__text">It's overcast…</p><footer class="quote__by">Wednesday 15</footer></blockquote> */
2636
+ .quote { margin: 0; display: grid; gap: var(--sp-3); }
2637
+ .quote::before {
2638
+ content: "\201C";
2639
+ font: 900 calc(var(--fs-h1) * 3)/0.55 var(--font-display);
2640
+ height: var(--sp-10);
2641
+ opacity: 0.9;
2642
+ }
2643
+ .quote__text { margin: 0; font: var(--type-h1); letter-spacing: var(--track-h1); text-wrap: balance; }
2644
+ .quote--sm .quote__text { font: var(--type-h2); letter-spacing: var(--track-h2); }
2645
+ .quote__by { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; color: var(--card-muted); }
2646
+ .card--3 .quote__by { color: inherit; opacity: 0.7; }
2647
+
2648
+ /* ---- tile grid: big square tiles — icon top, label bottom ----
2649
+ <div class="tile-grid"><button class="tile"><svg class="icon"/><span class="tile__label">Light</span></button>
2650
+ <label class="tile tile--pick"><input type="checkbox"><svg class="icon"/><span class="tile__label">Music</span></label></div> */
2651
+ .tile-grid {
2652
+ display: grid;
2653
+ grid-template-columns: repeat(auto-fill, minmax(min(var(--tile-min), 100%), 1fr));
2654
+ gap: var(--sp-3);
2655
+ }
2656
+ .tile-grid--3 { --tile-min: 6rem; } /* three across on a phone (attachments, upload kinds) */
2657
+ .tile {
2658
+ display: grid;
2659
+ align-content: space-between;
2660
+ justify-items: start;
2661
+ gap: var(--sp-5);
2662
+ min-height: var(--tile-h);
2663
+ min-width: 0;
2664
+ padding: var(--sp-4);
2665
+ border: var(--border-w) solid var(--outline);
2666
+ border-radius: var(--r-card);
2667
+ background: var(--action-bg);
2668
+ color: var(--action-ink);
2669
+ font: inherit;
2670
+ text-align: left;
2671
+ position: relative;
2672
+ cursor: pointer;
2673
+ }
2674
+ .tile__label { font: var(--type-title); }
2675
+ .tile__value { font: var(--type-h2); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
2676
+ .tile__sub { font: var(--type-caption); opacity: 0.7; }
2677
+ .tile__text { display: grid; gap: var(--sp-0); }
2678
+ .tile--on { background: var(--accent); color: var(--on-accent); border-color: transparent; }
2679
+ .tile--pick > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
2680
+ .tile--pick:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
2681
+ .tile--pick:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
2682
+ .tile--wide { grid-column: 1 / -1; }
2683
+ .tile--stat { min-height: var(--avatar-xl); gap: var(--sp-3); grid-template-columns: auto 1fr; align-items: center; }
2684
+ /* upload: dashed, centred plus + label */
2685
+ .tile--upload {
2686
+ border-style: dashed;
2687
+ background: transparent;
2688
+ color: inherit;
2689
+ align-content: center;
2690
+ justify-items: center;
2691
+ gap: var(--sp-2);
2692
+ text-align: center;
2693
+ }
2694
+ /* tinted tiles: slot colors inside a card (schedule, notes) */
2695
+ .tile--2 { background: var(--card-2-bg); color: var(--card-2-ink); border-color: transparent; }
2696
+ .tile--3 { background: var(--card-3-bg); color: var(--card-3-ink); border-color: transparent; }
2697
+
2698
+ /* ---- fold: a sticky-note corner on any card (decorative) ----
2699
+ No border. The card's outline follows its border box, so it ran straight
2700
+ across the squared bottom-right corner and cut through the fold's diagonal —
2701
+ the corner read as a notch with a line through it rather than a lifted flap.
2702
+ The COLOUR goes transparent rather than the border going to 0, so the box
2703
+ keeps its geometry and a fold card still measures the same as the cards
2704
+ beside it in a deck. */
2705
+ .card--fold {
2706
+ border-color: transparent;
2707
+ border-bottom-right-radius: 0;
2708
+ background-image: linear-gradient(to top left,
2709
+ var(--surface) 0 50%,
2710
+ color-mix(in srgb, currentColor 18%, var(--card-bg)) 50% 100%);
2711
+ background-repeat: no-repeat;
2712
+ background-position: bottom right;
2713
+ background-size: var(--sp-8) var(--sp-8);
2714
+ }
2715
+
2716
+ /* ---- wave: audio waveform stand-in for voice rows ----
2717
+ <span class="wave"></span> — sits in a .row__body, fills the width */
2718
+ .wave {
2719
+ display: block;
2720
+ height: var(--icon);
2721
+ width: 100%;
2722
+ background: repeating-linear-gradient(90deg, currentColor 0 var(--sp-1), transparent var(--sp-1) var(--sp-2));
2723
+ /* masks only read alpha: currentColor = "opaque" without naming a color */
2724
+ mask: radial-gradient(ellipse 60% 100% at 50% 50%, currentColor 55%, transparent 58%) 0 0 / var(--tap-lg) 100% repeat-x;
2725
+ -webkit-mask: radial-gradient(ellipse 60% 100% at 50% 50%, currentColor 55%, transparent 58%) 0 0 / var(--tap-lg) 100% repeat-x;
2726
+ opacity: 0.8;
2727
+ }
2728
+
2729
+ /* ---- mosaic: one big photo + two small, inside a card ----
2730
+ <div class="mosaic"><div class="ph"></div><div class="ph"></div><div class="ph"></div></div> */
2731
+ .mosaic {
2732
+ display: grid;
2733
+ grid-template-columns: 2fr 1fr;
2734
+ grid-auto-rows: 1fr;
2735
+ gap: var(--sp-1);
2736
+ aspect-ratio: 4 / 3;
2737
+ border-radius: var(--r-media);
2738
+ overflow: hidden;
2739
+ }
2740
+ .mosaic > :first-child { grid-row: span 2; }
2741
+ .mosaic > * { min-height: 0; border-radius: 0; opacity: 1; border-width: 0; }
2742
+ .mosaic > .ph { opacity: 0.4; border-width: var(--border-w); }
2743
+ .mosaic > img { width: 100%; height: 100%; object-fit: cover; }
2744
+
2745
+ /* ---- postcard: a member's introduction, a card in the hand — photo in front,
2746
+ words on the back; tap to turn it over (Lh, 2026-09-12; card #63) ----
2747
+ <div class="postcard">
2748
+ <button class="postcard__face postcard__front" type="button" aria-label="พลิกดูด้านหลัง">
2749
+ <img src="…" alt=""> — or <div class="ph"></div> —
2750
+ <span class="postcard__label">ป้าแจ๋ว</span>
2751
+ </button>
2752
+ <button class="postcard__face postcard__back" type="button" aria-label="พลิกดูด้านหน้า">
2753
+ <p class="postcard__caption">ชอบชงกาแฟดริปตอนเช้า</p>
2754
+ <span class="postcard__label">ป้าแจ๋ว</span>
2755
+ </button>
2756
+ </div>
2757
+ Each face is the whole tap target; cardds.js toggles .is-flipped on the
2758
+ postcard (either face, Enter/Space too). A voice note, a like, a reply live
2759
+ OUTSIDE the postcard (a Row under it): a face is a button, and a button
2760
+ holds no button. The faces share one box (a 3:2 landscape card, the media
2761
+ radius); the turn is a Y rotation at --motion-flip, a plain swap under
2762
+ prefers-reduced-motion. Reads ink from the card slot it sits in. */
2763
+ .postcard {
2764
+ position: relative;
2765
+ aspect-ratio: 3 / 2;
2766
+ perspective: 60rem;
2767
+ border-radius: var(--r-media);
2768
+ }
2769
+ .postcard__face {
2770
+ position: absolute;
2771
+ inset: 0;
2772
+ display: grid;
2773
+ align-content: end;
2774
+ padding: var(--sp-4);
2775
+ gap: var(--sp-2);
2776
+ border: var(--border-w) solid var(--card-border);
2777
+ border-radius: inherit;
2778
+ overflow: hidden;
2779
+ isolation: isolate;
2780
+ color: inherit;
2781
+ text-align: left;
2782
+ backface-visibility: hidden;
2783
+ -webkit-backface-visibility: hidden;
2784
+ transition: transform var(--motion-flip) ease-in-out;
2785
+ box-shadow: var(--shadow-float);
2786
+ }
2787
+ .postcard__face:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
2788
+ /* the front: the photo (or the wireframe box) fills it, the name reads on a scrim */
2789
+ .postcard__front { background: var(--card-3-bg); color: var(--card-3-ink); }
2790
+ .postcard__front > img,
2791
+ .postcard__front > .ph {
2792
+ position: absolute;
2793
+ inset: 0;
2794
+ width: 100%;
2795
+ height: 100%;
2796
+ object-fit: cover;
2797
+ z-index: -2;
2798
+ border: 0;
2799
+ border-radius: 0;
2800
+ min-height: 0;
2801
+ }
2802
+ .postcard__front::before {
2803
+ content: "";
2804
+ position: absolute;
2805
+ inset: 0;
2806
+ z-index: -1;
2807
+ background: linear-gradient(to top, var(--scrim), transparent 45%);
2808
+ }
2809
+ /* the back: paper — the words, big enough to read at arm's length */
2810
+ .postcard__back {
2811
+ background: var(--card-2-bg);
2812
+ color: var(--card-2-ink);
2813
+ align-content: space-between;
2814
+ transform: rotateY(180deg);
2815
+ }
2816
+ .postcard__caption { margin: 0; font: var(--type-title); text-wrap: pretty; }
2817
+ .postcard__label { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; opacity: 0.85; }
2818
+ .postcard.is-flipped > .postcard__front { transform: rotateY(-180deg); }
2819
+ .postcard.is-flipped > .postcard__back { transform: rotateY(0); }
2820
+ @media (prefers-reduced-motion: reduce) {
2821
+ .postcard__face { transition: none; }
2822
+ }
2823
+
2824
+ /* ---- css/layover.css — ask drawer, popover menu, banner, float bar, callout, deck ---- */
2825
+ /* ============================================================
2826
+ cardds/layover.css — cards that lay OVER other content:
2827
+ ask drawer · popover menu · expanding banner · float bar ·
2828
+ callout · swipe deck. The sheet (peek / half / full / page)
2829
+ lives in sheet.css; these compose with .sheet-stage.
2830
+ ============================================================ */
2831
+
2832
+ /* ---- ask drawer: the content card lifts, a dark drawer asks ----
2833
+ <div class="sheet-stage sheet-stage--ask">
2834
+ <article class="card lift">…page content…</article>
2835
+ <div class="drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
2836
+ <div class="btn-trio"><button class="icon-btn">×</button><span class="btn-trio__dots"></span><button class="icon-btn icon-btn--invert">?</button><span class="btn-trio__dots"></span><button class="icon-btn">✓</button></div>
2837
+ </div></div> */
2838
+ .sheet-stage--ask {
2839
+ --drawer-h: 14rem;
2840
+ background: var(--card-3-bg);
2841
+ color: var(--card-3-ink);
2842
+ }
2843
+ .lift {
2844
+ position: absolute;
2845
+ inset: 0 0 var(--drawer-h) 0;
2846
+ border: 0;
2847
+ border-radius: 0 0 var(--r-card) var(--r-card);
2848
+ overflow: hidden;
2849
+ overscroll-behavior: contain;
2850
+ box-shadow: var(--shadow-sheet);
2851
+ align-content: start;
2852
+ }
2853
+ .drawer {
2854
+ position: absolute;
2855
+ inset-inline: 0;
2856
+ bottom: 0;
2857
+ height: var(--drawer-h);
2858
+ padding: var(--sp-5);
2859
+ display: grid;
2860
+ align-content: center;
2861
+ justify-items: center;
2862
+ gap: var(--sp-1);
2863
+ text-align: center;
2864
+ --accent: var(--card-3-ink); /* the one solid button inverts on the dark drawer */
2865
+ --on-accent: var(--card-3-bg);
2866
+ }
2867
+ .drawer .t-caption { opacity: 0.7; }
2868
+ .drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
2869
+ .drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
2870
+ .btn-trio { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); }
2871
+ .btn-trio__dots {
2872
+ width: var(--sp-5);
2873
+ height: var(--sp-1);
2874
+ 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;
2875
+ opacity: 0.4;
2876
+ }
2877
+
2878
+ /* ---- popover menu: a card anchored to its button, rows of icon + label ----
2879
+ <button class="chip" popovertarget="when">Now ▾</button>
2880
+ <div class="menu" id="when" popover><button class="menu__item"><svg class="icon"/>Now</button>…</div>
2881
+ Native popover: light dismiss, Esc, top layer. Anchored to the invoker where
2882
+ anchor positioning exists; the fallback is the browser's centred placement. */
2883
+ .menu {
2884
+ display: none; /* explicit closed state — never display on the base rule */
2885
+ margin: 0;
2886
+ inset: auto;
2887
+ border: var(--border-w) solid var(--action-border);
2888
+ border-radius: var(--r-card);
2889
+ padding: var(--sp-2);
2890
+ min-width: 12rem;
2891
+ background: var(--action-bg);
2892
+ color: var(--action-ink);
2893
+ box-shadow: var(--shadow-float);
2894
+ }
2895
+ .menu:popover-open { display: grid; }
2896
+ .menu:not([popover]) { display: grid; position: relative; } /* inline usage (no attribute) */
2897
+ .menu__item {
2898
+ display: flex;
2899
+ align-items: center;
2900
+ gap: var(--sp-3);
2901
+ min-height: var(--tap);
2902
+ width: 100%;
2903
+ padding: 0 var(--sp-3);
2904
+ border: 0;
2905
+ border-radius: var(--r-control);
2906
+ background: transparent;
2907
+ color: inherit;
2908
+ font: var(--type-title);
2909
+ text-align: left;
2910
+ cursor: pointer;
2911
+ }
2912
+ .menu__item:hover, .menu__item:focus-visible { background: var(--ink-tint); outline: none; }
2913
+ .menu__item[aria-current], .menu__item.is-on { background: var(--accent); color: var(--on-accent); }
2914
+ @supports (anchor-name: --a) {
2915
+ .menu[popover] {
2916
+ position-area: block-end span-inline-end;
2917
+ margin-top: var(--sp-2);
2918
+ min-width: max(12rem, anchor-size(width)); /* at least as wide as its trigger */
2919
+ position-try-fallbacks: flip-block, flip-inline;
2920
+ }
2921
+ }
2922
+ @media (prefers-reduced-motion: no-preference) {
2923
+ .menu[popover] {
2924
+ opacity: 0;
2925
+ translate: 0 calc(-1 * var(--sp-2));
2926
+ transition: display 0.2s allow-discrete, overlay 0.2s allow-discrete, opacity 0.2s, translate 0.2s;
2927
+ }
2928
+ .menu:popover-open { opacity: 1; translate: 0 0; }
2929
+ @starting-style { .menu:popover-open { opacity: 0; translate: 0 calc(-1 * var(--sp-2)); } }
2930
+ }
2931
+
2932
+ /* ---- banner: a chip that opens into a tinted card, no JS ----
2933
+ <details class="banner"><summary class="chip chip--on">Save $30 on orders over $70 <svg class="icon icon--xs"/></summary>
2934
+ <div class="card card--3 banner__body">…</div></details> */
2935
+ .banner { display: grid; gap: var(--sp-2); justify-items: start; }
2936
+ .banner > summary { list-style: none; cursor: pointer; }
2937
+ .banner > summary::-webkit-details-marker { display: none; }
2938
+ .banner > summary .icon { transition: rotate 0.2s ease; }
2939
+ .banner[open] > summary .icon { rotate: 180deg; }
2940
+ .banner__body { width: 100%; padding: var(--sp-4) var(--sp-5); --card-gap: var(--sp-1); }
2941
+ .banner::details-content { width: 100%; } /* Chromium wraps the content in this pseudo; justify-items: start shrink-wrapped it, so the body never reached the width */
2942
+ @supports (interpolate-size: allow-keywords) {
2943
+ @media (prefers-reduced-motion: no-preference) {
2944
+ .banner { interpolate-size: allow-keywords; }
2945
+ .banner::details-content { block-size: 0; overflow: clip; transition: block-size 0.25s ease, content-visibility 0.25s allow-discrete; }
2946
+ .banner[open]::details-content { block-size: auto; }
2947
+ }
2948
+ }
2949
+
2950
+ /* ---- float bar: glass pill nav + detached round button, floats over content ----
2951
+ <nav class="float-bar"><div class="float-bar__pill"><a class="float-bar__item" aria-current="page"><svg class="icon"/>Left</a>…</div><button class="icon-btn">…</button></nav> */
2952
+ .float-bar {
2953
+ position: fixed;
2954
+ inset-inline: 0;
2955
+ bottom: var(--sp-5);
2956
+ max-width: var(--screen-w);
2957
+ margin-inline: auto;
2958
+ padding-inline: var(--sp-4);
2959
+ display: flex;
2960
+ gap: var(--sp-2);
2961
+ align-items: center;
2962
+ z-index: 30;
2963
+ }
2964
+ .sheet-stage > .float-bar { position: absolute; padding-inline: var(--sp-4); }
2965
+ /* .screen's default bottom padding clears an .action-bar; a .float-bar is
2966
+ taller (--bar-h + its own bottom offset), so reserve the difference. Any
2967
+ parent that hosts the bar counts (see .action-bar's reserve, actions.css) */
2968
+ :has(> .float-bar) .screen:not(.screen--fill) { padding-bottom: calc(var(--bar-h) + var(--sp-5) + var(--sp-6)); }
2969
+ .float-bar__pill {
2970
+ flex: 1;
2971
+ min-width: 0;
2972
+ display: flex;
2973
+ justify-content: space-around;
2974
+ align-items: center;
2975
+ min-height: var(--bar-h);
2976
+ padding: var(--sp-1);
2977
+ border-radius: var(--r-chip);
2978
+ border: var(--border-w) solid var(--action-border);
2979
+ background: color-mix(in srgb, var(--action-bg) 82%, transparent);
2980
+ color: var(--action-ink);
2981
+ backdrop-filter: blur(var(--sp-3));
2982
+ -webkit-backdrop-filter: blur(var(--sp-3));
2983
+ box-shadow: var(--shadow-float);
2984
+ }
2985
+ .float-bar__item {
2986
+ flex: 1;
2987
+ min-width: 0;
2988
+ display: grid;
2989
+ justify-items: center;
2990
+ gap: var(--sp-0);
2991
+ min-height: var(--tap-lg);
2992
+ padding: var(--sp-1);
2993
+ border: 0;
2994
+ border-radius: var(--r-chip);
2995
+ background: transparent;
2996
+ color: inherit;
2997
+ font: var(--type-caption);
2998
+ text-decoration: none;
2999
+ opacity: 0.6;
3000
+ overflow: hidden;
3001
+ white-space: nowrap;
3002
+ text-overflow: ellipsis;
3003
+ }
3004
+ /* aria-current="false" is the correct way to mark the OTHER tabs, so the
3005
+ bare attribute selector lights every one of them up */
3006
+ .float-bar__item[aria-current]:not([aria-current="false"]),
3007
+ .float-bar__item.is-on { background: var(--accent); color: var(--on-accent); opacity: 1; }
3008
+ .float-bar > .icon-btn { width: var(--bar-h); height: var(--bar-h); box-shadow: var(--shadow-float); }
3009
+
3010
+ /* ---- callout: a small card with a tail — over a map, a media, a mascot ----
3011
+ <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> */
3012
+ .callout {
3013
+ position: absolute;
3014
+ display: grid;
3015
+ gap: var(--sp-0);
3016
+ width: max-content;
3017
+ max-width: 15rem;
3018
+ padding: var(--sp-3) var(--sp-4);
3019
+ border: var(--border-w) solid var(--action-border);
3020
+ border-radius: var(--r-card);
3021
+ background: var(--action-bg);
3022
+ color: var(--action-ink);
3023
+ box-shadow: var(--shadow-float);
3024
+ text-align: center;
3025
+ }
3026
+ .callout::after {
3027
+ content: "";
3028
+ position: absolute;
3029
+ left: 50%;
3030
+ bottom: calc(-1 * var(--sp-2));
3031
+ width: var(--sp-3);
3032
+ height: var(--sp-3);
3033
+ translate: -50% 0;
3034
+ rotate: 45deg;
3035
+ background: inherit;
3036
+ border: inherit;
3037
+ border-top: 0;
3038
+ border-left: 0;
3039
+ }
3040
+ .callout__title { font: var(--type-title); }
3041
+ .callout__sub { font: var(--type-caption); opacity: 0.7; }
3042
+ /* speech chip: a chip with the same tail (mascot / greeting) */
3043
+ .chip--bubble { position: relative; }
3044
+ .chip--bubble::after {
3045
+ content: "";
3046
+ position: absolute;
3047
+ left: var(--sp-4);
3048
+ bottom: calc(-1 * var(--sp-2) + var(--border-w));
3049
+ width: var(--sp-3);
3050
+ height: var(--sp-3);
3051
+ rotate: 45deg;
3052
+ background: inherit;
3053
+ border: inherit;
3054
+ border-top: 0;
3055
+ border-left: 0;
3056
+ }
3057
+
3058
+ /* ---- deck: swipe cards; the last child is on top ----
3059
+ <div class="deck"><article class="card">…</article><article class="card">…</article><article class="card card--fold">front</article></div>
3060
+ <div class="deck__actions"><button class="icon-btn">×</button><button class="icon-btn icon-btn--invert">✓</button></div> */
3061
+ .deck { display: grid; place-items: center; padding: var(--sp-4) var(--sp-4) var(--sp-6); }
3062
+ .deck > .card { grid-area: 1 / 1; width: min(100%, 20rem); min-height: 12.5rem; transition: rotate 0.3s ease, translate 0.3s ease; }
3063
+ .deck > .card:nth-last-child(2) { rotate: -4deg; translate: calc(-1 * var(--sp-2)) var(--sp-2); opacity: 0.85; }
3064
+ .deck > .card:nth-last-child(3) { rotate: 5deg; translate: var(--sp-3) var(--sp-4); opacity: 0.6; }
3065
+ .deck > .card:nth-last-child(n + 4) { display: none; }
3066
+ .deck__actions { display: flex; justify-content: center; gap: var(--sp-8); }
3067
+ .deck__actions > .icon-btn { width: var(--bar-h); height: var(--bar-h); }
3068
+