@ponchia/ui 0.5.0 → 0.6.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 (117) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/MIGRATIONS.json +14 -0
  3. package/README.md +28 -5
  4. package/annotations/index.d.ts +398 -276
  5. package/annotations/index.d.ts.map +1 -0
  6. package/annotations/index.js +315 -45
  7. package/behaviors/carousel.js +17 -16
  8. package/behaviors/combobox.js +47 -16
  9. package/behaviors/command.js +18 -15
  10. package/behaviors/connectors.js +4 -5
  11. package/behaviors/crosshair.js +4 -5
  12. package/behaviors/dialog.js +3 -2
  13. package/behaviors/disclosure.js +3 -2
  14. package/behaviors/dismissible.js +3 -2
  15. package/behaviors/forms.js +41 -13
  16. package/behaviors/glyph.js +4 -5
  17. package/behaviors/internal.js +47 -0
  18. package/behaviors/legend.js +23 -2
  19. package/behaviors/menu.js +3 -2
  20. package/behaviors/popover.js +78 -7
  21. package/behaviors/spotlight.js +4 -5
  22. package/behaviors/table.js +39 -12
  23. package/behaviors/tabs.js +14 -14
  24. package/behaviors/theme.js +5 -3
  25. package/behaviors/toast.js +13 -1
  26. package/classes/classes.json +1857 -0
  27. package/classes/index.d.ts +28 -13
  28. package/classes/index.js +34 -18
  29. package/classes/vscode.css-custom-data.json +12 -0
  30. package/connectors/index.d.ts +189 -69
  31. package/connectors/index.d.ts.map +1 -0
  32. package/connectors/index.js +120 -24
  33. package/css/app.css +43 -13
  34. package/css/base.css +15 -10
  35. package/css/connectors.css +17 -0
  36. package/css/content.css +7 -1
  37. package/css/dataviz.css +5 -1
  38. package/css/disclosure.css +38 -6
  39. package/css/dots.css +57 -0
  40. package/css/feedback.css +60 -2
  41. package/css/forms.css +42 -1
  42. package/css/legend.css +11 -7
  43. package/css/marks.css +38 -8
  44. package/css/motion.css +24 -44
  45. package/css/navigation.css +7 -0
  46. package/css/overlay.css +31 -1
  47. package/css/primitives.css +91 -5
  48. package/css/report.css +40 -63
  49. package/css/site.css +16 -2
  50. package/css/sources.css +43 -1
  51. package/css/spotlight.css +1 -1
  52. package/css/tokens.css +36 -1
  53. package/css/workbench.css +1 -1
  54. package/dist/bronto.css +1 -1
  55. package/dist/css/analytical.css +1 -1
  56. package/dist/css/app.css +1 -1
  57. package/dist/css/base.css +1 -1
  58. package/dist/css/connectors.css +1 -1
  59. package/dist/css/content.css +1 -1
  60. package/dist/css/disclosure.css +1 -1
  61. package/dist/css/dots.css +1 -1
  62. package/dist/css/feedback.css +1 -1
  63. package/dist/css/forms.css +1 -1
  64. package/dist/css/legend.css +1 -1
  65. package/dist/css/marks.css +1 -1
  66. package/dist/css/motion.css +1 -1
  67. package/dist/css/navigation.css +1 -1
  68. package/dist/css/overlay.css +1 -1
  69. package/dist/css/primitives.css +1 -1
  70. package/dist/css/report.css +1 -1
  71. package/dist/css/site.css +1 -1
  72. package/dist/css/sources.css +1 -1
  73. package/dist/css/spotlight.css +1 -1
  74. package/dist/css/tokens.css +1 -1
  75. package/dist/css/workbench.css +1 -1
  76. package/docs/adr/0003-theme-model.md +1 -1
  77. package/docs/annotations.md +94 -14
  78. package/docs/architecture.md +50 -6
  79. package/docs/contrast.md +116 -92
  80. package/docs/d2.md +195 -0
  81. package/docs/legends.md +18 -2
  82. package/docs/marks.md +9 -2
  83. package/docs/mermaid.md +152 -0
  84. package/docs/reference.md +78 -22
  85. package/docs/reporting.md +395 -57
  86. package/docs/sources.md +27 -0
  87. package/docs/stability.md +9 -2
  88. package/docs/usage.md +101 -4
  89. package/docs/vega.md +225 -0
  90. package/docs/workbench.md +7 -1
  91. package/glyphs/glyphs.js +6 -4
  92. package/llms.txt +139 -14
  93. package/package.json +50 -12
  94. package/qwik/index.d.ts +42 -59
  95. package/qwik/index.d.ts.map +1 -0
  96. package/qwik/index.js +55 -3
  97. package/react/index.d.ts +39 -61
  98. package/react/index.d.ts.map +1 -0
  99. package/react/index.js +57 -3
  100. package/solid/index.d.ts +64 -61
  101. package/solid/index.d.ts.map +1 -0
  102. package/solid/index.js +60 -3
  103. package/tokens/d2.d.ts +38 -0
  104. package/tokens/d2.js +71 -0
  105. package/tokens/d2.json +43 -0
  106. package/tokens/index.d.ts +5 -5
  107. package/tokens/index.js +15 -1
  108. package/tokens/index.json +9 -0
  109. package/tokens/mermaid.d.ts +23 -0
  110. package/tokens/mermaid.js +181 -0
  111. package/tokens/mermaid.json +163 -0
  112. package/tokens/resolved.json +45 -1
  113. package/tokens/skins.js +3 -2
  114. package/tokens/tokens.dtcg.json +26 -0
  115. package/tokens/vega.d.ts +34 -0
  116. package/tokens/vega.js +155 -0
  117. package/tokens/vega.json +179 -0
