@helixui/tokens 3.2.0-next.98 → 3.3.0-next.111

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/index.d.ts CHANGED
@@ -403,6 +403,14 @@ export declare const tokens: {
403
403
  value: string;
404
404
  description: string;
405
405
  };
406
+ "on-dark-overlay-default": {
407
+ value: string;
408
+ description: string;
409
+ };
410
+ "on-dark-overlay-subtle": {
411
+ value: string;
412
+ description: string;
413
+ };
406
414
  };
407
415
  border: {
408
416
  default: {
@@ -422,14 +430,6 @@ export declare const tokens: {
422
430
  value: string;
423
431
  description: string;
424
432
  };
425
- "on-dark-default": {
426
- value: string;
427
- description: string;
428
- };
429
- "on-dark-subtle": {
430
- value: string;
431
- description: string;
432
- };
433
433
  };
434
434
  "focus-ring": {
435
435
  value: string;
@@ -1263,6 +1263,14 @@ export declare const tokens: {
1263
1263
  overlay: {
1264
1264
  value: string;
1265
1265
  };
1266
+ "on-dark-overlay-default": {
1267
+ value: string;
1268
+ description: string;
1269
+ };
1270
+ "on-dark-overlay-subtle": {
1271
+ value: string;
1272
+ description: string;
1273
+ };
1266
1274
  };
1267
1275
  border: {
1268
1276
  default: {
@@ -1282,14 +1290,6 @@ export declare const tokens: {
1282
1290
  value: string;
1283
1291
  description: string;
1284
1292
  };
1285
- "on-dark-default": {
1286
- value: string;
1287
- description: string;
1288
- };
1289
- "on-dark-subtle": {
1290
- value: string;
1291
- description: string;
1292
- };
1293
1293
  };
1294
1294
  "focus-ring": {
1295
1295
  value: string;
@@ -1555,6 +1555,14 @@ export declare const tokens: {
1555
1555
  value: string;
1556
1556
  description: string;
1557
1557
  };
1558
+ "on-dark-overlay-default": {
1559
+ value: string;
1560
+ description: string;
1561
+ };
1562
+ "on-dark-overlay-subtle": {
1563
+ value: string;
1564
+ description: string;
1565
+ };
1558
1566
  };
1559
1567
  border: {
1560
1568
  default: {
@@ -1575,14 +1583,6 @@ export declare const tokens: {
1575
1583
  value: string;
1576
1584
  description: string;
1577
1585
  };
1578
- "on-dark-default": {
1579
- value: string;
1580
- description: string;
1581
- };
1582
- "on-dark-subtle": {
1583
- value: string;
1584
- description: string;
1585
- };
1586
1586
  };
1587
1587
  "focus-ring": {
1588
1588
  value: string;
package/dist/tokens.css CHANGED
@@ -119,13 +119,13 @@
119
119
  --hx-color-surface-warning-strong: var(--hx-color-warning-500);
120
120
  --hx-color-surface-danger-strong: var(--hx-color-error-600);
121
121
  --hx-color-surface-info-strong: var(--hx-color-primary-600);
122
+ --hx-color-surface-on-dark-overlay-default: var(--hx-overlay-white-30);
123
+ --hx-color-surface-on-dark-overlay-subtle: var(--hx-overlay-white-10);
122
124
  --hx-color-border-default: var(--hx-color-neutral-200);
123
125
  --hx-color-border-subtle: var(--hx-color-neutral-100);
124
126
  --hx-color-border-strong: var(--hx-color-neutral-500);
125
127
  --hx-color-border-focus: var(--hx-color-primary-500);
126
128
  --hx-color-border-on-dark-strong: var(--hx-overlay-white-70);
127
- --hx-color-border-on-dark-default: var(--hx-overlay-white-30);
128
- --hx-color-border-on-dark-subtle: var(--hx-overlay-white-10);
129
129
  --hx-color-focus-ring: var(--hx-color-primary-600);
130
130
  --hx-color-selection-bg: var(--hx-color-primary-200);
131
131
  --hx-color-selection-color: var(--hx-color-neutral-900);
@@ -368,13 +368,13 @@
368
368
  --hx-color-surface-sunken: var(--hx-color-neutral-950);
369
369
  --hx-color-surface-inverse: var(--hx-color-neutral-100);
370
370
  --hx-color-surface-overlay: rgba(0, 0, 0, 0.85);
371
+ --hx-color-surface-on-dark-overlay-default: var(--hx-overlay-black-30);
372
+ --hx-color-surface-on-dark-overlay-subtle: var(--hx-overlay-black-10);
371
373
  --hx-color-border-default: var(--hx-color-neutral-700);
372
374
  --hx-color-border-subtle: var(--hx-color-neutral-800);
373
375
  --hx-color-border-strong: var(--hx-color-neutral-400);
374
376
  --hx-color-border-focus: var(--hx-color-primary-400);
375
377
  --hx-color-border-on-dark-strong: var(--hx-overlay-black-50);
376
- --hx-color-border-on-dark-default: var(--hx-overlay-black-30);
377
- --hx-color-border-on-dark-subtle: var(--hx-overlay-black-10);
378
378
  --hx-color-focus-ring: var(--hx-color-primary-400);
379
379
  --hx-color-selection-bg: var(--hx-color-primary-800);
380
380
  --hx-color-selection-color: var(--hx-color-neutral-100);
@@ -414,13 +414,13 @@
414
414
  --hx-color-surface-sunken: var(--hx-color-neutral-950);
415
415
  --hx-color-surface-inverse: var(--hx-color-neutral-100);
416
416
  --hx-color-surface-overlay: rgba(0, 0, 0, 0.85);
417
+ --hx-color-surface-on-dark-overlay-default: var(--hx-overlay-black-30);
418
+ --hx-color-surface-on-dark-overlay-subtle: var(--hx-overlay-black-10);
417
419
  --hx-color-border-default: var(--hx-color-neutral-700);
418
420
  --hx-color-border-subtle: var(--hx-color-neutral-800);
419
421
  --hx-color-border-strong: var(--hx-color-neutral-400);
420
422
  --hx-color-border-focus: var(--hx-color-primary-400);
421
423
  --hx-color-border-on-dark-strong: var(--hx-overlay-black-50);
422
- --hx-color-border-on-dark-default: var(--hx-overlay-black-30);
423
- --hx-color-border-on-dark-subtle: var(--hx-overlay-black-10);
424
424
  --hx-color-focus-ring: var(--hx-color-primary-400);
425
425
  --hx-color-selection-bg: var(--hx-color-primary-800);
426
426
  --hx-color-selection-color: var(--hx-color-neutral-100);
@@ -486,13 +486,13 @@
486
486
  --hx-color-surface-warning-strong: var(--hx-color-warning-500);
487
487
  --hx-color-surface-danger-strong: var(--hx-color-error-500);
488
488
  --hx-color-surface-info-strong: var(--hx-color-primary-500);
489
+ --hx-color-surface-on-dark-overlay-default: #FFFFFF;
490
+ --hx-color-surface-on-dark-overlay-subtle: #C0C0C0;
489
491
  --hx-color-border-default: #FFFFFF;
490
492
  --hx-color-border-subtle: #C0C0C0;
491
493
  --hx-color-border-strong: #FFFFFF;
492
494
  --hx-color-border-focus: #FFFF00;
493
495
  --hx-color-border-on-dark-strong: #FFFFFF;
494
- --hx-color-border-on-dark-default: #FFFFFF;
495
- --hx-color-border-on-dark-subtle: #C0C0C0;
496
496
  --hx-color-focus-ring: #FFFF00;
497
497
  --hx-color-selection-bg: #1AEBFF;
498
498
  --hx-color-selection-color: #000000;
@@ -562,13 +562,13 @@
562
562
  --hx-color-surface-warning-strong: var(--hx-color-warning-500);
563
563
  --hx-color-surface-danger-strong: var(--hx-color-error-500);
564
564
  --hx-color-surface-info-strong: var(--hx-color-primary-500);
565
+ --hx-color-surface-on-dark-overlay-default: #FFFFFF;
566
+ --hx-color-surface-on-dark-overlay-subtle: #C0C0C0;
565
567
  --hx-color-border-default: #FFFFFF;
566
568
  --hx-color-border-subtle: #C0C0C0;
567
569
  --hx-color-border-strong: #FFFFFF;
568
570
  --hx-color-border-focus: #FFFF00;
569
571
  --hx-color-border-on-dark-strong: #FFFFFF;
570
- --hx-color-border-on-dark-default: #FFFFFF;
571
- --hx-color-border-on-dark-subtle: #C0C0C0;
572
572
  --hx-color-focus-ring: #FFFF00;
573
573
  --hx-color-selection-bg: #1AEBFF;
574
574
  --hx-color-selection-color: #000000;
package/dist/tokens.json CHANGED
@@ -195,6 +195,14 @@
195
195
  "info-strong": {
196
196
  "value": "var(--hx-color-primary-600)",
197
197
  "description": "Emphasis info surface for high-prominence components (e.g. hx-toast--info). Pairs with text.on-primary-strong (neutral-0, no dark-mode flip) for AA — neutral-0 on primary-600 (#0F7078) = 5.82:1. Do NOT pair with text.inverse: surface.info-strong itself does not dark-mode-flip (the value stays primary-600 in light and dark), but text.inverse does flip to neutral-900 in dark mode — pairing the two produces dark text on a dark fill (sub-AA). Tracks action.primary.bg-hover by value but exposed under surface.* so toasts/banners consume a surface semantic, not an action-state semantic."
198
+ },
199
+ "on-dark-overlay-default": {
200
+ "value": "var(--hx-overlay-white-30)",
201
+ "description": "Translucent fill on dark surfaces — the resting/hover background for inverted secondary, ghost, and tertiary buttons painted on surface.inverse (which is dark in light mode, light in dark mode). Renamed from border.on-dark-default in 3.2.2: it was always used as a fill, never a border, and its 30% alpha cannot honour WCAG 1.4.11 3:1 against either neutral-900 (2.07:1) or neutral-0 (1.30:1) — borders need higher alpha. As a fill, contrast is irrelevant: it tints, it doesn't delimit."
202
+ },
203
+ "on-dark-overlay-subtle": {
204
+ "value": "var(--hx-overlay-white-10)",
205
+ "description": "Lowest-emphasis translucent fill on dark surfaces — the resting background for inverted-tertiary buttons and inverted hover affordances on dark side-nav. Renamed from border.on-dark-subtle in 3.2.2 for the same reason as on-dark-overlay-default: it is a fill, not a border."
198
206
  }
199
207
  },
200
208
  "border": {
@@ -207,20 +215,12 @@
207
215
  "focus": { "value": "var(--hx-color-primary-500)" },
208
216
  "on-dark-strong": {
209
217
  "value": "var(--hx-overlay-white-70)",
210
- "description": "Strong border treatment on dark surfaces (e.g. inverted button outlines on a dark page). Routes overlay-white-70 through the semantic tier so component-level inverted-border rules don't bind to raw overlay primitives. Added in 3.2.1 alongside the action.* layer for the token-cascade remediation."
211
- },
212
- "on-dark-default": {
213
- "value": "var(--hx-overlay-white-30)",
214
- "description": "Default border treatment on dark surfaces (inverted secondary/ghost outlines, divider borders inside dark panels). Overlay-mediated so the border tints itself against whatever sits underneath rather than fighting it with a hard color."
215
- },
216
- "on-dark-subtle": {
217
- "value": "var(--hx-overlay-white-10)",
218
- "description": "Subtle border treatment on dark surfaces (low-emphasis dividers inside dark panels)."
218
+ "description": "Strong border treatment on dark surfaces (e.g. inverted button outlines on a dark page). overlay-white-70 on neutral-900 5.0:1 clears WCAG 1.4.11 3:1 floor for non-text UI. Added in 3.2.1 alongside the action.* layer for the token-cascade remediation; only the strong tier survived the 3.2.2 audit because the lower-alpha siblings (overlay-white-30/10) cannot honour a border-contract — they were renamed to surface.on-dark-overlay-* (which is what they always were: translucent fills). DEPRECATED ALIAS NOTE: --hx-color-border-on-dark-{default,subtle} shipped in 3.2.0/3.2.1 and were renamed to surface.on-dark-overlay-{default,subtle} in 3.2.2. The deprecated names are NOT emitted at :root in dist/tokens.css. Two `:root`-alias variants were considered and rejected for the same root cause — both shadow host-scoped canonical-name overrides at every component consume site. Variant A (`var()` alias): `:root { --hx-color-border-on-dark-default: var(--hx-color-surface-on-dark-overlay-default); }` — the inner var() resolves at :root's computed-value time (CSS Custom Properties §3) and inherits as an opaque resolved value to every descendant. Variant B (concrete-value alias, light + dark): `:root { --hx-color-border-on-dark-default: rgba(255,255,255,0.30); } .dark { --hx-color-border-on-dark-default: <dark-mode value>; }` — no inner var() to substitute, but inheritance still delivers a non-empty value to every descendant. Both variants cause the same failure at the consume site: components read the deprecated name FIRST (`var(--hx-color-border-on-dark-*, var(--hx-color-surface-on-dark-overlay-*, …))`), so as long as the deprecated name resolves to ANY value above the host, the var() chain never falls through to the host's canonical override. Variant B has additional cost: it bakes a literal value into :root, breaking the primitive-chain (a consumer who overrides --hx-overlay-white-30 at :root would no longer see that change reflected in --hx-color-border-on-dark-default). Backwards compatibility is preserved at the consume sites instead: every component rule that paints with surface.on-dark-overlay-* reads the both-name fallback chain so a consumer override on either name reaches paint. The dark-mode-resolution.test.ts override-path tests pin both directions (line 209-227 covers deprecated-name + canonical-name overrides); both alias variants would fail the canonical-override test. The deliberate trade-off: downstream code that reads the deprecated names DIRECTLY (app-level CSS using `var(--hx-color-border-on-dark-default)` outside an hx-* component, or `getComputedStyle(el).getPropertyValue('--hx-color-border-on-dark-default')`) will resolve to an empty/invalid value in 3.2.2 — those readers must migrate to the canonical `--hx-color-surface-on-dark-overlay-{default,subtle}` (or set the deprecated name explicitly on their own scope). The chosen design breaks an undocumented direct-reader path explicitly, rather than breaking a documented host-override path silently. Removal scheduled for 4.0.0."
219
219
  }
220
220
  },
221
221
  "focus-ring": {
222
222
  "value": "var(--hx-color-primary-600)",
223
- "description": "Light-mode focus-ring color. Pairs with surface.default (white) at 5.82:1 — clears the WCAG 1.4.11 3:1 floor for non-text UI components with comfortable headroom. Pre-3.2.2 this resolved to primary-400 (#6AB1B1) which lands at 2.45:1 on white — a sub-3:1 fail for any focus indicator drawn at full opacity (outline-style focus on hx-link, border-flip on hx-text-input, etc.). The dark.color.focus-ring override keeps primary-400 because primary-400 on dark surface.default = 7.27:1 (AA pass) while primary-600 on dark = 3.07:1 (just below 3:1 floor). Color-mix box-shadow ring treatments (hx-text-input wrapper) layer their alpha on top of this base; the change only sharpens, never weakens, the ring against light surfaces."
223
+ "description": "Light-mode focus-ring color. Pairs with surface.default (white) at 5.82:1 — clears the WCAG 1.4.11 3:1 floor for non-text UI components with comfortable headroom. Pre-3.2.2 this resolved to primary-400 (#6AB1B1) which lands at 2.45:1 on white — a sub-3:1 fail for any focus indicator drawn at full opacity (outline-style focus on hx-link, border-flip on hx-text-input, etc.). The dark.color.focus-ring override keeps primary-400 because primary-400 on dark surface.default = 7.27:1 (AA pass) while primary-600 on dark = 3.07:1 (barely clears the 3:1 UI floor with no headroom). Color-mix box-shadow ring treatments (hx-text-input wrapper) layer their alpha on top of this base; the change only sharpens, never weakens, the ring against light surfaces."
224
224
  },
225
225
  "selection": {
226
226
  "bg": { "value": "var(--hx-color-primary-200)" },
@@ -631,7 +631,15 @@
631
631
  "raised": { "value": "var(--hx-color-neutral-800)" },
632
632
  "sunken": { "value": "var(--hx-color-neutral-950)" },
633
633
  "inverse": { "value": "var(--hx-color-neutral-100)" },
634
- "overlay": { "value": "rgba(0, 0, 0, 0.85)" }
634
+ "overlay": { "value": "rgba(0, 0, 0, 0.85)" },
635
+ "on-dark-overlay-default": {
636
+ "value": "var(--hx-overlay-black-30)",
637
+ "description": "Dark-mode override. surface.inverse flips light (#EBEEE9) in dark mode, so the base overlay-white-30 binding tints white-on-light (~1.05:1) and disappears. overlay-black-30 tints dark-on-light against the now-light inverse surface and reads correctly."
638
+ },
639
+ "on-dark-overlay-subtle": {
640
+ "value": "var(--hx-overlay-black-10)",
641
+ "description": "Dark-mode override mirroring on-dark-overlay-default's flip rationale. Lowest-emphasis tint on the (now-light in dark mode) inverse surface."
642
+ }
635
643
  },
636
644
  "border": {
637
645
  "default": { "value": "var(--hx-color-neutral-700)" },
@@ -644,14 +652,6 @@
644
652
  "on-dark-strong": {
645
653
  "value": "var(--hx-overlay-black-50)",
646
654
  "description": "Dark-mode override. surface.inverse flips light (#EBEEE9) in dark mode, so the original overlay-white-70 binding paints white-ish on light = 1.12:1 (invisible). overlay-black-50 over #EBEEE9 = 3.84:1, clears the WCAG 1.4.11 3:1 floor for inverted-mode button outlines and focus rings drawn on the (now-light) inverse surface. Symmetrical with the light-mode 70% intent: dark text on light reaches the equivalent perceptual strength at a lower alpha than light text on dark."
647
- },
648
- "on-dark-default": {
649
- "value": "var(--hx-overlay-black-30)",
650
- "description": "Dark-mode override mirroring on-dark-strong's flip rationale. Used as a translucent fill (inverted secondary/ghost hover bg) on surface.inverse — the original overlay-white-30 binding produces ~1.05:1 against the now-light inverse surface."
651
- },
652
- "on-dark-subtle": {
653
- "value": "var(--hx-overlay-black-10)",
654
- "description": "Dark-mode override mirroring the on-dark-default flip. Used as the resting fill for inverted tertiary buttons; matches the original light-mode subtle elevation against the inverse surface."
655
655
  }
656
656
  },
657
657
  "focus-ring": { "value": "var(--hx-color-primary-400)" },
@@ -660,7 +660,7 @@
660
660
  "color": { "value": "var(--hx-color-neutral-100)" }
661
661
  },
662
662
  "action": {
663
- "_comment": "Dark-mode overrides for the action.* semantic layer. Filled-action surfaces (primary.bg/bg-hover/bg-active, danger.*) intentionally do NOT flip in dark mode for the standard non-inverted button — the brand fill remains brand-colored against the dark page, which is the standard dark-theme treatment for primary/destructive buttons. Outline/ghost treatments DO flip: the resting fg primary-600 fails AA (3.07:1) on dark surface.default, and primary-50 hover-bg is far too bright on a dark page. Mirrors the dark.text.link cascade (light primary-600 link → dark primary-400 link). Inverted-rest filled surfaces (action.primary.bg-inverted-rest) ALSO flip — they paint on surface.inverse, which becomes light (#EBEEE9) in dark mode, and the brand-resting fill needs to clear the WCAG 1.4.11 3:1 UI-component floor against the now-light inverse surface.",
663
+ "_comment": "Dark-mode overrides for the action.* semantic layer. Filled-action surfaces (primary.bg/bg-hover/bg-active, danger.*) intentionally do NOT flip in dark mode for the standard non-inverted button — the brand fill remains brand-colored against the dark page, which is the standard dark-theme treatment for primary/destructive buttons. Outline/ghost treatments DO flip: the resting fg primary-600 fails AA (3.07:1) on dark surface.default, and primary-50 hover-bg is far too bright on a dark page. Mirrors the dark.text.link cascade (light primary-600 link → dark primary-400 link). Inverted-rest filled surfaces (action.primary.bg-inverted-rest) ALSO flip — they paint on surface.inverse, which becomes light (#EBEEE9) in dark mode, and the brand-resting fill needs to clear the WCAG 1.4.11 3:1 UI-component floor against the now-light inverse surface. The paired bg-inverted-hover does NOT have a dark-mode override — flipping it in isolation breaks body-text AA (the foreground binding in hx-button.styles.ts pins text.on-primary = neutral-900, which fails 4.5:1 on any primary stop ≥ 600 against neutral-900). The coordinated bg+fg dark-inverted-hover fix (mode-aware fill stop + mode-aware foreground via text.on-primary-strong) is tracked as a 3.3.x follow-up; see contrast.test.ts:540-546.",
664
664
  "primary": {
665
665
  "bg-inverted-rest": {
666
666
  "value": "var(--hx-color-primary-600)",
@@ -700,7 +700,7 @@
700
700
  "focus": {
701
701
  "ring-color": {
702
702
  "value": "var(--hx-color-primary-400)",
703
- "description": "Dark-mode focus ring flips to primary-400 (#6AB1B1) — primary-600 on dark surface.default = 3.07:1 (sub-3:1 UI floor fail). primary-400 on dark surface = 7.27:1, AA pass. Mirrors the dark.color.focus-ring override and the dark.text.link cascade (primary-600 → primary-400)."
703
+ "description": "Dark-mode focus ring flips to primary-400 (#6AB1B1) — primary-600 on dark surface.default = 3.07:1, razor above the 3:1 UI floor (1.4.11) with no headroom. primary-400 on dark surface = 7.27:1, comfortable headroom. Mirrors the dark.color.focus-ring override and the dark.text.link cascade (primary-600 → primary-400)."
704
704
  }
705
705
  },
706
706
  "shadow": {
@@ -810,6 +810,14 @@
810
810
  "info-strong": {
811
811
  "value": "var(--hx-color-primary-500)",
812
812
  "description": "HC override for surface.info-strong. Light/dark resolves to primary-600. HC primary-600 (#60A5FA) is defined but pinning to HC primary-500 (#3B82F6, 5.71:1 on #000) keeps the info-strong surface visually distinct from the primary-strong action layer. Pairs with text.inverse (HC = #000000) for 5.71:1 AA pass."
813
+ },
814
+ "on-dark-overlay-default": {
815
+ "value": "#FFFFFF",
816
+ "description": "HC override for surface.on-dark-overlay-default. Translucent overlays disappear on the HC canvas; pin to solid white so inverted-secondary/ghost hover fills remain visible. Pairs with text.inverse (HC = #000000) for 21:1 AAA when inverted controls are in their hover state."
817
+ },
818
+ "on-dark-overlay-subtle": {
819
+ "value": "#C0C0C0",
820
+ "description": "HC override for surface.on-dark-overlay-subtle. Pinned to neutral silver so the subtle inverted hover affordance remains distinguishable from the strong tier in HC."
813
821
  }
814
822
  },
815
823
  "border": {
@@ -820,14 +828,6 @@
820
828
  "on-dark-strong": {
821
829
  "value": "#FFFFFF",
822
830
  "description": "HC override for border.on-dark-strong. The base overlay-white-70 reads softly against translucent darks but is invisible against the HC #000 canvas — pin to solid #FFFFFF (21:1 on #000) so inverted button outlines remain visible in HC."
823
- },
824
- "on-dark-default": {
825
- "value": "#FFFFFF",
826
- "description": "HC override for border.on-dark-default. Same rationale as on-dark-strong; HC requires solid borders rather than translucent overlays."
827
- },
828
- "on-dark-subtle": {
829
- "value": "#C0C0C0",
830
- "description": "HC override for border.on-dark-subtle. Matches HC border.subtle so subtle dividers stay distinguishable from the strong tier in HC."
831
831
  }
832
832
  },
833
833
  "focus-ring": { "value": "#FFFF00", "description": "High-visibility yellow focus ring" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helixui/tokens",
3
- "version": "3.2.0-next.98",
3
+ "version": "3.3.0-next.111",
4
4
  "description": "Design tokens for the HELiX enterprise healthcare web component library",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/tokens.json CHANGED
@@ -195,6 +195,14 @@
195
195
  "info-strong": {
196
196
  "value": "var(--hx-color-primary-600)",
197
197
  "description": "Emphasis info surface for high-prominence components (e.g. hx-toast--info). Pairs with text.on-primary-strong (neutral-0, no dark-mode flip) for AA — neutral-0 on primary-600 (#0F7078) = 5.82:1. Do NOT pair with text.inverse: surface.info-strong itself does not dark-mode-flip (the value stays primary-600 in light and dark), but text.inverse does flip to neutral-900 in dark mode — pairing the two produces dark text on a dark fill (sub-AA). Tracks action.primary.bg-hover by value but exposed under surface.* so toasts/banners consume a surface semantic, not an action-state semantic."
198
+ },
199
+ "on-dark-overlay-default": {
200
+ "value": "var(--hx-overlay-white-30)",
201
+ "description": "Translucent fill on dark surfaces — the resting/hover background for inverted secondary, ghost, and tertiary buttons painted on surface.inverse (which is dark in light mode, light in dark mode). Renamed from border.on-dark-default in 3.2.2: it was always used as a fill, never a border, and its 30% alpha cannot honour WCAG 1.4.11 3:1 against either neutral-900 (2.07:1) or neutral-0 (1.30:1) — borders need higher alpha. As a fill, contrast is irrelevant: it tints, it doesn't delimit."
202
+ },
203
+ "on-dark-overlay-subtle": {
204
+ "value": "var(--hx-overlay-white-10)",
205
+ "description": "Lowest-emphasis translucent fill on dark surfaces — the resting background for inverted-tertiary buttons and inverted hover affordances on dark side-nav. Renamed from border.on-dark-subtle in 3.2.2 for the same reason as on-dark-overlay-default: it is a fill, not a border."
198
206
  }
199
207
  },
200
208
  "border": {
@@ -207,20 +215,12 @@
207
215
  "focus": { "value": "var(--hx-color-primary-500)" },
208
216
  "on-dark-strong": {
209
217
  "value": "var(--hx-overlay-white-70)",
210
- "description": "Strong border treatment on dark surfaces (e.g. inverted button outlines on a dark page). Routes overlay-white-70 through the semantic tier so component-level inverted-border rules don't bind to raw overlay primitives. Added in 3.2.1 alongside the action.* layer for the token-cascade remediation."
211
- },
212
- "on-dark-default": {
213
- "value": "var(--hx-overlay-white-30)",
214
- "description": "Default border treatment on dark surfaces (inverted secondary/ghost outlines, divider borders inside dark panels). Overlay-mediated so the border tints itself against whatever sits underneath rather than fighting it with a hard color."
215
- },
216
- "on-dark-subtle": {
217
- "value": "var(--hx-overlay-white-10)",
218
- "description": "Subtle border treatment on dark surfaces (low-emphasis dividers inside dark panels)."
218
+ "description": "Strong border treatment on dark surfaces (e.g. inverted button outlines on a dark page). overlay-white-70 on neutral-900 5.0:1 clears WCAG 1.4.11 3:1 floor for non-text UI. Added in 3.2.1 alongside the action.* layer for the token-cascade remediation; only the strong tier survived the 3.2.2 audit because the lower-alpha siblings (overlay-white-30/10) cannot honour a border-contract — they were renamed to surface.on-dark-overlay-* (which is what they always were: translucent fills). DEPRECATED ALIAS NOTE: --hx-color-border-on-dark-{default,subtle} shipped in 3.2.0/3.2.1 and were renamed to surface.on-dark-overlay-{default,subtle} in 3.2.2. The deprecated names are NOT emitted at :root in dist/tokens.css. Two `:root`-alias variants were considered and rejected for the same root cause — both shadow host-scoped canonical-name overrides at every component consume site. Variant A (`var()` alias): `:root { --hx-color-border-on-dark-default: var(--hx-color-surface-on-dark-overlay-default); }` — the inner var() resolves at :root's computed-value time (CSS Custom Properties §3) and inherits as an opaque resolved value to every descendant. Variant B (concrete-value alias, light + dark): `:root { --hx-color-border-on-dark-default: rgba(255,255,255,0.30); } .dark { --hx-color-border-on-dark-default: <dark-mode value>; }` — no inner var() to substitute, but inheritance still delivers a non-empty value to every descendant. Both variants cause the same failure at the consume site: components read the deprecated name FIRST (`var(--hx-color-border-on-dark-*, var(--hx-color-surface-on-dark-overlay-*, …))`), so as long as the deprecated name resolves to ANY value above the host, the var() chain never falls through to the host's canonical override. Variant B has additional cost: it bakes a literal value into :root, breaking the primitive-chain (a consumer who overrides --hx-overlay-white-30 at :root would no longer see that change reflected in --hx-color-border-on-dark-default). Backwards compatibility is preserved at the consume sites instead: every component rule that paints with surface.on-dark-overlay-* reads the both-name fallback chain so a consumer override on either name reaches paint. The dark-mode-resolution.test.ts override-path tests pin both directions (line 209-227 covers deprecated-name + canonical-name overrides); both alias variants would fail the canonical-override test. The deliberate trade-off: downstream code that reads the deprecated names DIRECTLY (app-level CSS using `var(--hx-color-border-on-dark-default)` outside an hx-* component, or `getComputedStyle(el).getPropertyValue('--hx-color-border-on-dark-default')`) will resolve to an empty/invalid value in 3.2.2 — those readers must migrate to the canonical `--hx-color-surface-on-dark-overlay-{default,subtle}` (or set the deprecated name explicitly on their own scope). The chosen design breaks an undocumented direct-reader path explicitly, rather than breaking a documented host-override path silently. Removal scheduled for 4.0.0."
219
219
  }
220
220
  },
221
221
  "focus-ring": {
222
222
  "value": "var(--hx-color-primary-600)",
223
- "description": "Light-mode focus-ring color. Pairs with surface.default (white) at 5.82:1 — clears the WCAG 1.4.11 3:1 floor for non-text UI components with comfortable headroom. Pre-3.2.2 this resolved to primary-400 (#6AB1B1) which lands at 2.45:1 on white — a sub-3:1 fail for any focus indicator drawn at full opacity (outline-style focus on hx-link, border-flip on hx-text-input, etc.). The dark.color.focus-ring override keeps primary-400 because primary-400 on dark surface.default = 7.27:1 (AA pass) while primary-600 on dark = 3.07:1 (just below 3:1 floor). Color-mix box-shadow ring treatments (hx-text-input wrapper) layer their alpha on top of this base; the change only sharpens, never weakens, the ring against light surfaces."
223
+ "description": "Light-mode focus-ring color. Pairs with surface.default (white) at 5.82:1 — clears the WCAG 1.4.11 3:1 floor for non-text UI components with comfortable headroom. Pre-3.2.2 this resolved to primary-400 (#6AB1B1) which lands at 2.45:1 on white — a sub-3:1 fail for any focus indicator drawn at full opacity (outline-style focus on hx-link, border-flip on hx-text-input, etc.). The dark.color.focus-ring override keeps primary-400 because primary-400 on dark surface.default = 7.27:1 (AA pass) while primary-600 on dark = 3.07:1 (barely clears the 3:1 UI floor with no headroom). Color-mix box-shadow ring treatments (hx-text-input wrapper) layer their alpha on top of this base; the change only sharpens, never weakens, the ring against light surfaces."
224
224
  },
225
225
  "selection": {
226
226
  "bg": { "value": "var(--hx-color-primary-200)" },
@@ -631,7 +631,15 @@
631
631
  "raised": { "value": "var(--hx-color-neutral-800)" },
632
632
  "sunken": { "value": "var(--hx-color-neutral-950)" },
633
633
  "inverse": { "value": "var(--hx-color-neutral-100)" },
634
- "overlay": { "value": "rgba(0, 0, 0, 0.85)" }
634
+ "overlay": { "value": "rgba(0, 0, 0, 0.85)" },
635
+ "on-dark-overlay-default": {
636
+ "value": "var(--hx-overlay-black-30)",
637
+ "description": "Dark-mode override. surface.inverse flips light (#EBEEE9) in dark mode, so the base overlay-white-30 binding tints white-on-light (~1.05:1) and disappears. overlay-black-30 tints dark-on-light against the now-light inverse surface and reads correctly."
638
+ },
639
+ "on-dark-overlay-subtle": {
640
+ "value": "var(--hx-overlay-black-10)",
641
+ "description": "Dark-mode override mirroring on-dark-overlay-default's flip rationale. Lowest-emphasis tint on the (now-light in dark mode) inverse surface."
642
+ }
635
643
  },
636
644
  "border": {
637
645
  "default": { "value": "var(--hx-color-neutral-700)" },
@@ -644,14 +652,6 @@
644
652
  "on-dark-strong": {
645
653
  "value": "var(--hx-overlay-black-50)",
646
654
  "description": "Dark-mode override. surface.inverse flips light (#EBEEE9) in dark mode, so the original overlay-white-70 binding paints white-ish on light = 1.12:1 (invisible). overlay-black-50 over #EBEEE9 = 3.84:1, clears the WCAG 1.4.11 3:1 floor for inverted-mode button outlines and focus rings drawn on the (now-light) inverse surface. Symmetrical with the light-mode 70% intent: dark text on light reaches the equivalent perceptual strength at a lower alpha than light text on dark."
647
- },
648
- "on-dark-default": {
649
- "value": "var(--hx-overlay-black-30)",
650
- "description": "Dark-mode override mirroring on-dark-strong's flip rationale. Used as a translucent fill (inverted secondary/ghost hover bg) on surface.inverse — the original overlay-white-30 binding produces ~1.05:1 against the now-light inverse surface."
651
- },
652
- "on-dark-subtle": {
653
- "value": "var(--hx-overlay-black-10)",
654
- "description": "Dark-mode override mirroring the on-dark-default flip. Used as the resting fill for inverted tertiary buttons; matches the original light-mode subtle elevation against the inverse surface."
655
655
  }
656
656
  },
657
657
  "focus-ring": { "value": "var(--hx-color-primary-400)" },
@@ -660,7 +660,7 @@
660
660
  "color": { "value": "var(--hx-color-neutral-100)" }
661
661
  },
662
662
  "action": {
663
- "_comment": "Dark-mode overrides for the action.* semantic layer. Filled-action surfaces (primary.bg/bg-hover/bg-active, danger.*) intentionally do NOT flip in dark mode for the standard non-inverted button — the brand fill remains brand-colored against the dark page, which is the standard dark-theme treatment for primary/destructive buttons. Outline/ghost treatments DO flip: the resting fg primary-600 fails AA (3.07:1) on dark surface.default, and primary-50 hover-bg is far too bright on a dark page. Mirrors the dark.text.link cascade (light primary-600 link → dark primary-400 link). Inverted-rest filled surfaces (action.primary.bg-inverted-rest) ALSO flip — they paint on surface.inverse, which becomes light (#EBEEE9) in dark mode, and the brand-resting fill needs to clear the WCAG 1.4.11 3:1 UI-component floor against the now-light inverse surface.",
663
+ "_comment": "Dark-mode overrides for the action.* semantic layer. Filled-action surfaces (primary.bg/bg-hover/bg-active, danger.*) intentionally do NOT flip in dark mode for the standard non-inverted button — the brand fill remains brand-colored against the dark page, which is the standard dark-theme treatment for primary/destructive buttons. Outline/ghost treatments DO flip: the resting fg primary-600 fails AA (3.07:1) on dark surface.default, and primary-50 hover-bg is far too bright on a dark page. Mirrors the dark.text.link cascade (light primary-600 link → dark primary-400 link). Inverted-rest filled surfaces (action.primary.bg-inverted-rest) ALSO flip — they paint on surface.inverse, which becomes light (#EBEEE9) in dark mode, and the brand-resting fill needs to clear the WCAG 1.4.11 3:1 UI-component floor against the now-light inverse surface. The paired bg-inverted-hover does NOT have a dark-mode override — flipping it in isolation breaks body-text AA (the foreground binding in hx-button.styles.ts pins text.on-primary = neutral-900, which fails 4.5:1 on any primary stop ≥ 600 against neutral-900). The coordinated bg+fg dark-inverted-hover fix (mode-aware fill stop + mode-aware foreground via text.on-primary-strong) is tracked as a 3.3.x follow-up; see contrast.test.ts:540-546.",
664
664
  "primary": {
665
665
  "bg-inverted-rest": {
666
666
  "value": "var(--hx-color-primary-600)",
@@ -700,7 +700,7 @@
700
700
  "focus": {
701
701
  "ring-color": {
702
702
  "value": "var(--hx-color-primary-400)",
703
- "description": "Dark-mode focus ring flips to primary-400 (#6AB1B1) — primary-600 on dark surface.default = 3.07:1 (sub-3:1 UI floor fail). primary-400 on dark surface = 7.27:1, AA pass. Mirrors the dark.color.focus-ring override and the dark.text.link cascade (primary-600 → primary-400)."
703
+ "description": "Dark-mode focus ring flips to primary-400 (#6AB1B1) — primary-600 on dark surface.default = 3.07:1, razor above the 3:1 UI floor (1.4.11) with no headroom. primary-400 on dark surface = 7.27:1, comfortable headroom. Mirrors the dark.color.focus-ring override and the dark.text.link cascade (primary-600 → primary-400)."
704
704
  }
705
705
  },
706
706
  "shadow": {
@@ -810,6 +810,14 @@
810
810
  "info-strong": {
811
811
  "value": "var(--hx-color-primary-500)",
812
812
  "description": "HC override for surface.info-strong. Light/dark resolves to primary-600. HC primary-600 (#60A5FA) is defined but pinning to HC primary-500 (#3B82F6, 5.71:1 on #000) keeps the info-strong surface visually distinct from the primary-strong action layer. Pairs with text.inverse (HC = #000000) for 5.71:1 AA pass."
813
+ },
814
+ "on-dark-overlay-default": {
815
+ "value": "#FFFFFF",
816
+ "description": "HC override for surface.on-dark-overlay-default. Translucent overlays disappear on the HC canvas; pin to solid white so inverted-secondary/ghost hover fills remain visible. Pairs with text.inverse (HC = #000000) for 21:1 AAA when inverted controls are in their hover state."
817
+ },
818
+ "on-dark-overlay-subtle": {
819
+ "value": "#C0C0C0",
820
+ "description": "HC override for surface.on-dark-overlay-subtle. Pinned to neutral silver so the subtle inverted hover affordance remains distinguishable from the strong tier in HC."
813
821
  }
814
822
  },
815
823
  "border": {
@@ -820,14 +828,6 @@
820
828
  "on-dark-strong": {
821
829
  "value": "#FFFFFF",
822
830
  "description": "HC override for border.on-dark-strong. The base overlay-white-70 reads softly against translucent darks but is invisible against the HC #000 canvas — pin to solid #FFFFFF (21:1 on #000) so inverted button outlines remain visible in HC."
823
- },
824
- "on-dark-default": {
825
- "value": "#FFFFFF",
826
- "description": "HC override for border.on-dark-default. Same rationale as on-dark-strong; HC requires solid borders rather than translucent overlays."
827
- },
828
- "on-dark-subtle": {
829
- "value": "#C0C0C0",
830
- "description": "HC override for border.on-dark-subtle. Matches HC border.subtle so subtle dividers stay distinguishable from the strong tier in HC."
831
831
  }
832
832
  },
833
833
  "focus-ring": { "value": "#FFFF00", "description": "High-visibility yellow focus ring" },