@echodial/deck 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/LICENSE +21 -0
- package/README.md +1327 -0
- package/bin/deck.mjs +219 -0
- package/build.mjs +338 -0
- package/dist/api-buckets.json +10486 -0
- package/dist/api.json +57896 -0
- package/dist/brand/deck-apple-touch-icon.png +0 -0
- package/dist/brand/deck-logo-dark.svg +1 -0
- package/dist/brand/deck-logo-light.svg +1 -0
- package/dist/brand/deck-logo-stacked.svg +1 -0
- package/dist/brand/deck-logo.svg +1 -0
- package/dist/brand/deck-mark.svg +7 -0
- package/dist/brand/deck-og.png +0 -0
- package/dist/collisions.json +223 -0
- package/dist/deck-adapters.js +482 -0
- package/dist/deck-adapters.min.js +23 -0
- package/dist/deck-extras.js +823 -0
- package/dist/deck-extras.min.js +5 -0
- package/dist/deck-icons.svg +184 -0
- package/dist/deck.bundle.js +2638 -0
- package/dist/deck.bundle.min.js +86 -0
- package/dist/deck.css +7904 -0
- package/dist/deck.esm.js +2644 -0
- package/dist/deck.js +1331 -0
- package/dist/deck.min.css +32 -0
- package/dist/deck.min.js +60 -0
- package/dist/layers/anchor.css +192 -0
- package/dist/layers/buttons.css +210 -0
- package/dist/layers/charts.css +337 -0
- package/dist/layers/combobox.css +248 -0
- package/dist/layers/components.css +868 -0
- package/dist/layers/container.css +199 -0
- package/dist/layers/datagrid.css +335 -0
- package/dist/layers/datepicker.css +305 -0
- package/dist/layers/forms.css +401 -0
- package/dist/layers/gradients.css +302 -0
- package/dist/layers/inputs.css +522 -0
- package/dist/layers/layers.css +87 -0
- package/dist/layers/layout.css +235 -0
- package/dist/layers/logical.css +226 -0
- package/dist/layers/media.css +400 -0
- package/dist/layers/mobile.css +245 -0
- package/dist/layers/motion.css +379 -0
- package/dist/layers/nav.css +771 -0
- package/dist/layers/perf.css +234 -0
- package/dist/layers/print.css +198 -0
- package/dist/layers/reset.css +103 -0
- package/dist/layers/space3d.css +280 -0
- package/dist/layers/toasts.css +201 -0
- package/dist/layers/tokens.css +229 -0
- package/dist/layers/type.css +114 -0
- package/dist/layers/utilities.css +241 -0
- package/dist/sizes.json +52 -0
- package/dist/usage.json +6340 -0
- package/package.json +94 -0
- package/src/00-layers.css +87 -0
- package/src/01-tokens.css +229 -0
- package/src/02-reset.css +103 -0
- package/src/03-type.css +114 -0
- package/src/04-layout.css +235 -0
- package/src/05-buttons.css +210 -0
- package/src/06-forms.css +401 -0
- package/src/07-components.css +868 -0
- package/src/08-mobile.css +245 -0
- package/src/09-utilities.css +241 -0
- package/src/10-datepicker.css +305 -0
- package/src/11-combobox.css +248 -0
- package/src/12-datagrid.css +335 -0
- package/src/13-toasts.css +201 -0
- package/src/14-charts.css +337 -0
- package/src/16-motion.css +379 -0
- package/src/18-container.css +199 -0
- package/src/19-logical.css +226 -0
- package/src/20-gradients.css +302 -0
- package/src/21-space3d.css +280 -0
- package/src/22-nav.css +771 -0
- package/src/23-inputs.css +522 -0
- package/src/24-media.css +400 -0
- package/src/25-anchor.css +192 -0
- package/src/26-perf.css +234 -0
- package/src/99-print.css +198 -0
- package/src/brand/deck-apple-touch-icon.png +0 -0
- package/src/brand/deck-logo-dark.svg +1 -0
- package/src/brand/deck-logo-light.svg +1 -0
- package/src/brand/deck-logo-stacked.svg +1 -0
- package/src/brand/deck-logo.svg +1 -0
- package/src/brand/deck-mark.svg +7 -0
- package/src/brand/deck-og.png +0 -0
- package/src/brand/sources.json +7 -0
- package/src/deck-icons.svg +184 -0
- package/src/js/deck-adapters.js +482 -0
- package/src/js/deck-extras.js +823 -0
- package/src/js/deck.js +1331 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Deck — 07 Components
|
|
3
|
+
========================================================================== */
|
|
4
|
+
|
|
5
|
+
@layer deck.components {
|
|
6
|
+
|
|
7
|
+
/* --- Card --------------------------------------------------------------- */
|
|
8
|
+
/* A surface with a border, radius and shadow. It brings no padding of its
|
|
9
|
+
own — put .card-body inside so the header, body and footer can each own
|
|
10
|
+
their spacing. */
|
|
11
|
+
.card {
|
|
12
|
+
background: var(--surface);
|
|
13
|
+
border: 1px solid var(--line);
|
|
14
|
+
border-radius: var(--r-md);
|
|
15
|
+
box-shadow: var(--shadow-1);
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
min-inline-size: 0;
|
|
19
|
+
overflow: clip;
|
|
20
|
+
}
|
|
21
|
+
/* The bottom region. margin-block-start: auto pins it to the bottom of
|
|
22
|
+
the card, so a row of cards of unequal length still has its footers
|
|
23
|
+
level without anything being measured. */
|
|
24
|
+
/* The top region of a card. A flex row, so a title and a control sit
|
|
25
|
+
on one line; the divider below it belongs to the header, so a card
|
|
26
|
+
with no header has no stray line. */
|
|
27
|
+
.card-header, .card-footer {
|
|
28
|
+
padding: var(--space-4) var(--space-5);
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: var(--space-3);
|
|
32
|
+
}
|
|
33
|
+
.card-header { border-block-end: 1px solid var(--line); }
|
|
34
|
+
.card-footer { border-block-start: 1px solid var(--line); background: var(--surface-2); margin-block-start: auto; }
|
|
35
|
+
/* The padded region of a card. A column with a gap, so children stack
|
|
36
|
+
without margins. */
|
|
37
|
+
.card-body { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-3); }
|
|
38
|
+
/* The heading inside a card. Sized independently of h1-h6 so the same
|
|
39
|
+
card works at any heading level. */
|
|
40
|
+
.card-title { font-size: var(--text-md); font-weight: 620; letter-spacing: -.012em; }
|
|
41
|
+
/* An image region that reaches the card's edges. The card's own
|
|
42
|
+
overflow: clip does the rounding, so the image needs no radius.
|
|
43
|
+
Put it beside .card-body, never inside it. */
|
|
44
|
+
.card-media { margin: 0; }
|
|
45
|
+
.card-media img { inline-size: 100%; object-fit: cover; }
|
|
46
|
+
|
|
47
|
+
/* Whole card is the link, but nested links still work */
|
|
48
|
+
.card-link { position: relative; transition: box-shadow var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out); }
|
|
49
|
+
.card-link:hover { box-shadow: var(--shadow-3); border-color: var(--line-strong); }
|
|
50
|
+
.card-link .stretch::after { content: ""; position: absolute; inset: 0; }
|
|
51
|
+
|
|
52
|
+
/* A card with no border, shadow or background. Grouping without a box
|
|
53
|
+
drawn around it. */
|
|
54
|
+
.card-flush { border: 0; box-shadow: none; background: none; }
|
|
55
|
+
/* A deeper shadow and no visible border, for a surface that should read
|
|
56
|
+
as floating above the page. */
|
|
57
|
+
.card-raised { box-shadow: var(--shadow-3); border-color: transparent; }
|
|
58
|
+
/* Brand-tinted fill and border, for the one card on a page that is an
|
|
59
|
+
offer rather than a record. */
|
|
60
|
+
.card-brand { border-color: var(--brand-200); background: var(--brand-soft); }
|
|
61
|
+
|
|
62
|
+
/* --- Panel: a card that goes edge to edge on a phone --------------------- */
|
|
63
|
+
.panel {
|
|
64
|
+
background: var(--surface);
|
|
65
|
+
border-block: 1px solid var(--line);
|
|
66
|
+
padding: var(--space-5) var(--space-gutter);
|
|
67
|
+
margin-inline: calc(var(--space-gutter) * -1);
|
|
68
|
+
}
|
|
69
|
+
@media (min-width: 40rem) {
|
|
70
|
+
.panel {
|
|
71
|
+
border: 1px solid var(--line);
|
|
72
|
+
border-radius: var(--r-md);
|
|
73
|
+
margin-inline: 0;
|
|
74
|
+
padding-inline: var(--space-5);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* --- Badge and chip ----------------------------------------------------- */
|
|
79
|
+
/* A small inline status pill. Sized in em, so it tracks whatever text it
|
|
80
|
+
sits next to rather than needing a size variant per context. */
|
|
81
|
+
.badge {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: var(--space-1);
|
|
85
|
+
padding: .18em .55em;
|
|
86
|
+
border-radius: var(--r-xs);
|
|
87
|
+
background: var(--surface-2);
|
|
88
|
+
color: var(--text-muted);
|
|
89
|
+
border: 1px solid var(--line);
|
|
90
|
+
font-size: var(--text-xs);
|
|
91
|
+
font-weight: 580;
|
|
92
|
+
line-height: 1.45;
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
}
|
|
95
|
+
/* Brand tint. For a label that names a category rather than a status. */
|
|
96
|
+
.badge-brand { background: var(--brand-soft); color: var(--brand-soft-text); border-color: transparent; }
|
|
97
|
+
/* Paid, shipped, passing, active. */
|
|
98
|
+
.badge-good { background: var(--good-100); color: var(--good-700); border-color: transparent; }
|
|
99
|
+
/* Pending, expiring: needs attention, nothing is broken. */
|
|
100
|
+
.badge-warn { background: var(--warn-100); color: var(--warn-700); border-color: transparent; }
|
|
101
|
+
/* Failed, overdue, rejected. */
|
|
102
|
+
.badge-bad { background: var(--bad-100); color: var(--bad-700); border-color: transparent; }
|
|
103
|
+
/* Filled with the brand colour. The highest-emphasis badge; use one per
|
|
104
|
+
view or it stops meaning anything. */
|
|
105
|
+
.badge-solid { background: var(--brand-600); color: var(--text-on-brand); border-color: transparent; }
|
|
106
|
+
/* Adds a leading dot in currentColor, so it takes the tone's text colour
|
|
107
|
+
without a second class. The dot is decoration — never ship one with no
|
|
108
|
+
text beside it, because colour alone is not a status. */
|
|
109
|
+
.badge-dot::before {
|
|
110
|
+
content: "";
|
|
111
|
+
inline-size: 6px; block-size: 6px;
|
|
112
|
+
border-radius: var(--r-full);
|
|
113
|
+
background: currentColor;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.chip {
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: var(--space-2);
|
|
120
|
+
min-block-size: 34px;
|
|
121
|
+
padding-inline: var(--space-3);
|
|
122
|
+
border-radius: var(--r-full);
|
|
123
|
+
border: 1px solid var(--line-strong);
|
|
124
|
+
background: var(--surface);
|
|
125
|
+
font-size: var(--text-sm);
|
|
126
|
+
font-weight: 520;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
white-space: nowrap;
|
|
129
|
+
transition: background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
|
|
130
|
+
}
|
|
131
|
+
.chip:hover { background: var(--surface-hover); }
|
|
132
|
+
.chip[aria-pressed="true"], .chip.is-active {
|
|
133
|
+
background: var(--brand-600);
|
|
134
|
+
border-color: var(--brand-600);
|
|
135
|
+
color: var(--text-on-brand);
|
|
136
|
+
}
|
|
137
|
+
.chip-remove { display: grid; place-items: center; opacity: .6; }
|
|
138
|
+
.chip-remove:hover { opacity: 1; }
|
|
139
|
+
|
|
140
|
+
/* --- Alert -------------------------------------------------------------- */
|
|
141
|
+
/* A block of prose the reader is meant to stop and read. The tone is a
|
|
142
|
+
three-pixel border-inline-start, so it moves to the right edge under
|
|
143
|
+
dir="rtl" with no second rule. Deck sets no ARIA role: role="alert" on
|
|
144
|
+
a message that is present at page load interrupts a screen reader
|
|
145
|
+
before it has read anything. */
|
|
146
|
+
.alert {
|
|
147
|
+
display: flex;
|
|
148
|
+
gap: var(--space-3);
|
|
149
|
+
padding: var(--space-4);
|
|
150
|
+
border-radius: var(--r-md);
|
|
151
|
+
border: 1px solid var(--line);
|
|
152
|
+
background: var(--surface-2);
|
|
153
|
+
border-inline-start: 3px solid var(--ink-400);
|
|
154
|
+
}
|
|
155
|
+
/* The first line of an alert. A weight change, not a heading — if it
|
|
156
|
+
belongs in the document outline, put this class on a real h2-h4. */
|
|
157
|
+
.alert-title { font-weight: 620; margin-block-end: 2px; }
|
|
158
|
+
/* The explanatory line. min-inline-size: 0 so a long unbroken string
|
|
159
|
+
cannot push the alert wider than its container. */
|
|
160
|
+
.alert-body { font-size: var(--text-sm); color: var(--text-muted); min-inline-size: 0; }
|
|
161
|
+
.alert > .icon { flex: 0 0 auto; margin-block-start: 1px; }
|
|
162
|
+
/* Something the reader should know before continuing. */
|
|
163
|
+
.alert-info { background: var(--brand-soft); border-color: transparent; border-inline-start-color: var(--brand-500); color: var(--brand-soft-text); }
|
|
164
|
+
/* Confirmation that something worked. */
|
|
165
|
+
.alert-good { background: var(--good-100); border-color: transparent; border-inline-start-color: var(--good-500); color: var(--good-700); }
|
|
166
|
+
/* A consequence the reader should understand first. */
|
|
167
|
+
.alert-warn { background: var(--warn-100); border-color: transparent; border-inline-start-color: var(--warn-500); color: var(--warn-700); }
|
|
168
|
+
/* Something failed, or is about to. */
|
|
169
|
+
.alert-bad { background: var(--bad-100); border-color: transparent; border-inline-start-color: var(--bad-500); color: var(--bad-700); }
|
|
170
|
+
.alert :is(.alert-body, p) { color: inherit; opacity: .9; }
|
|
171
|
+
|
|
172
|
+
/* --- Avatar ------------------------------------------------------------- */
|
|
173
|
+
/* A round box holding an image or initials. --size drives the width, the
|
|
174
|
+
height and the font size together, so a size variant is one
|
|
175
|
+
declaration and the initials can never overflow the circle. */
|
|
176
|
+
.avatar {
|
|
177
|
+
--size: 40px;
|
|
178
|
+
inline-size: var(--size);
|
|
179
|
+
block-size: var(--size);
|
|
180
|
+
border-radius: var(--r-full);
|
|
181
|
+
background: var(--brand-soft);
|
|
182
|
+
color: var(--brand-soft-text);
|
|
183
|
+
display: grid;
|
|
184
|
+
place-items: center;
|
|
185
|
+
font-size: calc(var(--size) * .38);
|
|
186
|
+
font-weight: 620;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
flex: 0 0 auto;
|
|
189
|
+
user-select: none;
|
|
190
|
+
}
|
|
191
|
+
.avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; }
|
|
192
|
+
/* 24px. */
|
|
193
|
+
.avatar-xs { --size: 24px; }
|
|
194
|
+
/* 32px. */
|
|
195
|
+
.avatar-sm { --size: 32px; }
|
|
196
|
+
/* 56px. */
|
|
197
|
+
.avatar-lg { --size: 56px; }
|
|
198
|
+
/* 80px. */
|
|
199
|
+
.avatar-xl { --size: 80px; }
|
|
200
|
+
/* A square avatar, for anything that is not a person — an organisation,
|
|
201
|
+
a project, a repository. */
|
|
202
|
+
.avatar-square { border-radius: var(--r-sm); }
|
|
203
|
+
|
|
204
|
+
/* Overlapping avatars. The offset is a fraction of --size, so it stays
|
|
205
|
+
proportional at every size, and the ring is the surface colour, which
|
|
206
|
+
is wrong on any other background. */
|
|
207
|
+
.avatar-stack { display: flex; }
|
|
208
|
+
.avatar-stack > .avatar + .avatar { margin-inline-start: calc(var(--size, 40px) * -.32); }
|
|
209
|
+
.avatar-stack > .avatar { box-shadow: 0 0 0 2px var(--surface); }
|
|
210
|
+
|
|
211
|
+
.presence { position: relative; }
|
|
212
|
+
.presence::after {
|
|
213
|
+
content: "";
|
|
214
|
+
position: absolute;
|
|
215
|
+
inset-block-end: 0;
|
|
216
|
+
inset-inline-end: 0;
|
|
217
|
+
inline-size: 30%;
|
|
218
|
+
block-size: 30%;
|
|
219
|
+
border-radius: var(--r-full);
|
|
220
|
+
background: var(--good-500);
|
|
221
|
+
box-shadow: 0 0 0 2px var(--surface);
|
|
222
|
+
}
|
|
223
|
+
.presence-away::after { background: var(--warn-500); }
|
|
224
|
+
.presence-off::after { background: var(--ink-400); }
|
|
225
|
+
|
|
226
|
+
/* --- Table -------------------------------------------------------------- */
|
|
227
|
+
/* The scroll container for a table, and not optional. It carries the
|
|
228
|
+
border, radius, background and overflow-x, without which a wide table
|
|
229
|
+
pushes the whole page sideways. Add tabindex="0" when the table
|
|
230
|
+
overflows, or a keyboard user cannot scroll it. */
|
|
231
|
+
.table-wrap {
|
|
232
|
+
inline-size: 100%;
|
|
233
|
+
overflow-x: auto;
|
|
234
|
+
-webkit-overflow-scrolling: touch;
|
|
235
|
+
border: 1px solid var(--line);
|
|
236
|
+
border-radius: var(--r-md);
|
|
237
|
+
background: var(--surface);
|
|
238
|
+
}
|
|
239
|
+
/* A real table, kept a table at every width, because values in a column
|
|
240
|
+
are only comparable while the columns exist. */
|
|
241
|
+
.table { inline-size: 100%; font-size: var(--text-sm); }
|
|
242
|
+
.table :is(th, td) {
|
|
243
|
+
padding: var(--space-3) var(--space-4);
|
|
244
|
+
text-align: start;
|
|
245
|
+
vertical-align: middle;
|
|
246
|
+
}
|
|
247
|
+
.table thead th {
|
|
248
|
+
background: var(--surface-2);
|
|
249
|
+
font-weight: 600;
|
|
250
|
+
color: var(--text-muted);
|
|
251
|
+
font-size: var(--text-xs);
|
|
252
|
+
letter-spacing: .01em;
|
|
253
|
+
white-space: nowrap;
|
|
254
|
+
position: sticky;
|
|
255
|
+
inset-block-start: 0;
|
|
256
|
+
z-index: 1;
|
|
257
|
+
}
|
|
258
|
+
.table tbody tr + tr { border-block-start: 1px solid var(--line); }
|
|
259
|
+
.table tbody tr:hover { background: var(--surface-hover); }
|
|
260
|
+
/* A numeric table cell: aligned to the end edge with tabular figures, so
|
|
261
|
+
digits line up down the column. */
|
|
262
|
+
.table .num { text-align: end; font-variant-numeric: tabular-nums; }
|
|
263
|
+
/* Tighter cell padding, for a table the reader scans rather than reads. */
|
|
264
|
+
.table-compact :is(th, td) { padding: var(--space-2) var(--space-3); }
|
|
265
|
+
|
|
266
|
+
/* Below 40rem the table restacks into labelled rows.
|
|
267
|
+
Put the column name in data-label on each cell. */
|
|
268
|
+
@media (max-width: 39.999rem) {
|
|
269
|
+
.table-stack thead { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
270
|
+
/* Below 40rem, restacks each row into labelled lines, taking the label
|
|
271
|
+
from data-label on the cell. Every cell needs one. The thead is
|
|
272
|
+
clipped rather than removed, so header association survives; print
|
|
273
|
+
undoes the whole thing, because paper is never narrow. */
|
|
274
|
+
.table-stack, .table-stack tbody, .table-stack tr, .table-stack td { display: block; inline-size: 100%; }
|
|
275
|
+
.table-stack tr {
|
|
276
|
+
border: 1px solid var(--line);
|
|
277
|
+
border-radius: var(--r-sm);
|
|
278
|
+
padding: var(--space-2);
|
|
279
|
+
margin-block-end: var(--space-3);
|
|
280
|
+
}
|
|
281
|
+
.table-stack tbody tr + tr { border-block-start: 1px solid var(--line); }
|
|
282
|
+
.table-stack td {
|
|
283
|
+
display: flex;
|
|
284
|
+
justify-content: space-between;
|
|
285
|
+
gap: var(--space-4);
|
|
286
|
+
padding: var(--space-2) var(--space-3);
|
|
287
|
+
text-align: end;
|
|
288
|
+
}
|
|
289
|
+
.table-stack td::before {
|
|
290
|
+
content: attr(data-label);
|
|
291
|
+
font-weight: 580;
|
|
292
|
+
color: var(--text-muted);
|
|
293
|
+
text-align: start;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* --- List rows ---------------------------------------------------------- */
|
|
298
|
+
/* A bordered, clipped column of rows. Use it when the reader reads one
|
|
299
|
+
row at a time; if they compare values down a column, that is a table. */
|
|
300
|
+
.list {
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-direction: column;
|
|
303
|
+
background: var(--surface);
|
|
304
|
+
border: 1px solid var(--line);
|
|
305
|
+
border-radius: var(--r-md);
|
|
306
|
+
overflow: clip;
|
|
307
|
+
}
|
|
308
|
+
/* One row. min-block-size: var(--tap) puts every row at the touch
|
|
309
|
+
target, and the colour and text-decoration resets mean the row can be
|
|
310
|
+
an <a> directly — one element, one tab stop, the whole row clickable. */
|
|
311
|
+
.list-row {
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: center;
|
|
314
|
+
gap: var(--space-3);
|
|
315
|
+
padding: var(--space-3) var(--space-4);
|
|
316
|
+
min-block-size: var(--tap);
|
|
317
|
+
color: inherit;
|
|
318
|
+
text-decoration: none;
|
|
319
|
+
transition: background-color var(--dur-1) var(--ease-out);
|
|
320
|
+
}
|
|
321
|
+
.list-row + .list-row { border-block-start: 1px solid var(--line); }
|
|
322
|
+
.list-row:hover { background: var(--surface-hover); }
|
|
323
|
+
/* The text column of a row. min-inline-size: 0 is the important half: a
|
|
324
|
+
flex item's minimum is its content by default, so without it a long
|
|
325
|
+
value pushes the trailing content off the end of the row. */
|
|
326
|
+
.list-row .list-main { flex: 1 1 auto; min-inline-size: 0; display: flex; flex-direction: column; gap: 1px; }
|
|
327
|
+
/* The first line of a row. */
|
|
328
|
+
.list-row .list-title { font-weight: 560; }
|
|
329
|
+
/* The second, muted line of a row. */
|
|
330
|
+
.list-row .list-sub { font-size: var(--text-sm); color: var(--text-muted); }
|
|
331
|
+
/* What sits at the end of a row — a time, a count, a chevron. Faint and
|
|
332
|
+
never shrinking. Below 4.5:1 by design, so nothing load-bearing
|
|
333
|
+
belongs here. */
|
|
334
|
+
.list-row .list-trail { color: var(--text-faint); flex: 0 0 auto; font-size: var(--text-sm); }
|
|
335
|
+
/* A group heading inside a list. Styling only: a screen reader hears
|
|
336
|
+
ordinary text, so if the grouping is meaningful use a real heading. */
|
|
337
|
+
.list-header {
|
|
338
|
+
padding: var(--space-2) var(--space-4);
|
|
339
|
+
background: var(--surface-2);
|
|
340
|
+
font-size: var(--text-xs);
|
|
341
|
+
font-weight: 620;
|
|
342
|
+
color: var(--text-muted);
|
|
343
|
+
position: sticky;
|
|
344
|
+
inset-block-start: 0;
|
|
345
|
+
z-index: 1;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* --- Tabs --------------------------------------------------------------- */
|
|
349
|
+
/* The strip. Scrolls sideways rather than wrapping, because tabs that wrap
|
|
350
|
+
stop reading as a strip. Deck styles the tabs and ships no keyboard
|
|
351
|
+
behaviour: if you use role="tablist" you must add the roving tabindex and
|
|
352
|
+
the arrow keys yourself, or the role promises what the page does not do. */
|
|
353
|
+
.tabs {
|
|
354
|
+
display: flex;
|
|
355
|
+
gap: var(--space-1);
|
|
356
|
+
border-block-end: 1px solid var(--line);
|
|
357
|
+
overflow-x: auto;
|
|
358
|
+
scrollbar-width: none;
|
|
359
|
+
}
|
|
360
|
+
.tabs::-webkit-scrollbar { display: none; }
|
|
361
|
+
/* One trigger. Styled from [aria-selected="true"], so the visual state and
|
|
362
|
+
the accessible state are the same attribute and cannot disagree. The
|
|
363
|
+
unselected state carries the same transparent border, so selecting one
|
|
364
|
+
changes a colour and never moves anything. */
|
|
365
|
+
.tab {
|
|
366
|
+
padding: var(--space-3) var(--space-4);
|
|
367
|
+
min-block-size: var(--tap);
|
|
368
|
+
display: inline-flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
gap: var(--space-2);
|
|
371
|
+
color: var(--text-muted);
|
|
372
|
+
font-weight: 560;
|
|
373
|
+
font-size: var(--text-sm);
|
|
374
|
+
white-space: nowrap;
|
|
375
|
+
cursor: pointer;
|
|
376
|
+
border-block-end: 2px solid transparent;
|
|
377
|
+
margin-block-end: -1px;
|
|
378
|
+
text-decoration: none;
|
|
379
|
+
transition: color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
|
|
380
|
+
}
|
|
381
|
+
.tab:hover { color: var(--text); }
|
|
382
|
+
.tab[aria-selected="true"], .tab.is-active {
|
|
383
|
+
color: var(--brand);
|
|
384
|
+
border-block-end-color: var(--brand);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* --- Segmented control (iOS-style, good on a phone) --------------------- */
|
|
388
|
+
/* A one-of-N control: equal-width children in a sunken track with the
|
|
389
|
+
selected one raised. Styled from [aria-selected="true"] or .is-active.
|
|
390
|
+
Built from radio inputs it needs no JavaScript and gets arrow keys free —
|
|
391
|
+
but Deck does not style :has(:checked), so that version needs one rule of
|
|
392
|
+
your own. */
|
|
393
|
+
.segmented {
|
|
394
|
+
display: inline-flex;
|
|
395
|
+
padding: 3px;
|
|
396
|
+
gap: 2px;
|
|
397
|
+
background: var(--bg-sunken);
|
|
398
|
+
border-radius: var(--r-sm);
|
|
399
|
+
border: 1px solid var(--line);
|
|
400
|
+
inline-size: 100%;
|
|
401
|
+
}
|
|
402
|
+
.segmented > * {
|
|
403
|
+
flex: 1 1 0;
|
|
404
|
+
min-block-size: 36px;
|
|
405
|
+
padding-inline: var(--space-3);
|
|
406
|
+
border-radius: calc(var(--r-sm) - 2px);
|
|
407
|
+
display: inline-flex;
|
|
408
|
+
align-items: center;
|
|
409
|
+
justify-content: center;
|
|
410
|
+
gap: var(--space-2);
|
|
411
|
+
font-size: var(--text-sm);
|
|
412
|
+
font-weight: 560;
|
|
413
|
+
color: var(--text-muted);
|
|
414
|
+
cursor: pointer;
|
|
415
|
+
white-space: nowrap;
|
|
416
|
+
transition: background-color var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
|
|
417
|
+
}
|
|
418
|
+
.segmented > [aria-selected="true"], .segmented > .is-active {
|
|
419
|
+
background: var(--surface);
|
|
420
|
+
color: var(--text);
|
|
421
|
+
box-shadow: var(--shadow-1);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* --- Accordion (native details, animates open) -------------------------- */
|
|
425
|
+
/* A bordered container for native <details>. The open state, the keyboard
|
|
426
|
+
and the accessible name are the element's. Deck restyles the summary,
|
|
427
|
+
replaces the marker with a chevron, and animates the height using
|
|
428
|
+
::details-content plus the interpolate-size opt-in in 02-reset.css — the
|
|
429
|
+
first time a disclosure could animate to auto height without JavaScript.
|
|
430
|
+
Give the details elements a shared name attribute for one-at-a-time. */
|
|
431
|
+
.accordion { border: 1px solid var(--line); border-radius: var(--r-md); overflow: clip; background: var(--surface); }
|
|
432
|
+
.accordion > details + details { border-block-start: 1px solid var(--line); }
|
|
433
|
+
.accordion summary, .disclosure summary {
|
|
434
|
+
display: flex;
|
|
435
|
+
align-items: center;
|
|
436
|
+
gap: var(--space-3);
|
|
437
|
+
padding: var(--space-4);
|
|
438
|
+
min-block-size: var(--tap);
|
|
439
|
+
font-weight: 560;
|
|
440
|
+
cursor: pointer;
|
|
441
|
+
list-style: none;
|
|
442
|
+
transition: background-color var(--dur-1) var(--ease-out);
|
|
443
|
+
}
|
|
444
|
+
.accordion summary::-webkit-details-marker, .disclosure summary::-webkit-details-marker { display: none; }
|
|
445
|
+
.accordion summary:hover, .disclosure summary:hover { background: var(--surface-hover); }
|
|
446
|
+
.accordion summary::after, .disclosure summary::after {
|
|
447
|
+
content: "";
|
|
448
|
+
margin-inline-start: auto;
|
|
449
|
+
inline-size: 8px;
|
|
450
|
+
block-size: 8px;
|
|
451
|
+
border-inline-end: 2px solid var(--text-faint);
|
|
452
|
+
border-block-end: 2px solid var(--text-faint);
|
|
453
|
+
rotate: 45deg;
|
|
454
|
+
translate: 0 -2px;
|
|
455
|
+
transition: rotate var(--dur-2) var(--ease-out);
|
|
456
|
+
}
|
|
457
|
+
.accordion details[open] summary::after, .disclosure[open] summary::after { rotate: 225deg; translate: 0 2px; }
|
|
458
|
+
/* The padded region below a summary. Indented to line up with the summary
|
|
459
|
+
text rather than the chevron. */
|
|
460
|
+
.accordion .accordion-body, .disclosure .disclosure-body {
|
|
461
|
+
padding: 0 var(--space-4) var(--space-4);
|
|
462
|
+
color: var(--text-muted);
|
|
463
|
+
font-size: var(--text-sm);
|
|
464
|
+
}
|
|
465
|
+
.accordion details::details-content, .disclosure::details-content {
|
|
466
|
+
block-size: 0;
|
|
467
|
+
overflow: hidden;
|
|
468
|
+
transition: block-size var(--dur-3) var(--ease-out), content-visibility var(--dur-3) allow-discrete;
|
|
469
|
+
}
|
|
470
|
+
.accordion details[open]::details-content, .disclosure[open]::details-content { block-size: auto; }
|
|
471
|
+
|
|
472
|
+
/* --- Breadcrumb --------------------------------------------------------- */
|
|
473
|
+
.breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); font-size: var(--text-sm); }
|
|
474
|
+
.breadcrumb a { color: var(--text-muted); text-decoration: none; }
|
|
475
|
+
.breadcrumb a:hover { color: var(--brand); }
|
|
476
|
+
.breadcrumb li + li::before { content: "/"; color: var(--text-faint); margin-inline-end: var(--space-2); }
|
|
477
|
+
.breadcrumb li { display: flex; align-items: center; }
|
|
478
|
+
.breadcrumb [aria-current] { color: var(--text); font-weight: 560; }
|
|
479
|
+
|
|
480
|
+
/* --- Pagination --------------------------------------------------------- */
|
|
481
|
+
.pagination { display: flex; gap: var(--space-1); align-items: center; flex-wrap: wrap; }
|
|
482
|
+
.pagination > * {
|
|
483
|
+
min-inline-size: var(--control-h-sm);
|
|
484
|
+
min-block-size: var(--control-h-sm);
|
|
485
|
+
padding-inline: var(--space-2);
|
|
486
|
+
display: inline-flex;
|
|
487
|
+
align-items: center;
|
|
488
|
+
justify-content: center;
|
|
489
|
+
border-radius: var(--r-xs);
|
|
490
|
+
color: var(--text-muted);
|
|
491
|
+
font-size: var(--text-sm);
|
|
492
|
+
font-variant-numeric: tabular-nums;
|
|
493
|
+
text-decoration: none;
|
|
494
|
+
}
|
|
495
|
+
.pagination > a:hover { background: var(--surface-hover); color: var(--text); }
|
|
496
|
+
.pagination > [aria-current="page"] { background: var(--brand-600); color: var(--text-on-brand); font-weight: 600; }
|
|
497
|
+
|
|
498
|
+
/* --- Progress, ring, spinner, skeleton ---------------------------------- */
|
|
499
|
+
|
|
500
|
+
/* Three ways to say "something is happening", and the choice is only ever
|
|
501
|
+
about one thing: whether you know how much is left. .progress and .ring
|
|
502
|
+
do, .spinner does not. There is no standalone meter -- a bar drawn from a
|
|
503
|
+
value rather than from an element is .chart-meter, in 14-charts.css.
|
|
504
|
+
|
|
505
|
+
.progress goes on a real <progress>, so the element carries the value, the
|
|
506
|
+
maximum and the role. The two vendor pseudo-elements have to be written as
|
|
507
|
+
separate rules: a vendor pseudo-element in a shared selector invalidates
|
|
508
|
+
the whole rule for the other engine. Same constraint as .range. */
|
|
509
|
+
.progress {
|
|
510
|
+
inline-size: 100%;
|
|
511
|
+
block-size: 8px;
|
|
512
|
+
border-radius: var(--r-full);
|
|
513
|
+
background: var(--ink-200);
|
|
514
|
+
overflow: hidden;
|
|
515
|
+
appearance: none;
|
|
516
|
+
border: 0;
|
|
517
|
+
}
|
|
518
|
+
.progress::-webkit-progress-bar { background: var(--ink-200); }
|
|
519
|
+
.progress::-webkit-progress-value { background: var(--brand-600); border-radius: var(--r-full); transition: inline-size var(--dur-3) var(--ease-out); }
|
|
520
|
+
.progress::-moz-progress-bar { background: var(--brand-600); border-radius: var(--r-full); }
|
|
521
|
+
|
|
522
|
+
/* A conic gradient for the fill, a radial mask for the hole. Three custom
|
|
523
|
+
properties shape it: --value, --size, --thickness.
|
|
524
|
+
|
|
525
|
+
The mask is the thing to understand. It applies to this element AND its
|
|
526
|
+
whole subtree, so a label centred inside a .ring is punched out along with
|
|
527
|
+
the middle -- it renders, it takes up space, it is in the accessibility
|
|
528
|
+
tree, and it is invisible. That is what .ring-wrap and .ring-label below
|
|
529
|
+
exist for: an unmasked wrapper that stacks the label over the ring as a
|
|
530
|
+
sibling rather than a child.
|
|
531
|
+
|
|
532
|
+
FINDINGS 60: place-items and the three font declarations here are the
|
|
533
|
+
fossil of the approach that does not work. They style a child nobody can
|
|
534
|
+
see -- .ring-label repeats all three on the sibling, which is the tell.
|
|
535
|
+
Due for removal; kept for now because the API is frozen. */
|
|
536
|
+
.ring {
|
|
537
|
+
--value: 0;
|
|
538
|
+
--thickness: 6px;
|
|
539
|
+
--size: 56px;
|
|
540
|
+
inline-size: var(--size);
|
|
541
|
+
block-size: var(--size);
|
|
542
|
+
display: grid;
|
|
543
|
+
place-items: center;
|
|
544
|
+
border-radius: var(--r-full);
|
|
545
|
+
font-size: var(--text-xs);
|
|
546
|
+
font-weight: 620;
|
|
547
|
+
font-variant-numeric: tabular-nums;
|
|
548
|
+
background: conic-gradient(var(--brand-600) calc(var(--value) * 1%), var(--ink-200) 0);
|
|
549
|
+
mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), #000 calc(100% - var(--thickness) + 1px));
|
|
550
|
+
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), #000 calc(100% - var(--thickness) + 1px));
|
|
551
|
+
}
|
|
552
|
+
.ring-wrap { position: relative; display: inline-grid; place-items: center; }
|
|
553
|
+
.ring-wrap > .ring-label { position: absolute; font-size: var(--text-xs); font-weight: 620; font-variant-numeric: tabular-nums; }
|
|
554
|
+
|
|
555
|
+
/* Exempt from the reduced-motion reset in 02-reset.css: instead of freezing
|
|
556
|
+
it slows to 2.4s. A frozen spinner reads as a hung page, which is worse
|
|
557
|
+
for everyone than a slow one. Note border-block-start-color rather than
|
|
558
|
+
border-top-color -- the lit segment starts at the block start in any
|
|
559
|
+
writing mode. */
|
|
560
|
+
.spinner {
|
|
561
|
+
inline-size: 20px;
|
|
562
|
+
block-size: 20px;
|
|
563
|
+
border: 2px solid var(--ink-200);
|
|
564
|
+
border-block-start-color: var(--brand-600);
|
|
565
|
+
border-radius: var(--r-full);
|
|
566
|
+
animation: dk-spin .62s linear infinite;
|
|
567
|
+
}
|
|
568
|
+
.spinner-lg { inline-size: 34px; block-size: 34px; border-width: 3px; }
|
|
569
|
+
|
|
570
|
+
.skeleton {
|
|
571
|
+
background: linear-gradient(100deg,
|
|
572
|
+
var(--ink-100) 30%, var(--ink-200) 48%, var(--ink-100) 66%);
|
|
573
|
+
background-size: 300% 100%;
|
|
574
|
+
animation: dk-shimmer 1.4s var(--ease-in-out) infinite;
|
|
575
|
+
border-radius: var(--r-xs);
|
|
576
|
+
block-size: 1em;
|
|
577
|
+
color: transparent;
|
|
578
|
+
user-select: none;
|
|
579
|
+
}
|
|
580
|
+
.skeleton-text { block-size: .8em; margin-block: .35em; }
|
|
581
|
+
.skeleton-circle { border-radius: var(--r-full); }
|
|
582
|
+
@keyframes dk-shimmer { to { background-position: -300% 0; } }
|
|
583
|
+
|
|
584
|
+
/* --- Tooltip ------------------------------------------------------------ */
|
|
585
|
+
/* A CSS-only tooltip: the text comes from data-tip through ::after, so there
|
|
586
|
+
is no second element. It cannot flip near a window edge, cannot escape an
|
|
587
|
+
overflow:hidden ancestor, and is hidden entirely on a coarse pointer. Never
|
|
588
|
+
put anything essential in it, and pair it with aria-label — generated
|
|
589
|
+
content is not a reliable accessible name. */
|
|
590
|
+
.tooltip { position: relative; }
|
|
591
|
+
.tooltip::after {
|
|
592
|
+
content: attr(data-tip);
|
|
593
|
+
position: absolute;
|
|
594
|
+
inset-block-end: calc(100% + 8px);
|
|
595
|
+
inset-inline-start: 50%;
|
|
596
|
+
translate: -50% 4px;
|
|
597
|
+
background: var(--ink-900);
|
|
598
|
+
color: var(--ink-50);
|
|
599
|
+
font-size: var(--text-xs);
|
|
600
|
+
font-weight: 500;
|
|
601
|
+
line-height: 1.4;
|
|
602
|
+
padding: var(--space-2) var(--space-3);
|
|
603
|
+
border-radius: var(--r-xs);
|
|
604
|
+
inline-size: max-content;
|
|
605
|
+
max-inline-size: 16rem;
|
|
606
|
+
box-shadow: var(--shadow-3);
|
|
607
|
+
opacity: 0;
|
|
608
|
+
pointer-events: none;
|
|
609
|
+
transition: opacity var(--dur-2) var(--ease-out), translate var(--dur-2) var(--ease-out);
|
|
610
|
+
z-index: var(--z-toast);
|
|
611
|
+
}
|
|
612
|
+
.tooltip:is(:hover, :focus-visible)::after { opacity: 1; translate: -50% 0; }
|
|
613
|
+
@media (pointer: coarse) { .tooltip::after { display: none; } }
|
|
614
|
+
|
|
615
|
+
/* --- Menu / dropdown, built on the native popover ----------------------- */
|
|
616
|
+
/* A dropdown built on the native popover attribute: light dismiss, Escape,
|
|
617
|
+
the top layer and one-at-a-time behaviour all come from the platform.
|
|
618
|
+
25-anchor.css positions it against its trigger with anchor positioning and
|
|
619
|
+
a fallback chain, so it flips when there is no room below. */
|
|
620
|
+
.menu {
|
|
621
|
+
position: absolute;
|
|
622
|
+
margin: 0;
|
|
623
|
+
padding: var(--space-1);
|
|
624
|
+
inline-size: max-content;
|
|
625
|
+
min-inline-size: 12rem;
|
|
626
|
+
max-inline-size: min(20rem, calc(100vw - var(--space-8)));
|
|
627
|
+
background: var(--surface);
|
|
628
|
+
border: 1px solid var(--line);
|
|
629
|
+
border-radius: var(--r-md);
|
|
630
|
+
box-shadow: var(--shadow-4);
|
|
631
|
+
inset: auto;
|
|
632
|
+
opacity: 0;
|
|
633
|
+
translate: 0 -6px;
|
|
634
|
+
transition: opacity var(--dur-2) var(--ease-out),
|
|
635
|
+
translate var(--dur-2) var(--ease-out),
|
|
636
|
+
overlay var(--dur-2) allow-discrete,
|
|
637
|
+
display var(--dur-2) allow-discrete;
|
|
638
|
+
}
|
|
639
|
+
.menu:popover-open { opacity: 1; translate: 0; }
|
|
640
|
+
@starting-style { .menu:popover-open { opacity: 0; translate: 0 -6px; } }
|
|
641
|
+
.menu::backdrop { background: transparent; }
|
|
642
|
+
|
|
643
|
+
/* One row in a menu. Works on a button or an anchor — colour and
|
|
644
|
+
text-decoration are reset so a link needs no help. Hover and focus share a
|
|
645
|
+
style, so a keyboard user sees what a mouse user sees. 40px tall, which is
|
|
646
|
+
under the 44px target: a deliberate density trade for a list of eight. */
|
|
647
|
+
.menu-item {
|
|
648
|
+
display: flex;
|
|
649
|
+
align-items: center;
|
|
650
|
+
gap: var(--space-3);
|
|
651
|
+
inline-size: 100%;
|
|
652
|
+
min-block-size: 40px;
|
|
653
|
+
padding: var(--space-2) var(--space-3);
|
|
654
|
+
border-radius: var(--r-xs);
|
|
655
|
+
color: var(--text);
|
|
656
|
+
font-size: var(--text-sm);
|
|
657
|
+
text-align: start;
|
|
658
|
+
text-decoration: none;
|
|
659
|
+
cursor: pointer;
|
|
660
|
+
transition: background-color var(--dur-1) var(--ease-out);
|
|
661
|
+
}
|
|
662
|
+
.menu-item:hover, .menu-item:focus-visible { background: var(--surface-hover); }
|
|
663
|
+
/* A destructive item. Red is not announced, so the label has to say what it
|
|
664
|
+
does and a confirmation should stand behind it. */
|
|
665
|
+
.menu-item-danger { color: var(--bad-700); }
|
|
666
|
+
.menu-item-danger:hover { background: var(--bad-100); }
|
|
667
|
+
.menu-item .push { margin-inline-start: auto; color: var(--text-faint); }
|
|
668
|
+
/* A one-pixel divider between groups of items. */
|
|
669
|
+
.menu-sep { block-size: 1px; background: var(--line); margin: var(--space-1) 0; }
|
|
670
|
+
/* A small heading above a group of items. Styling only — a screen reader
|
|
671
|
+
hears ordinary text. */
|
|
672
|
+
.menu-label { padding: var(--space-2) var(--space-3) var(--space-1); font-size: var(--text-xs); font-weight: 620; color: var(--text-faint); }
|
|
673
|
+
|
|
674
|
+
/* --- Modal, built on native dialog -------------------------------------- */
|
|
675
|
+
/* A native <dialog>. Open it with showModal(), never show() and never the
|
|
676
|
+
open attribute: only showModal() gives the top layer, the backdrop, the
|
|
677
|
+
focus trap and an inert background. The open and close animate because
|
|
678
|
+
display and overlay are in the transition with allow-discrete, which is
|
|
679
|
+
what keeps the element rendered long enough to fade out. */
|
|
680
|
+
.modal {
|
|
681
|
+
inline-size: min(34rem, calc(100vw - var(--space-8)));
|
|
682
|
+
max-block-size: min(85dvh, 48rem);
|
|
683
|
+
padding: 0;
|
|
684
|
+
border: 1px solid var(--line);
|
|
685
|
+
border-radius: var(--r-lg);
|
|
686
|
+
background: var(--surface);
|
|
687
|
+
color: var(--text);
|
|
688
|
+
box-shadow: var(--shadow-4);
|
|
689
|
+
overflow: clip;
|
|
690
|
+
opacity: 0;
|
|
691
|
+
scale: .97;
|
|
692
|
+
translate: 0 8px;
|
|
693
|
+
transition: opacity var(--dur-2) var(--ease-out),
|
|
694
|
+
scale var(--dur-2) var(--ease-spring),
|
|
695
|
+
translate var(--dur-2) var(--ease-spring),
|
|
696
|
+
overlay var(--dur-2) allow-discrete,
|
|
697
|
+
display var(--dur-2) allow-discrete;
|
|
698
|
+
}
|
|
699
|
+
.modal[open] { opacity: 1; scale: 1; translate: 0; }
|
|
700
|
+
@starting-style { .modal[open] { opacity: 0; scale: .97; translate: 0 8px; } }
|
|
701
|
+
.modal::backdrop {
|
|
702
|
+
background: oklch(18% .02 var(--hue-neutral) / .48);
|
|
703
|
+
backdrop-filter: blur(3px);
|
|
704
|
+
-webkit-backdrop-filter: blur(3px);
|
|
705
|
+
opacity: 0;
|
|
706
|
+
transition: opacity var(--dur-2) var(--ease-out), display var(--dur-2) allow-discrete, overlay var(--dur-2) allow-discrete;
|
|
707
|
+
}
|
|
708
|
+
.modal[open]::backdrop { opacity: 1; }
|
|
709
|
+
@starting-style { .modal[open]::backdrop { opacity: 0; } }
|
|
710
|
+
|
|
711
|
+
/* The title row. A flex row aligned to flex-start, so a long title and a
|
|
712
|
+
close button line up on the first line. */
|
|
713
|
+
.modal-header {
|
|
714
|
+
display: flex;
|
|
715
|
+
align-items: flex-start;
|
|
716
|
+
gap: var(--space-3);
|
|
717
|
+
padding: var(--space-5);
|
|
718
|
+
border-block-end: 1px solid var(--line);
|
|
719
|
+
}
|
|
720
|
+
/* The dialog's heading. Give it an id and point aria-labelledby at it, or
|
|
721
|
+
the dialog is announced with no name. */
|
|
722
|
+
.modal-title { font-size: var(--text-lg); font-weight: 640; letter-spacing: -.018em; }
|
|
723
|
+
/* The scrolling region. The modal caps at min(85dvh, 48rem) and clips, so a
|
|
724
|
+
long body scrolls while the header and footer stay put — which is what
|
|
725
|
+
keeps the actions above the fold. */
|
|
726
|
+
.modal-body { padding: var(--space-5); overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-4); }
|
|
727
|
+
/* The action row, end-aligned. Below 32rem it becomes column-reverse with
|
|
728
|
+
full-width buttons, so the primary action — last in the markup — is on top
|
|
729
|
+
where a thumb reaches it, without changing DOM order. */
|
|
730
|
+
.modal-footer {
|
|
731
|
+
display: flex;
|
|
732
|
+
gap: var(--space-3);
|
|
733
|
+
justify-content: flex-end;
|
|
734
|
+
padding: var(--space-4) var(--space-5);
|
|
735
|
+
border-block-start: 1px solid var(--line);
|
|
736
|
+
background: var(--surface-2);
|
|
737
|
+
flex-wrap: wrap;
|
|
738
|
+
}
|
|
739
|
+
@media (max-width: 32rem) {
|
|
740
|
+
.modal-footer { flex-direction: column-reverse; }
|
|
741
|
+
.modal-footer > .btn { inline-size: 100%; }
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/* --- Empty state -------------------------------------------------------- */
|
|
745
|
+
.empty {
|
|
746
|
+
display: grid;
|
|
747
|
+
place-items: center;
|
|
748
|
+
gap: var(--space-3);
|
|
749
|
+
padding: var(--space-12) var(--space-5);
|
|
750
|
+
text-align: center;
|
|
751
|
+
color: var(--text-muted);
|
|
752
|
+
}
|
|
753
|
+
.empty-art {
|
|
754
|
+
inline-size: 56px;
|
|
755
|
+
block-size: 56px;
|
|
756
|
+
display: grid;
|
|
757
|
+
place-items: center;
|
|
758
|
+
border-radius: var(--r-full);
|
|
759
|
+
background: var(--brand-soft);
|
|
760
|
+
color: var(--brand);
|
|
761
|
+
font-size: 26px;
|
|
762
|
+
}
|
|
763
|
+
.empty-title { font-size: var(--text-md); font-weight: 620; color: var(--text); }
|
|
764
|
+
.empty p { max-inline-size: 34ch; }
|
|
765
|
+
|
|
766
|
+
/* --- Stat --------------------------------------------------------------- */
|
|
767
|
+
.stat { display: flex; flex-direction: column; gap: var(--space-1); }
|
|
768
|
+
.stat-value {
|
|
769
|
+
font-size: var(--text-2xl);
|
|
770
|
+
font-weight: 660;
|
|
771
|
+
letter-spacing: -.03em;
|
|
772
|
+
font-variant-numeric: tabular-nums;
|
|
773
|
+
line-height: 1.05;
|
|
774
|
+
}
|
|
775
|
+
.stat-label { font-size: var(--text-sm); color: var(--text-muted); }
|
|
776
|
+
.stat-delta { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-sm); font-weight: 560; }
|
|
777
|
+
.stat-delta-up { color: var(--good-700); }
|
|
778
|
+
.stat-delta-down { color: var(--bad-700); }
|
|
779
|
+
|
|
780
|
+
/* --- Timeline ----------------------------------------------------------- */
|
|
781
|
+
.timeline { display: flex; flex-direction: column; }
|
|
782
|
+
.timeline-item {
|
|
783
|
+
display: grid;
|
|
784
|
+
grid-template-columns: 22px 1fr;
|
|
785
|
+
gap: var(--space-3);
|
|
786
|
+
padding-block-end: var(--space-5);
|
|
787
|
+
position: relative;
|
|
788
|
+
}
|
|
789
|
+
.timeline-item:not(:last-child)::before {
|
|
790
|
+
content: "";
|
|
791
|
+
position: absolute;
|
|
792
|
+
inset-block: 22px 0;
|
|
793
|
+
inset-inline-start: 10px;
|
|
794
|
+
inline-size: 2px;
|
|
795
|
+
background: var(--line);
|
|
796
|
+
}
|
|
797
|
+
.timeline-dot {
|
|
798
|
+
inline-size: 22px;
|
|
799
|
+
block-size: 22px;
|
|
800
|
+
border-radius: var(--r-full);
|
|
801
|
+
background: var(--surface);
|
|
802
|
+
border: 2px solid var(--line-strong);
|
|
803
|
+
display: grid;
|
|
804
|
+
place-items: center;
|
|
805
|
+
z-index: 1;
|
|
806
|
+
}
|
|
807
|
+
.timeline-item.is-done .timeline-dot { background: var(--brand-600); border-color: var(--brand-600); color: var(--text-on-brand); }
|
|
808
|
+
|
|
809
|
+
/* --- Nav bar ------------------------------------------------------------ */
|
|
810
|
+
.navbar {
|
|
811
|
+
display: flex;
|
|
812
|
+
align-items: center;
|
|
813
|
+
gap: var(--space-4);
|
|
814
|
+
min-block-size: var(--control-h-lg);
|
|
815
|
+
padding-block: var(--space-2);
|
|
816
|
+
border-block-end: 1px solid var(--line);
|
|
817
|
+
}
|
|
818
|
+
.navbar-brand {
|
|
819
|
+
display: inline-flex;
|
|
820
|
+
align-items: center;
|
|
821
|
+
gap: var(--space-2);
|
|
822
|
+
font-weight: 680;
|
|
823
|
+
font-size: var(--text-md);
|
|
824
|
+
letter-spacing: -.024em;
|
|
825
|
+
color: var(--text);
|
|
826
|
+
text-decoration: none;
|
|
827
|
+
}
|
|
828
|
+
.navbar-links { display: none; gap: var(--space-1); align-items: center; }
|
|
829
|
+
@media (min-width: 48rem) { .navbar-links { display: flex; } }
|
|
830
|
+
/* One destination in a .navbar. Carries the hover and current-page
|
|
831
|
+
states; use .btn only for actions, never for navigation. */
|
|
832
|
+
.nav-link {
|
|
833
|
+
padding: var(--space-2) var(--space-3);
|
|
834
|
+
border-radius: var(--r-sm);
|
|
835
|
+
color: var(--text-muted);
|
|
836
|
+
font-size: var(--text-sm);
|
|
837
|
+
font-weight: 540;
|
|
838
|
+
text-decoration: none;
|
|
839
|
+
transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
|
|
840
|
+
}
|
|
841
|
+
.nav-link:hover { background: var(--surface-hover); color: var(--text); }
|
|
842
|
+
.nav-link[aria-current] { color: var(--brand); background: var(--brand-soft); }
|
|
843
|
+
|
|
844
|
+
/* --- Sidebar ------------------------------------------------------------ */
|
|
845
|
+
.sidebar {
|
|
846
|
+
display: flex;
|
|
847
|
+
flex-direction: column;
|
|
848
|
+
gap: var(--space-1);
|
|
849
|
+
padding: var(--space-3);
|
|
850
|
+
}
|
|
851
|
+
.sidebar-group { font-size: var(--text-xs); font-weight: 620; color: var(--text-faint); padding: var(--space-3) var(--space-3) var(--space-1); }
|
|
852
|
+
.sidebar-link {
|
|
853
|
+
display: flex;
|
|
854
|
+
align-items: center;
|
|
855
|
+
gap: var(--space-3);
|
|
856
|
+
min-block-size: 40px;
|
|
857
|
+
padding: var(--space-2) var(--space-3);
|
|
858
|
+
border-radius: var(--r-sm);
|
|
859
|
+
color: var(--text-muted);
|
|
860
|
+
font-size: var(--text-sm);
|
|
861
|
+
font-weight: 540;
|
|
862
|
+
text-decoration: none;
|
|
863
|
+
transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
|
|
864
|
+
}
|
|
865
|
+
.sidebar-link:hover { background: var(--surface-hover); color: var(--text); }
|
|
866
|
+
.sidebar-link[aria-current] { background: var(--brand-soft); color: var(--brand-soft-text); font-weight: 600; }
|
|
867
|
+
.sidebar-link .push { margin-inline-start: auto; }
|
|
868
|
+
}
|