@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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1327 -0
  3. package/bin/deck.mjs +219 -0
  4. package/build.mjs +338 -0
  5. package/dist/api-buckets.json +10486 -0
  6. package/dist/api.json +57896 -0
  7. package/dist/brand/deck-apple-touch-icon.png +0 -0
  8. package/dist/brand/deck-logo-dark.svg +1 -0
  9. package/dist/brand/deck-logo-light.svg +1 -0
  10. package/dist/brand/deck-logo-stacked.svg +1 -0
  11. package/dist/brand/deck-logo.svg +1 -0
  12. package/dist/brand/deck-mark.svg +7 -0
  13. package/dist/brand/deck-og.png +0 -0
  14. package/dist/collisions.json +223 -0
  15. package/dist/deck-adapters.js +482 -0
  16. package/dist/deck-adapters.min.js +23 -0
  17. package/dist/deck-extras.js +823 -0
  18. package/dist/deck-extras.min.js +5 -0
  19. package/dist/deck-icons.svg +184 -0
  20. package/dist/deck.bundle.js +2638 -0
  21. package/dist/deck.bundle.min.js +86 -0
  22. package/dist/deck.css +7904 -0
  23. package/dist/deck.esm.js +2644 -0
  24. package/dist/deck.js +1331 -0
  25. package/dist/deck.min.css +32 -0
  26. package/dist/deck.min.js +60 -0
  27. package/dist/layers/anchor.css +192 -0
  28. package/dist/layers/buttons.css +210 -0
  29. package/dist/layers/charts.css +337 -0
  30. package/dist/layers/combobox.css +248 -0
  31. package/dist/layers/components.css +868 -0
  32. package/dist/layers/container.css +199 -0
  33. package/dist/layers/datagrid.css +335 -0
  34. package/dist/layers/datepicker.css +305 -0
  35. package/dist/layers/forms.css +401 -0
  36. package/dist/layers/gradients.css +302 -0
  37. package/dist/layers/inputs.css +522 -0
  38. package/dist/layers/layers.css +87 -0
  39. package/dist/layers/layout.css +235 -0
  40. package/dist/layers/logical.css +226 -0
  41. package/dist/layers/media.css +400 -0
  42. package/dist/layers/mobile.css +245 -0
  43. package/dist/layers/motion.css +379 -0
  44. package/dist/layers/nav.css +771 -0
  45. package/dist/layers/perf.css +234 -0
  46. package/dist/layers/print.css +198 -0
  47. package/dist/layers/reset.css +103 -0
  48. package/dist/layers/space3d.css +280 -0
  49. package/dist/layers/toasts.css +201 -0
  50. package/dist/layers/tokens.css +229 -0
  51. package/dist/layers/type.css +114 -0
  52. package/dist/layers/utilities.css +241 -0
  53. package/dist/sizes.json +52 -0
  54. package/dist/usage.json +6340 -0
  55. package/package.json +94 -0
  56. package/src/00-layers.css +87 -0
  57. package/src/01-tokens.css +229 -0
  58. package/src/02-reset.css +103 -0
  59. package/src/03-type.css +114 -0
  60. package/src/04-layout.css +235 -0
  61. package/src/05-buttons.css +210 -0
  62. package/src/06-forms.css +401 -0
  63. package/src/07-components.css +868 -0
  64. package/src/08-mobile.css +245 -0
  65. package/src/09-utilities.css +241 -0
  66. package/src/10-datepicker.css +305 -0
  67. package/src/11-combobox.css +248 -0
  68. package/src/12-datagrid.css +335 -0
  69. package/src/13-toasts.css +201 -0
  70. package/src/14-charts.css +337 -0
  71. package/src/16-motion.css +379 -0
  72. package/src/18-container.css +199 -0
  73. package/src/19-logical.css +226 -0
  74. package/src/20-gradients.css +302 -0
  75. package/src/21-space3d.css +280 -0
  76. package/src/22-nav.css +771 -0
  77. package/src/23-inputs.css +522 -0
  78. package/src/24-media.css +400 -0
  79. package/src/25-anchor.css +192 -0
  80. package/src/26-perf.css +234 -0
  81. package/src/99-print.css +198 -0
  82. package/src/brand/deck-apple-touch-icon.png +0 -0
  83. package/src/brand/deck-logo-dark.svg +1 -0
  84. package/src/brand/deck-logo-light.svg +1 -0
  85. package/src/brand/deck-logo-stacked.svg +1 -0
  86. package/src/brand/deck-logo.svg +1 -0
  87. package/src/brand/deck-mark.svg +7 -0
  88. package/src/brand/deck-og.png +0 -0
  89. package/src/brand/sources.json +7 -0
  90. package/src/deck-icons.svg +184 -0
  91. package/src/js/deck-adapters.js +482 -0
  92. package/src/js/deck-extras.js +823 -0
  93. 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
+ }