@neuravision/construct 2.0.3 → 2.1.2

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 (138) hide show
  1. package/CHANGELOG.md +318 -0
  2. package/README.md +158 -149
  3. package/behaviors/combobox.js +486 -0
  4. package/behaviors/drawer.js +18 -0
  5. package/behaviors/dropdown.js +310 -0
  6. package/behaviors/index.d.ts +335 -0
  7. package/behaviors/index.js +9 -0
  8. package/behaviors/internal/dialog-controller.js +421 -0
  9. package/behaviors/internal/dom.js +408 -0
  10. package/behaviors/internal/overlay.js +241 -0
  11. package/behaviors/internal/popup-layer.js +190 -0
  12. package/behaviors/modal.js +17 -0
  13. package/behaviors/popover.js +278 -0
  14. package/behaviors/select-menu.js +344 -0
  15. package/behaviors/tabs.js +241 -0
  16. package/behaviors/toggle-group.js +266 -0
  17. package/behaviors/tooltip.js +257 -0
  18. package/components/README.md +319 -83
  19. package/components/_shared.css +22 -10
  20. package/components/_shell-shared.css +20 -4
  21. package/components/accordion.css +8 -0
  22. package/components/app-shell-v2.css +28 -14
  23. package/components/app-shell.css +30 -14
  24. package/components/badge.css +15 -0
  25. package/components/banner.css +32 -1
  26. package/components/button.css +103 -15
  27. package/components/card.css +67 -8
  28. package/components/chart.css +26 -0
  29. package/components/checkbox.css +35 -2
  30. package/components/chip.css +30 -2
  31. package/components/combobox.css +21 -0
  32. package/components/datepicker.css +58 -9
  33. package/components/datum.css +277 -0
  34. package/components/dropdown.css +23 -3
  35. package/components/field.css +6 -4
  36. package/components/file-upload.css +1 -1
  37. package/components/index.css +5 -0
  38. package/components/list.css +28 -2
  39. package/components/meter.css +182 -0
  40. package/components/navbar.css +102 -4
  41. package/components/pagination.css +18 -0
  42. package/components/progress-bar.css +22 -1
  43. package/components/select-menu.css +8 -0
  44. package/components/select.css +8 -0
  45. package/components/sidebar.css +15 -2
  46. package/components/skip-link.css +6 -2
  47. package/components/slider.css +7 -1
  48. package/components/status.css +107 -0
  49. package/components/switch.css +66 -5
  50. package/components/table.css +53 -4
  51. package/components/tabs.css +55 -3
  52. package/components/toast.css +10 -1
  53. package/components/toggle-group.css +16 -5
  54. package/components/toolbar.css +17 -1
  55. package/components/tooltip.css +4 -0
  56. package/components/tree.css +30 -0
  57. package/docs/architecture.md +79 -0
  58. package/docs/behaviors.md +122 -0
  59. package/docs/fonts.md +70 -0
  60. package/docs/governance.md +85 -0
  61. package/docs/guidelines.md +72 -25
  62. package/examples/react/README.md +20 -0
  63. package/examples/react/SettingsModal.jsx +66 -0
  64. package/examples/react/construct.css +5 -0
  65. package/fonts.css +10 -0
  66. package/foundations.css +14 -1
  67. package/package.json +76 -21
  68. package/schemas/components.schema.json +122 -0
  69. package/schemas/examples/theme.example.json +27 -0
  70. package/schemas/primitives.schema.json +135 -0
  71. package/schemas/semantic.schema.json +224 -0
  72. package/schemas/theme.schema.json +63 -0
  73. package/scripts/build-tokens.mjs +1499 -0
  74. package/scripts/construct-theme.mjs +43 -0
  75. package/scripts/validate-tokens.mjs +25 -0
  76. package/standalone/accordion.css +3 -0
  77. package/standalone/alert.css +3 -0
  78. package/standalone/app-shell-v2.css +4 -0
  79. package/standalone/app-shell.css +4 -0
  80. package/standalone/avatar.css +3 -0
  81. package/standalone/badge.css +3 -0
  82. package/standalone/banner.css +3 -0
  83. package/standalone/breadcrumbs.css +3 -0
  84. package/standalone/button.css +3 -0
  85. package/standalone/card.css +3 -0
  86. package/standalone/chart.css +3 -0
  87. package/standalone/checkbox.css +3 -0
  88. package/standalone/chip.css +3 -0
  89. package/standalone/combobox.css +3 -0
  90. package/standalone/core.css +4 -0
  91. package/standalone/data-table.css +3 -0
  92. package/standalone/datepicker.css +3 -0
  93. package/standalone/datum.css +3 -0
  94. package/standalone/divider.css +3 -0
  95. package/standalone/drawer.css +3 -0
  96. package/standalone/dropdown.css +3 -0
  97. package/standalone/empty-state.css +3 -0
  98. package/standalone/field.css +3 -0
  99. package/standalone/file-upload.css +3 -0
  100. package/standalone/icon.css +3 -0
  101. package/standalone/input.css +3 -0
  102. package/standalone/list.css +3 -0
  103. package/standalone/meter.css +3 -0
  104. package/standalone/modal.css +3 -0
  105. package/standalone/navbar.css +3 -0
  106. package/standalone/pagination.css +3 -0
  107. package/standalone/popover.css +3 -0
  108. package/standalone/progress-bar.css +3 -0
  109. package/standalone/select-menu.css +3 -0
  110. package/standalone/select.css +3 -0
  111. package/standalone/sidebar.css +3 -0
  112. package/standalone/skeleton.css +3 -0
  113. package/standalone/skip-link.css +3 -0
  114. package/standalone/slider.css +3 -0
  115. package/standalone/spinner.css +3 -0
  116. package/standalone/status.css +3 -0
  117. package/standalone/switch.css +3 -0
  118. package/standalone/table.css +3 -0
  119. package/standalone/tabs.css +3 -0
  120. package/standalone/textarea.css +3 -0
  121. package/standalone/toast.css +3 -0
  122. package/standalone/toggle-group.css +3 -0
  123. package/standalone/toolbar.css +3 -0
  124. package/standalone/tooltip.css +3 -0
  125. package/standalone/tree.css +3 -0
  126. package/themes/README.md +37 -0
  127. package/themes/walnut.theme.json +106 -0
  128. package/tokens/README.md +135 -123
  129. package/tokens/components.json +64 -0
  130. package/tokens/primitives.json +268 -0
  131. package/tokens/semantic.dark.json +94 -0
  132. package/tokens/semantic.high-contrast-dark.json +94 -0
  133. package/tokens/semantic.high-contrast.json +94 -0
  134. package/tokens/semantic.light.json +179 -0
  135. package/tokens/tokens.css +960 -58
  136. package/tokens/tokens.js +729 -120
  137. package/tokens/tokens.json +1158 -116
  138. package/tokens/tokens.ts +729 -120
