@devslab/dds-css 0.3.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/dist/dds.css ADDED
@@ -0,0 +1,1206 @@
1
+ /* @devslab/dds-css — generated bundle, do not edit. Source: src/*.css
2
+ * Requires @devslab/dds-tokens/tokens.css to be loaded first. */
3
+
4
+ /* dds-css shared plumbing. Components reference ONLY semantic tokens
5
+ * (var(--dds-*)) — tokens.css must be loaded first. */
6
+ [class^="dds-"],
7
+ [class*=" dds-"] {
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ [dir="rtl"] .dds-icon--directional {
12
+ transform: scaleX(-1);
13
+ }
14
+
15
+ @media (prefers-reduced-motion: reduce) {
16
+ :root [class^="dds-"],
17
+ :root [class*=" dds-"] {
18
+ animation-duration: 0.01ms;
19
+ animation-iteration-count: 1;
20
+ scroll-behavior: auto;
21
+ transition-duration: 0.01ms;
22
+ }
23
+ }
24
+
25
+ @media (forced-colors: active) {
26
+ [class^="dds-"],
27
+ [class*=" dds-"] {
28
+ forced-color-adjust: auto;
29
+ }
30
+
31
+ :is([class^="dds-"], [class*=" dds-"]):focus-visible {
32
+ outline-color: Highlight;
33
+ }
34
+ }
35
+
36
+ /* Button — spec §4.2: default → hover → pressed → focus-visible → disabled → loading.
37
+ * Variants: primary / secondary / ghost / danger. Sizes: sm / md(default) / lg.
38
+ * Control heights come from the spacing scale (32/40/48); type from the typo
39
+ * scale (label for sm/md, body-1 size for lg). Loading = aria-busy="true" with
40
+ * a .dds-spinner child. */
41
+
42
+ .dds-btn {
43
+ appearance: none;
44
+ display: inline-flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ gap: var(--dds-space-8);
48
+ min-block-size: var(--dds-space-40);
49
+ padding-block: var(--dds-space-8);
50
+ padding-inline: var(--dds-space-16);
51
+ border: 1px solid transparent;
52
+ border-radius: var(--dds-radius-md);
53
+ background: none;
54
+ font-family: var(--dds-font-family-sans);
55
+ font-size: var(--dds-typo-label-font-size);
56
+ line-height: var(--dds-typo-label-line-height);
57
+ font-weight: var(--dds-typo-label-font-weight);
58
+ color: var(--dds-color-text-primary);
59
+ cursor: pointer;
60
+ transition: background-color var(--dds-motion-duration-fast) var(--dds-motion-easing-standard);
61
+ -webkit-tap-highlight-color: transparent;
62
+ white-space: normal;
63
+ overflow-wrap: anywhere;
64
+ }
65
+
66
+ .dds-btn:focus-visible {
67
+ outline: var(--dds-space-2) solid var(--dds-color-border-focus);
68
+ outline-offset: var(--dds-space-2);
69
+ }
70
+
71
+ /* opacity is allowed for disabled only (spec §4.2) */
72
+ .dds-btn[disabled],
73
+ .dds-btn[aria-busy="true"] {
74
+ opacity: 0.45;
75
+ cursor: not-allowed;
76
+ }
77
+ .dds-btn[aria-busy="true"] {
78
+ pointer-events: none;
79
+ }
80
+
81
+ /* variants ---------------------------------------------------------------- */
82
+
83
+ .dds-btn--primary {
84
+ background: var(--dds-color-bg-brand);
85
+ color: var(--dds-color-text-on-brand); /* zinc.950, not white — spec §3.1 on-brand note */
86
+ font-weight: 600;
87
+ }
88
+ .dds-btn--primary:not([disabled]):not([aria-busy="true"]):hover {
89
+ background: var(--dds-color-bg-brand-hover);
90
+ }
91
+ .dds-btn--primary:not([disabled]):active {
92
+ background: var(--dds-color-bg-brand-hover);
93
+ box-shadow: inset 0 0 0 999px var(--dds-color-overlay-pressed-tint);
94
+ }
95
+
96
+ .dds-btn--secondary {
97
+ border-color: var(--dds-color-border-strong);
98
+ }
99
+ .dds-btn--secondary:not([disabled]):not([aria-busy="true"]):hover {
100
+ background: var(--dds-color-overlay-hover-tint);
101
+ }
102
+ .dds-btn--secondary:not([disabled]):active {
103
+ background: var(--dds-color-overlay-pressed-tint);
104
+ }
105
+
106
+ .dds-btn--ghost {
107
+ color: var(--dds-color-text-brand);
108
+ }
109
+ .dds-btn--ghost:not([disabled]):not([aria-busy="true"]):hover {
110
+ background: var(--dds-color-overlay-hover-tint);
111
+ }
112
+ .dds-btn--ghost:not([disabled]):active {
113
+ background: var(--dds-color-overlay-pressed-tint);
114
+ }
115
+
116
+ .dds-btn--danger {
117
+ background: var(--dds-color-status-danger);
118
+ color: var(--dds-color-text-on-status);
119
+ font-weight: 600;
120
+ }
121
+ .dds-btn--danger:not([disabled]):not([aria-busy="true"]):hover {
122
+ box-shadow: inset 0 0 0 999px var(--dds-color-overlay-hover-tint);
123
+ }
124
+ .dds-btn--danger:not([disabled]):active {
125
+ box-shadow: inset 0 0 0 999px var(--dds-color-overlay-pressed-tint);
126
+ }
127
+
128
+ /* sizes ------------------------------------------------------------------- */
129
+
130
+ .dds-btn--sm {
131
+ min-block-size: var(--dds-space-32);
132
+ padding-block: var(--dds-space-4);
133
+ padding-inline: var(--dds-space-12);
134
+ }
135
+
136
+ .dds-btn--lg {
137
+ min-block-size: var(--dds-space-48);
138
+ padding-block: var(--dds-space-12);
139
+ padding-inline: var(--dds-space-20);
140
+ font-size: var(--dds-typo-body-1-font-size);
141
+ line-height: var(--dds-typo-body-1-line-height);
142
+ }
143
+
144
+ @media (pointer: coarse) {
145
+ .dds-btn,
146
+ .dds-btn--sm {
147
+ min-block-size: 44px;
148
+ }
149
+ }
150
+
151
+ /* IconButton — a Button whose only content is an icon (spec §4.3 v1 inventory).
152
+ * Same state set and sizes as .dds-btn (32/40/48 from the spacing scale) so an
153
+ * icon button never lands a few px off the text button next to it — the row
154
+ * misalignment this house rejects. The icon inherits currentColor.
155
+ * Markup MUST carry an accessible name: aria-label (spec §6). */
156
+
157
+ .dds-iconbtn {
158
+ appearance: none;
159
+ display: inline-flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ flex: none;
163
+ inline-size: var(--dds-space-40);
164
+ block-size: var(--dds-space-40);
165
+ border: 1px solid transparent;
166
+ border-radius: var(--dds-radius-md);
167
+ background: none;
168
+ color: var(--dds-color-text-primary);
169
+ cursor: pointer;
170
+ transition: background-color var(--dds-motion-duration-fast) var(--dds-motion-easing-standard);
171
+ -webkit-tap-highlight-color: transparent;
172
+ }
173
+
174
+ .dds-iconbtn:not([disabled]):hover {
175
+ background: var(--dds-color-overlay-hover-tint);
176
+ }
177
+ .dds-iconbtn:not([disabled]):active {
178
+ background: var(--dds-color-overlay-pressed-tint);
179
+ }
180
+ .dds-iconbtn:focus-visible {
181
+ outline: var(--dds-space-2) solid var(--dds-color-border-focus);
182
+ outline-offset: var(--dds-space-2);
183
+ }
184
+
185
+ /* opacity is allowed for disabled only (spec §4.2) */
186
+ .dds-iconbtn[disabled] {
187
+ opacity: 0.45;
188
+ cursor: not-allowed;
189
+ }
190
+
191
+ /* variants ---------------------------------------------------------------- */
192
+
193
+ .dds-iconbtn--secondary {
194
+ border-color: var(--dds-color-border-strong);
195
+ }
196
+
197
+ .dds-iconbtn--danger {
198
+ color: var(--dds-color-status-danger);
199
+ }
200
+
201
+ /* sizes ------------------------------------------------------------------- */
202
+
203
+ /* --sm is 32×32: still above the 24×24 web floor, but below the 44×44 mobile
204
+ * target — on touch surfaces keep the default size (spec §6). */
205
+ .dds-iconbtn--sm {
206
+ inline-size: var(--dds-space-32);
207
+ block-size: var(--dds-space-32);
208
+ }
209
+
210
+ .dds-iconbtn--lg {
211
+ inline-size: var(--dds-space-48);
212
+ block-size: var(--dds-space-48);
213
+ }
214
+
215
+ @media (pointer: coarse) {
216
+ .dds-iconbtn,
217
+ .dds-iconbtn--sm {
218
+ min-inline-size: 44px;
219
+ min-block-size: 44px;
220
+ }
221
+ }
222
+
223
+ /* TextField — label + input + help, error via .dds-field--error OR
224
+ * aria-invalid="true" on the input (markup should set aria-invalid +
225
+ * aria-describedby; error is never conveyed by border color alone — the
226
+ * help line carries the message, spec §6). */
227
+
228
+ .dds-field {
229
+ display: flex;
230
+ flex-direction: column;
231
+ gap: var(--dds-space-4);
232
+ font-family: var(--dds-font-family-sans);
233
+ }
234
+
235
+ .dds-field__label {
236
+ font-size: var(--dds-typo-label-font-size);
237
+ line-height: var(--dds-typo-label-line-height);
238
+ font-weight: var(--dds-typo-label-font-weight);
239
+ color: var(--dds-color-text-primary);
240
+ }
241
+
242
+ .dds-input {
243
+ font-family: var(--dds-font-family-sans);
244
+ font-size: var(--dds-typo-body-2-font-size);
245
+ line-height: var(--dds-typo-body-2-line-height);
246
+ font-weight: var(--dds-typo-body-2-font-weight);
247
+ height: var(--dds-space-40);
248
+ padding: 0 var(--dds-space-12);
249
+ background: var(--dds-color-bg-default);
250
+ color: var(--dds-color-text-primary);
251
+ border: 1px solid var(--dds-color-border-strong);
252
+ border-radius: var(--dds-radius-md);
253
+ }
254
+
255
+ .dds-input::placeholder {
256
+ color: var(--dds-color-text-muted);
257
+ }
258
+
259
+ .dds-input:focus-visible {
260
+ outline: var(--dds-space-2) solid var(--dds-color-border-focus);
261
+ outline-offset: 0;
262
+ border-color: transparent;
263
+ }
264
+
265
+ /* opacity is allowed for disabled only (spec §4.2) */
266
+ .dds-input[disabled] {
267
+ opacity: 0.45;
268
+ cursor: not-allowed;
269
+ background: var(--dds-color-bg-subtle);
270
+ }
271
+
272
+ .dds-field__help {
273
+ font-size: var(--dds-typo-caption-font-size);
274
+ line-height: var(--dds-typo-caption-line-height);
275
+ font-weight: var(--dds-typo-caption-font-weight);
276
+ color: var(--dds-color-text-muted);
277
+ }
278
+
279
+ .dds-field--error .dds-input,
280
+ .dds-input[aria-invalid="true"] {
281
+ border-color: var(--dds-color-status-danger);
282
+ }
283
+ .dds-field--error .dds-field__help {
284
+ color: var(--dds-color-status-danger);
285
+ }
286
+
287
+ /* Textarea — the multi-line member of the TextField family: it lives in the
288
+ * same .dds-field wrapper and shares label/help/error behavior with .dds-input.
289
+ * Row rule from the house UI discipline: a textarea gets its own row, never a
290
+ * row shared with single-line controls (their heights can't be reconciled).
291
+ * min-height is two control heights so an empty textarea already reads as
292
+ * multi-line. */
293
+
294
+ .dds-textarea {
295
+ font-family: var(--dds-font-family-sans);
296
+ font-size: var(--dds-typo-body-2-font-size);
297
+ line-height: var(--dds-typo-body-2-line-height);
298
+ font-weight: var(--dds-typo-body-2-font-weight);
299
+ min-height: calc(var(--dds-space-40) * 2);
300
+ padding: var(--dds-space-8) var(--dds-space-12);
301
+ background: var(--dds-color-bg-default);
302
+ color: var(--dds-color-text-primary);
303
+ border: 1px solid var(--dds-color-border-strong);
304
+ border-radius: var(--dds-radius-md);
305
+ resize: vertical;
306
+ }
307
+
308
+ .dds-textarea::placeholder {
309
+ color: var(--dds-color-text-muted);
310
+ }
311
+
312
+ .dds-textarea:focus-visible {
313
+ outline: var(--dds-space-2) solid var(--dds-color-border-focus);
314
+ outline-offset: 0;
315
+ border-color: transparent;
316
+ }
317
+
318
+ /* opacity is allowed for disabled only (spec §4.2) */
319
+ .dds-textarea[disabled] {
320
+ opacity: 0.45;
321
+ cursor: not-allowed;
322
+ background: var(--dds-color-bg-subtle);
323
+ }
324
+
325
+ .dds-field--error .dds-textarea,
326
+ .dds-textarea[aria-invalid="true"] {
327
+ border-color: var(--dds-color-status-danger);
328
+ }
329
+
330
+ /* Select — native <select> with the native arrow replaced. Leaving the OS
331
+ * arrow in place is a rejection criterion in this house: the same control
332
+ * then looks different on every platform and never matches the input next to
333
+ * it. The wrapper owns the chevron so the element stays a real <select>
334
+ * (keyboard, mobile pickers, form semantics all free).
335
+ *
336
+ * Markup: <span class="dds-select"><select class="dds-select__input">…
337
+ * Lives inside .dds-field like .dds-input, so label/help/error come for free. */
338
+
339
+ .dds-select {
340
+ position: relative;
341
+ display: inline-flex;
342
+ align-items: center;
343
+ width: 100%;
344
+ }
345
+
346
+ .dds-select__input {
347
+ appearance: none;
348
+ font-family: var(--dds-font-family-sans);
349
+ font-size: var(--dds-typo-body-2-font-size);
350
+ line-height: var(--dds-typo-body-2-line-height);
351
+ font-weight: var(--dds-typo-body-2-font-weight);
352
+ width: 100%;
353
+ height: var(--dds-space-40);
354
+ padding-block: 0;
355
+ padding-inline: var(--dds-space-12) var(--dds-space-32);
356
+ background: var(--dds-color-bg-default);
357
+ color: var(--dds-color-text-primary);
358
+ border: 1px solid var(--dds-color-border-strong);
359
+ border-radius: var(--dds-radius-md);
360
+ cursor: pointer;
361
+ }
362
+
363
+ .dds-select__input:focus-visible {
364
+ outline: var(--dds-space-2) solid var(--dds-color-border-focus);
365
+ outline-offset: 0;
366
+ border-color: transparent;
367
+ }
368
+
369
+ /* opacity is allowed for disabled only (spec §4.2) */
370
+ .dds-select__input[disabled] {
371
+ opacity: 0.45;
372
+ cursor: not-allowed;
373
+ background: var(--dds-color-bg-subtle);
374
+ }
375
+
376
+ .dds-field--error .dds-select__input,
377
+ .dds-select__input[aria-invalid="true"] {
378
+ border-color: var(--dds-color-status-danger);
379
+ }
380
+
381
+ /* Chevron glyph geometry (8px box, 1.6px stroke) is drawn, not spaced — the
382
+ * same allowance as the 6px badge dot. translateY(-70%) centers the drawn
383
+ * corner, which sits below the box center once rotated. */
384
+ .dds-select::after {
385
+ content: "";
386
+ position: absolute;
387
+ inset-block-start: 50%;
388
+ inset-inline-end: var(--dds-space-12);
389
+ width: 8px;
390
+ height: 8px;
391
+ border-inline-end: 1.6px solid var(--dds-color-text-muted);
392
+ border-block-end: 1.6px solid var(--dds-color-text-muted);
393
+ transform: translateY(-70%) rotate(45deg);
394
+ pointer-events: none;
395
+ }
396
+
397
+ /* Checkbox / Radio — one class for both; the input's own type decides the
398
+ * shape (square + tick vs circle + dot), so markup never has to declare the
399
+ * shape twice. The control stays a real <input> (keyboard, form data, AT
400
+ * roles); only its box is redrawn.
401
+ *
402
+ * Box 18px, tick 10×6, dot 8px: glyph geometry, not spacing (same allowance
403
+ * as the 6px badge dot). The 18px box sits inside a label whose hit area is
404
+ * the whole row — keep the label text clickable rather than growing the box. */
405
+
406
+ .dds-check {
407
+ display: inline-flex;
408
+ align-items: center;
409
+ gap: var(--dds-space-8);
410
+ font-family: var(--dds-font-family-sans);
411
+ font-size: var(--dds-typo-body-2-font-size);
412
+ line-height: var(--dds-typo-body-2-line-height);
413
+ color: var(--dds-color-text-primary);
414
+ cursor: pointer;
415
+ }
416
+
417
+ .dds-check__input {
418
+ appearance: none;
419
+ flex: none;
420
+ display: grid;
421
+ place-items: center;
422
+ margin: 0;
423
+ width: 18px;
424
+ height: 18px;
425
+ border: 1px solid var(--dds-color-border-strong);
426
+ border-radius: var(--dds-radius-sm);
427
+ background: var(--dds-color-bg-default);
428
+ cursor: inherit;
429
+ transition: background-color var(--dds-motion-duration-fast) var(--dds-motion-easing-standard);
430
+ }
431
+
432
+ .dds-check__input[type="radio"] {
433
+ border-radius: var(--dds-radius-full);
434
+ }
435
+
436
+ .dds-check__input:not([disabled]):hover {
437
+ border-color: var(--dds-color-text-muted);
438
+ }
439
+
440
+ .dds-check__input:checked {
441
+ background: var(--dds-color-bg-brand);
442
+ border-color: var(--dds-color-bg-brand);
443
+ }
444
+
445
+ /* the mark is on-brand (zinc.950), never white — spec §3.1 on-brand note */
446
+ .dds-check__input[type="checkbox"]:checked::before {
447
+ content: "";
448
+ width: 10px;
449
+ height: 6px;
450
+ border-inline-start: 2px solid var(--dds-color-text-on-brand);
451
+ border-block-end: 2px solid var(--dds-color-text-on-brand);
452
+ transform: translateY(-1px) rotate(-45deg);
453
+ }
454
+
455
+ .dds-check__input[type="checkbox"]:indeterminate::before {
456
+ content: "";
457
+ width: 10px;
458
+ height: 2px;
459
+ background: var(--dds-color-text-on-brand);
460
+ }
461
+ .dds-check__input[type="checkbox"]:indeterminate {
462
+ background: var(--dds-color-bg-brand);
463
+ border-color: var(--dds-color-bg-brand);
464
+ }
465
+
466
+ .dds-check__input[type="radio"]:checked::before {
467
+ content: "";
468
+ width: 8px;
469
+ height: 8px;
470
+ border-radius: var(--dds-radius-full);
471
+ background: var(--dds-color-text-on-brand);
472
+ }
473
+
474
+ .dds-check__input:focus-visible {
475
+ outline: var(--dds-space-2) solid var(--dds-color-border-focus);
476
+ outline-offset: var(--dds-space-2);
477
+ }
478
+
479
+ /* opacity is allowed for disabled only (spec §4.2) — it goes on the label so
480
+ * the box and its text dim once, not twice (nested opacity compounds). */
481
+ .dds-check:has(.dds-check__input[disabled]) {
482
+ opacity: 0.45;
483
+ cursor: not-allowed;
484
+ }
485
+ .dds-check__input[disabled] {
486
+ cursor: not-allowed;
487
+ }
488
+
489
+ /* Switch — an immediate on/off (a checkbox that applies itself). Use a
490
+ * checkbox when the value only takes effect on submit.
491
+ * Markup: <input type="checkbox" role="switch" class="dds-switch__input">
492
+ *
493
+ * Track 36×20 and knob 16px are glyph geometry, not spacing. The knob is
494
+ * on-brand (zinc.950) when the track is brand-filled — the same rule that
495
+ * keeps white off cyan (spec §3.1) — and an elevated surface when off, so it
496
+ * reads as a raised knob in both themes. */
497
+
498
+ .dds-switch {
499
+ display: inline-flex;
500
+ align-items: center;
501
+ gap: var(--dds-space-8);
502
+ font-family: var(--dds-font-family-sans);
503
+ font-size: var(--dds-typo-body-2-font-size);
504
+ line-height: var(--dds-typo-body-2-line-height);
505
+ color: var(--dds-color-text-primary);
506
+ cursor: pointer;
507
+ }
508
+
509
+ .dds-switch__input {
510
+ appearance: none;
511
+ position: relative;
512
+ flex: none;
513
+ margin: 0;
514
+ width: 36px;
515
+ height: 20px;
516
+ border-radius: var(--dds-radius-full);
517
+ background: var(--dds-color-text-muted);
518
+ cursor: inherit;
519
+ transition: background-color var(--dds-motion-duration-base) var(--dds-motion-easing-standard);
520
+ }
521
+
522
+ .dds-switch__input::before {
523
+ content: "";
524
+ position: absolute;
525
+ inset-block-start: 2px;
526
+ inset-inline-start: 2px;
527
+ width: 16px;
528
+ height: 16px;
529
+ border-radius: var(--dds-radius-full);
530
+ background: var(--dds-color-bg-elevated);
531
+ box-shadow: var(--dds-elevation-1);
532
+ transition: transform var(--dds-motion-duration-base) var(--dds-motion-easing-standard);
533
+ }
534
+
535
+ .dds-switch__input:checked {
536
+ background: var(--dds-color-bg-brand);
537
+ }
538
+ .dds-switch__input:checked::before {
539
+ background: var(--dds-color-text-on-brand);
540
+ transform: translateX(16px);
541
+ }
542
+
543
+ [dir="rtl"] .dds-switch__input:checked::before {
544
+ transform: translateX(-16px);
545
+ }
546
+
547
+ .dds-switch__input:focus-visible {
548
+ outline: var(--dds-space-2) solid var(--dds-color-border-focus);
549
+ outline-offset: var(--dds-space-2);
550
+ }
551
+
552
+ /* opacity is allowed for disabled only (spec §4.2) — on the label, once */
553
+ .dds-switch:has(.dds-switch__input[disabled]) {
554
+ opacity: 0.45;
555
+ cursor: not-allowed;
556
+ }
557
+ .dds-switch__input[disabled] {
558
+ cursor: not-allowed;
559
+ }
560
+
561
+ @media (prefers-reduced-motion: reduce) {
562
+ .dds-switch__input,
563
+ .dds-switch__input::before {
564
+ transition: none;
565
+ }
566
+ }
567
+
568
+ /* Badge — brand + the four status pairs. Status colors always ship as
569
+ * foreground + -bg pair (spec §3.1); the leading dot repeats the state so
570
+ * color is not the only signal (spec §6). Dot size 6px is glyph geometry,
571
+ * not spacing (same 6px as the toast dot — keep them identical). */
572
+
573
+ .dds-badge {
574
+ display: inline-flex;
575
+ align-items: center;
576
+ gap: var(--dds-space-4);
577
+ font-family: var(--dds-font-family-sans);
578
+ font-size: var(--dds-typo-label-font-size);
579
+ line-height: var(--dds-typo-label-line-height);
580
+ font-weight: var(--dds-typo-label-font-weight);
581
+ padding: var(--dds-space-2) var(--dds-space-8);
582
+ border-radius: var(--dds-radius-full);
583
+ white-space: nowrap;
584
+ }
585
+
586
+ .dds-badge::before {
587
+ content: "";
588
+ flex: none;
589
+ width: 6px;
590
+ height: 6px;
591
+ border-radius: var(--dds-radius-full);
592
+ background: currentColor;
593
+ }
594
+
595
+ .dds-badge--brand {
596
+ color: var(--dds-color-text-brand);
597
+ background: var(--dds-color-bg-brand-subtle);
598
+ }
599
+ .dds-badge--success {
600
+ color: var(--dds-color-status-success);
601
+ background: var(--dds-color-status-success-bg);
602
+ }
603
+ .dds-badge--warning {
604
+ color: var(--dds-color-status-warning);
605
+ background: var(--dds-color-status-warning-bg);
606
+ }
607
+ .dds-badge--danger {
608
+ color: var(--dds-color-status-danger);
609
+ background: var(--dds-color-status-danger-bg);
610
+ }
611
+ .dds-badge--info {
612
+ color: var(--dds-color-status-info);
613
+ background: var(--dds-color-status-info-bg);
614
+ }
615
+
616
+ /* Chip — a small interactive control: filter toggle, suggested question,
617
+ * removable tag. Badge and Chip are different components on purpose: a badge
618
+ * is state you read, a chip is something you press. Never restyle one into
619
+ * the other (the duplicated-.chip collision this house has already paid for).
620
+ *
621
+ * Selected state is carried by aria-pressed="true" (toggle chips) so the
622
+ * state is announced, not only colored; --selected exists for markup that
623
+ * can't use aria-pressed (e.g. a link chip). */
624
+
625
+ .dds-chip {
626
+ appearance: none;
627
+ display: inline-flex;
628
+ align-items: center;
629
+ gap: var(--dds-space-4);
630
+ font-family: var(--dds-font-family-sans);
631
+ font-size: var(--dds-typo-label-font-size);
632
+ line-height: var(--dds-typo-label-line-height);
633
+ font-weight: var(--dds-typo-label-font-weight);
634
+ padding: var(--dds-space-4) var(--dds-space-12);
635
+ border: 1px solid var(--dds-color-border-strong);
636
+ border-radius: var(--dds-radius-full);
637
+ background: none;
638
+ color: var(--dds-color-text-primary);
639
+ cursor: pointer;
640
+ transition: background-color var(--dds-motion-duration-fast) var(--dds-motion-easing-standard);
641
+ -webkit-tap-highlight-color: transparent;
642
+ }
643
+
644
+ .dds-chip:not([disabled]):hover {
645
+ background: var(--dds-color-overlay-hover-tint);
646
+ }
647
+ .dds-chip:not([disabled]):active {
648
+ background: var(--dds-color-overlay-pressed-tint);
649
+ }
650
+ .dds-chip:focus-visible {
651
+ outline: var(--dds-space-2) solid var(--dds-color-border-focus);
652
+ outline-offset: var(--dds-space-2);
653
+ }
654
+
655
+ .dds-chip[aria-pressed="true"],
656
+ .dds-chip--selected {
657
+ background: var(--dds-color-bg-brand-subtle);
658
+ border-color: var(--dds-color-bg-brand);
659
+ color: var(--dds-color-text-brand);
660
+ }
661
+
662
+ /* opacity is allowed for disabled only (spec §4.2) */
663
+ .dds-chip[disabled] {
664
+ opacity: 0.45;
665
+ cursor: not-allowed;
666
+ }
667
+
668
+ /* Avatar — a person or tenant mark: image if there is one, initials if not.
669
+ * Sizes come from the spacing scale (32/40/48) so an avatar lines up with the
670
+ * control of the same size in a list row. The initials fallback uses the
671
+ * brand-subtle pair, which re-tints with the product brand automatically. */
672
+
673
+ .dds-avatar {
674
+ display: inline-flex;
675
+ align-items: center;
676
+ justify-content: center;
677
+ flex: none;
678
+ width: var(--dds-space-40);
679
+ height: var(--dds-space-40);
680
+ border-radius: var(--dds-radius-full);
681
+ overflow: hidden;
682
+ background: var(--dds-color-bg-brand-subtle);
683
+ color: var(--dds-color-text-brand);
684
+ font-family: var(--dds-font-family-sans);
685
+ font-size: var(--dds-typo-label-font-size);
686
+ line-height: var(--dds-typo-label-line-height);
687
+ font-weight: 600;
688
+ user-select: none;
689
+ }
690
+
691
+ .dds-avatar__img {
692
+ width: 100%;
693
+ height: 100%;
694
+ object-fit: cover;
695
+ }
696
+
697
+ .dds-avatar--sm {
698
+ width: var(--dds-space-32);
699
+ height: var(--dds-space-32);
700
+ font-size: var(--dds-typo-caption-font-size);
701
+ line-height: var(--dds-typo-caption-line-height);
702
+ }
703
+
704
+ .dds-avatar--lg {
705
+ width: var(--dds-space-48);
706
+ height: var(--dds-space-48);
707
+ font-size: var(--dds-typo-body-1-font-size);
708
+ line-height: var(--dds-typo-body-1-line-height);
709
+ }
710
+
711
+ /* square variant for tenants/logos — a company mark in a circle gets clipped */
712
+ .dds-avatar--square {
713
+ border-radius: var(--dds-radius-md);
714
+ }
715
+
716
+ /* Spinner — indeterminate progress. Inherits color via currentColor so it
717
+ * reads correctly on any surface (inside a primary button it spins in
718
+ * on-brand). The rotation is a continuous state indicator, not a transition,
719
+ * so its period is not a motion token; under reduced motion it slows down
720
+ * instead of freezing (a stopped spinner reads as a hang — the motion IS the
721
+ * state, spec §3.6). Standalone spinners need role="status" + aria-label. */
722
+
723
+ .dds-spinner {
724
+ display: inline-block;
725
+ flex: none;
726
+ width: var(--dds-space-16);
727
+ height: var(--dds-space-16);
728
+ border: 2px solid currentColor;
729
+ border-top-color: transparent;
730
+ border-radius: var(--dds-radius-full);
731
+ animation: dds-spin 800ms linear infinite;
732
+ }
733
+
734
+ .dds-spinner--lg {
735
+ width: var(--dds-space-24);
736
+ height: var(--dds-space-24);
737
+ }
738
+
739
+ @keyframes dds-spin {
740
+ to {
741
+ transform: rotate(360deg);
742
+ }
743
+ }
744
+
745
+ @media (prefers-reduced-motion: reduce) {
746
+ .dds-spinner {
747
+ animation-duration: 1600ms;
748
+ }
749
+ }
750
+
751
+ /* Skeleton — loading placeholder. The fill is an alpha tint (spec §3.1:
752
+ * overlays/tints are alpha-only) so it sits naturally on default and subtle
753
+ * backgrounds in both themes. Size is the consumer's job (width/height on
754
+ * the element); --text matches a line of the surrounding font. Mark the
755
+ * loading region with aria-busy="true" and keep skeletons aria-hidden. */
756
+
757
+ .dds-skeleton {
758
+ display: block;
759
+ background: var(--dds-color-overlay-pressed-tint);
760
+ border-radius: var(--dds-radius-sm);
761
+ animation: dds-skeleton-pulse 1600ms var(--dds-motion-easing-standard) infinite;
762
+ }
763
+
764
+ .dds-skeleton--text {
765
+ height: 1em;
766
+ }
767
+
768
+ .dds-skeleton--circle {
769
+ border-radius: var(--dds-radius-full);
770
+ }
771
+
772
+ @keyframes dds-skeleton-pulse {
773
+ 50% {
774
+ opacity: 0.4;
775
+ }
776
+ }
777
+
778
+ @media (prefers-reduced-motion: reduce) {
779
+ .dds-skeleton {
780
+ animation: none;
781
+ }
782
+ }
783
+
784
+ /* Divider — a rule between sections. Use <hr class="dds-divider"> when the
785
+ * split is meaningful; when it is purely decorative next to text that already
786
+ * separates the groups, add role="presentation".
787
+ * The vertical variant needs a stretchable parent (flex row / grid cell). */
788
+
789
+ .dds-divider {
790
+ border: none;
791
+ border-top: 1px solid var(--dds-color-border-default);
792
+ margin: var(--dds-space-16) 0;
793
+ }
794
+
795
+ .dds-divider--vertical {
796
+ width: 0;
797
+ align-self: stretch;
798
+ border-top: none;
799
+ border-inline-start: 1px solid var(--dds-color-border-default);
800
+ margin: 0 var(--dds-space-16);
801
+ }
802
+
803
+ /* Card — a grouped surface. Border + elevated background carry the grouping;
804
+ * elevation.1 is the lift. The same shadow token ships in both themes (as it
805
+ * does for Dialog and Toast): on a zinc.950 page it resolves to almost
806
+ * nothing, which is the intended dark look — a component may not branch on
807
+ * theme (spec §3.1: dark is a mapping swap, never an `if (dark)`).
808
+ *
809
+ * --subtle is the flat variant for cards inside an already elevated surface
810
+ * (a card in a dialog), where a second shadow reads as noise. */
811
+
812
+ .dds-card {
813
+ background: var(--dds-color-bg-elevated);
814
+ color: var(--dds-color-text-primary);
815
+ font-family: var(--dds-font-family-sans);
816
+ border: 1px solid var(--dds-color-border-default);
817
+ border-radius: var(--dds-radius-lg);
818
+ box-shadow: var(--dds-elevation-1);
819
+ padding: var(--dds-space-16);
820
+ }
821
+
822
+ .dds-card--subtle {
823
+ background: var(--dds-color-bg-subtle);
824
+ box-shadow: none;
825
+ }
826
+
827
+ .dds-card__title {
828
+ margin: 0 0 var(--dds-space-4);
829
+ font-size: var(--dds-typo-title-3-font-size);
830
+ line-height: var(--dds-typo-title-3-line-height);
831
+ font-weight: var(--dds-typo-title-3-font-weight);
832
+ }
833
+
834
+ .dds-card__body {
835
+ margin: 0;
836
+ font-size: var(--dds-typo-body-2-font-size);
837
+ line-height: var(--dds-typo-body-2-line-height);
838
+ font-weight: var(--dds-typo-body-2-font-weight);
839
+ color: var(--dds-color-text-secondary);
840
+ }
841
+
842
+ /* ListRow — one item in a list: leading slot, body (title + sub), trailing
843
+ * slot. House rule this encodes: rows are self-contained and rich — if the
844
+ * information fits in the row, it does not get deferred to a detail page.
845
+ *
846
+ * --interactive is for rows that are themselves a button/link (use a real
847
+ * <button> or <a> so keyboard and AT get it for free). The focus ring is
848
+ * inset because a full-bleed row has no outer margin to draw into. */
849
+
850
+ .dds-listrow {
851
+ display: flex;
852
+ align-items: center;
853
+ gap: var(--dds-space-12);
854
+ width: 100%;
855
+ padding: var(--dds-space-12) var(--dds-space-16);
856
+ background: none;
857
+ border: none;
858
+ text-align: start;
859
+ font-family: var(--dds-font-family-sans);
860
+ color: var(--dds-color-text-primary);
861
+ }
862
+
863
+ .dds-listrow + .dds-listrow {
864
+ border-top: 1px solid var(--dds-color-border-default);
865
+ }
866
+
867
+ .dds-listrow__body {
868
+ flex: 1;
869
+ min-width: 0;
870
+ }
871
+
872
+ /* display:block so the two lines stack whether the markup uses <p> or <span>
873
+ * (a <span> title and sub otherwise render on one line) */
874
+ .dds-listrow__title {
875
+ display: block;
876
+ margin: 0;
877
+ font-size: var(--dds-typo-body-2-font-size);
878
+ line-height: var(--dds-typo-body-2-line-height);
879
+ font-weight: var(--dds-typo-label-font-weight);
880
+ /* long titles truncate rather than reflowing the row height */
881
+ overflow: hidden;
882
+ text-overflow: ellipsis;
883
+ white-space: nowrap;
884
+ }
885
+
886
+ .dds-listrow__sub {
887
+ display: block;
888
+ margin: 0;
889
+ font-size: var(--dds-typo-caption-font-size);
890
+ line-height: var(--dds-typo-caption-line-height);
891
+ color: var(--dds-color-text-secondary);
892
+ }
893
+
894
+ .dds-listrow__actions {
895
+ display: flex;
896
+ align-items: center;
897
+ gap: var(--dds-space-4);
898
+ flex: none;
899
+ }
900
+
901
+ /* interactive ------------------------------------------------------------- */
902
+
903
+ .dds-listrow--interactive {
904
+ cursor: pointer;
905
+ transition: background-color var(--dds-motion-duration-fast) var(--dds-motion-easing-standard);
906
+ }
907
+ .dds-listrow--interactive:not([disabled]):hover {
908
+ background: var(--dds-color-overlay-hover-tint);
909
+ }
910
+ .dds-listrow--interactive:not([disabled]):active {
911
+ background: var(--dds-color-overlay-pressed-tint);
912
+ }
913
+ .dds-listrow--interactive:focus-visible {
914
+ outline: var(--dds-space-2) solid var(--dds-color-border-focus);
915
+ outline-offset: calc(var(--dds-space-2) * -1);
916
+ }
917
+
918
+ /* opacity is allowed for disabled only (spec §4.2) */
919
+ .dds-listrow[disabled] {
920
+ opacity: 0.45;
921
+ cursor: not-allowed;
922
+ }
923
+
924
+ /* Tabs — ink-underline tabs (the shell the admin surfaces converged on).
925
+ * Markup is the ARIA tabs pattern: .dds-tabs[role="tablist"] >
926
+ * .dds-tab[role="tab"][aria-selected] each controlling a [role="tabpanel"].
927
+ * Selection is aria-selected, never a class alone — the state has to be
928
+ * announced, and color is not the only signal (the underline carries it too).
929
+ *
930
+ * The button border-radius reset is deliberate: a shared radius reset leaking
931
+ * into the underline bows its ends — this house has shipped that bug once. */
932
+
933
+ .dds-tabs {
934
+ display: flex;
935
+ gap: var(--dds-space-4);
936
+ border-bottom: 1px solid var(--dds-color-border-default);
937
+ font-family: var(--dds-font-family-sans);
938
+ overflow-x: auto;
939
+ scrollbar-width: none;
940
+ }
941
+ .dds-tabs::-webkit-scrollbar {
942
+ display: none;
943
+ }
944
+
945
+ .dds-tab {
946
+ appearance: none;
947
+ flex: none;
948
+ padding: var(--dds-space-12);
949
+ background: none;
950
+ border: none;
951
+ border-radius: 0;
952
+ border-bottom: var(--dds-space-2) solid transparent;
953
+ /* the underline sits on the tablist's own 1px rule */
954
+ margin-bottom: -1px;
955
+ font-family: inherit;
956
+ font-size: var(--dds-typo-label-font-size);
957
+ line-height: var(--dds-typo-label-line-height);
958
+ font-weight: var(--dds-typo-label-font-weight);
959
+ color: var(--dds-color-text-secondary);
960
+ white-space: nowrap;
961
+ cursor: pointer;
962
+ transition: color var(--dds-motion-duration-fast) var(--dds-motion-easing-standard);
963
+ }
964
+
965
+ .dds-tab:not([disabled]):hover {
966
+ color: var(--dds-color-text-primary);
967
+ }
968
+ .dds-tab:focus-visible {
969
+ outline: var(--dds-space-2) solid var(--dds-color-border-focus);
970
+ outline-offset: calc(var(--dds-space-2) * -1);
971
+ }
972
+
973
+ .dds-tab[aria-selected="true"] {
974
+ color: var(--dds-color-text-brand);
975
+ border-bottom-color: var(--dds-color-bg-brand);
976
+ }
977
+
978
+ /* opacity is allowed for disabled only (spec §4.2) */
979
+ .dds-tab[disabled] {
980
+ opacity: 0.45;
981
+ cursor: not-allowed;
982
+ }
983
+
984
+ /* Dialog — overlay + panel. Two markup shapes are supported:
985
+ * 1. div pattern: .dds-dialog-overlay > .dds-dialog[role="dialog"][aria-modal="true"]
986
+ * 2. native <dialog class="dds-dialog"> (backdrop styled via ::backdrop)
987
+ * Behavior contract (spec §5): closes on outside tap and Esc; focus is
988
+ * trapped while open and returns to the opener on close — that is the
989
+ * opener JS's job, this file only owns the look. Label the panel with
990
+ * aria-labelledby pointing at the title. */
991
+
992
+ .dds-dialog-overlay {
993
+ position: fixed;
994
+ inset: 0;
995
+ z-index: 1000;
996
+ display: grid;
997
+ place-items: center;
998
+ padding: var(--dds-space-16);
999
+ background: var(--dds-color-overlay-dim);
1000
+ }
1001
+
1002
+ .dds-dialog {
1003
+ background: var(--dds-color-bg-elevated);
1004
+ color: var(--dds-color-text-primary);
1005
+ font-family: var(--dds-font-family-sans);
1006
+ border-radius: var(--dds-radius-lg);
1007
+ box-shadow: var(--dds-elevation-3);
1008
+ padding: var(--dds-space-20);
1009
+ width: 100%;
1010
+ max-width: 400px; /* layout default, not a token — override per product */
1011
+ }
1012
+
1013
+ /* native <dialog> resets */
1014
+ dialog.dds-dialog {
1015
+ border: none;
1016
+ margin: auto;
1017
+ }
1018
+ dialog.dds-dialog::backdrop {
1019
+ background: var(--dds-color-overlay-dim);
1020
+ }
1021
+
1022
+ .dds-dialog__title {
1023
+ margin: 0 0 var(--dds-space-8);
1024
+ font-size: var(--dds-typo-title-2-font-size);
1025
+ line-height: var(--dds-typo-title-2-line-height);
1026
+ font-weight: var(--dds-typo-title-2-font-weight);
1027
+ }
1028
+
1029
+ .dds-dialog__body {
1030
+ margin: 0 0 var(--dds-space-16);
1031
+ font-size: var(--dds-typo-body-2-font-size);
1032
+ line-height: var(--dds-typo-body-2-line-height);
1033
+ font-weight: var(--dds-typo-body-2-font-weight);
1034
+ color: var(--dds-color-text-secondary);
1035
+ }
1036
+
1037
+ .dds-dialog__actions {
1038
+ display: flex;
1039
+ justify-content: flex-end;
1040
+ gap: var(--dds-space-8);
1041
+ }
1042
+
1043
+ /* Toast — transient notice. Region markup:
1044
+ * .dds-toast-region[role="status"] (polite) — use role="alert" per-toast
1045
+ * only for danger. Toasts stack bottom-end; entry animates with
1046
+ * duration.base + easing.enter and respects reduced motion. The status
1047
+ * dot repeats the tone so color is not the only signal (spec §6) — same
1048
+ * 6px dot as the badge. */
1049
+
1050
+ .dds-toast-region {
1051
+ position: fixed;
1052
+ z-index: 1100;
1053
+ inset-block-end: var(--dds-space-16);
1054
+ inset-inline-end: var(--dds-space-16);
1055
+ display: grid;
1056
+ gap: var(--dds-space-8);
1057
+ justify-items: end;
1058
+ pointer-events: none;
1059
+ }
1060
+
1061
+ .dds-toast {
1062
+ pointer-events: auto;
1063
+ display: flex;
1064
+ align-items: center;
1065
+ gap: var(--dds-space-8);
1066
+ background: var(--dds-color-bg-elevated);
1067
+ color: var(--dds-color-text-primary);
1068
+ font-family: var(--dds-font-family-sans);
1069
+ font-size: var(--dds-typo-body-2-font-size);
1070
+ line-height: var(--dds-typo-body-2-line-height);
1071
+ font-weight: var(--dds-typo-body-2-font-weight);
1072
+ border: 1px solid var(--dds-color-border-default);
1073
+ border-radius: var(--dds-radius-md);
1074
+ box-shadow: var(--dds-elevation-2);
1075
+ padding: var(--dds-space-12) var(--dds-space-16);
1076
+ max-width: min(360px, calc(100vw - var(--dds-space-32)));
1077
+ animation: dds-toast-in var(--dds-motion-duration-base) var(--dds-motion-easing-enter);
1078
+ }
1079
+
1080
+ .dds-toast--success::before,
1081
+ .dds-toast--warning::before,
1082
+ .dds-toast--danger::before,
1083
+ .dds-toast--info::before {
1084
+ content: "";
1085
+ flex: none;
1086
+ width: 6px;
1087
+ height: 6px;
1088
+ border-radius: var(--dds-radius-full);
1089
+ }
1090
+ .dds-toast--success::before {
1091
+ background: var(--dds-color-status-success);
1092
+ }
1093
+ .dds-toast--warning::before {
1094
+ background: var(--dds-color-status-warning);
1095
+ }
1096
+ .dds-toast--danger::before {
1097
+ background: var(--dds-color-status-danger);
1098
+ }
1099
+ .dds-toast--info::before {
1100
+ background: var(--dds-color-status-info);
1101
+ }
1102
+
1103
+ @keyframes dds-toast-in {
1104
+ from {
1105
+ opacity: 0;
1106
+ transform: translateY(var(--dds-space-8));
1107
+ }
1108
+ }
1109
+
1110
+ @media (prefers-reduced-motion: reduce) {
1111
+ .dds-toast {
1112
+ animation: none;
1113
+ }
1114
+ }
1115
+
1116
+ /* Tooltip (web only — spec §4.3; on native the same information goes in a
1117
+ * label or a sheet). A supplementary hint on an inverse surface, shown on
1118
+ * hover AND focus-within so it is reachable from the keyboard.
1119
+ *
1120
+ * Contract: the tooltip may never carry information that is required to use
1121
+ * the control — an icon button still needs its own aria-label, and the bubble
1122
+ * is referenced with aria-describedby, not aria-label (a describedby bubble
1123
+ * that never opens still reaches AT; a hover-only one does not). */
1124
+
1125
+ .dds-tooltip {
1126
+ position: relative;
1127
+ display: inline-flex;
1128
+ }
1129
+
1130
+ .dds-tooltip__bubble {
1131
+ position: absolute;
1132
+ inset-block-end: calc(100% + var(--dds-space-8));
1133
+ inset-inline-start: 50%;
1134
+ transform: translateX(-50%);
1135
+ z-index: 1200; /* above dialog (1000) and toast (1100) */
1136
+ padding: var(--dds-space-4) var(--dds-space-8);
1137
+ border-radius: var(--dds-radius-sm);
1138
+ background: var(--dds-color-bg-inverse);
1139
+ color: var(--dds-color-text-on-inverse);
1140
+ font-family: var(--dds-font-family-sans);
1141
+ font-size: var(--dds-typo-caption-font-size);
1142
+ line-height: var(--dds-typo-caption-line-height);
1143
+ white-space: nowrap;
1144
+ pointer-events: none;
1145
+ opacity: 0;
1146
+ transition: opacity var(--dds-motion-duration-fast) var(--dds-motion-easing-standard);
1147
+ }
1148
+
1149
+ .dds-tooltip:hover .dds-tooltip__bubble,
1150
+ .dds-tooltip:focus-within .dds-tooltip__bubble {
1151
+ opacity: 1;
1152
+ }
1153
+
1154
+ @media (prefers-reduced-motion: reduce) {
1155
+ .dds-tooltip__bubble {
1156
+ transition: none;
1157
+ }
1158
+ }
1159
+
1160
+ /* EmptyState — what a list shows when it has nothing yet. House rule: the
1161
+ * empty view keeps the same skeleton as the filled one (summary chips and
1162
+ * filters stay put) — the empty state replaces the rows, not the screen.
1163
+ *
1164
+ * Layout note for consumers: give the element `display: none`'s job to
1165
+ * [hidden] and never override it with a display rule that lacks a
1166
+ * `[hidden]` guard — a shared .empty that did exactly that once painted
1167
+ * "no results" on top of a populated table. */
1168
+
1169
+ .dds-empty {
1170
+ display: flex;
1171
+ flex-direction: column;
1172
+ align-items: center;
1173
+ justify-content: center;
1174
+ gap: var(--dds-space-8);
1175
+ padding: var(--dds-space-40) var(--dds-space-24);
1176
+ border: 1px dashed var(--dds-color-border-strong);
1177
+ border-radius: var(--dds-radius-lg);
1178
+ text-align: center;
1179
+ font-family: var(--dds-font-family-sans);
1180
+ color: var(--dds-color-text-secondary);
1181
+ }
1182
+
1183
+ .dds-empty[hidden] {
1184
+ display: none;
1185
+ }
1186
+
1187
+ .dds-empty__title {
1188
+ margin: 0;
1189
+ font-size: var(--dds-typo-title-3-font-size);
1190
+ line-height: var(--dds-typo-title-3-line-height);
1191
+ font-weight: var(--dds-typo-title-3-font-weight);
1192
+ color: var(--dds-color-text-primary);
1193
+ }
1194
+
1195
+ .dds-empty__desc {
1196
+ margin: 0;
1197
+ max-width: 36em;
1198
+ font-size: var(--dds-typo-body-2-font-size);
1199
+ line-height: var(--dds-typo-body-2-line-height);
1200
+ }
1201
+
1202
+ .dds-empty__actions {
1203
+ display: flex;
1204
+ gap: var(--dds-space-8);
1205
+ margin-top: var(--dds-space-8);
1206
+ }