package/css/feedback.css CHANGED
@@ -212,8 +212,27 @@
212
212
  line-height: 1;
213
213
  }
214
214
 
215
- .ui-toast__close:hover {
216
- color: var(--text);
215
+ /* Touch: enlarge the dismiss/close hit areas to the project's coarse-pointer
216
+ tap-target floor (2.9rem, as in primitives.css / forms.css) without changing
217
+ the desktop glyph size. The box is centred so the glyph stays put. */
218
+ @media (pointer: coarse) {
219
+ .ui-alert__dismiss,
220
+ .ui-toast__close {
221
+ display: inline-grid;
222
+ place-items: center;
223
+ min-block-size: 2.9rem;
224
+ min-inline-size: 2.9rem;
225
+ }
226
+
227
+ .ui-alert__dismiss {
228
+ padding: 0;
229
+ }
230
+ }
231
+
232
+ @media (hover: hover) {
233
+ .ui-toast__close:hover {
234
+ color: var(--text);
235
+ }
217
236
  }
218
237
 
219
238
  .ui-toast--accent::before {
@@ -420,6 +439,7 @@
420
439
  .ui-progress__bar {
421
440
  background: var(--accent);
422
441
  block-size: 100%;
442
+ display: block;
423
443
  transition: inline-size var(--duration-base) var(--ease-out);
424
444
  inline-size: calc(clamp(0, var(--value, 0), 100) * 1%);
425
445
  }
@@ -465,6 +485,7 @@
465
485
  .ui-meter__fill {
466
486
  background: var(--text-dim);
467
487
  block-size: 100%;
488
+ display: block;
468
489
  transition: inline-size var(--duration-base) var(--ease-out);
469
490
  inline-size: calc(clamp(0, var(--value, 0), 100) * 1%);
470
491
  }
@@ -485,6 +506,10 @@
485
506
  background: var(--danger);
486
507
  }
487
508
 
509
+ .ui-meter--info .ui-meter__fill {
510
+ background: var(--info);
511
+ }
512
+
488
513
  /* --- Steps — progress through a multi-step flow. Use an <ol>; the
489
514
  current step is aria-current="step" (no class), completed steps take
490
515
  --done. Counter-numbered markers, hairline connectors. --- */
@@ -553,3 +578,36 @@
553
578
  border-color: var(--accent);
554
579
  color: var(--button-text);
555
580
  }