@@ -86,6 +86,27 @@
86
86
  fill: var(--color-text-secondary);
87
87
  }
88
88
 
89
+ /* ── Datum reference line ──
90
+ Construct's signature applied to data: an orange reference line marks
91
+ a threshold, target, or "today". The dash pattern separates the
92
+ reference from data series without relying on hue, and the value it
93
+ encodes must also appear as text — in the label and in the chart's
94
+ data-table fallback — so the line itself stays supplementary. */
95
+ .ct-chart__datum-line {
96
+ stroke: var(--color-brand-accent-strong);
97
+ stroke-width: var(--ct-chart-line-width);
98
+ stroke-dasharray: 6 4;
99
+ }
100
+
101
+ .ct-chart__datum-label {
102
+ fill: var(--color-text-secondary);
103
+ font-family: var(--font-family-mono);
104
+ font-size: var(--font-size-xs);
105
+ font-weight: var(--font-weight-semibold);
106
+ letter-spacing: 0.08em;
107
+ text-transform: uppercase;
108
+ }
109
+
89
110
  /* ── Line & area ── */
90
111
  .ct-chart__line {
91
112
  fill: none;
@@ -366,4 +387,9 @@
366
387
  .ct-chart__axis-line {
367
388
  stroke: GrayText;
368
389
  }
390
+
391
+ /* The datum reference line stays distinct from the dimmed chrome. */
392
+ .ct-chart__datum-line {
393
+ stroke: CanvasText;
394
+ }
369
395
  }
@@ -13,7 +13,7 @@
13
13
  appearance: none;
14
14
  width: var(--space-6);
15
15
  height: var(--space-6);
16
- border: var(--border-thin) solid var(--color-border-default);
16
+ border: var(--border-thin) solid var(--ct-control-border);
17
17
  background: var(--color-bg-elevated);
18
18
  cursor: pointer;
19
19
  display: grid;
@@ -24,7 +24,8 @@
24
24
  transform var(--duration-fast) var(--easing-standard);
25
25
  }
26
26
 
