@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,400 @@
1
+ /* ==========================================================================
2
+ Deck — 24 Media and messaging
3
+ ========================================================================== */
4
+
5
+ @layer deck.components {
6
+
7
+ /* --- Video ---------------------------------------------------------------
8
+ A responsive frame for a native video or an embed, plus styling for the
9
+ native controls where the browser allows it.
10
+ ---------------------------------------------------------------------- */
11
+ .video {
12
+ position: relative;
13
+ inline-size: 100%;
14
+ aspect-ratio: var(--ratio, 16 / 9);
15
+ background: var(--ink-950);
16
+ border-radius: var(--r-md);
17
+ overflow: clip;
18
+ isolation: isolate;
19
+ }
20
+ .video > :is(video, iframe, embed, object) {
21
+ position: absolute;
22
+ inset: 0;
23
+ inline-size: 100%;
24
+ block-size: 100%;
25
+ border: 0;
26
+ object-fit: contain;
27
+ }
28
+ .video-cover > video { object-fit: cover; }
29
+ .video-square { --ratio: 1; }
30
+ .video-portrait { --ratio: 9 / 16; }
31
+ .video-wide { --ratio: 21 / 9; }
32
+
33
+ .video video::-webkit-media-controls-panel { background-image: none; }
34
+ .video video { accent-color: var(--brand-500); }
35
+
36
+ /* Poster overlay with a play button, for a click-to-load embed */
37
+ .video-poster {
38
+ position: absolute;
39
+ inset: 0;
40
+ z-index: 1;
41
+ display: grid;
42
+ place-items: center;
43
+ cursor: pointer;
44
+ border: 0;
45
+ padding: 0;
46
+ background: none;
47
+ }
48
+ .video-poster img { inline-size: 100%; block-size: 100%; object-fit: cover; }
49
+ .video-poster::after {
50
+ content: "";
51
+ position: absolute;
52
+ inset: 0;
53
+ background: linear-gradient(to top, oklch(12% .02 var(--hue-neutral) / .55), transparent 60%);
54
+ }
55
+ .video-play {
56
+ position: relative;
57
+ z-index: 1;
58
+ inline-size: 64px;
59
+ block-size: 64px;
60
+ border-radius: var(--r-full);
61
+ display: grid;
62
+ place-items: center;
63
+ background: color-mix(in oklab, var(--surface) 90%, transparent);
64
+ backdrop-filter: blur(8px);
65
+ -webkit-backdrop-filter: blur(8px);
66
+ color: var(--text);
67
+ box-shadow: var(--shadow-3);
68
+ transition: scale var(--dur-2) var(--ease-spring), background-color var(--dur-1) var(--ease-out);
69
+ }
70
+ .video-poster:hover .video-play { scale: 1.08; background: var(--surface); }
71
+ .video-meta {
72
+ position: absolute;
73
+ inset-block-end: var(--space-3);
74
+ inset-inline-start: var(--space-3);
75
+ z-index: 2;
76
+ display: flex;
77
+ gap: var(--space-2);
78
+ color: oklch(98% 0 0);
79
+ font-size: var(--text-xs);
80
+ text-shadow: 0 1px 3px oklch(0% 0 0 / .6);
81
+ }
82
+ .video-duration {
83
+ padding: 2px 6px;
84
+ border-radius: var(--r-xs);
85
+ background: oklch(12% 0 0 / .72);
86
+ font-variant-numeric: tabular-nums;
87
+ }
88
+
89
+ /* --- Masonry gallery -----------------------------------------------------
90
+ Two implementations. Columns is universal and good for photos where the
91
+ reading order down each column is fine. Grid masonry is the real thing
92
+ and preserves row order; it is used automatically where supported.
93
+ ---------------------------------------------------------------------- */
94
+ .masonry {
95
+ columns: var(--cols, 1);
96
+ column-gap: var(--gap, var(--space-3));
97
+ }
98
+ @media (min-width: 30rem) { .masonry { --cols: 2; } }
99
+ @media (min-width: 48rem) { .masonry { --cols: 3; } }
100
+ @media (min-width: 72rem) { .masonry { --cols: 4; } }
101
+ .masonry-2 { --cols: 2; }
102
+ .masonry-3 { --cols: 3; }
103
+
104
+ .masonry > * {
105
+ break-inside: avoid;
106
+ margin-block-end: var(--gap, var(--space-3));
107
+ border-radius: var(--r-md);
108
+ overflow: clip;
109
+ display: block;
110
+ }
111
+ .masonry img { inline-size: 100%; block-size: auto; display: block; }
112
+
113
+ @supports (grid-template-rows: masonry) {
114
+ .masonry {
115
+ columns: auto;
116
+ display: grid;
117
+ grid-template-columns: repeat(auto-fill, minmax(min(14rem, 100%), 1fr));
118
+ grid-template-rows: masonry;
119
+ gap: var(--gap, var(--space-3));
120
+ }
121
+ .masonry > * { margin-block-end: 0; }
122
+ }
123
+
124
+ /* A simple gallery grid, when equal tiles are wanted instead */
125
+ .gallery {
126
+ display: grid;
127
+ grid-template-columns: repeat(auto-fill, minmax(min(9rem, 100%), 1fr));
128
+ gap: var(--space-2);
129
+ }
130
+ .gallery > * {
131
+ aspect-ratio: 1;
132
+ border-radius: var(--r-sm);
133
+ overflow: clip;
134
+ position: relative;
135
+ }
136
+ .gallery img { inline-size: 100%; block-size: 100%; object-fit: cover; transition: scale var(--dur-3) var(--ease-out); }
137
+ .gallery a:hover img { scale: 1.05; }
138
+ /* Let one tile take a bigger cell */
139
+ .gallery .span-2 { grid-column: span 2; grid-row: span 2; aspect-ratio: 1; }
140
+ .gallery .span-wide { grid-column: span 2; aspect-ratio: 2; }
141
+
142
+ /* --- Lazy loading --------------------------------------------------------
143
+ A placeholder that holds the space so nothing shifts, and fades the image
144
+ in once it decodes. Set the aspect ratio on the frame and the layout is
145
+ stable before a byte arrives.
146
+ ---------------------------------------------------------------------- */
147
+ .lazy {
148
+ position: relative;
149
+ display: block;
150
+ background: var(--bg-sunken);
151
+ overflow: clip;
152
+ aspect-ratio: var(--ratio, 3 / 2);
153
+ }
154
+ .lazy > img {
155
+ inline-size: 100%;
156
+ block-size: 100%;
157
+ object-fit: cover;
158
+ opacity: 0;
159
+ transition: opacity var(--dur-3) var(--ease-out);
160
+ }
161
+ .lazy > img.is-loaded, .lazy > img[data-loaded] { opacity: 1; }
162
+ /* Two things about this shimmer are recorded in FINDINGS.md:
163
+
164
+ 67: it runs the same dk-shimmer keyframes as .skeleton, for the same
165
+ purpose, but .lazy::after is NOT in 02-reset.css's reduced-motion
166
+ exemption list -- so it freezes where .skeleton slows to 2.4s. The
167
+ durations differ too (1.6s here, 1.4s there). One of the two is wrong.
168
+
169
+ 66: deck-extras.js adds .is-error to the wrapper when an image fails to
170
+ load, and nothing in src/ styles that class. So the shimmer carries on
171
+ forever and a broken image is indistinguishable from a loading one. */
172
+ .lazy::after {
173
+ content: "";
174
+ position: absolute;
175
+ inset: 0;
176
+ background: linear-gradient(100deg, transparent 20%,
177
+ color-mix(in oklab, var(--ink-300) 40%, transparent) 45%, transparent 70%);
178
+ background-size: 250% 100%;
179
+ background-repeat: no-repeat;
180
+ animation: dk-shimmer 1.6s var(--ease-in-out) infinite;
181
+ pointer-events: none;
182
+ }
183
+ .lazy:has(img.is-loaded)::after, .lazy:has(img[data-loaded])::after { display: none; }
184
+
185
+ /* A blurred low-res preview underneath, if you have one */
186
+ .lazy-blur { background-size: cover; background-position: center; }
187
+ .lazy-blur::before {
188
+ content: "";
189
+ position: absolute;
190
+ inset: -8px;
191
+ background: inherit;
192
+ filter: blur(14px);
193
+ transition: opacity var(--dur-3) var(--ease-out);
194
+ }
195
+ .lazy-blur:has(img.is-loaded)::before { opacity: 0; }
196
+
197
+ /* Generic content placeholder while a region streams in */
198
+ .lazy-block {
199
+ display: grid;
200
+ place-items: center;
201
+ min-block-size: 8rem;
202
+ border: 1px dashed var(--line);
203
+ border-radius: var(--r-md);
204
+ color: var(--text-faint);
205
+ gap: var(--space-2);
206
+ }
207
+
208
+ /* --- Chat bubbles --------------------------------------------------------
209
+ Conversation view. Consecutive messages from one side group together and
210
+ only the last one in a run gets the tail.
211
+ ---------------------------------------------------------------------- */
212
+ .chat {
213
+ display: flex;
214
+ flex-direction: column;
215
+ gap: var(--space-1);
216
+ padding: var(--space-4);
217
+ overflow-y: auto;
218
+ overscroll-behavior: contain;
219
+ }
220
+ /* A centred date pill between days. Styling, not structure — a screen
221
+ reader hears ordinary text. */
222
+ .chat-day {
223
+ align-self: center;
224
+ margin-block: var(--space-4) var(--space-2);
225
+ padding: var(--space-1) var(--space-3);
226
+ border-radius: var(--r-full);
227
+ background: var(--surface-2);
228
+ color: var(--text-faint);
229
+ font-size: var(--text-xs);
230
+ font-weight: 560;
231
+ }
232
+
233
+ /* One message row. .msg-out flips it to the other edge and reverses the row
234
+ so the avatar swaps sides. The incoming/outgoing distinction is position
235
+ and colour only, so name the sender in text for anyone not seeing it. */
236
+ .msg {
237
+ display: flex;
238
+ gap: var(--space-2);
239
+ align-items: flex-end;
240
+ max-inline-size: min(80%, 34rem);
241
+ align-self: flex-start;
242
+ }
243
+ .msg-out { align-self: flex-end; flex-direction: row-reverse; }
244
+
245
+ /* The message body. Its corner radii are logical, which is what lets the
246
+ grouping flatten the correct side in either writing direction. */
247
+ .bubble {
248
+ position: relative;
249
+ padding: var(--space-2) var(--space-3);
250
+ border-radius: var(--r-md);
251
+ background: var(--surface-2);
252
+ border: 1px solid var(--line);
253
+ font-size: var(--text-sm);
254
+ line-height: 1.45;
255
+ overflow-wrap: anywhere;
256
+ border-end-start-radius: var(--r-xs);
257
+ }
258
+ .msg-out .bubble {
259
+ background: var(--brand-600);
260
+ color: var(--text-on-brand);
261
+ border-color: transparent;
262
+ border-end-start-radius: var(--r-md);
263
+ border-end-end-radius: var(--r-xs);
264
+ }
265
+ /* Grouping: only the last bubble of a run keeps its tail */
266
+ .msg:not(:last-of-type) + .msg .bubble { border-start-start-radius: var(--r-xs); }
267
+ .msg-out:not(:last-of-type) + .msg-out .bubble { border-start-end-radius: var(--r-xs); }
268
+
269
+ .bubble-meta {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: var(--space-1);
273
+ margin-block-start: 2px;
274
+ font-size: var(--text-2xs);
275
+ opacity: .7;
276
+ justify-content: flex-end;
277
+ font-variant-numeric: tabular-nums;
278
+ }
279
+ .msg:not(.msg-out) .bubble-meta { justify-content: flex-start; }
280
+ .bubble-name { font-weight: 620; font-size: var(--text-xs); margin-block-end: 2px; color: var(--brand); }
281
+ .msg-out .bubble-name { color: inherit; }
282
+
283
+ .bubble-attachment {
284
+ margin: calc(var(--space-2) * -1) calc(var(--space-3) * -1) var(--space-2);
285
+ border-start-start-radius: var(--r-md);
286
+ border-start-end-radius: var(--r-md);
287
+ overflow: clip;
288
+ }
289
+ .bubble-attachment img { inline-size: 100%; display: block; }
290
+
291
+ .msg .avatar { --size: 28px; align-self: flex-end; margin-block-end: 2px; }
292
+ .msg:not(:last-of-type) + .msg .avatar { visibility: hidden; }
293
+
294
+ .bubble-system {
295
+ align-self: center;
296
+ background: none;
297
+ border: 0;
298
+ color: var(--text-faint);
299
+ font-size: var(--text-xs);
300
+ text-align: center;
301
+ max-inline-size: 42ch;
302
+ }
303
+
304
+ /* Typing indicator */
305
+ .bubble-typing { display: inline-flex; gap: 4px; padding-block: var(--space-3); }
306
+ .bubble-typing > span {
307
+ inline-size: 6px;
308
+ block-size: 6px;
309
+ border-radius: var(--r-full);
310
+ background: var(--text-faint);
311
+ }
312
+ @media (prefers-reduced-motion: no-preference) {
313
+ .bubble-typing > span { animation: dk-typing 1.2s var(--ease-in-out) infinite; }
314
+ .bubble-typing > span:nth-child(2) { animation-delay: .16s; }
315
+ .bubble-typing > span:nth-child(3) { animation-delay: .32s; }
316
+ @keyframes dk-typing {
317
+ 0%, 60%, 100% { translate: 0 0; opacity: .45; }
318
+ 30% { translate: 0 -4px; opacity: 1; }
319
+ }
320
+ }
321
+
322
+ /* Composer pinned to the bottom */
323
+ .chat-composer {
324
+ display: flex;
325
+ align-items: flex-end;
326
+ gap: var(--space-2);
327
+ padding: var(--space-3) var(--space-4);
328
+ padding-block-end: max(var(--space-3), env(safe-area-inset-bottom));
329
+ border-block-start: 1px solid var(--line);
330
+ background: var(--surface);
331
+ }
332
+ .chat-composer .textarea {
333
+ min-block-size: var(--control-h);
334
+ max-block-size: 8rem;
335
+ field-sizing: content;
336
+ border-radius: var(--r-lg);
337
+ resize: none;
338
+ }
339
+
340
+ /* --- QR code -------------------------------------------------------------
341
+ deck.js renders the modules into this frame. Kept square, with a quiet
342
+ zone, so it stays scannable at any size.
343
+ ---------------------------------------------------------------------- */
344
+ .qr {
345
+ display: inline-grid;
346
+ place-items: center;
347
+ padding: var(--qr-quiet, 12px);
348
+ background: var(--qr-bg, #fff);
349
+ border-radius: var(--r-sm);
350
+ border: 1px solid var(--line);
351
+ inline-size: fit-content;
352
+ -webkit-print-color-adjust: exact;
353
+ print-color-adjust: exact;
354
+ }
355
+ .qr svg { display: block; inline-size: var(--qr-size, 160px); block-size: var(--qr-size, 160px); }
356
+ .qr svg rect { fill: var(--qr-fg, #000); shape-rendering: crispEdges; }
357
+ .qr-lg { --qr-size: 240px; }
358
+ .qr-sm { --qr-size: 104px; }
359
+ /* A logo punched out of the middle. Only safe at error correction Q or H. */
360
+ .qr-logo {
361
+ position: relative;
362
+ }
363
+ .qr-logo > .qr-logo-mark {
364
+ position: absolute;
365
+ inset: 50%;
366
+ translate: -50% -50%;
367
+ inline-size: 22%;
368
+ block-size: 22%;
369
+ display: grid;
370
+ place-items: center;
371
+ background: var(--qr-bg, #fff);
372
+ border-radius: var(--r-xs);
373
+ padding: 4px;
374
+ }
375
+ .qr-caption {
376
+ margin-block-start: var(--space-2);
377
+ font-size: var(--text-xs);
378
+ color: var(--text-muted);
379
+ text-align: center;
380
+ max-inline-size: var(--qr-size, 160px);
381
+ overflow-wrap: anywhere;
382
+ }
383
+ }
384
+
385
+ @layer deck.print {
386
+ @media print {
387
+ /* A QR code is often the whole point of printing the page */
388
+ .qr { border: 0; break-inside: avoid; }
389
+ .video, .chat-composer, .carousel-arrow, .carousel-dots,
390
+ .speed-dial, .drawer, .mega, .banner { display: none !important; }
391
+ .masonry { columns: 2; }
392
+ .chat { overflow: visible; max-block-size: none; }
393
+ .msg { break-inside: avoid; }
394
+ .bubble { border: 1px solid #bbb !important; background: #fff !important; color: #000 !important; }
395
+ .msg-out .bubble { background: #f2f2f2 !important; }
396
+ .lazy::after { display: none !important; }
397
+ .editor-toolbar, .editor-footer { display: none !important; }
398
+ .editor-content { max-block-size: none !important; overflow: visible !important; }
399
+ }
400
+ }
@@ -0,0 +1,245 @@
1
+ /* ==========================================================================
2
+ Deck — 08 Mobile
3
+ The parts Tailwind makes you build yourself every time.
4
+ ========================================================================== */
5
+
6
+ @layer deck.mobile {
7
+
8
+ /* --- Bottom sheet -------------------------------------------------------
9
+ <dialog class="sheet"> on a phone, a centred modal from 40rem up.
10
+ ---------------------------------------------------------------------- */
11
+ .sheet {
12
+ inline-size: 100%;
13
+ max-inline-size: 100%;
14
+ max-block-size: 88dvh;
15
+ margin: 0;
16
+ margin-block-start: auto;
17
+ padding: 0;
18
+ border: 0;
19
+ border-start-start-radius: var(--r-lg);
20
+ border-start-end-radius: var(--r-lg);
21
+ background: var(--surface);
22
+ color: var(--text);
23
+ box-shadow: var(--shadow-4);
24
+ overflow: clip;
25
+ translate: 0 100%;
26
+ transition: translate var(--dur-3) var(--ease-out),
27
+ display var(--dur-3) allow-discrete,
28
+ overlay var(--dur-3) allow-discrete;
29
+ }
30
+ .sheet[open] { translate: 0; }
31
+ @starting-style { .sheet[open] { translate: 0 100%; } }
32
+
33
+ .sheet::backdrop {
34
+ background: oklch(18% .02 var(--hue-neutral) / .5);
35
+ opacity: 0;
36
+ transition: opacity var(--dur-3) var(--ease-out),
37
+ display var(--dur-3) allow-discrete,
38
+ overlay var(--dur-3) allow-discrete;
39
+ }
40
+ .sheet[open]::backdrop { opacity: 1; }
41
+ @starting-style { .sheet[open]::backdrop { opacity: 0; } }
42
+
43
+ /* The drag handle at the top of a bottom sheet. Deck draws it and does not
44
+ implement the drag — swiping down does nothing — so always ship a real
45
+ close button as well. */
46
+ .sheet-grip {
47
+ inline-size: 38px;
48
+ block-size: 4px;
49
+ border-radius: var(--r-full);
50
+ background: var(--ink-300);
51
+ margin: var(--space-3) auto var(--space-1);
52
+ }
53
+ /* The title row of a sheet. */
54
+ .sheet-header { padding: var(--space-3) var(--space-5); display: flex; align-items: center; gap: var(--space-3); }
55
+ /* The sheet's heading. Point aria-labelledby at it. */
56
+ .sheet-title { font-size: var(--text-md); font-weight: 640; }
57
+ /* The scrolling region. Pads for the home indicator, and contains its
58
+ overscroll so reaching the end does not scroll the page behind. */
59
+ .sheet-body {
60
+ padding: var(--space-2) var(--space-5) calc(var(--space-6) + env(safe-area-inset-bottom));
61
+ overflow-y: auto;
62
+ overscroll-behavior: contain;
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: var(--space-4);
66
+ }
67
+
68
+ @media (min-width: 40rem) {
69
+ .sheet {
70
+ inline-size: min(30rem, calc(100vw - var(--space-8)));
71
+ margin: auto;
72
+ border-radius: var(--r-lg);
73
+ translate: 0;
74
+ opacity: 0;
75
+ scale: .97;
76
+ transition: opacity var(--dur-2) var(--ease-out), scale var(--dur-2) var(--ease-spring),
77
+ display var(--dur-2) allow-discrete, overlay var(--dur-2) allow-discrete;
78
+ }
79
+ .sheet[open] { opacity: 1; scale: 1; }
80
+ @starting-style { .sheet[open] { opacity: 0; scale: .97; } }
81
+ }
82
+
83
+ /* --- Bottom tab bar ----------------------------------------------------- */
84
+ .tabbar {
85
+ position: fixed;
86
+ inset-inline: 0;
87
+ inset-block-end: 0;
88
+ z-index: var(--z-nav);
89
+ display: flex;
90
+ background: color-mix(in oklab, var(--surface) 88%, transparent);
91
+ backdrop-filter: saturate(1.8) blur(16px);
92
+ -webkit-backdrop-filter: saturate(1.8) blur(16px);
93
+ border-block-start: 1px solid var(--line);
94
+ padding-block-end: env(safe-area-inset-bottom);
95
+ }
96
+ @media (min-width: 48rem) { .tabbar { display: none; } }
97
+
98
+ .tabbar-item {
99
+ flex: 1 1 0;
100
+ min-block-size: var(--tap);
101
+ padding-block: var(--space-2);
102
+ display: flex;
103
+ flex-direction: column;
104
+ align-items: center;
105
+ justify-content: center;
106
+ gap: 3px;
107
+ color: var(--text-faint);
108
+ font-size: var(--text-2xs);
109
+ font-weight: 560;
110
+ text-decoration: none;
111
+ -webkit-tap-highlight-color: transparent;
112
+ transition: color var(--dur-1) var(--ease-out);
113
+ }
114
+ .tabbar-item[aria-current] { color: var(--brand); }
115
+ .tabbar-item[aria-current] .icon { scale: 1.06; }
116
+ .tabbar-item .icon { transition: scale var(--dur-2) var(--ease-spring); }
117
+
118
+ /* Give the page room so the tab bar never covers the last row */
119
+ body:has(.tabbar) { padding-block-end: calc(var(--tap) + var(--space-4) + env(safe-area-inset-bottom)); }
120
+ @media (min-width: 48rem) { body:has(.tabbar) { padding-block-end: 0; } }
121
+
122
+ /* --- Safe area helpers --------------------------------------------------- */
123
+ .safe-top { padding-block-start: env(safe-area-inset-top); }
124
+ .safe-bottom { padding-block-end: env(safe-area-inset-bottom); }
125
+ .safe-x { padding-inline: max(var(--space-gutter), env(safe-area-inset-left)) max(var(--space-gutter), env(safe-area-inset-right)); }
126
+
127
+ /* --- Icons --------------------------------------------------------------
128
+ <svg class="icon"><use href="deck-icons.svg#check"></use></svg>
129
+ Icons inherit color and font-size, so they line up with text by default.
130
+ ---------------------------------------------------------------------- */
131
+ /* An SVG icon sized in em, so it scales with the text it sits beside and
132
+ lands on the baseline. Icons are filled with currentColor and inherit
133
+ the surrounding colour. */
134
+ .icon {
135
+ inline-size: 1.25em;
136
+ block-size: 1.25em;
137
+ flex: 0 0 auto;
138
+ fill: none;
139
+ stroke: currentColor;
140
+ stroke-width: 1.75;
141
+ stroke-linecap: round;
142
+ stroke-linejoin: round;
143
+ vertical-align: -.22em;
144
+ }
145
+ /* Small icons reference the heavier cut of the sprite, not a thicker stroke:
146
+ the symbols are filled outlines, so stroke-width does nothing to them.
147
+ Every icon ships twice, #name at wght 400 and #name-sm at wght 500, and
148
+ .icon-sm goes with the -sm symbol. CSS cannot rewrite a <use href>, so the
149
+ pairing lives in the markup:
150
+ <svg class="icon icon-sm"><use href="deck-icons.svg#check-sm"></use></svg> */
151
+ .icon-sm { inline-size: 1em; block-size: 1em; }
152
+ /* A 1.6em icon, for icon tiles and empty states where the default would
153
+ look undersized. */
154
+ .icon-lg { inline-size: 1.6em; block-size: 1.6em; stroke-width: 1.6; }
155
+ .icon-xl { inline-size: 2.25em; block-size: 2.25em; stroke-width: 1.4; }
156
+ .icon-fill { fill: currentColor; stroke: none; }
157
+ .icon-muted { color: var(--text-faint); }
158
+ .icon-spin { animation: dk-spin 1.1s linear infinite; }
159
+
160
+ /* Icon in a tinted tile — for feature rows and empty states */
161
+ .icon-tile {
162
+ inline-size: 40px;
163
+ block-size: 40px;
164
+ border-radius: var(--r-sm);
165
+ display: grid;
166
+ place-items: center;
167
+ background: var(--brand-soft);
168
+ color: var(--brand);
169
+ flex: 0 0 auto;
170
+ }
171
+ .icon-tile-good { background: var(--good-100); color: var(--good-700); }
172
+ .icon-tile-warn { background: var(--warn-100); color: var(--warn-700); }
173
+ .icon-tile-bad { background: var(--bad-100); color: var(--bad-700); }
174
+ .icon-tile-lg { inline-size: 52px; block-size: 52px; border-radius: var(--r-md); }
175
+
176
+ /* --- Emoji --------------------------------------------------------------
177
+ Emoji render at wildly different sizes and baselines across platforms.
178
+ .emoji pins the font stack and the metrics so they sit on the line.
179
+ ---------------------------------------------------------------------- */
180
+ /* Pins the emoji font stack and metrics so an emoji renders at the same
181
+ size and baseline on Windows, iOS and Android instead of jumping
182
+ between them. */
183
+ .emoji {
184
+ font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
185
+ "Twemoji Mozilla", "Android Emoji", sans-serif;
186
+ font-size: 1.05em;
187
+ line-height: 1;
188
+ font-style: normal;
189
+ font-weight: 400;
190
+ vertical-align: -.12em;
191
+ display: inline-block;
192
+ text-rendering: auto;
193
+ -webkit-font-smoothing: auto;
194
+ }
195
+ .emoji-lg { font-size: 1.6em; vertical-align: -.2em; }
196
+ .emoji-xl { font-size: 2.5em; vertical-align: -.24em; }
197
+ .emoji-hero { font-size: 3.5rem; line-height: 1; }
198
+
199
+ /* Emoji as an avatar or a status marker */
200
+ .emoji-tile {
201
+ inline-size: 40px;
202
+ block-size: 40px;
203
+ border-radius: var(--r-sm);
204
+ background: var(--surface-2);
205
+ border: 1px solid var(--line);
206
+ display: grid;
207
+ place-items: center;
208
+ font-size: 20px;
209
+ flex: 0 0 auto;
210
+ }
211
+ .emoji-tile-round { border-radius: var(--r-full); }
212
+
213
+ /* Emoji picker grid — drop your own set in */
214
+ .emoji-grid {
215
+ display: grid;
216
+ grid-template-columns: repeat(auto-fill, minmax(40px, 1fr));
217
+ gap: 2px;
218
+ }
219
+ .emoji-grid > button {
220
+ aspect-ratio: 1;
221
+ border-radius: var(--r-xs);
222
+ font-size: 21px;
223
+ line-height: 1;
224
+ cursor: pointer;
225
+ transition: background-color var(--dur-1) var(--ease-out), scale var(--dur-1) var(--ease-spring);
226
+ }
227
+ .emoji-grid > button:hover { background: var(--surface-hover); scale: 1.16; }
228
+
229
+ /* Reaction pill, like a Slack or Teams reaction */
230
+ .reaction {
231
+ display: inline-flex;
232
+ align-items: center;
233
+ gap: var(--space-1);
234
+ min-block-size: 28px;
235
+ padding-inline: var(--space-2);
236
+ border-radius: var(--r-full);
237
+ border: 1px solid var(--line);
238
+ background: var(--surface);
239
+ font-size: var(--text-xs);
240
+ font-variant-numeric: tabular-nums;
241
+ color: var(--text-muted);
242
+ cursor: pointer;
243
+ }
244
+ .reaction[aria-pressed="true"] { background: var(--brand-soft); border-color: var(--brand-300); color: var(--brand-soft-text); }
245
+ }