581
+
582
+ /* Print: the progress/meter fill is the data (a measured proportion), so its
583
+ painted background must survive the print "economy" default. The fill tokens
584
+ are re-pointed to print-legible inks by the global print remap in base.css. */
585
+ @media print {
586
+ .ui-progress__bar,
587
+ .ui-meter__fill {
588
+ -webkit-print-color-adjust: exact;
589
+ print-color-adjust: exact;
590
+ }
591
+ }
592
+
593
+ /* Forced-colors flattens the fill's tone to the system palette and can erase it
594
+ against the track, dropping the only visual cue of the measured proportion.
595
+ Re-assert a system colour so the bar stays visible; the tone's *semantic* is
596
+ carried by the author-written label beside it, not the colour. (a11y C10.)
597
+ The toned `.ui-meter--TONE .ui-meter__fill` rules are (0,2,0); the bare
598
+ `.ui-meter__fill` reset is only (0,1,0), so it lost — a toned fill stayed
599
+ `var(--TONE)` and was forced to black-on-black (component-audit C3). Match the
600
+ tone specificity here (and set `forced-color-adjust: none`) so every meter,
601
+ toned or not, paints `Highlight`, mirroring the `.ui-dot` precedent. */
602
+ @media (forced-colors: active) {
603
+ .ui-progress__bar,
604
+ .ui-meter__fill,
605
+ .ui-meter--accent .ui-meter__fill,
606
+ .ui-meter--success .ui-meter__fill,
607
+ .ui-meter--warning .ui-meter__fill,
608
+ .ui-meter--danger .ui-meter__fill,
609
+ .ui-meter--info .ui-meter__fill {
610
+ background: Highlight;
611
+ forced-color-adjust: none;
612
+ }
613
+ }
package/css/forms.css CHANGED
@@ -77,6 +77,29 @@
77
77
  opacity: 0.5;
78
78
  }
79
79
 
80
+ /* Disabled affordance parity. The text inputs above style :disabled directly;
81
+ the controls that WRAP a native input (switch/check/segmented) showed no
82
+ disabled cue and their label kept cursor:pointer — a lie. Mirror the cue via
83
+ :has(input:disabled); the native-element controls (range/file) take :disabled
84
+ directly. (a11y/forms review C4.) */
85
+ .ui-range:disabled,
86
+ .ui-file:disabled,
87
+ .ui-switch:has(input:disabled),
88
+ .ui-check:has(input:disabled),
89
+ .ui-segmented__option:has(input:disabled) {
90
+ cursor: not-allowed;
91
+ opacity: 0.5;
92
+ }
93
+
94
+ /* Keep autofilled fields on-theme — the UA's yellow fill otherwise paints over
95
+ the monochrome surface and breaks the contrast story. (forms review C24.) */
96
+ .ui-input:autofill,
97
+ .ui-search input:autofill {
98
+ -webkit-text-fill-color: var(--text);
99
+ box-shadow: inset 0 0 0 100rem var(--bg-elevated);
100
+ caret-color: var(--text);
101
+ }
102
+
80
103
  .ui-input[aria-invalid='true'],
81
104
  .ui-select[aria-invalid='true'],
82
105
  .ui-textarea[aria-invalid='true'] {
@@ -221,7 +244,10 @@
221
244
 
222
245
  .ui-error-summary__title {
223
246
  color: var(--danger);
224
- font-family: var(--display);
247
+
248
+ /* The must-be-read failure heading uses the legible sans, not the low-legibility
249
+ Doto display face — reserve --display for decorative numerals. (forms C27.) */
250
+ font-family: var(--sans);
225
251
  font-size: var(--text-sm);
226
252
  font-weight: 700;
227
253
  letter-spacing: var(--tracking-wide);
@@ -262,6 +288,14 @@
262
288
  border-color: var(--accent);
263
289
  }
264
290
 
291
+ /* Keyboard focus ring to match every sibling input (which use a 2px ring); the
292
+ border-colour shift alone read markedly fainter on this one control. Keyed to
293
+ :focus-visible on the inner input so it stays keyboard-only. (a11y review C12.) */
294
+ .ui-search:has(input:focus-visible) {
295
+ outline: 2px solid var(--focus-ring);
296
+ outline-offset: 1px;
297
+ }
298
+
265
299
  .ui-search:focus-within::before {
266
300
  background: var(--accent);
267
301
  }
@@ -384,6 +418,13 @@
384
418
  min-block-size: 2.9rem;
385
419
  }