27
- /* Invisible touch-target expansion (WCAG 2.5.8: 44×44px minimum) */
27
+ /* Construct 44×44 target policy: meets the size in WCAG 2.2 SC 2.5.5
28
+ (AAA) and exceeds the AA minimum in SC 2.5.8. */
28
29
  @media (pointer: coarse) {
29
30
  .ct-check__input::after,
30
31
  .ct-radio__input::after {
@@ -97,6 +98,9 @@
97
98
  background: var(--color-bg-muted);
98
99
  border-color: var(--color-border-subtle);
99
100
  cursor: not-allowed;
101
+ /* Disabled is fully colour-encoded above; cancel foundations' global
102
+ :disabled opacity so the control is not double-dimmed. */
103
+ opacity: 1;
100
104
  }
101
105
 
102
106
  .ct-check__input:focus-visible,
@@ -118,3 +122,32 @@
118
122
  border-inline-end: 0;
119
123
  transform: scale(1);
120
124
  }
125
+
126
+ @media (prefers-reduced-motion: reduce) {
127
+ .ct-check__input,
128
+ .ct-radio__input,
129
+ .ct-check__input::before,
130
+ .ct-radio__input::before {
131
+ transition: none;
132
+ }
133
+ }
134
+
135
+ @media (forced-colors: active) {
136
+ .ct-check__input:checked,
137
+ .ct-check__input:indeterminate,
138
+ .ct-radio__input:checked {
139
+ border-color: ButtonText;
140
+ }
141
+
142
+ /* The tick and indeterminate dash are border-drawn and survive on their
143
+ own; the radio dot is background-drawn and would be forced to Canvas. */
144
+ .ct-radio__input:checked::before {
145
+ background: SelectedItem;
146
+ forced-color-adjust: none;
147
+ }
148
+
149
+ .ct-check__input:disabled,
150
+ .ct-radio__input:disabled {
151
+ border-color: GrayText;
152
+ }
153
+ }
@@ -35,7 +35,9 @@
35
35
  --ct-chip-padding-x: var(--space-2);
36
36
  --ct-chip-font-size: var(--font-size-xs);
37
37
  --ct-chip-line-height: var(--line-height-xs);
38
- --ct-chip-remove-size: var(--icon-sm);
38
+ /* WCAG 2.2 SC 2.5.8: the remove button stays at least 24px in every
39
+ pointer context, so the small chip cannot shrink it below that. */
40
+ --ct-chip-remove-size: var(--icon-lg);
39
41
  --ct-chip-gap: var(--space-1);
40
42
  }
41
43
 
@@ -340,12 +342,23 @@
340
342
  outline-offset: 2px;
341
343
  }
342
344
 
343
- /* ── Touch-target minimums (WCAG 2.5.8: 44×44px) ──────────── */
345
+ /* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ─── */
344
346
 
345
347
  @media (pointer: coarse) {
346
348
  .ct-chip {
347
349
  min-height: 44px;
348
350
  }
351
+
352
+ /* Hit area may extend past the chip edge, mirroring checkbox.css. */
353
+ .ct-chip__remove {
354
+ position: relative;
355
+ }
356
+
357
+ .ct-chip__remove::before {
358
+ content: '';
359
+ position: absolute;
360
+ inset: calc((var(--ct-chip-remove-size) - 44px) / 2);
361
+ }
349
362
  }
350
363
 
351
364
  /* ── Reduced motion ──────────────────────────────────────────── */
@@ -361,3 +374,18 @@
361
374
  transform: none;
362
375
  }
363
376
  }
377
+
378
+ /* ── Forced colors ───────────────────────────────────────────── */
379
+
380
+ @media (forced-colors: active) {
381
+ /* Mask-drawn glyphs and the status dot paint via background-color,
382
+ which forced colors resets — repaint them with system ink. The
383
+ selected checkmark is border-drawn and survives on its own. */
384
+ .ct-chip__icon--info,
385
+ .ct-chip__icon--success,
386
+ .ct-chip__icon--warning,
387
+ .ct-chip__icon--danger,
388
+ .ct-chip__dot {
389
+ background-color: CanvasText;
390
+ }
391
+ }
@@ -374,6 +374,27 @@
374
374
  border-color: transparent;
375
375
  }
376
376
 
