@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.
- package/README.md +562 -0
- package/cardds.css +18 -0
- package/cardds.js +473 -0
- package/css/actions.css +292 -0
- package/css/base.css +128 -0
- package/css/card.css +283 -0
- package/css/choice.css +297 -0
- package/css/fonts.css +37 -0
- package/css/forms.css +64 -0
- package/css/journey.css +61 -0
- package/css/layover.css +243 -0
- package/css/lists.css +142 -0
- package/css/media.css +232 -0
- package/css/numbers.css +258 -0
- package/css/palette.css +112 -0
- package/css/people.css +79 -0
- package/css/sheet.css +295 -0
- package/css/stack.css +262 -0
- package/css/step.css +71 -0
- package/css/tokens.css +177 -0
- package/dist/actions/Btn.d.ts +311 -0
- package/dist/actions/Btn.js +14 -0
- package/dist/actions/BtnRow.d.ts +5 -0
- package/dist/actions/BtnRow.js +6 -0
- package/dist/actions/Chip.d.ts +328 -0
- package/dist/actions/Chip.js +25 -0
- package/dist/actions/Dot.d.ts +7 -0
- package/dist/actions/Dot.js +6 -0
- package/dist/actions/Dropdown.d.ts +34 -0
- package/dist/actions/Dropdown.js +19 -0
- package/dist/actions/Fab.d.ts +9 -0
- package/dist/actions/Fab.js +7 -0
- package/dist/actions/IconBtn.d.ts +308 -0
- package/dist/actions/IconBtn.js +8 -0
- package/dist/actions/Link.d.ts +301 -0
- package/dist/actions/Link.js +8 -0
- package/dist/actions/Segment.d.ts +11 -0
- package/dist/actions/Segment.js +10 -0
- package/dist/cardds.css +3068 -0
- package/dist/cards/Card.d.ts +310 -0
- package/dist/cards/Card.js +14 -0
- package/dist/cards/CardFoot.d.ts +5 -0
- package/dist/cards/CardFoot.js +6 -0
- package/dist/cards/CardHead.d.ts +21 -0
- package/dist/cards/CardHead.js +11 -0
- package/dist/cards/CardParts.d.ts +25 -0
- package/dist/cards/CardParts.js +26 -0
- package/dist/cards/Placeholder.d.ts +9 -0
- package/dist/cards/Placeholder.js +6 -0
- package/dist/cards/Stat.d.ts +17 -0
- package/dist/cards/Stat.js +14 -0
- package/dist/choice/Calendar.d.ts +23 -0
- package/dist/choice/Calendar.js +7 -0
- package/dist/choice/Check.d.ts +14 -0
- package/dist/choice/Check.js +10 -0
- package/dist/choice/DayStrip.d.ts +16 -0
- package/dist/choice/DayStrip.js +10 -0
- package/dist/choice/Mood.d.ts +11 -0
- package/dist/choice/Mood.js +6 -0
- package/dist/choice/Slider.d.ts +11 -0
- package/dist/choice/Slider.js +14 -0
- package/dist/cx.d.ts +2 -0
- package/dist/cx.js +2 -0
- package/dist/forms/AddRow.d.ts +5 -0
- package/dist/forms/AddRow.js +7 -0
- package/dist/forms/Composer.d.ts +13 -0
- package/dist/forms/Composer.js +6 -0
- package/dist/forms/Field.d.ts +17 -0
- package/dist/forms/Field.js +10 -0
- package/dist/forms/FileBtn.d.ts +18 -0
- package/dist/forms/FileBtn.js +10 -0
- package/dist/forms/Pin.d.ts +19 -0
- package/dist/forms/Pin.js +27 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +75 -0
- package/dist/journey/NoteRow.d.ts +7 -0
- package/dist/journey/NoteRow.js +7 -0
- package/dist/journey/Route.d.ts +18 -0
- package/dist/journey/Route.js +8 -0
- package/dist/journey/TileBadge.d.ts +15 -0
- package/dist/journey/TileBadge.js +6 -0
- package/dist/layover/Banner.d.ts +9 -0
- package/dist/layover/Banner.js +6 -0
- package/dist/layover/Callout.d.ts +10 -0
- package/dist/layover/Callout.js +6 -0
- package/dist/layover/Deck.d.ts +9 -0
- package/dist/layover/Deck.js +10 -0
- package/dist/lists/Kv.d.ts +27 -0
- package/dist/lists/Kv.js +19 -0
- package/dist/lists/Legend.d.ts +11 -0
- package/dist/lists/Legend.js +11 -0
- package/dist/lists/Row.d.ts +319 -0
- package/dist/lists/Row.js +17 -0
- package/dist/lists/Timeline.d.ts +11 -0
- package/dist/lists/Timeline.js +10 -0
- package/dist/media/MapArea.d.ts +20 -0
- package/dist/media/MapArea.js +11 -0
- package/dist/media/Mosaic.d.ts +11 -0
- package/dist/media/Mosaic.js +10 -0
- package/dist/media/Postcard.d.ts +20 -0
- package/dist/media/Postcard.js +10 -0
- package/dist/media/Quote.d.ts +11 -0
- package/dist/media/Quote.js +6 -0
- package/dist/media/Tile.d.ts +320 -0
- package/dist/media/Tile.js +16 -0
- package/dist/numbers/Badge.d.ts +7 -0
- package/dist/numbers/Badge.js +6 -0
- package/dist/numbers/Band.d.ts +16 -0
- package/dist/numbers/Band.js +10 -0
- package/dist/numbers/Bars.d.ts +14 -0
- package/dist/numbers/Bars.js +10 -0
- package/dist/numbers/DotGrid.d.ts +12 -0
- package/dist/numbers/DotGrid.js +6 -0
- package/dist/numbers/Picker.d.ts +13 -0
- package/dist/numbers/Picker.js +18 -0
- package/dist/numbers/Ring.d.ts +15 -0
- package/dist/numbers/Ring.js +6 -0
- package/dist/numbers/Track.d.ts +12 -0
- package/dist/numbers/Track.js +10 -0
- package/dist/people/Avatar.d.ts +314 -0
- package/dist/people/Avatar.js +13 -0
- package/dist/people/AvatarPick.d.ts +18 -0
- package/dist/people/AvatarPick.js +10 -0
- package/dist/scaffold/ActionBar.d.ts +21 -0
- package/dist/scaffold/ActionBar.js +16 -0
- package/dist/scaffold/AppBar.d.ts +18 -0
- package/dist/scaffold/AppBar.js +10 -0
- package/dist/scaffold/Centre.d.ts +30 -0
- package/dist/scaffold/Centre.js +41 -0
- package/dist/scaffold/EmptyState.d.ts +14 -0
- package/dist/scaffold/EmptyState.js +8 -0
- package/dist/scaffold/FilterRow.d.ts +5 -0
- package/dist/scaffold/FilterRow.js +6 -0
- package/dist/scaffold/FloatBar.d.ts +310 -0
- package/dist/scaffold/FloatBar.js +15 -0
- package/dist/scaffold/PagerAt.d.ts +11 -0
- package/dist/scaffold/PagerAt.js +6 -0
- package/dist/scaffold/Screen.d.ts +299 -0
- package/dist/scaffold/Screen.js +11 -0
- package/dist/scaffold/Section.d.ts +12 -0
- package/dist/scaffold/Section.js +13 -0
- package/dist/scaffold/TopBar.d.ts +15 -0
- package/dist/scaffold/TopBar.js +9 -0
- package/dist/sheets/CardBack.d.ts +7 -0
- package/dist/sheets/CardBack.js +7 -0
- package/dist/sheets/Drawer.d.ts +17 -0
- package/dist/sheets/Drawer.js +16 -0
- package/dist/sheets/Sheet.d.ts +21 -0
- package/dist/sheets/Sheet.js +13 -0
- package/dist/sheets/SheetBody.d.ts +5 -0
- package/dist/sheets/SheetBody.js +6 -0
- package/dist/sheets/SheetHead.d.ts +13 -0
- package/dist/sheets/SheetHead.js +6 -0
- package/dist/sheets/SheetStack.d.ts +21 -0
- package/dist/sheets/SheetStack.js +14 -0
- package/dist/sheets/SheetStage.d.ts +20 -0
- package/dist/sheets/SheetStage.js +11 -0
- package/dist/sheets/Step.d.ts +15 -0
- package/dist/sheets/Step.js +15 -0
- package/dist/type/Icon.d.ts +16 -0
- package/dist/type/Icon.js +12 -0
- package/dist/type/Text.d.ts +302 -0
- package/dist/type/Text.js +16 -0
- package/dist/type/icons.d.ts +6 -0
- package/dist/type/icons.js +2 -0
- package/fonts/LICENSES.md +44 -0
- package/fonts/fc-pride-medium.otf +0 -0
- package/fonts/pk-nonthaburi-demo.ttf +0 -0
- package/icons.svg +61 -0
- package/package.json +62 -0
package/dist/cardds.css
ADDED
|
@@ -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
|
+
|