386
420
 
421
+ /* The whole label is the toggle target — float it to the ~44px floor so the
422
+ small track/box isn't a sub-target on touch (WCAG 2.5.8 — C24). */
423
+ .ui-switch,
424
+ .ui-check {
425
+ min-block-size: 2.9rem;
426
+ }
427
+
387
428
  .ui-check input {
388
429
  block-size: 1.15rem;
389
430
  inline-size: 1.15rem;
package/css/legend.css CHANGED
@@ -1,10 +1,10 @@
1
1
  /* ==========================================================================
2
2
  legend — opt-in data keys for charts, reports, and analytical figures.
3
3
 
4
- A standalone, portable successor to the report kit's inline chart legend:
5
- `.ui-legend` reads the Tier-4 `--chart-*` tokens and pairs with any chart
6
- (Bronto's own `.ui-chart` bars, an SVG/canvas figure, or an external chart
7
- engine). Not imported by core.css — import it beside css/dataviz.css.
4
+ A standalone, portable data key: `.ui-legend` reads the Tier-4 `--chart-*`
5
+ tokens and pairs with any chart (a token-themed inline SVG/canvas figure, or
6
+ an external engine like Vega-Lite see docs/vega.md). Not imported by
7
+ core.css — import it beside css/dataviz.css.
8
8
 
9
9
  Bronto paints and positions; it owns no scales, data mapping, or series
10
10
  state. Colour is never the sole channel: a swatch mirrors its chart mark
@@ -205,12 +205,16 @@
205
205
 
206
206
  .ui-legend--with-values .ui-legend__value {
207
207
  margin-inline-start: 0;
208
+ text-align: end;
208
209
  }
209
210
 
210
211
  /* Interactive entries are real <button aria-pressed> controls. Bronto styles
211
212
  the control + the inactive state; behaviors/legend.js (optional) flips
212
- aria-pressed and emits an event. The host hides the series and announces it. */
213
- .ui-legend--interactive .ui-legend__item {
213
+ aria-pressed and emits an event. The host hides the series and announces it.
214
+ Scoped to button/role=button so a non-button entry never gets cursor:pointer
215
+ — a pointer-only affordance with no keyboard path is a lie (WCAG 2.1.1 — C11;
216
+ the behavior ignores and warns about non-button entries to match). */
217
+ .ui-legend--interactive .ui-legend__item:is(button, [role='button']) {
214
218
  background: none;
215
219
  border: 0;
216
220
  border-radius: var(--radius-sm);
@@ -222,7 +226,7 @@
222
226
  text-align: start;
223
227
  }
224
228
 
225
- .ui-legend--interactive .ui-legend__item:focus-visible {
229
+ .ui-legend--interactive .ui-legend__item:is(button, [role='button']):focus-visible {
226
230
  outline: 2px solid var(--accent);
227
231
  outline-offset: 2px;
228
232
  }
package/css/marks.css CHANGED
@@ -12,12 +12,20 @@
12
12
  ========================================================================== */
13
13
 
14
14
  .ui-mark {
15
- --mark-color: var(--line-strong);
16
-
15
+ /* No base `--mark-color`: each draw style supplies its own fallback default
16
+ (highlight/box keep the subtle `--line-strong`; the full-opacity
17
+ `--underline`/`--strike` decorations default to the darker `--text-dim` so
18
+ they read on a light surface — C33). A tone modifier SETS `--mark-color`,
19
+ so it still wins everywhere the fallback would otherwise apply. */
20
+
21
+ /* A bare `<mark>` carries the UA `background-color: yellow`; the translucent
22
+ highlight gradient sits on top of it, so without this reset the UA yellow
23
+ bleeds through (an author declaration always beats the UA origin). */
24
+ background-color: transparent;
17
25
  background-image: linear-gradient(
18
26
  90deg,
19
- color-mix(in srgb, var(--mark-color) 30%, transparent),
20
- color-mix(in srgb, var(--mark-color) 30%, transparent)
27
+ color-mix(in srgb, var(--mark-color, var(--line-strong)) 30%, transparent),
28
+ color-mix(in srgb, var(--mark-color, var(--line-strong)) 30%, transparent)
21
29
  );
22
30
  background-repeat: no-repeat;
23
31
  background-size: 100% 100%;
@@ -59,7 +67,7 @@
59
67
  .ui-mark--underline {
60
68
  background: none;
61
69
  padding: 0;
62
- text-decoration-color: var(--mark-color);
70
+ text-decoration-color: var(--mark-color, var(--text-dim));
63
71
  text-decoration-line: underline;
64
72
  text-decoration-thickness: 0.12em;
65
73
  text-underline-offset: 0.18em;
@@ -67,21 +75,24 @@
67
75
 
68
76
  .ui-mark--box {
69
77
  background: none;
70
- border: 1px solid var(--mark-color);
78
+ border: 1px solid var(--mark-color, var(--line-strong));
71
79
  border-radius: var(--radius-sm);
72
80
  }
73
81
 
74
82
  .ui-mark--strike {
75
83
  background: none;
76
84
  padding: 0;
77
- text-decoration-color: var(--mark-color);
85
+ text-decoration-color: var(--mark-color, var(--text-dim));
78
86
  text-decoration-line: line-through;
79
87
  }
80
88
 
81
89
  /* Draw-on highlight sweep. Only the highlight fill animates, and only when
82
90
  motion is welcome — reduced-motion keeps the resting full highlight. */
83
91
  @media (prefers-reduced-motion: no-preference) {
84
- .ui-mark--draw {
92
+ /* The sweep animates `background-size`, i.e. the highlight FILL — so it is
93
+ inert on the no-fill styles (underline/box/strike). Scope it out rather
94
+ than let `--draw` look applied but do nothing. (content review C14.) */
95
+ .ui-mark--draw:not(.ui-mark--underline, .ui-mark--box, .ui-mark--strike) {
85
96
  animation: ui-mark-draw 0.6s var(--ease, ease) both;
86
97
  }
87
98
  }
@@ -120,6 +131,10 @@
120
131
  --mark-color: var(--accent);
121
132
  }
122
133
 
134
+ .ui-bracket-note--success {
135
+ --mark-color: var(--success);
136
+ }
137
+
123
138
  .ui-bracket-note--warning {
124
139
  --mark-color: var(--warning);
125
140
  }
@@ -142,3 +157,18 @@
142
157
  text-decoration-line: underline;
143
158
  }
144
159
  }
160
+
161
+ /* Print: the highlight is a painted background, so force it through the print
162
+ "economy" default that would otherwise drop it. Settle the draw sweep to its
163
+ resting full highlight in case it is mid-animation when the page is printed. */
164
+ @media print {
165
+ .ui-mark {
166
+ -webkit-print-color-adjust: exact;
167
+ background-size: 100% 100%;
168
+ print-color-adjust: exact;
169
+ }
170
+
171
+ .ui-mark--draw {
172
+ animation: none;
173
+ }
174
+ }
package/css/motion.css CHANGED
@@ -1,8 +1,6 @@
1
1
  /* ==========================================================================
2
2
  motion — keyframes + animation utilities
3
3
  Restrained, dot-flavoured. Everything collapses under reduced-motion.
4
- Keyframes from the legacy responsive layer live here so consumers that
5
- import only core.css keep their existing animations.
6
4
  ========================================================================== */
7
5
 
8
6
  @keyframes pulseDot {
@@ -18,45 +16,6 @@
18
16
  }
19
17
  }
20
18
 
21
- @keyframes scan {
22
- 0% {
23
- transform: translateY(-120%);
24
- }
25
-
26
- 100% {
27
- transform: translateY(320%);
28
- }
29
- }
30
-
31
- @keyframes growBar {
32
- to {
33
- transform: scaleX(1);
34
- }
35
- }
36
-
37
- @keyframes drawLine {
38
- from {
39
- opacity: 0;
40
- stroke-dasharray: 0 999;
41
- }
42
-
43
- to {
44
- opacity: 1;
45
- stroke-dasharray: 999 0;
46
- }
47
- }
48
-
49
- @keyframes pulseNode {
50
- 0%,
51
- 100% {
52
- transform: scale(1);
53
- }
54
-
55
- 50% {
56
- transform: scale(1.08);
57
- }
58
- }
59
-
60
19
  @keyframes pulseRing {
61
20
  0% {
62
21
  opacity: 0.7;
@@ -233,9 +192,13 @@
233
192
  animation-delay: 360ms;
234
193
  }
235
194
 
236
- /* Reveal-on-scroll: add .ui-reveal, toggle .is-visible via IntersectionObserver.
237
- Degrades to visible with no JS. */
238
- @media (prefers-reduced-motion: no-preference) {
195
+ /* Reveal-on-scroll: add .ui-reveal, then toggle .is-visible (e.g. from an
196
+ IntersectionObserver you own) to play it in. For a ZERO-JS reveal, use
197
+ `.ui-scroll-reveal` (scroll-driven, CSS-only) instead — that is the path for
198
+ an LLM-authored or no-build report. The hidden initial state below is gated
199
+ on `scripting: enabled`, so with scripting OFF the content is fully visible
200
+ and never silently hidden behind a script that will never run. */
201
+ @media (prefers-reduced-motion: no-preference) and (scripting: enabled) {
239
202
  .ui-reveal {
240
203
  opacity: 0;
241
204
  transform: translateY(14px);
@@ -365,14 +328,31 @@
365
328
  scroll-behavior: auto;
366
329
  }
367
330
 
331
+ /* !important is required: inside @layer bronto, layered rules lose to
332
+ unlayered author rules at equal specificity — only a layered !important
333
+ stays authoritative over unlayered declarations (CSS cascade §6.2). */
368
334
  *,
369
335
  *::before,
370
336
  *::after {
371
337
  animation-duration: 0.01ms !important;
372
338
  animation-iteration-count: 1 !important;
339
+
340
+ /* Zero the delay too: `.ui-stagger` children animate `uiRise` with
341
+ `fill-mode: both`, so a non-zero `animation-delay` holds them at the
342
+ `opacity: 0` from-state for the full delay and then pops them in — the
343
+ exact late flash a reduced-motion user asked to avoid (C4). */
344
+ animation-delay: 0s !important;
373
345
  transition-duration: 0.01ms !important;
374
346
  }
375
347
 
348
+ /* Freeze the shimmer gradient mid-sweep would expose a diagonal band.
349
+ Flatten the skeleton to its base panel colour instead. */
350
+ .ui-skeleton {
351
+ animation: none;
352
+ background: var(--panel-soft);
353
+ background-size: auto;
354
+ }
355
+
376
356
  .ui-reveal {
377
357
  opacity: 1 !important;
378
358
  transform: none !important;
@@ -80,3 +80,10 @@
80
80
  transform: translateY(1px);
81
81
  }
82
82
  }
83
+
84
+ @media (pointer: coarse) {
85
+ .ui-themetoggle__button {
86
+ min-block-size: 2.9rem;
87
+ padding-inline: 0.9rem;
88
+ }
89
+ }
package/css/overlay.css CHANGED
@@ -249,7 +249,7 @@ dialog.ui-modal[open]::backdrop {
249
249
  }
250
250
 
251
251
  .ui-menu__item::before {
252
- background: currentColor;
252
+ background: currentcolor;
253
253
  border-radius: 50%;
254
254
  content: '';
255
255
  block-size: 0.3rem;
@@ -275,6 +275,15 @@ dialog.ui-modal[open]::backdrop {
275
275
  }
276
276
  }
277
277
 
278
+ .ui-menu__item:focus-visible {
279
+ background: var(--bg-accent);
280
+ color: var(--text);
281
+ }
282
+
283
+ .ui-menu__item:focus-visible::before {
284
+ opacity: 1;
285
+ }
286
+
278
287
  /* --- Combobox: an input with a filtered listbox popup (APG pattern,
279
288
  wired by initCombobox). Reuses the menu surface tokens. --- */
280
289
  .ui-combobox {
@@ -363,6 +372,14 @@ dialog.ui-modal[open]::backdrop {
363
372
  inset-inline: 0;
364
373
  min-inline-size: 0;
365
374
  }
375
+
376
+ /* Let the combobox shed its 14rem floor and span the viewport so the input
377
+ (and its full-width listbox) can't overflow a narrow screen. */
378
+ .ui-combobox {
379
+ display: block;
380
+ inline-size: 100%;
381
+ min-inline-size: 0;
382
+ }
366
383
  }
367
384
 
368
385
  @media (prefers-reduced-motion: reduce) {
@@ -376,3 +393,16 @@ dialog.ui-modal[open]::backdrop {
376
393
  transition: none;
377
394
  }
378
395
  }
396
+
397
+ /* Forced-colors drops both `backdrop-filter: blur()` and the `color-mix()`
398
+ scrim, so the dialog/lightbox would float over an undimmed page with no
399
+ separation. Re-assert a translucent scrim with `forced-color-adjust: none`
400
+ so the layering reads in High Contrast, matching the meter/dot precedent
401
+ (C28). */
402
+ @media (forced-colors: active) {
403
+ .ui-modal::backdrop,
404
+ .ui-lightbox::backdrop {
405
+ background: color-mix(in srgb, #000 50%, transparent);
406
+ forced-color-adjust: none;
407
+ }
408
+ }
@@ -87,7 +87,11 @@
87
87
  (island-safe). Inert until applied, so existing layouts/baselines
88
88
  are unaffected. */
89
89
  .ui-cq {
90
- container: var(--cq-name, bronto) / inline-size;
90
+ /* The container-name is fixed: the `@container bronto (…)` collapse queries
91
+ below hardcode `bronto`. There is no `--cq-name` knob — the name is not
92
+ author-tunable, and an author-set `--cq-name` is simply ignored (nothing
93
+ reads it). Name a different container yourself if you need a custom scope. */
94
+ container: bronto / inline-size;
91
95
  }
92
96
 
93
97
  @container bronto (max-width: 34rem) {
@@ -196,8 +200,9 @@
196
200
  font-family: var(--display);
197
201
  font-size: 1.9rem;
198
202
  font-variant-numeric: tabular-nums;
199
- letter-spacing: 0.01em;
200
- line-height: 1;
203
+ font-weight: var(--display-weight-strong);
204
+ letter-spacing: 0.02em;
205
+ line-height: 1.05;
201
206
  }
202
207
 
203
208
  .ui-stat__delta,
@@ -216,6 +221,22 @@
216
221
  color: var(--danger);
217
222
  }
218
223
 
224
+ /* A direction arrow is the non-colour channel — colour alone (success/danger)
225
+ fails WCAG 1.4.1, so the tile carries the same ▲/▼ glyph as `.ui-delta`
226
+ (C13). Marked aria-hidden-equivalent by being generated content; the sign is
227
+ still in the author's text ("+12%"). */
228
+ .ui-stat__delta.is-pos::before,
229
+ .ui-app-metric__delta.is-pos::before {
230
+ content: '▲ ';
231
+ font-size: 0.85em;
232
+ }
233
+
234
+ .ui-stat__delta.is-neg::before,
235
+ .ui-app-metric__delta.is-neg::before {
236
+ content: '▼ ';
237
+ font-size: 0.85em;
238
+ }
239
+
219
240
  /* --- Numeric value vocabulary ---
220
241
  The same tabular/aligned/tone intent the table has shipped since
221
242
  0.1.0, but freed from `.ui-table` so cards, stats and inline figures
@@ -238,6 +259,58 @@
238
259
  color: var(--text-dim);
239
260
  }
240
261
 
262
+ /* --- Trend delta ---
263
+ A standalone change indicator: a direction arrow (the non-colour channel,
264
+ WCAG 1.4.1) plus the figure, tabular so a column of deltas aligns.
265
+ Direction sets the glyph AND the conventional tone (up = positive,
266
+ down = negative). When "up" is the bad direction (latency, error rate,
267
+ cost, churn), add `ui-delta--invert` to swap only the tone — the arrow
268
+ still reports real direction. Pair with words; the arrow is visual, not a
269
+ substitute for stating the change. */
270
+ .ui-delta {
271
+ align-items: baseline;
272
+ color: var(--text-soft);
273
+ display: inline-flex;
274
+ font-variant-numeric: tabular-nums;
275
+ gap: 0.2em;
276
+ }
277
+
278
+ .ui-delta::before {
279
+ font-size: 0.85em;
280
+ }
281
+
282
+ .ui-delta--up {
283
+ color: var(--success);
284
+ }
285
+
286
+ .ui-delta--up::before {
287
+ content: '▲';
288
+ }
289
+
290
+ .ui-delta--down {
291
+ color: var(--danger);
292
+ }
293
+
294
+ .ui-delta--down::before {
295
+ content: '▼';
296
+ }
297
+
298
+ .ui-delta--flat {
299
+ color: var(--text-dim);
300
+ }
301
+
302
+ .ui-delta--flat::before {
303
+ content: '—';
304
+ }
305
+
306
+ .ui-delta--invert.ui-delta--up {
307
+ color: var(--danger);
308
+ }
309
+
310
+ .ui-delta--invert.ui-delta--down {
311
+ color: var(--success);
312
+ }
313
+
241
314
  /* --- Eyebrow / section label — dot-matrix face --- */
242
315
 
243
316
  .ui-eyebrow {
@@ -338,6 +411,15 @@
338
411
  opacity: 0.45;
339
412
  }
340
413
 
414
+ /* aria-disabled keeps the element in the a11y tree but the browser does NOT
415
+ block activation (a real <a class="ui-button" aria-disabled> still navigates,
416
+ a <button> still fires). Looking dead while staying live is the defect — kill
417
+ pointer activation. (a11y review C3; native :disabled already inert.) */
418
+ .ui-button[aria-disabled='true'],
419
+ .ui-link[aria-disabled='true'] {
420
+ pointer-events: none;
421
+ }
422
+
341
423
  .ui-button:active {
342
424
  transform: translateY(1px);
343
425
  }
@@ -401,8 +483,8 @@
401
483
 
402
484
  .ui-link--arrow::after,
403
485
  .ui-link--cta::after {
404
- border-inline-end: 1px solid currentColor;
405
- border-block-start: 1px solid currentColor;
486
+ border-inline-end: 1px solid currentcolor;
487
+ border-block-start: 1px solid currentcolor;
406
488
  content: '';
407
489
  block-size: 0.42rem;
408
490
  transform: rotate(45deg);
@@ -459,6 +541,10 @@
459
541
  (the soft tint can't carry 4.5:1 small-bold tone-on-tone — WCAG). */
460
542
  .ui-badge--accent {
461
543
  background: var(--accent-soft);
544
+
545
+ /* Accent mixes 45% (vs 40% for the status tones below) on purpose: the brand
546
+ hue is lower-chroma here than the status hues, so it needs a touch more to
547
+ read at the same border weight. Intentional, not a copy-paste slip. (Q15.) */
462
548
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
463
549
  }
464
550