377
+ /* Replacement indicator for the removed input outline (mirrors the --multi pattern) */
378
+ .ct-combobox--inline:focus-within {
379
+ outline: 2px solid var(--color-focus-ring);
380
+ outline-offset: 2px;
381
+ }
382
+
383
+ /* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ── */
384
+
385
+ @media (pointer: coarse) {
386
+ .ct-combobox__option {
387
+ min-block-size: 44px;
388
+ }
389
+
390
+ /* Hit area extends past the visual 24px trigger, mirroring checkbox.css. */
391
+ .ct-combobox__trigger::before {
392
+ content: '';
393
+ position: absolute;
394
+ inset: calc((var(--icon-lg) - 44px) / 2);
395
+ }
396
+ }
397
+
377
398
  /* ── Reduced motion ──────────────────────────────────────────── */
378
399
 
379
400
  @media (prefers-reduced-motion: reduce) {
@@ -1,6 +1,8 @@
1
1
  /* ─── Datepicker ────────────────────────────────────────────────── */
2
2
 
3
3
  .ct-datepicker {
4
+ --ct-datepicker-selected-surface: color-mix(in srgb, var(--color-brand-primary) 12%, transparent);
5
+
4
6
  position: relative;
5
7
  display: inline-block;
6
8
  }
@@ -77,10 +79,22 @@
77
79
  justify-items: center;
78
80
  }
79
81
 
82
+ /* Rows keep grid placement on the shared 7-column grid. display: contents
83
+ once dropped element semantics in older engines; current Chromium/Firefox/
84
+ Safari expose role="row" on display: contents elements correctly. */
80
85
  .ct-datepicker__row {
81
86
  display: contents;
82
87
  }
83
88
 
89
+ /* Optional gridcell wrapper (APG date-picker dialog markup): the cell is the
90
+ grid item and carries role="gridcell" + aria-selected; the day button
91
+ inside keeps its native button role. */
92
+ .ct-datepicker__cell {
93
+ display: flex;
94
+ width: 100%;
95
+ justify-content: center;
96
+ }
97
+
84
98
  .ct-datepicker__weekday {
85
99
  font-size: var(--font-size-xs);
86
100
  color: var(--color-text-muted);
@@ -113,9 +127,10 @@
113
127
  outline-offset: 2px;
114
128
  }
115
129
 
116
- .ct-datepicker__day[aria-selected='true'] {
130
+ .ct-datepicker__day[aria-selected='true'],
131
+ .ct-datepicker__cell[aria-selected='true'] .ct-datepicker__day {
117
132
  background: var(--color-brand-primary);
118
- color: var(--color-text-inverse);
133
+ color: var(--color-text-on-primary);
119
134
  }
120
135
 
121
136
  .ct-datepicker__day[data-outside='true'] {
@@ -150,19 +165,19 @@
150
165
  /* ── Range selection ────────────────────────────────────────────── */
151
166
 
152
167
  .ct-datepicker__day[data-in-range] {
153
- background: var(--color-brand-primary-surface, color-mix(in srgb, var(--color-brand-primary) 12%, transparent));
168
+ background: var(--ct-datepicker-selected-surface);
154
169
  border-radius: 0;
155
170
  }
156
171
 
157
172
  .ct-datepicker__day[data-range-start] {
158
173
  background: var(--color-brand-primary);
159
- color: var(--color-text-inverse);
174
+ color: var(--color-text-on-primary);
160
175
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
161
176
  }
162
177
 
163
178
  .ct-datepicker__day[data-range-end] {
164
179
  background: var(--color-brand-primary);
165
- color: var(--color-text-inverse);
180
+ color: var(--color-text-on-primary);
166
181
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
167
182
  }
168
183
 
@@ -170,6 +185,38 @@
170
185
  border-radius: var(--radius-sm);
171
186
  }
172
187
 
188
+ /* ── Coarse pointers ────────────────────────────────────────────── */
189
+
190
+ @media (pointer: coarse) {
191
+ /* Day cells may compress inside the min(90vw, …) popover; tap size
192
+ wins over the shared column width (44×44 policy). */
193
+ .ct-datepicker__day {
194
+ min-width: var(--control-height-calendar-day);
195
+ min-height: var(--control-height-calendar-day);
196
+ }
197
+ }
198
+
199
+ /* ── Forced colors ──────────────────────────────────────────────── */
200
+
201
+ @media (forced-colors: active) {
202
+ /* Selection fills are background-only and would be stripped to Canvas —
203
+ re-encode with system selection colors (data-today's border and the
204
+ highlight outline survive on their own). */
205
+ .ct-datepicker__day[aria-selected='true'],
206
+ .ct-datepicker__cell[aria-selected='true'] .ct-datepicker__day,
207
+ .ct-datepicker__cell[aria-selected='true'] .ct-datepicker__month,
208
+ .ct-datepicker__cell[aria-selected='true'] .ct-datepicker__year,
209
+ .ct-datepicker__month[aria-selected='true'],
210
+ .ct-datepicker__year[aria-selected='true'],
211
+ .ct-datepicker__day[data-in-range],
212
+ .ct-datepicker__day[data-range-start],
213
+ .ct-datepicker__day[data-range-end] {
214
+ background: SelectedItem;
215
+ color: SelectedItemText;
216
+ forced-color-adjust: none;
217
+ }
218
+ }
219
+
173
220
  /* ── Month grid (3×4) ───────────────────────────────────────────── */
174
221
 
175
222
  .ct-datepicker__month-grid {
@@ -203,9 +250,10 @@
203
250
  outline-offset: 2px;
204
251
  }
205
252
 
206
- .ct-datepicker__month[aria-selected='true'] {
253
+ .ct-datepicker__month[aria-selected='true'],
254
+ .ct-datepicker__cell[aria-selected='true'] .ct-datepicker__month {
207
255
  background: var(--color-brand-primary);
208
- color: var(--color-text-inverse);
256
+ color: var(--color-text-on-primary);
209
257
  }
210
258
 
211
259
  .ct-datepicker__month[data-highlighted] {
@@ -252,9 +300,10 @@
252
300
  outline-offset: 2px;
253
301
  }
254
302
 
255
- .ct-datepicker__year[aria-selected='true'] {
303
+ .ct-datepicker__year[aria-selected='true'],
304
+ .ct-datepicker__cell[aria-selected='true'] .ct-datepicker__year {
256
305
  background: var(--color-brand-primary);
257
- color: var(--color-text-inverse);
306
+ color: var(--color-text-on-primary);
258
307
  }
259
308
 
260
309
  .ct-datepicker__year[data-highlighted] {
@@ -0,0 +1,277 @@
1
+ /* ============================================================
2
+ Datum — the orange reference-line grammar.
3
+
4
+ Construct's signature is one orange reference line ("the datum")
5
+ that marks what is focused / active / current. These primitives
6
+ promote that signature from focus rings and the intro page into
7
+ reusable structural vocabulary every surface can speak.
8
+
9
+ Accessibility contract (must hold across all built-in themes):
10
+ · orange as TEXT → never; use --color-text-* neutrals (orange fails
11
+ 4.5:1 as small text on several theme surfaces)
12
+ · orange as FILL vs muted tracks → --color-brand-accent-strong
13
+ (validated ≥3:1 against bg.muted/canvas/surface per theme)
14
+ · orange as EDGE/RULE (decorative, on canvas/surface) → --color-brand-accent
15
+ ============================================================ */
16
+
17
+ /* ── Eyebrow ──
18
+ A mono kicker with a leading datum tick. Sits above a heading to
19
+ label a section in the engineered "data annotation" voice. */
20
+ .ct-eyebrow {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ gap: var(--space-4);
24
+ margin: 0 0 var(--space-5);
25
+ font-family: var(--font-family-mono);
26
+ font-size: var(--font-size-xs);
27
+ font-weight: var(--font-weight-semibold);
28
+ text-transform: uppercase;
29
+ letter-spacing: 0.16em;
30
+ /* Text stays a theme-safe neutral (AA on every surface in all three
31
+ themes); the orange datum lives in the non-text tick below. Orange
32
+ as small text fails contrast on dark/elevated and high-contrast. */
33
+ color: var(--color-text-secondary);
34
+ }
35
+
36
+ .ct-eyebrow::before {
37
+ content: '';
38
+ width: var(--component-datum-tick-width);
39
+ height: var(--component-datum-tick-height);
40
+ border-radius: var(--radius-xs);
41
+ background: var(--color-brand-accent);
42
+ }
43
+
44
+ /* ── Numbered eyebrow ──
45
+ Sheet numbers, like an engineering drawing set: `--numbered` inserts
46
+ an auto-incremented, zero-padded counter between tick and label.
47
+ Scope numbering with a `.ct-eyebrow-set` ancestor (or any element
48
+ that declares `counter-reset: ct-eyebrow`). */
49
+ .ct-eyebrow-set {
50
+ counter-reset: ct-eyebrow;
51
+ }
52
+
53
+ .ct-eyebrow--numbered {
54
+ counter-increment: ct-eyebrow;
55
+ }
56
+
57
+ .ct-eyebrow--numbered::before {
58
+ order: -2;
59
+ }
60
+
61
+ .ct-eyebrow--numbered::after {
62
+ content: counter(ct-eyebrow, decimal-leading-zero);
63
+ order: -1;
64
+ color: var(--color-text-muted);
65
+ font-variant-numeric: tabular-nums;
66
+ }
67
+
68
+ /* ── Datum scale ──
69
+ A measured reference line: a hairline baseline with hanging ruler
70
+ ticks (minor every 12px, major every 60px) and an orange "origin"
71
+ segment marking zero. Decorative — mark it role="presentation". */
72
+ .ct-datum-scale {
73
+ --ct-datum-scale-accent: var(--color-brand-accent);
74
+ --ct-datum-scale-origin: var(--component-datum-origin-length);
75
+
76
+ height: var(--component-datum-scale-height);
77
+ background:
78
+ /* orange origin segment — the datum's zero */
79
+ linear-gradient(var(--ct-datum-scale-accent), var(--ct-datum-scale-accent)) 0 0 / var(--ct-datum-scale-origin) 2px no-repeat,
80
+ /* full-width baseline hairline */
81
+ linear-gradient(var(--color-border-strong), var(--color-border-strong)) 0 0 / 100% 1px no-repeat,
82
+ /* major ticks */
83
+ repeating-linear-gradient(90deg, var(--color-border-strong) 0 1px, transparent 1px var(--component-datum-scale-major-step)) 0 0 / 100% 8px no-repeat,
84
+ /* minor ticks */
85
+ repeating-linear-gradient(90deg, var(--color-border-default) 0 1px, transparent 1px var(--component-datum-scale-minor-step)) 0 0 / 100% 5px no-repeat;
86
+ }
87
+
88
+ /* RTL: the origin and tick phase anchor to the inline start. */
89
+ .ct-datum-scale:is([dir='rtl'] *, :dir(rtl)) {
90
+ background:
91
+ linear-gradient(var(--ct-datum-scale-accent), var(--ct-datum-scale-accent)) 100% 0 / var(--ct-datum-scale-origin) 2px no-repeat,
92
+ linear-gradient(var(--color-border-strong), var(--color-border-strong)) 0 0 / 100% 1px no-repeat,
93
+ repeating-linear-gradient(270deg, var(--color-border-strong) 0 1px, transparent 1px var(--component-datum-scale-major-step)) 0 0 / 100% 8px no-repeat,
94
+ repeating-linear-gradient(270deg, var(--color-border-default) 0 1px, transparent 1px var(--component-datum-scale-minor-step)) 0 0 / 100% 5px no-repeat;
95
+ }
96
+
97
+ /* ── Progress variant ──
98
+ The origin segment becomes meaning-bearing: its length reports
99
+ progress. Set `--ct-datum-scale-progress` (a percentage) on the
100
+ element and give it `role="progressbar"` with the matching
101
+ `aria-valuenow/min/max`. Meaning-bearing fill uses accent-strong,
102
+ validated ≥3:1 against muted/canvas/surface tracks per theme. */
103
+ .ct-datum-scale--progress {
104
+ --ct-datum-scale-accent: var(--color-brand-accent-strong);
105
+ --ct-datum-scale-origin: var(--ct-datum-scale-progress, 0%);
106
+
107
+ transition: background-size var(--duration-normal) var(--easing-standard);
108
+ }
109
+
110
+ /* ── Spec datasheet ──
111
+ Engineering datasheet for key figures — hairline-gridded cells
112
+ instead of free-floating big numbers. Mark the lead cell with
113
+ `--origin` to carry the datum. */
114
+ :is(.ct-metrics, .ct-specs) {
115
+ --ct-metric-bg: var(--component-metric-background);
116
+ --ct-metric-border: var(--component-metric-border);
117
+ --ct-metric-radius: var(--component-metric-radius);
118
+ --ct-metric-padding: var(--component-metric-padding);
119
+ --ct-metric-value-font-size: var(--component-metric-value-font-size);
120
+ --ct-metric-label-font-size: var(--component-metric-label-font-size);
121
+
122
+ display: grid;
123
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
124
+ gap: var(--border-thin);
125
+ border: var(--border-thin) solid var(--ct-metric-border);
126
+ border-radius: var(--ct-metric-radius);
127
+ background: var(--ct-metric-border);
128
+ overflow: hidden;
129
+ }
130
+
131
+ /* Compatibility profile: retain the original datasheet scale while the
132
+ canonical metric pattern uses the denser component-token defaults. */
133
+ .ct-specs {
134
+ --ct-metric-radius: var(--radius-lg);
135
+ --ct-metric-padding: var(--space-7);
136
+ --ct-metric-value-font-size: var(--font-size-4xl);
137
+ --ct-metric-label-font-size: var(--font-size-xs);
138
+ }
139
+
140
+ :is(.ct-metric, .ct-spec) {
141
+ --ct-metric-accent: transparent;
142
+ --ct-metric-status-color: var(--color-text-secondary);
143
+
144
+ position: relative;
145
+ min-width: 0;
146
+ margin: 0;
147
+ padding: var(--ct-metric-padding);
148
+ background: var(--ct-metric-bg);
149
+ }
150
+
151
+ .ct-metric {
152
+ display: grid;
153
+ }
154
+
155
+ :is(.ct-metric--active, .ct-metric[data-state='active'], .ct-spec--origin) {
156
+ --ct-metric-accent: var(--color-brand-accent);
157
+
158
+ box-shadow: inset 0 var(--component-datum-edge-width) 0 0 var(--ct-metric-accent);
159
+ }
160
+
161
+ :is(.ct-metric__value, .ct-spec__value) {
162
+ margin: 0;
163
+ font-family: var(--font-family-brand);
164
+ font-size: var(--ct-metric-value-font-size);
165
+ font-weight: var(--font-weight-extrabold);
166
+ line-height: 1;
167
+ color: var(--color-text-primary);
168
+ font-variant-numeric: tabular-nums lining-nums;
169
+ }
170
+
171
+ .ct-metric__value {
172
+ order: 1;
173
+ }
174
+
175
+ :is(.ct-metric__value, .ct-spec__value) small {
176
+ font-size: var(--font-size-xl);
177
+ font-weight: var(--font-weight-bold);
178
+ }
179
+
180
+ :is(.ct-metric__label, .ct-spec__label) {
181
+ display: block;
182
+ margin-top: var(--space-4);
183
+ font-family: var(--font-family-mono);
184
+ font-size: var(--ct-metric-label-font-size);
185
+ font-weight: var(--font-weight-semibold);
186
+ text-transform: uppercase;
187
+ letter-spacing: 0.14em;
188
+ color: var(--color-text-secondary);
189
+ }
190
+
191
+ .ct-metric__label {
192
+ order: 2;
193
+ }
194
+
195
+ .ct-metric__meta {
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: space-between;
199
+ gap: var(--space-3);
200
+ margin-top: var(--space-4);
201
+ color: var(--color-text-secondary);
202
+ font-size: var(--font-size-xs);
203
+ font-family: var(--font-family-body);
204
+ font-weight: var(--font-weight-regular);
205
+ line-height: var(--line-height-xs);
206
+ text-transform: none;
207
+ letter-spacing: normal;
208
+ order: 3;
209
+ }
210
+
211
+ .ct-metric__status {
212
+ color: var(--ct-metric-status-color);
213
+ font-weight: var(--font-weight-semibold);
214
+ }
215
+
216
+ /* Status wording stays neutral and AA-safe on every metric surface;
217
+ semantic colour is carried by the non-text datum edge. */
218
+
219
+ .ct-metric[data-status='success'] {
220
+ --ct-metric-accent: var(--color-state-success);
221
+ --ct-metric-status-color: var(--color-text-primary);
222
+ }
223
+
224
+ .ct-metric[data-status='info'] {
225
+ --ct-metric-accent: var(--color-state-info);
226
+ --ct-metric-status-color: var(--color-text-primary);
227
+ }
228
+
229
+ .ct-metric[data-status='warning'] {
230
+ --ct-metric-accent: var(--color-state-warning);
231
+ --ct-metric-status-color: var(--color-text-primary);
232
+ }
233
+
234
+ .ct-metric:is([data-status='danger'], [data-status='error'], [data-state='error']) {
235
+ --ct-metric-accent: var(--color-state-danger);
236
+ --ct-metric-status-color: var(--color-text-primary);
237
+ }
238
+
239
+ .ct-metric[data-status],
240
+ .ct-metric[data-state='error'] {
241
+ box-shadow: inset var(--component-datum-edge-width) 0 0 0 var(--ct-metric-accent);
242
+ }
243
+
244
+ .ct-metric:is([aria-busy='true'], [data-state='loading']) {
245
+ cursor: wait;
246
+ }
247
+
248
+ .ct-metric:is([aria-disabled='true'], [data-state='disabled']) {
249
+ opacity: var(--opacity-disabled);
250
+ }
251
+
252
+ /* ── Datum frame ──
253
+ The corner registration mark generalised beyond cards: an opt-in
254
+ "exhibit" treatment for figures, code samples, and media. Decorative;
255
+ forced colors keeps the borders via CanvasText automatically. */
256
+ .ct-datum-frame {
257
+ position: relative;
258
+ }
259
+
260
+ .ct-datum-frame::before {
261
+ content: '';
262
+ position: absolute;
263
+ inset-block-start: 0;
264
+ inset-inline-start: 0;
265
+ width: var(--component-datum-mark-size);
266
+ height: var(--component-datum-mark-size);
267
+ border-block-start: var(--border-medium) solid var(--color-brand-accent);
268
+ border-inline-start: var(--border-medium) solid var(--color-brand-accent);
269
+ border-start-start-radius: var(--radius-lg);
270
+ pointer-events: none;
271
+ }
272
+
273
+ @media (forced-colors: active) {
274
+ :is(.ct-metric--active, .ct-metric[data-state='active'], .ct-metric[data-status], .ct-metric[data-state='error'], .ct-spec--origin) {
275
+ border-block-start: var(--border-medium) solid currentColor;
276
+ }
277
+ }
@@ -146,7 +146,7 @@
146
146
  /* ── Danger item ─────────────────────────────────────────────────── */
147
147
 
148
148
  .ct-dropdown__item--danger {
149
- color: var(--color-state-danger);
149
+ color: var(--color-state-danger-text);
150
150
  }
151
151
 
152
152
  @media (hover: hover) {
@@ -158,11 +158,11 @@
158
158
  .ct-dropdown__item--danger:focus-visible,
159
159
  .ct-dropdown__item--danger[data-highlighted] {
160
160
  background: var(--color-state-danger-surface);
161
- outline-color: var(--color-state-danger);
161
+ outline-color: var(--color-state-danger-text);
162
162
  }
163
163
 
164
164
  .ct-dropdown__item--danger .ct-dropdown__item-icon {
165
- color: var(--color-state-danger);
165
+ color: var(--color-state-danger-text);
166
166
  }
167
167
 
168
168
  .ct-dropdown__item--danger .ct-dropdown__item-shortcut {
@@ -389,6 +389,15 @@
389
389
  --ct-dropdown-item-gap: var(--space-4);
390
390
  }
391
391
 
392
+ /* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ────── */
393
+
394
+ @media (pointer: coarse) {
395
+ .ct-dropdown__item,
396
+ .ct-dropdown__sub-trigger {
397
+ min-block-size: 44px;
398
+ }
399
+ }
400
+
392
401
  /* ── Reduced motion ──────────────────────────────────────────────── */
393
402
 
394
403
  @media (prefers-reduced-motion: reduce) {
@@ -399,3 +408,14 @@
399
408
  transition: none;
400
409
  }
401
410
  }
411
+
412
+ /* ── Forced colors ───────────────────────────────────────────────── */
413
+
414
+ @media (forced-colors: active) {
415
+ /* The radio-item dot is background-drawn and must survive forced
416
+ backgrounds. */
417
+ .ct-dropdown__item[aria-checked='true'] .ct-dropdown__item-radio::before {
418
+ background: SelectedItem;
419
+ forced-color-adjust: none;
420
+ }
421
+ }
@@ -16,12 +16,14 @@
16
16
 
17
17
  .ct-field__error {
18
18
  font-size: var(--font-size-sm);
19
- color: var(--color-state-danger);
19
+ color: var(--color-state-danger-text);
20
20
  }
21
21
 
22
22
  .ct-field__label--required::after {
23
- content: ' *';
24
- color: var(--color-state-danger);
23
+ /* Alt-text syntax hides the decorative asterisk from assistive tech;
24
+ the `required` attribute is the programmatic signal. */
25
+ content: ' *' / '';
26
+ color: var(--color-state-danger-text);
25
27
  }
26
28
 
27
29
  .ct-field__counter {
@@ -31,7 +33,7 @@
31
33
  }
32
34
 
33
35
  .ct-field__counter--limit {
34
- color: var(--color-state-danger);
36
+ color: var(--color-state-danger-text);
35
37
  }
36
38
 
37
39
  .ct-field__row {
@@ -106,7 +106,7 @@
106
106
  }
107
107
 
108
108
  .ct-file-upload__error {
109
- color: var(--color-state-danger);
109
+ color: var(--color-state-danger-text);
110
110
  font-size: var(--font-size-sm);
111
111
  }
112
112