@pure-ds/core 0.7.45 → 0.7.47

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.
@@ -190,20 +190,70 @@ export class Generator {
190
190
  );
191
191
  }
192
192
 
193
- // NEW: Generate interactive semantic tokens for buttons vs links/outlines
194
- // Use purpose-specific shade selection for optimal contrast
193
+ // Generate semantic interactive role tokens so components can avoid hardcoded ramp picks.
195
194
  colors.interactive = {
195
+ light: this.#buildInteractiveRoleTokens(
196
+ colors.primary,
197
+ colors.surface.base,
198
+ ),
199
+ dark: this.#buildInteractiveRoleTokens(
200
+ colors.dark.primary,
201
+ colors.dark.surface.base,
202
+ ),
203
+ };
204
+
205
+ // Accent role tokens mirror primary behavior for expressive accents on UI surfaces.
206
+ colors.accentInteractive = {
207
+ light: this.#buildInteractiveRoleTokens(
208
+ colors.accent,
209
+ colors.surface.base,
210
+ ),
211
+ dark: this.#buildInteractiveRoleTokens(
212
+ colors.dark.accent,
213
+ colors.dark.surface.base,
214
+ ),
215
+ };
216
+
217
+ colors.surfaceAccent = {
218
+ light: this.#buildSurfaceAccentTokens(colors.accent, colors.surface),
219
+ dark: this.#buildSurfaceAccentTokens(colors.dark.accent, colors.dark.surface),
220
+ };
221
+
222
+ // Semantic role tokens for danger (mirrors primary/accent pattern).
223
+ colors.dangerInteractive = {
224
+ light: this.#buildInteractiveRoleTokens(colors.danger, colors.surface.base),
225
+ dark: this.#buildInteractiveRoleTokens(colors.dark.danger, colors.dark.surface.base),
226
+ };
227
+
228
+ // Status role tokens for success/warning/info to avoid hardcoded shade picks in UI roles.
229
+ colors.successInteractive = {
230
+ light: this.#buildInteractiveRoleTokens(colors.success, colors.surface.base),
231
+ dark: this.#buildInteractiveRoleTokens(colors.dark.success, colors.dark.surface.base),
232
+ };
233
+
234
+ colors.warningInteractive = {
235
+ light: this.#buildInteractiveRoleTokens(colors.warning, colors.surface.base),
236
+ dark: this.#buildInteractiveRoleTokens(colors.dark.warning, colors.dark.surface.base),
237
+ };
238
+
239
+ colors.infoInteractive = {
240
+ light: this.#buildInteractiveRoleTokens(colors.info, colors.surface.base),
241
+ dark: this.#buildInteractiveRoleTokens(colors.dark.info, colors.dark.surface.base),
242
+ };
243
+
244
+ // Surface-aware semantic status text tokens for all core status families.
245
+ colors.surfaceStatus = {
196
246
  light: {
197
- fill: this.#pickFillShadeForWhite(colors.primary, 4.5), // For button fills with white text
198
- text: colors.primary[600], // For links/outlines on light backgrounds
247
+ success: this.#buildSurfaceAccentTokens(colors.success, colors.surface),
248
+ warning: this.#buildSurfaceAccentTokens(colors.warning, colors.surface),
249
+ info: this.#buildSurfaceAccentTokens(colors.info, colors.surface),
250
+ danger: this.#buildSurfaceAccentTokens(colors.danger, colors.surface),
199
251
  },
200
252
  dark: {
201
- fill: this.#pickFillShadeForWhite(colors.dark.primary, 4.5), // For button fills with white text
202
- text: this.#pickReadablePrimaryOnSurface(
203
- colors.dark.primary,
204
- colors.dark.surface.base,
205
- 4.5,
206
- ), // For links/outlines on dark backgrounds
253
+ success: this.#buildSurfaceAccentTokens(colors.dark.success, colors.dark.surface),
254
+ warning: this.#buildSurfaceAccentTokens(colors.dark.warning, colors.dark.surface),
255
+ info: this.#buildSurfaceAccentTokens(colors.dark.info, colors.dark.surface),
256
+ danger: this.#buildSurfaceAccentTokens(colors.dark.danger, colors.dark.surface),
207
257
  },
208
258
  };
209
259
 
@@ -338,34 +388,26 @@ export class Generator {
338
388
 
339
389
  const darkSurface = this.#generateBackgroundShades(darkBackgroundBase);
340
390
 
341
- // Determine a readable primary text shade for outline/link on dark surfaces
342
- const derivedPrimaryScale = overrides.primary
343
- ? this.#generateColorScale(overrides.primary)
344
- : this.#adjustColorsForDarkMode(lightColors.primary);
391
+ const buildScale = (overrideColor, fallbackScale) =>
392
+ overrideColor ? this.#generateColorScale(overrideColor) : fallbackScale;
345
393
 
346
394
  return {
347
395
  surface: {
348
396
  ...darkSurface,
349
397
  fieldset: this.#generateDarkModeFieldsetColors(darkSurface),
350
398
  },
351
- // For primary colors, use override, or adjust light colors for dark mode (dimmed for accessibility)
352
- primary: derivedPrimaryScale,
353
- // Adjust other colors for dark mode, with optional overrides
354
- secondary: overrides.secondary
355
- ? this.#generateColorScale(overrides.secondary)
356
- : this.#adjustColorsForDarkMode(lightColors.secondary),
357
- accent: overrides.accent
358
- ? this.#generateColorScale(overrides.accent)
359
- : this.#adjustColorsForDarkMode(lightColors.accent),
360
- // Regenerate grays if secondary override is provided (grays are derived from secondary)
399
+ // Keep scale direction stable in both themes (50 = lightest, 900 = darkest).
400
+ primary: buildScale(overrides.primary, lightColors.primary),
401
+ secondary: buildScale(overrides.secondary, lightColors.secondary),
402
+ accent: buildScale(overrides.accent, lightColors.accent),
403
+ // Grays are derived from secondary, so only regenerate when secondary is overridden.
361
404
  gray: overrides.secondary
362
405
  ? this.#generateGrayScale(overrides.secondary)
363
406
  : lightColors.gray,
364
- // Adjust semantic colors for dark mode
365
- success: this.#adjustColorsForDarkMode(lightColors.success),
366
- info: this.#adjustColorsForDarkMode(lightColors.info),
367
- warning: this.#adjustColorsForDarkMode(lightColors.warning),
368
- danger: this.#adjustColorsForDarkMode(lightColors.danger),
407
+ success: buildScale(overrides.success, lightColors.success),
408
+ info: buildScale(overrides.info, lightColors.info),
409
+ warning: buildScale(overrides.warning, lightColors.warning),
410
+ danger: buildScale(overrides.danger, lightColors.danger),
369
411
  };
370
412
  }
371
413
 
@@ -473,6 +515,55 @@ export class Generator {
473
515
  return best.color || primaryScale?.["600"] || primaryScale?.["500"];
474
516
  }
475
517
 
518
+ #buildInteractiveRoleTokens(scale = {}, surfaceBg = "#ffffff") {
519
+ const isDarkSurface = this.#luminance(surfaceBg) < 0.18;
520
+ const fill = this.#pickFillShadeForWhite(scale, 4.5);
521
+ // On dark surfaces hover/active should lighten the fill (move toward visible);
522
+ // on light surfaces they should deepen it.
523
+ const fillHover = isDarkSurface
524
+ ? this.#lightenColor(fill, 0.15)
525
+ : this.#darkenColor(fill, 0.1);
526
+ const fillActive = isDarkSurface
527
+ ? this.#lightenColor(fill, 0.07)
528
+ : this.#darkenColor(fill, 0.2);
529
+ const text = this.#pickReadablePrimaryOnSurface(scale, surfaceBg, 4.5);
530
+ const textHover = this.#pickReadablePrimaryOnSurface(scale, surfaceBg, 5.5);
531
+ const focusRing = this.#pickReadablePrimaryOnSurface(scale, surfaceBg, 3.0);
532
+
533
+ return {
534
+ fill,
535
+ fillHover,
536
+ fillActive,
537
+ text,
538
+ textHover: textHover || text,
539
+ // Keep visited links legible by subtly mixing with the current surface context.
540
+ textVisited: this.#mixTowards(text || fill, surfaceBg, 0.2),
541
+ focusRing: focusRing || text || fill,
542
+ selectionBg: text || fill,
543
+ selectionText: this.#findReadableOnColor(text || fill, 4.5),
544
+ contrast: this.#findReadableOnColor(fill, 4.5),
545
+ };
546
+ }
547
+
548
+ #buildSurfaceAccentTokens(scale = {}, surfaces = {}) {
549
+ const roles = {};
550
+
551
+ Object.entries(surfaces).forEach(([surfaceKey, bgColor]) => {
552
+ if (!bgColor || typeof bgColor !== "string" || !bgColor.startsWith("#")) {
553
+ return;
554
+ }
555
+
556
+ const text = this.#pickReadablePrimaryOnSurface(scale, bgColor, 4.5);
557
+ const textHover = this.#pickReadablePrimaryOnSurface(scale, bgColor, 5.0);
558
+ roles[surfaceKey] = {
559
+ text,
560
+ textHover: textHover || text,
561
+ };
562
+ });
563
+
564
+ return roles;
565
+ }
566
+
476
567
  // Pick a color scale shade that supports white text at AA
477
568
  #pickFillShadeForWhite(scale = {}, target = 4.5) {
478
569
  const order = ["600", "700", "800", "500", "400", "900"]; // typical UI fills
@@ -551,46 +642,6 @@ export class Generator {
551
642
  return this.#hslToHex(hsl.h, hsl.s, lighterLightness);
552
643
  }
553
644
 
554
- #adjustColorsForDarkMode(colorScale) {
555
- // Create dimmed and inverted colors for dark mode
556
- const dimmedScale = {};
557
-
558
- // Invert the scale and apply dimming for better dark mode appearance
559
- // For accessibility, mid-range colors (used for buttons/interactive elements) are more heavily dimmed
560
- const mapping = {
561
- 50: { source: "900", dimFactor: 0.8 },
562
- 100: { source: "800", dimFactor: 0.8 },
563
- 200: { source: "700", dimFactor: 0.8 }, // Increased dimming
564
- 300: { source: "600", dimFactor: 0.8 }, // Increased dimming
565
- 400: { source: "500", dimFactor: 0.85 }, // Increased dimming
566
- 500: { source: "400", dimFactor: 0.85 }, // Increased dimming
567
- 600: { source: "300", dimFactor: 0.85 }, // Increased dimming (buttons use this!)
568
- 700: { source: "200", dimFactor: 0.85 }, // Increased dimming (button hover)
569
- 800: { source: "100", dimFactor: 0.95 }, // Less dimming for text
570
- 900: { source: "50", dimFactor: 0.95 }, // Less dimming for text
571
- };
572
-
573
- Object.entries(mapping).forEach(([key, config]) => {
574
- const sourceColor = colorScale[config.source];
575
- dimmedScale[key] = this.#dimColorForDarkMode(
576
- sourceColor,
577
- config.dimFactor,
578
- );
579
- });
580
-
581
- return dimmedScale;
582
- }
583
-
584
- #dimColorForDarkMode(hexColor, dimFactor = 0.8) {
585
- const hsl = this.#hexToHsl(hexColor);
586
-
587
- // Reduce saturation and lightness for dark mode, similar to image dimming
588
- const dimmedSaturation = Math.max(hsl.s * dimFactor, 5);
589
- const dimmedLightness = Math.max(hsl.l * dimFactor, 5);
590
-
591
- return this.#hslToHex(hsl.h, dimmedSaturation, dimmedLightness);
592
- }
593
-
594
645
  /**
595
646
  * Generate spacing tokens based on the provided configuration.
596
647
  * @param {Object} spatialConfig
@@ -1044,6 +1095,13 @@ export class Generator {
1044
1095
  if (category === "dark") return; // handled elsewhere
1045
1096
  if (category === "surfaceSmart") return; // handled below
1046
1097
  if (category === "interactive") return; // handled below with semantic tokens
1098
+ if (category === "accentInteractive") return;
1099
+ if (category === "dangerInteractive") return;
1100
+ if (category === "successInteractive") return;
1101
+ if (category === "warningInteractive") return;
1102
+ if (category === "infoInteractive") return;
1103
+ if (category === "surfaceAccent") return;
1104
+ if (category === "surfaceStatus") return;
1047
1105
  if (typeof values === "object" && values !== null) {
1048
1106
  generateNestedColors(values, `${category}-`);
1049
1107
  }
@@ -1075,7 +1133,7 @@ export class Generator {
1075
1133
  chunks.push(` /* Semantic Text Colors */\n`);
1076
1134
  chunks.push(` --color-text-primary: var(--color-gray-900);\n`);
1077
1135
  chunks.push(` --color-text-secondary: var(--color-gray-600);\n`);
1078
- chunks.push(` --color-text-muted: var(--color-gray-600);\n`);
1136
+ chunks.push(` --color-text-muted: var(--color-gray-500);\n`);
1079
1137
  chunks.push(` --color-border: var(--color-gray-300);\n`);
1080
1138
  chunks.push(` --color-input-bg: var(--color-surface-base);\n`);
1081
1139
  chunks.push(` --color-input-disabled-bg: var(--color-gray-50);\n`);
@@ -1090,9 +1148,124 @@ export class Generator {
1090
1148
  chunks.push(
1091
1149
  ` --color-primary-fill: ${colors.interactive.light.fill}; /* For button backgrounds with white text */\n`,
1092
1150
  );
1151
+ chunks.push(
1152
+ ` --color-primary-fill-hover: ${colors.interactive.light.fillHover};\n`,
1153
+ );
1154
+ chunks.push(
1155
+ ` --color-primary-fill-active: ${colors.interactive.light.fillActive};\n`,
1156
+ );
1093
1157
  chunks.push(
1094
1158
  ` --color-primary-text: ${colors.interactive.light.text}; /* For links and outline buttons on light surfaces */\n`,
1095
1159
  );
1160
+ chunks.push(
1161
+ ` --color-primary-text-hover: ${colors.interactive.light.textHover};\n`,
1162
+ );
1163
+ chunks.push(
1164
+ ` --color-primary-text-visited: ${colors.interactive.light.textVisited};\n`,
1165
+ );
1166
+ chunks.push(
1167
+ ` --color-primary-contrast: ${colors.interactive.light.contrast};\n`,
1168
+ );
1169
+ chunks.push(
1170
+ ` --color-focus-ring: ${colors.interactive.light.focusRing};\n`,
1171
+ );
1172
+ chunks.push(
1173
+ ` --color-selection-bg: ${colors.interactive.light.selectionBg};\n`,
1174
+ );
1175
+ chunks.push(
1176
+ ` --color-selection-text: ${colors.interactive.light.selectionText};\n`,
1177
+ );
1178
+ chunks.push(` --color-link: var(--color-primary-text);\n`);
1179
+ chunks.push(` --color-link-hover: var(--color-primary-text-hover);\n`);
1180
+ chunks.push(` --color-link-visited: var(--color-primary-text-visited);\n`);
1181
+ }
1182
+
1183
+ if (colors.accentInteractive?.light) {
1184
+ chunks.push(` /* Accent Role Colors */\n`);
1185
+ chunks.push(
1186
+ ` --color-accent-fill: ${colors.accentInteractive.light.fill};\n`,
1187
+ );
1188
+ chunks.push(
1189
+ ` --color-accent-fill-hover: ${colors.accentInteractive.light.fillHover};\n`,
1190
+ );
1191
+ chunks.push(
1192
+ ` --color-accent-fill-active: ${colors.accentInteractive.light.fillActive};\n`,
1193
+ );
1194
+ chunks.push(
1195
+ ` --color-accent-text: ${colors.accentInteractive.light.text};\n`,
1196
+ );
1197
+ chunks.push(
1198
+ ` --color-accent-text-hover: ${colors.accentInteractive.light.textHover};\n`,
1199
+ );
1200
+ }
1201
+
1202
+ if (colors.surfaceAccent?.light) {
1203
+ chunks.push(` /* Surface-Aware Accent Text Tokens */\n`);
1204
+ Object.entries(colors.surfaceAccent.light).forEach(([surfaceKey, role]) => {
1205
+ chunks.push(` --surface-${surfaceKey}-accent-text: ${role.text};\n`);
1206
+ chunks.push(
1207
+ ` --surface-${surfaceKey}-accent-text-hover: ${role.textHover};\n`,
1208
+ );
1209
+ });
1210
+ }
1211
+
1212
+ if (colors.dangerInteractive?.light) {
1213
+ chunks.push(` /* Danger Role Colors */\n`);
1214
+ chunks.push(` --color-danger-fill: ${colors.dangerInteractive.light.fill};\n`);
1215
+ chunks.push(` --color-danger-fill-hover: ${colors.dangerInteractive.light.fillHover};\n`);
1216
+ chunks.push(` --color-danger-fill-active: ${colors.dangerInteractive.light.fillActive};\n`);
1217
+ chunks.push(` --color-danger-text: ${colors.dangerInteractive.light.text};\n`);
1218
+ chunks.push(` --color-danger-text-hover: ${colors.dangerInteractive.light.textHover};\n`);
1219
+ chunks.push(` --color-danger-contrast: ${colors.dangerInteractive.light.contrast};\n`);
1220
+ }
1221
+
1222
+ if (colors.successInteractive?.light) {
1223
+ chunks.push(` /* Success Role Colors */\n`);
1224
+ chunks.push(` --color-success-fill: ${colors.successInteractive.light.fill};\n`);
1225
+ chunks.push(` --color-success-fill-hover: ${colors.successInteractive.light.fillHover};\n`);
1226
+ chunks.push(` --color-success-fill-active: ${colors.successInteractive.light.fillActive};\n`);
1227
+ chunks.push(` --color-success-text: ${colors.successInteractive.light.text};\n`);
1228
+ chunks.push(` --color-success-text-hover: ${colors.successInteractive.light.textHover};\n`);
1229
+ chunks.push(` --color-success-contrast: ${colors.successInteractive.light.contrast};\n`);
1230
+ }
1231
+
1232
+ if (colors.warningInteractive?.light) {
1233
+ chunks.push(` /* Warning Role Colors */\n`);
1234
+ chunks.push(` --color-warning-fill: ${colors.warningInteractive.light.fill};\n`);
1235
+ chunks.push(` --color-warning-fill-hover: ${colors.warningInteractive.light.fillHover};\n`);
1236
+ chunks.push(` --color-warning-fill-active: ${colors.warningInteractive.light.fillActive};\n`);
1237
+ chunks.push(` --color-warning-text: ${colors.warningInteractive.light.text};\n`);
1238
+ chunks.push(` --color-warning-text-hover: ${colors.warningInteractive.light.textHover};\n`);
1239
+ chunks.push(` --color-warning-contrast: ${colors.warningInteractive.light.contrast};\n`);
1240
+ }
1241
+
1242
+ if (colors.infoInteractive?.light) {
1243
+ chunks.push(` /* Info Role Colors */\n`);
1244
+ chunks.push(` --color-info-fill: ${colors.infoInteractive.light.fill};\n`);
1245
+ chunks.push(` --color-info-fill-hover: ${colors.infoInteractive.light.fillHover};\n`);
1246
+ chunks.push(` --color-info-fill-active: ${colors.infoInteractive.light.fillActive};\n`);
1247
+ chunks.push(` --color-info-text: ${colors.infoInteractive.light.text};\n`);
1248
+ chunks.push(` --color-info-text-hover: ${colors.infoInteractive.light.textHover};\n`);
1249
+ chunks.push(` --color-info-contrast: ${colors.infoInteractive.light.contrast};\n`);
1250
+ }
1251
+
1252
+ if (colors.surfaceStatus?.light) {
1253
+ chunks.push(` /* Surface-Aware Status Text Tokens */\n`);
1254
+ Object.entries(colors.surfaceStatus.light).forEach(([family, roleBySurface]) => {
1255
+ Object.entries(roleBySurface).forEach(([surfaceKey, role]) => {
1256
+ chunks.push(` --surface-${surfaceKey}-${family}-text: ${role.text};\n`);
1257
+ chunks.push(` --surface-${surfaceKey}-${family}-text-hover: ${role.textHover};\n`);
1258
+ });
1259
+ });
1260
+ }
1261
+
1262
+ // Callout display tokens — theme-aware shade aliases for callout bg/border/text.
1263
+ // Dark mode overrides these in the dark variables section.
1264
+ chunks.push(` /* Semantic Callout Display Tokens */\n`);
1265
+ for (const family of ['success', 'info', 'warning', 'danger']) {
1266
+ chunks.push(` --color-${family}-display-bg: var(--color-${family}-50);\n`);
1267
+ chunks.push(` --color-${family}-display-border: var(--color-${family}-600);\n`);
1268
+ chunks.push(` --color-${family}-display-text: var(--color-${family}-900);\n`);
1096
1269
  }
1097
1270
 
1098
1271
  // Translucent surface tokens
@@ -1317,7 +1490,145 @@ export class Generator {
1317
1490
  smartLines.push(`\n`);
1318
1491
  }
1319
1492
 
1320
- const semantic = ` --color-text-primary: var(--color-gray-100);\n --color-text-secondary: var(--color-gray-300);\n --color-text-muted: var(--color-gray-600);\n --color-border: var(--color-gray-700);\n --color-input-bg: var(--color-gray-800);\n --color-input-disabled-bg: var(--color-gray-900);\n --color-input-disabled-text: var(--color-gray-600);\n --color-code-bg: var(--color-gray-800);\n`;
1493
+ const interactiveLines = [];
1494
+ if (colors.interactive?.dark) {
1495
+ interactiveLines.push(
1496
+ ` /* Interactive Colors - optimized for specific use cases (dark mode) */\n`,
1497
+ );
1498
+ interactiveLines.push(
1499
+ ` --color-primary-fill: ${colors.interactive.dark.fill};\n`,
1500
+ );
1501
+ interactiveLines.push(
1502
+ ` --color-primary-fill-hover: ${colors.interactive.dark.fillHover};\n`,
1503
+ );
1504
+ interactiveLines.push(
1505
+ ` --color-primary-fill-active: ${colors.interactive.dark.fillActive};\n`,
1506
+ );
1507
+ interactiveLines.push(
1508
+ ` --color-primary-text: ${colors.interactive.dark.text};\n`,
1509
+ );
1510
+ interactiveLines.push(
1511
+ ` --color-primary-text-hover: ${colors.interactive.dark.textHover};\n`,
1512
+ );
1513
+ interactiveLines.push(
1514
+ ` --color-primary-text-visited: ${colors.interactive.dark.textVisited};\n`,
1515
+ );
1516
+ interactiveLines.push(
1517
+ ` --color-primary-contrast: ${colors.interactive.dark.contrast};\n`,
1518
+ );
1519
+ interactiveLines.push(
1520
+ ` --color-focus-ring: ${colors.interactive.dark.focusRing};\n`,
1521
+ );
1522
+ interactiveLines.push(
1523
+ ` --color-selection-bg: ${colors.interactive.dark.selectionBg};\n`,
1524
+ );
1525
+ interactiveLines.push(
1526
+ ` --color-selection-text: ${colors.interactive.dark.selectionText};\n`,
1527
+ );
1528
+ interactiveLines.push(` --color-link: var(--color-primary-text);\n`);
1529
+ interactiveLines.push(` --color-link-hover: var(--color-primary-text-hover);\n`);
1530
+ interactiveLines.push(` --color-link-visited: var(--color-primary-text-visited);\n`);
1531
+ }
1532
+
1533
+ const accentLines = [];
1534
+ if (colors.accentInteractive?.dark) {
1535
+ accentLines.push(` /* Accent Role Colors (dark mode) */\n`);
1536
+ accentLines.push(
1537
+ ` --color-accent-fill: ${colors.accentInteractive.dark.fill};\n`,
1538
+ );
1539
+ accentLines.push(
1540
+ ` --color-accent-fill-hover: ${colors.accentInteractive.dark.fillHover};\n`,
1541
+ );
1542
+ accentLines.push(
1543
+ ` --color-accent-fill-active: ${colors.accentInteractive.dark.fillActive};\n`,
1544
+ );
1545
+ accentLines.push(
1546
+ ` --color-accent-text: ${colors.accentInteractive.dark.text};\n`,
1547
+ );
1548
+ accentLines.push(
1549
+ ` --color-accent-text-hover: ${colors.accentInteractive.dark.textHover};\n`,
1550
+ );
1551
+ }
1552
+
1553
+ const surfaceAccentLines = [];
1554
+ if (colors.surfaceAccent?.dark) {
1555
+ surfaceAccentLines.push(` /* Surface-Aware Accent Text Tokens (dark mode) */\n`);
1556
+ Object.entries(colors.surfaceAccent.dark).forEach(([surfaceKey, role]) => {
1557
+ surfaceAccentLines.push(
1558
+ ` --surface-${surfaceKey}-accent-text: ${role.text};\n`,
1559
+ );
1560
+ surfaceAccentLines.push(
1561
+ ` --surface-${surfaceKey}-accent-text-hover: ${role.textHover};\n`,
1562
+ );
1563
+ });
1564
+ }
1565
+
1566
+ const dangerLines = [];
1567
+ if (colors.dangerInteractive?.dark) {
1568
+ dangerLines.push(` /* Danger Role Colors (dark mode) */\n`);
1569
+ dangerLines.push(` --color-danger-fill: ${colors.dangerInteractive.dark.fill};\n`);
1570
+ dangerLines.push(` --color-danger-fill-hover: ${colors.dangerInteractive.dark.fillHover};\n`);
1571
+ dangerLines.push(` --color-danger-fill-active: ${colors.dangerInteractive.dark.fillActive};\n`);
1572
+ dangerLines.push(` --color-danger-text: ${colors.dangerInteractive.dark.text};\n`);
1573
+ dangerLines.push(` --color-danger-text-hover: ${colors.dangerInteractive.dark.textHover};\n`);
1574
+ dangerLines.push(` --color-danger-contrast: ${colors.dangerInteractive.dark.contrast};\n`);
1575
+ }
1576
+
1577
+ const successLines = [];
1578
+ if (colors.successInteractive?.dark) {
1579
+ successLines.push(` /* Success Role Colors (dark mode) */\n`);
1580
+ successLines.push(` --color-success-fill: ${colors.successInteractive.dark.fill};\n`);
1581
+ successLines.push(` --color-success-fill-hover: ${colors.successInteractive.dark.fillHover};\n`);
1582
+ successLines.push(` --color-success-fill-active: ${colors.successInteractive.dark.fillActive};\n`);
1583
+ successLines.push(` --color-success-text: ${colors.successInteractive.dark.text};\n`);
1584
+ successLines.push(` --color-success-text-hover: ${colors.successInteractive.dark.textHover};\n`);
1585
+ successLines.push(` --color-success-contrast: ${colors.successInteractive.dark.contrast};\n`);
1586
+ }
1587
+
1588
+ const warningLines = [];
1589
+ if (colors.warningInteractive?.dark) {
1590
+ warningLines.push(` /* Warning Role Colors (dark mode) */\n`);
1591
+ warningLines.push(` --color-warning-fill: ${colors.warningInteractive.dark.fill};\n`);
1592
+ warningLines.push(` --color-warning-fill-hover: ${colors.warningInteractive.dark.fillHover};\n`);
1593
+ warningLines.push(` --color-warning-fill-active: ${colors.warningInteractive.dark.fillActive};\n`);
1594
+ warningLines.push(` --color-warning-text: ${colors.warningInteractive.dark.text};\n`);
1595
+ warningLines.push(` --color-warning-text-hover: ${colors.warningInteractive.dark.textHover};\n`);
1596
+ warningLines.push(` --color-warning-contrast: ${colors.warningInteractive.dark.contrast};\n`);
1597
+ }
1598
+
1599
+ const infoLines = [];
1600
+ if (colors.infoInteractive?.dark) {
1601
+ infoLines.push(` /* Info Role Colors (dark mode) */\n`);
1602
+ infoLines.push(` --color-info-fill: ${colors.infoInteractive.dark.fill};\n`);
1603
+ infoLines.push(` --color-info-fill-hover: ${colors.infoInteractive.dark.fillHover};\n`);
1604
+ infoLines.push(` --color-info-fill-active: ${colors.infoInteractive.dark.fillActive};\n`);
1605
+ infoLines.push(` --color-info-text: ${colors.infoInteractive.dark.text};\n`);
1606
+ infoLines.push(` --color-info-text-hover: ${colors.infoInteractive.dark.textHover};\n`);
1607
+ infoLines.push(` --color-info-contrast: ${colors.infoInteractive.dark.contrast};\n`);
1608
+ }
1609
+
1610
+ const surfaceStatusLines = [];
1611
+ if (colors.surfaceStatus?.dark) {
1612
+ surfaceStatusLines.push(` /* Surface-Aware Status Text Tokens (dark mode) */\n`);
1613
+ Object.entries(colors.surfaceStatus.dark).forEach(([family, roleBySurface]) => {
1614
+ Object.entries(roleBySurface).forEach(([surfaceKey, role]) => {
1615
+ surfaceStatusLines.push(` --surface-${surfaceKey}-${family}-text: ${role.text};\n`);
1616
+ surfaceStatusLines.push(` --surface-${surfaceKey}-${family}-text-hover: ${role.textHover};\n`);
1617
+ });
1618
+ });
1619
+ }
1620
+
1621
+ // Callout display token overrides for dark mode.
1622
+ // Use color-mix() to create a subtle hue-tinted surface (adapts to any dark bg),
1623
+ // a visible medium-bright border, and light text — no hard shade guessing needed.
1624
+ const calloutDisplayLines = [` /* Semantic Callout Display Tokens (dark mode) */\n`];
1625
+ for (const family of ['success', 'info', 'warning', 'danger']) {
1626
+ calloutDisplayLines.push(` --color-${family}-display-bg: color-mix(in oklab, var(--color-${family}-400) 12%, var(--color-surface-base));\n`);
1627
+ calloutDisplayLines.push(` --color-${family}-display-border: var(--color-${family}-400);\n`);
1628
+ calloutDisplayLines.push(` --color-${family}-display-text: var(--color-${family}-100);\n`);
1629
+ }
1630
+
1631
+ const semantic = ` --color-text-primary: var(--color-gray-100);\n --color-text-secondary: var(--color-gray-300);\n --color-text-muted: var(--color-gray-500);\n --color-border: var(--color-gray-700);\n --color-input-bg: var(--color-gray-800);\n --color-input-disabled-bg: var(--color-gray-900);\n --color-input-disabled-text: var(--color-gray-600);\n --color-code-bg: var(--color-gray-800);\n`;
1321
1632
 
1322
1633
  const backdrop = ` /* Backdrop tokens - dark mode */\n --backdrop-bg: linear-gradient(\n 135deg,\n rgba(0, 0, 0, 0.6),\n rgba(0, 0, 0, 0.4)\n );\n --backdrop-blur: 10px;\n --backdrop-saturate: 120%;\n --backdrop-brightness: 0.7;\n --backdrop-filter: blur(var(--backdrop-blur)) saturate(var(--backdrop-saturate)) brightness(var(--backdrop-brightness));\n --backdrop-opacity: 1;\n \n /* Legacy alias for backwards compatibility */\n --backdrop-background: var(--backdrop-bg);\n`;
1323
1634
 
@@ -1332,6 +1643,15 @@ export class Generator {
1332
1643
  ...smartLines,
1333
1644
  ...shadowLines,
1334
1645
  semantic,
1646
+ ...interactiveLines,
1647
+ ...accentLines,
1648
+ ...surfaceAccentLines,
1649
+ ...dangerLines,
1650
+ ...successLines,
1651
+ ...warningLines,
1652
+ ...infoLines,
1653
+ ...surfaceStatusLines,
1654
+ ...calloutDisplayLines,
1335
1655
  backdrop,
1336
1656
  mesh,
1337
1657
  ].join("");
@@ -1405,17 +1725,141 @@ export class Generator {
1405
1725
  ` /* Interactive Colors - optimized for specific use cases (dark mode) */\n`,
1406
1726
  );
1407
1727
  interactiveLines.push(
1408
- ` --color-primary-fill: ${colors.interactive.dark.fill}; /* For button backgrounds with white text */\n`,
1728
+ ` --color-primary-fill: ${colors.interactive.dark.fill};\n`,
1729
+ );
1730
+ interactiveLines.push(
1731
+ ` --color-primary-fill-hover: ${colors.interactive.dark.fillHover};\n`,
1409
1732
  );
1410
1733
  interactiveLines.push(
1411
- ` --color-primary-text: ${colors.interactive.dark.text}; /* For links and outline buttons on dark surfaces */\n`,
1734
+ ` --color-primary-fill-active: ${colors.interactive.dark.fillActive};\n`,
1412
1735
  );
1736
+ interactiveLines.push(
1737
+ ` --color-primary-text: ${colors.interactive.dark.text};\n`,
1738
+ );
1739
+ interactiveLines.push(
1740
+ ` --color-primary-text-hover: ${colors.interactive.dark.textHover};\n`,
1741
+ );
1742
+ interactiveLines.push(
1743
+ ` --color-primary-text-visited: ${colors.interactive.dark.textVisited};\n`,
1744
+ );
1745
+ interactiveLines.push(
1746
+ ` --color-primary-contrast: ${colors.interactive.dark.contrast};\n`,
1747
+ );
1748
+ interactiveLines.push(
1749
+ ` --color-focus-ring: ${colors.interactive.dark.focusRing};\n`,
1750
+ );
1751
+ interactiveLines.push(
1752
+ ` --color-selection-bg: ${colors.interactive.dark.selectionBg};\n`,
1753
+ );
1754
+ interactiveLines.push(
1755
+ ` --color-selection-text: ${colors.interactive.dark.selectionText};\n`,
1756
+ );
1757
+ interactiveLines.push(` --color-link: var(--color-primary-text);\n`);
1758
+ interactiveLines.push(` --color-link-hover: var(--color-primary-text-hover);\n`);
1759
+ interactiveLines.push(` --color-link-visited: var(--color-primary-text-visited);\n`);
1760
+ }
1761
+
1762
+ const accentLines = [];
1763
+ if (colors.accentInteractive?.dark) {
1764
+ accentLines.push(` /* Accent Role Colors (dark mode) */\n`);
1765
+ accentLines.push(
1766
+ ` --color-accent-fill: ${colors.accentInteractive.dark.fill};\n`,
1767
+ );
1768
+ accentLines.push(
1769
+ ` --color-accent-fill-hover: ${colors.accentInteractive.dark.fillHover};\n`,
1770
+ );
1771
+ accentLines.push(
1772
+ ` --color-accent-fill-active: ${colors.accentInteractive.dark.fillActive};\n`,
1773
+ );
1774
+ accentLines.push(
1775
+ ` --color-accent-text: ${colors.accentInteractive.dark.text};\n`,
1776
+ );
1777
+ accentLines.push(
1778
+ ` --color-accent-text-hover: ${colors.accentInteractive.dark.textHover};\n`,
1779
+ );
1780
+ }
1781
+
1782
+ const surfaceAccentLines = [];
1783
+ if (colors.surfaceAccent?.dark) {
1784
+ surfaceAccentLines.push(
1785
+ ` /* Surface-Aware Accent Text Tokens (dark mode) */\n`,
1786
+ );
1787
+ Object.entries(colors.surfaceAccent.dark).forEach(([surfaceKey, role]) => {
1788
+ surfaceAccentLines.push(
1789
+ ` --surface-${surfaceKey}-accent-text: ${role.text};\n`,
1790
+ );
1791
+ surfaceAccentLines.push(
1792
+ ` --surface-${surfaceKey}-accent-text-hover: ${role.textHover};\n`,
1793
+ );
1794
+ });
1795
+ }
1796
+
1797
+ const dangerLines2 = [];
1798
+ if (colors.dangerInteractive?.dark) {
1799
+ dangerLines2.push(` /* Danger Role Colors (dark mode) */\n`);
1800
+ dangerLines2.push(` --color-danger-fill: ${colors.dangerInteractive.dark.fill};\n`);
1801
+ dangerLines2.push(` --color-danger-fill-hover: ${colors.dangerInteractive.dark.fillHover};\n`);
1802
+ dangerLines2.push(` --color-danger-fill-active: ${colors.dangerInteractive.dark.fillActive};\n`);
1803
+ dangerLines2.push(` --color-danger-text: ${colors.dangerInteractive.dark.text};\n`);
1804
+ dangerLines2.push(` --color-danger-text-hover: ${colors.dangerInteractive.dark.textHover};\n`);
1805
+ dangerLines2.push(` --color-danger-contrast: ${colors.dangerInteractive.dark.contrast};\n`);
1806
+ }
1807
+
1808
+ const successLines2 = [];
1809
+ if (colors.successInteractive?.dark) {
1810
+ successLines2.push(` /* Success Role Colors (dark mode) */\n`);
1811
+ successLines2.push(` --color-success-fill: ${colors.successInteractive.dark.fill};\n`);
1812
+ successLines2.push(` --color-success-fill-hover: ${colors.successInteractive.dark.fillHover};\n`);
1813
+ successLines2.push(` --color-success-fill-active: ${colors.successInteractive.dark.fillActive};\n`);
1814
+ successLines2.push(` --color-success-text: ${colors.successInteractive.dark.text};\n`);
1815
+ successLines2.push(` --color-success-text-hover: ${colors.successInteractive.dark.textHover};\n`);
1816
+ successLines2.push(` --color-success-contrast: ${colors.successInteractive.dark.contrast};\n`);
1817
+ }
1818
+
1819
+ const warningLines2 = [];
1820
+ if (colors.warningInteractive?.dark) {
1821
+ warningLines2.push(` /* Warning Role Colors (dark mode) */\n`);
1822
+ warningLines2.push(` --color-warning-fill: ${colors.warningInteractive.dark.fill};\n`);
1823
+ warningLines2.push(` --color-warning-fill-hover: ${colors.warningInteractive.dark.fillHover};\n`);
1824
+ warningLines2.push(` --color-warning-fill-active: ${colors.warningInteractive.dark.fillActive};\n`);
1825
+ warningLines2.push(` --color-warning-text: ${colors.warningInteractive.dark.text};\n`);
1826
+ warningLines2.push(` --color-warning-text-hover: ${colors.warningInteractive.dark.textHover};\n`);
1827
+ warningLines2.push(` --color-warning-contrast: ${colors.warningInteractive.dark.contrast};\n`);
1828
+ }
1829
+
1830
+ const infoLines2 = [];
1831
+ if (colors.infoInteractive?.dark) {
1832
+ infoLines2.push(` /* Info Role Colors (dark mode) */\n`);
1833
+ infoLines2.push(` --color-info-fill: ${colors.infoInteractive.dark.fill};\n`);
1834
+ infoLines2.push(` --color-info-fill-hover: ${colors.infoInteractive.dark.fillHover};\n`);
1835
+ infoLines2.push(` --color-info-fill-active: ${colors.infoInteractive.dark.fillActive};\n`);
1836
+ infoLines2.push(` --color-info-text: ${colors.infoInteractive.dark.text};\n`);
1837
+ infoLines2.push(` --color-info-text-hover: ${colors.infoInteractive.dark.textHover};\n`);
1838
+ infoLines2.push(` --color-info-contrast: ${colors.infoInteractive.dark.contrast};\n`);
1839
+ }
1840
+
1841
+ const surfaceStatusLines2 = [];
1842
+ if (colors.surfaceStatus?.dark) {
1843
+ surfaceStatusLines2.push(` /* Surface-Aware Status Text Tokens (dark mode) */\n`);
1844
+ Object.entries(colors.surfaceStatus.dark).forEach(([family, roleBySurface]) => {
1845
+ Object.entries(roleBySurface).forEach(([surfaceKey, role]) => {
1846
+ surfaceStatusLines2.push(` --surface-${surfaceKey}-${family}-text: ${role.text};\n`);
1847
+ surfaceStatusLines2.push(` --surface-${surfaceKey}-${family}-text-hover: ${role.textHover};\n`);
1848
+ });
1849
+ });
1850
+ }
1851
+
1852
+ const calloutDisplayLines2 = [` /* Semantic Callout Display Tokens (dark mode) */\n`];
1853
+ for (const family of ['success', 'info', 'warning', 'danger']) {
1854
+ calloutDisplayLines2.push(` --color-${family}-display-bg: color-mix(in oklab, var(--color-${family}-400) 12%, var(--color-surface-base));\n`);
1855
+ calloutDisplayLines2.push(` --color-${family}-display-border: var(--color-${family}-400);\n`);
1856
+ calloutDisplayLines2.push(` --color-${family}-display-text: var(--color-${family}-100);\n`);
1413
1857
  }
1414
1858
 
1415
1859
  const semantic = [
1416
1860
  ` --color-text-primary: var(--color-gray-100);\n`,
1417
1861
  ` --color-text-secondary: var(--color-gray-300);\n`,
1418
- ` --color-text-muted: var(--color-gray-600);\n`,
1862
+ ` --color-text-muted: var(--color-gray-500);\n`,
1419
1863
  ` --color-border: var(--color-gray-700);\n`,
1420
1864
  ` --color-input-bg: var(--color-gray-800);\n`,
1421
1865
  ` --color-input-disabled-bg: var(--color-gray-900);\n`,
@@ -1436,6 +1880,14 @@ export class Generator {
1436
1880
  ...smartLines,
1437
1881
  ...shadowLines,
1438
1882
  semantic,
1883
+ ...accentLines,
1884
+ ...surfaceAccentLines,
1885
+ ...dangerLines2,
1886
+ ...successLines2,
1887
+ ...warningLines2,
1888
+ ...infoLines2,
1889
+ ...surfaceStatusLines2,
1890
+ ...calloutDisplayLines2,
1439
1891
  backdrop,
1440
1892
  mesh,
1441
1893
  ].join("");
@@ -1517,12 +1969,10 @@ export class Generator {
1517
1969
 
1518
1970
  // Generate dark mode component adjustments (for components layer)
1519
1971
  #generateDarkModeComponentRules() {
1520
- const rules = /*css*/ `/* Callout dark mode adjustments */
1972
+ // Callout display tokens are now overridden in the dark mode token variables section,
1973
+ // so no explicit callout rules are needed here.
1974
+ const rules = /*css*/ `/* Dark mode component adjustments */
1521
1975
  html[data-theme="dark"] {
1522
- .callout-success { background-color: var(--color-success-50); border-color: var(--color-success-500); color: var(--color-success-900); }
1523
- .callout-info { background-color: var(--color-info-50); border-color: var(--color-info-500); color: var(--color-info-900); }
1524
- .callout-warning { background-color: var(--color-warning-50); border-color: var(--color-warning-500); color: var(--color-warning-900); }
1525
- .callout-danger, .callout-error { background-color: var(--color-danger-50); border-color: var(--color-danger-500); color: var(--color-danger-900); }
1526
1976
  img, video { opacity: 0.8; transition: opacity var(--transition-normal); }
1527
1977
  img:hover, video:hover { opacity: 1; }
1528
1978
  }`;
@@ -1791,7 +2241,7 @@ html[data-theme="dark"] .liquid-glass {
1791
2241
  :where(blockquote) {
1792
2242
  margin: 0 0 var(--spacing-4) 0;
1793
2243
  padding: var(--spacing-6) var(--spacing-8);
1794
- border-left: calc(var(--border-width-thick) + var(--border-width-thin)) solid var(--color-primary-500);
2244
+ border-left: calc(var(--border-width-thick) + var(--border-width-thin)) solid var(--color-link);
1795
2245
  background-color: var(--color-surface-elevated);
1796
2246
  border-radius: var(--radius-none);
1797
2247
  font-size: var(--font-size-lg);
@@ -1807,7 +2257,7 @@ html[data-theme="dark"] .liquid-glass {
1807
2257
  margin-top: var(--spacing-4);
1808
2258
  font-size: var(--font-size-base);
1809
2259
  font-style: normal;
1810
- color: var(--color-primary-500);
2260
+ color: var(--color-link);
1811
2261
  }
1812
2262
  }
1813
2263
 
@@ -1890,8 +2340,8 @@ html[data-theme="dark"] .liquid-glass {
1890
2340
  }
1891
2341
 
1892
2342
  :where(mark) {
1893
- background-color: var(--color-warning-200);
1894
- color: var(--color-warning-900);
2343
+ background-color: var(--color-warning-display-bg);
2344
+ color: var(--color-warning-display-text);
1895
2345
  padding: 0 var(--spacing-1);
1896
2346
  border-radius: var(--radius-sm);
1897
2347
  }
@@ -2044,7 +2494,7 @@ fieldset {
2044
2494
  margin-bottom: 0;
2045
2495
 
2046
2496
  &:hover {
2047
- color: var(--color-primary-700);
2497
+ color: var(--color-link-hover, var(--color-primary-text-hover));
2048
2498
  }
2049
2499
  }
2050
2500
 
@@ -2063,21 +2513,21 @@ fieldset {
2063
2513
  margin: 0;
2064
2514
  cursor: pointer;
2065
2515
  flex-shrink: 0;
2066
- accent-color: var(--color-primary-600);
2516
+ accent-color: var(--color-primary-fill);
2067
2517
 
2068
2518
  &:focus-visible {
2069
2519
  outline: none;
2070
2520
 
2071
2521
  box-shadow:
2072
- 0 0 0 2px var(--color-primary-500),
2522
+ 0 0 0 2px var(--color-focus-ring, var(--color-primary-500)),
2073
2523
  0 0 0 4px color-mix(in srgb,
2074
- var(--color-primary-500) 40%,
2524
+ var(--color-focus-ring, var(--color-primary-500)) 40%,
2075
2525
  transparent
2076
2526
  );
2077
2527
  }
2078
2528
 
2079
2529
  &:checked {
2080
- background-color: var(--color-primary-600);
2530
+ background-color: var(--color-primary-fill);
2081
2531
  }
2082
2532
 
2083
2533
  }
@@ -2155,7 +2605,7 @@ input, textarea, select {
2155
2605
  -webkit-appearance: none;
2156
2606
 
2157
2607
  &:focus {
2158
- border-color: var(--color-primary-500);
2608
+ border-color: var(--color-focus-ring, var(--color-primary-500));
2159
2609
  background-color: var(--color-surface-base);
2160
2610
  }
2161
2611
 
@@ -2168,10 +2618,10 @@ input, textarea, select {
2168
2618
  }
2169
2619
 
2170
2620
  &:invalid {
2171
- border-color: var(--color-danger-500);
2621
+ border-color: var(--color-danger-fill);
2172
2622
 
2173
2623
  &:focus {
2174
- box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-danger-500) ${Math.round(
2624
+ box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-danger-fill) ${Math.round(
2175
2625
  (focusRingOpacity || 0.3) * 100,
2176
2626
  )}%, transparent);
2177
2627
  }
@@ -2208,11 +2658,11 @@ input[type="range"] {
2208
2658
  width: var(--range-thumb-size, 28px);
2209
2659
  height: var(--range-thumb-size, 28px);
2210
2660
  margin-top: calc((var(--range-track-height, 8px) - var(--range-thumb-size, 28px)) / 2);
2211
- background: color-mix(in srgb, var(--color-primary-500) 15%, var(--color-surface-base));
2661
+ background: color-mix(in srgb, var(--color-primary-fill) 15%, var(--color-surface-base));
2212
2662
  border-radius: 50%;
2213
2663
  box-shadow: var(--shadow-sm);
2214
2664
  cursor: grab;
2215
- border: var(--border-width-thin) solid color-mix(in srgb, var(--color-primary-500) 30%, var(--color-border));
2665
+ border: var(--border-width-thin) solid color-mix(in srgb, var(--color-primary-fill) 30%, var(--color-border));
2216
2666
  }
2217
2667
 
2218
2668
  /* Mozilla track */
@@ -2226,10 +2676,10 @@ input[type="range"] {
2226
2676
  &::-moz-range-thumb {
2227
2677
  width: var(--range-thumb-size, 28px);
2228
2678
  height: var(--range-thumb-size, 28px);
2229
- background: color-mix(in srgb, var(--color-primary-500) 15%, var(--color-surface-base));
2679
+ background: color-mix(in srgb, var(--color-primary-fill) 15%, var(--color-surface-base));
2230
2680
  border-radius: 50%;
2231
2681
  box-shadow: var(--shadow-sm);
2232
- border: var(--border-width-thin) solid color-mix(in srgb, var(--color-primary-500) 30%, var(--color-border));
2682
+ border: var(--border-width-thin) solid color-mix(in srgb, var(--color-primary-fill) 30%, var(--color-border));
2233
2683
  transform: translateY(calc((var(--range-track-height, 8px) - var(--range-thumb-size, 28px)) / 2));
2234
2684
  }
2235
2685
 
@@ -2237,39 +2687,39 @@ input[type="range"] {
2237
2687
  &:hover::-webkit-slider-thumb,
2238
2688
  &:focus-visible::-webkit-slider-thumb {
2239
2689
  cursor: grabbing;
2240
- background: var(--color-primary-500);
2690
+ background: var(--color-primary-fill);
2241
2691
  box-shadow: 0 4px 12px rgba(0,0,0,0.2);
2242
- border-color: var(--color-primary-600);
2692
+ border-color: var(--color-primary-fill-hover);
2243
2693
  }
2244
2694
 
2245
2695
  /* Active state for WebKit */
2246
2696
  &:active::-webkit-slider-thumb {
2247
- background: var(--color-primary-600);
2697
+ background: var(--color-primary-fill-active);
2248
2698
  }
2249
2699
 
2250
2700
  /* Hover and focus states for Mozilla */
2251
2701
  &:hover::-moz-range-thumb,
2252
2702
  &:focus-visible::-moz-range-thumb {
2253
- background: var(--color-primary-500);
2703
+ background: var(--color-primary-fill);
2254
2704
  box-shadow: 0 4px 12px rgba(0,0,0,0.2);
2255
- border-color: var(--color-primary-600);
2705
+ border-color: var(--color-primary-fill-hover);
2256
2706
  cursor: grabbing;
2257
2707
  }
2258
2708
 
2259
2709
  /* Active state for Mozilla */
2260
2710
  &:active::-moz-range-thumb {
2261
- background: var(--color-primary-600);
2711
+ background: var(--color-primary-fill-active);
2262
2712
  }
2263
2713
  }
2264
2714
 
2265
2715
  /* Focus style for container to match input focus */
2266
2716
  .range-container:focus-within {
2267
- border-color: var(--color-primary-500);
2268
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary-500) 30%, transparent);
2717
+ border-color: var(--color-focus-ring, var(--color-primary-500));
2718
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-focus-ring, var(--color-primary-500)) 30%, transparent);
2269
2719
  }
2270
2720
 
2271
2721
  input[type="range"]:active::-moz-range-thumb {
2272
- background: var(--color-primary-600);
2722
+ background: var(--color-primary-fill-active);
2273
2723
  }
2274
2724
 
2275
2725
  input[type="color"] {
@@ -2345,27 +2795,27 @@ input[type="color"] {
2345
2795
 
2346
2796
  &:hover {
2347
2797
  background-color: var(--color-surface-subtle);
2348
- border-color: var(--color-primary-500);
2798
+ border-color: var(--color-primary-fill);
2349
2799
  }
2350
2800
 
2351
2801
  &:has(input[type="checkbox"]:checked),
2352
2802
  input[type="checkbox"]:checked + label:not(fieldset label):not(label[data-toggle]) {
2353
- background-color: color-mix(in oklab, var(--color-primary-500) 8%, transparent);
2354
- color: var(--color-primary-700);
2355
- border-color: var(--color-primary-500);
2803
+ background-color: color-mix(in oklab, var(--color-primary-fill) 8%, transparent);
2804
+ color: var(--color-primary-text);
2805
+ border-color: var(--color-primary-fill);
2356
2806
  border-width: var(--border-width-medium);
2357
2807
  font-weight: var(--font-weight-semibold);
2358
2808
 
2359
2809
  &:hover {
2360
- background-color: color-mix(in oklab, var(--color-primary-500) 15%, transparent);
2361
- border-color: var(--color-primary-600);
2810
+ background-color: color-mix(in oklab, var(--color-primary-fill) 15%, transparent);
2811
+ border-color: var(--color-primary-fill);
2362
2812
  }
2363
2813
  }
2364
2814
 
2365
2815
  &:has(input[type="checkbox"]:focus),
2366
2816
  input[type="checkbox"]:focus + label:not(fieldset label):not(label[data-toggle]) {
2367
2817
  outline: none;
2368
- box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-primary-500) ${Math.round(
2818
+ box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-focus-ring, var(--color-primary-500)) ${Math.round(
2369
2819
  (focusRingOpacity || 0.3) * 100,
2370
2820
  )}%, transparent);
2371
2821
  }
@@ -2399,6 +2849,7 @@ input[type="radio"]:not(fieldset input[type="radio"]) {
2399
2849
  opacity: 1;
2400
2850
  appearance: auto;
2401
2851
  -webkit-appearance: auto;
2852
+ accent-color: var(--color-primary-fill);
2402
2853
 
2403
2854
  &:disabled {
2404
2855
  cursor: not-allowed;
@@ -2453,7 +2904,7 @@ fieldset[role="group"].buttons {
2453
2904
 
2454
2905
  &:hover {
2455
2906
  background-color: var(--color-surface-subtle);
2456
- border-color: var(--color-primary-500);
2907
+ border-color: var(--color-primary-fill);
2457
2908
  color: var(--color-text-primary);
2458
2909
  }
2459
2910
 
@@ -2462,20 +2913,20 @@ fieldset[role="group"].buttons {
2462
2913
  }
2463
2914
 
2464
2915
  &:has(input:is([type="radio"], [type="checkbox"]):checked) {
2465
- background-color: color-mix(in oklab, var(--color-primary-500) 8%, transparent);
2466
- border-color: var(--color-primary-500);
2916
+ background-color: color-mix(in oklab, var(--color-primary-fill) 8%, transparent);
2917
+ border-color: var(--color-primary-fill);
2467
2918
  border-width: var(--border-width-medium);
2468
2919
  font-weight: var(--font-weight-semibold);
2469
2920
 
2470
2921
  &:hover {
2471
- background-color: color-mix(in oklab, var(--color-primary-500) 15%, transparent);
2472
- border-color: var(--color-primary-600);
2922
+ background-color: color-mix(in oklab, var(--color-primary-fill) 15%, transparent);
2923
+ border-color: var(--color-primary-fill-hover);
2473
2924
  }
2474
2925
  }
2475
2926
 
2476
2927
  &:has(input:is([type="radio"], [type="checkbox"]):focus) {
2477
2928
  outline: none;
2478
- box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-primary-500) ${Math.round(
2929
+ box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-focus-ring, var(--color-primary-500)) ${Math.round(
2479
2930
  (focusRingOpacity || 0.3) * 100,
2480
2931
  )}%, transparent);
2481
2932
  }
@@ -2561,7 +3012,7 @@ label[data-toggle] {
2561
3012
  &:has(input[type="checkbox"]:checked) {
2562
3013
  &.with-icons .toggle-knob::before {
2563
3014
  content: "✓";
2564
- color: var(--color-primary-600);
3015
+ color: var(--color-primary-contrast, white);
2565
3016
  }
2566
3017
 
2567
3018
  .toggle-switch {
@@ -2575,7 +3026,7 @@ label[data-toggle] {
2575
3026
 
2576
3027
  &:has(input[type="checkbox"]:focus) .toggle-switch,
2577
3028
  &:focus-visible .toggle-switch {
2578
- outline: 2px solid var(--color-primary-500);
3029
+ outline: 2px solid var(--color-focus-ring, var(--color-primary-500));
2579
3030
  outline-offset: 2px;
2580
3031
  }
2581
3032
 
@@ -2693,8 +3144,8 @@ label[data-color] {
2693
3144
  }
2694
3145
 
2695
3146
  &:focus-within .color-control {
2696
- border-color: var(--color-primary-500);
2697
- box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-primary-500) ${Math.round(
3147
+ border-color: var(--color-focus-ring, var(--color-primary-500));
3148
+ box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-focus-ring, var(--color-primary-500)) ${Math.round(
2698
3149
  (focusRingOpacity || 0.3) * 100,
2699
3150
  )}%, transparent);
2700
3151
  }
@@ -2764,7 +3215,7 @@ button, .btn, input[type="submit"], input[type="button"], input[type="reset"] {
2764
3215
 
2765
3216
  &:focus {
2766
3217
  outline: none;
2767
- box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-primary-500) ${Math.round(
3218
+ box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-focus-ring, var(--color-primary-500)) ${Math.round(
2768
3219
  (focusRingOpacity || 0.3) * 100,
2769
3220
  )}%, transparent);
2770
3221
  }
@@ -2784,19 +3235,19 @@ button, .btn, input[type="submit"], input[type="button"], input[type="reset"] {
2784
3235
  border-color: var(--color-primary-fill);
2785
3236
 
2786
3237
  &:hover {
2787
- background-color: color-mix(in oklab, var(--color-primary-fill) 90%, black 10%);
2788
- border-color: color-mix(in oklab, var(--color-primary-fill) 90%, black 10%);
3238
+ background-color: var(--color-primary-fill-hover);
3239
+ border-color: var(--color-primary-fill-hover);
2789
3240
  color: white;
2790
3241
  }
2791
3242
 
2792
3243
  &:active {
2793
- background-color: color-mix(in oklab, var(--color-primary-fill) 80%, black 20%);
2794
- border-color: color-mix(in oklab, var(--color-primary-fill) 80%, black 20%);
3244
+ background-color: var(--color-primary-fill-active);
3245
+ border-color: var(--color-primary-fill-active);
2795
3246
  color: white;
2796
3247
  }
2797
3248
 
2798
3249
  &:focus {
2799
- box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-primary-500) ${Math.round(
3250
+ box-shadow: 0 0 0 ${focusWidth}px color-mix(in oklab, var(--color-focus-ring, var(--color-primary-500)) ${Math.round(
2800
3251
  (focusRingOpacity || 0.3) * 100,
2801
3252
  )}%, transparent);
2802
3253
  }
@@ -2820,13 +3271,13 @@ button, .btn, input[type="submit"], input[type="button"], input[type="reset"] {
2820
3271
 
2821
3272
  .btn-outline {
2822
3273
  background-color: transparent;
2823
- color: var(--color-primary-500);
2824
- border-color: var(--color-primary-500);
3274
+ color: var(--color-link);
3275
+ border-color: var(--color-link);
2825
3276
 
2826
3277
  &:hover {
2827
- background-color: var(--color-primary-500);
3278
+ background-color: var(--color-primary-fill);
2828
3279
  color: var(--color-primary-contrast, #ffffff);
2829
- border-color: var(--color-primary-500);
3280
+ border-color: var(--color-primary-fill);
2830
3281
 
2831
3282
  pds-icon {
2832
3283
  color: var(--color-primary-contrast, #ffffff);
@@ -2834,8 +3285,8 @@ button, .btn, input[type="submit"], input[type="button"], input[type="reset"] {
2834
3285
  }
2835
3286
 
2836
3287
  &:active {
2837
- background-color: color-mix(in oklab, var(--color-primary-500) 80%, black 20%);
2838
- border-color: color-mix(in oklab, var(--color-primary-500) 80%, black 20%);
3288
+ background-color: var(--color-primary-fill-active);
3289
+ border-color: var(--color-primary-fill-active);
2839
3290
  color: var(--color-primary-contrast, #ffffff);
2840
3291
 
2841
3292
  pds-icon {
@@ -2851,31 +3302,31 @@ button, .btn, input[type="submit"], input[type="button"], input[type="reset"] {
2851
3302
  }
2852
3303
 
2853
3304
  .btn-danger {
2854
- background-color: var(--color-danger-600);
3305
+ background-color: var(--color-danger-fill);
2855
3306
  color: white;
2856
- border-color: var(--color-danger-600);
3307
+ border-color: var(--color-danger-fill);
2857
3308
 
2858
3309
  &:hover {
2859
- background-color: color-mix(in oklab, var(--color-danger-600) 90%, black 10%);
2860
- border-color: color-mix(in oklab, var(--color-danger-600) 90%, black 10%);
3310
+ background-color: var(--color-danger-fill-hover);
3311
+ border-color: var(--color-danger-fill-hover);
2861
3312
  color: white;
2862
3313
  }
2863
3314
 
2864
3315
  &:active {
2865
- background-color: color-mix(in oklab, var(--color-danger-600) 80%, black 20%);
2866
- border-color: color-mix(in oklab, var(--color-danger-600) 80%, black 20%);
3316
+ background-color: var(--color-danger-fill-active);
3317
+ border-color: var(--color-danger-fill-active);
2867
3318
  color: white;
2868
3319
  }
2869
3320
  }
2870
3321
 
2871
3322
  .btn-danger.btn-outline {
2872
3323
  background-color: transparent;
2873
- color: var(--color-danger-600);
2874
- border-color: var(--color-danger-600);
3324
+ color: var(--color-danger-fill);
3325
+ border-color: var(--color-danger-fill);
2875
3326
 
2876
3327
  &:hover {
2877
- background-color: var(--color-danger-600);
2878
- border-color: var(--color-danger-600);
3328
+ background-color: var(--color-danger-fill);
3329
+ border-color: var(--color-danger-fill);
2879
3330
  color: white;
2880
3331
 
2881
3332
  pds-icon {
@@ -2884,8 +3335,8 @@ button, .btn, input[type="submit"], input[type="button"], input[type="reset"] {
2884
3335
  }
2885
3336
 
2886
3337
  &:active {
2887
- background-color: color-mix(in oklab, var(--color-danger-600) 80%, black 20%);
2888
- border-color: color-mix(in oklab, var(--color-danger-600) 80%, black 20%);
3338
+ background-color: var(--color-danger-fill-active);
3339
+ border-color: var(--color-danger-fill-active);
2889
3340
  color: white;
2890
3341
 
2891
3342
  pds-icon {
@@ -3268,25 +3719,25 @@ tbody {
3268
3719
  }
3269
3720
  /* Variants: success/info/warning/danger mapped to tokens */
3270
3721
  .callout-success {
3271
- background-color: var(--color-success-50);
3272
- border-color: var(--color-success-600);
3273
- color: var(--color-success-900);
3722
+ background-color: var(--color-success-display-bg);
3723
+ border-color: var(--color-success-display-border);
3724
+ color: var(--color-success-display-text);
3274
3725
  }
3275
3726
  .callout-info {
3276
- background-color: var(--color-info-50);
3277
- border-color: var(--color-info-600);
3278
- color: var(--color-info-900);
3727
+ background-color: var(--color-info-display-bg);
3728
+ border-color: var(--color-info-display-border);
3729
+ color: var(--color-info-display-text);
3279
3730
  }
3280
3731
  .callout-warning {
3281
- background-color: var(--color-warning-50);
3282
- border-color: var(--color-warning-600);
3283
- color: var(--color-warning-900);
3732
+ background-color: var(--color-warning-display-bg);
3733
+ border-color: var(--color-warning-display-border);
3734
+ color: var(--color-warning-display-text);
3284
3735
  }
3285
3736
  .callout-danger,
3286
3737
  .callout-error {
3287
- background-color: var(--color-danger-50);
3288
- border-color: var(--color-danger-600);
3289
- color: var(--color-danger-900);
3738
+ background-color: var(--color-danger-display-bg);
3739
+ border-color: var(--color-danger-display-border);
3740
+ color: var(--color-danger-display-text);
3290
3741
  }
3291
3742
 
3292
3743
  .callout-title {
@@ -3393,7 +3844,7 @@ tbody {
3393
3844
  }
3394
3845
 
3395
3846
  &:focus-visible {
3396
- box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-primary-500) 30%, transparent);
3847
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-focus-ring, var(--color-primary-500)) 30%, transparent);
3397
3848
  }
3398
3849
 
3399
3850
  /* Chevron indicator */
@@ -3453,22 +3904,22 @@ tbody {
3453
3904
  }
3454
3905
 
3455
3906
  .badge-primary, .badge-secondary, .badge-success, .badge-info, .badge-warning, .badge-danger { color: white; }
3456
- .badge-primary { background-color: var(--color-primary-600); }
3907
+ .badge-primary { background-color: var(--color-primary-fill); }
3457
3908
  .badge-secondary { background-color: var(--color-secondary-600); }
3458
- .badge-success { background-color: var(--color-success-600); }
3459
- .badge-info { background-color: var(--color-info-600); }
3460
- .badge-warning { background-color: var(--color-warning-600); }
3461
- .badge-danger { background-color: var(--color-danger-600); }
3909
+ .badge-success { background-color: var(--color-success-fill); }
3910
+ .badge-info { background-color: var(--color-info-fill); }
3911
+ .badge-warning { background-color: var(--color-warning-fill); }
3912
+ .badge-danger { background-color: var(--color-danger-fill); }
3462
3913
 
3463
3914
  .badge-outline {
3464
3915
  background-color: transparent;
3465
3916
  border: var(--border-width-thin) solid currentColor;
3466
- &.badge-primary { color: var(--color-text-primary); }
3467
- &.badge-secondary { color: var(--color-secondary-600); }
3468
- &.badge-success { color: var(--color-success-600); }
3469
- &.badge-info { color: var(--color-info-600); }
3470
- &.badge-warning { color: var(--color-warning-600); }
3471
- &.badge-danger { color: var(--color-danger-600); }
3917
+ &.badge-primary { color: var(--color-primary-text); }
3918
+ &.badge-secondary { color: var(--color-text-secondary); }
3919
+ &.badge-success { color: var(--color-success-text); }
3920
+ &.badge-info { color: var(--color-info-text); }
3921
+ &.badge-warning { color: var(--color-warning-text); }
3922
+ &.badge-danger { color: var(--color-danger-text); }
3472
3923
  }
3473
3924
 
3474
3925
  .badge-sm { padding: 2px var(--spacing-1); font-size: 10px; }
@@ -3810,7 +4261,7 @@ pds-tabstrip {
3810
4261
  }
3811
4262
 
3812
4263
  &:focus-visible {
3813
- outline: var(--focus-ring-width, 2px) solid var(--color-primary-500);
4264
+ outline: var(--focus-ring-width, 2px) solid var(--color-focus-ring, var(--color-primary-500));
3814
4265
  outline-offset: -2px;
3815
4266
  border-radius: var(--radius-sm);
3816
4267
  z-index: 1;
@@ -3818,14 +4269,14 @@ pds-tabstrip {
3818
4269
 
3819
4270
  /* Active tab */
3820
4271
  &[aria-current="page"] {
3821
- color: var(--color-primary-600);
4272
+ color: var(--color-link);
3822
4273
  font-weight: var(--font-weight-semibold);
3823
- border-bottom-color: var(--color-primary-600);
4274
+ border-bottom-color: var(--color-link);
3824
4275
 
3825
4276
  &:hover {
3826
- color: var(--color-primary-700);
3827
- border-bottom-color: var(--color-primary-700);
3828
- background-color: var(--color-primary-50);
4277
+ color: var(--color-link-hover);
4278
+ border-bottom-color: var(--color-link-hover);
4279
+ background-color: color-mix(in oklab, var(--color-link) 10%, transparent);
3829
4280
  }
3830
4281
  }
3831
4282
  }
@@ -3936,13 +4387,13 @@ pds-icon {
3936
4387
 
3937
4388
 
3938
4389
  /* Icon color utilities */
3939
- .icon-primary, pds-icon.primary { color: var(--color-primary-600); }
3940
- .icon-secondary, pds-icon.secondary { color: var(--color-secondary-600); }
3941
- .icon-accent, pds-icon.accent { color: var(--color-accent-600); }
3942
- .icon-success, pds-icon.success { color: var(--color-success-600); }
3943
- .icon-warning, pds-icon.warning { color: var(--color-warning-600); }
3944
- .icon-danger, pds-icon.danger { color: var(--color-danger-600); }
3945
- .icon-info, pds-icon.info { color: var(--color-info-600); }
4390
+ .icon-primary, pds-icon.primary { color: var(--color-primary-text); }
4391
+ .icon-secondary, pds-icon.secondary { color: var(--color-text-secondary); }
4392
+ .icon-accent, pds-icon.accent { color: var(--color-accent-text); }
4393
+ .icon-success, pds-icon.success { color: var(--color-success-text); }
4394
+ .icon-warning, pds-icon.warning { color: var(--color-warning-text); }
4395
+ .icon-danger, pds-icon.danger { color: var(--color-danger-text); }
4396
+ .icon-info, pds-icon.info { color: var(--color-info-text); }
3946
4397
  .icon-muted, pds-icon.muted { color: var(--color-text-muted); }
3947
4398
  .icon-subtle, pds-icon.subtle { color: var(--color-text-subtle); }
3948
4399
 
@@ -4144,7 +4595,7 @@ nav[data-dropdown] {
4144
4595
  gap: var(--spacing-2);
4145
4596
 
4146
4597
  &.danger {
4147
- color: var(--color-danger-600);
4598
+ color: var(--color-danger-text);
4148
4599
  }
4149
4600
  }
4150
4601
 
@@ -4464,7 +4915,7 @@ nav[data-dropdown] {
4464
4915
 
4465
4916
  a {
4466
4917
  &:hover {
4467
- color: var(--color-primary-600);
4918
+ color: var(--color-link-hover);
4468
4919
  }
4469
4920
  }
4470
4921
  }
@@ -4725,7 +5176,7 @@ nav[data-dropdown] {
4725
5176
  box-sizing: border-box;
4726
5177
  font: inherit;
4727
5178
  color: var(--color-primary-contrast, white);
4728
- background: var(--color-primary-600);
5179
+ background: var(--color-primary-fill);
4729
5180
  padding: var(--spacing-2) var(--spacing-4);
4730
5181
  border: 0;
4731
5182
  border-radius: var(--radius-md);
@@ -4744,11 +5195,11 @@ nav[data-dropdown] {
4744
5195
 
4745
5196
  :where(button):hover:not(:disabled) {
4746
5197
  opacity: 0.9;
4747
- background-color: var(--color-primary-700);
5198
+ background-color: var(--color-primary-fill-hover);
4748
5199
  }
4749
5200
 
4750
5201
  :where(button):focus-visible {
4751
- outline: 2px solid var(--color-primary-500);
5202
+ outline: 2px solid var(--color-focus-ring, var(--color-primary-500));
4752
5203
  outline-offset: 2px;
4753
5204
  }
4754
5205
 
@@ -4781,8 +5232,8 @@ nav[data-dropdown] {
4781
5232
  :where(select):focus-visible,
4782
5233
  :where(textarea):focus-visible {
4783
5234
  outline: none;
4784
- border-color: var(--color-primary-500);
4785
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary-500) 30%, transparent);
5235
+ border-color: var(--color-focus-ring, var(--color-primary-500));
5236
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-focus-ring, var(--color-primary-500)) 30%, transparent);
4786
5237
  }
4787
5238
 
4788
5239
  :where(input):disabled,
@@ -4809,22 +5260,32 @@ nav[data-dropdown] {
4809
5260
 
4810
5261
  /* Link primitives */
4811
5262
  :where(a) {
4812
- color: var(--color-primary-text, var(--color-primary-600));
5263
+ color: var(--color-link, var(--color-primary-text, var(--color-primary-600)));
4813
5264
  text-decoration: underline;
4814
5265
  text-underline-offset: 0.2em;
4815
- transition: opacity var(--transition-fast);
5266
+ transition: color var(--transition-fast), opacity var(--transition-fast);
4816
5267
  }
4817
5268
 
4818
5269
  :where(a):hover {
4819
- opacity: 0.8;
5270
+ color: var(--color-link-hover, var(--color-link, var(--color-primary-text, var(--color-primary-600))));
5271
+ opacity: 0.9;
5272
+ }
5273
+
5274
+ :where(a):visited {
5275
+ color: var(--color-link-visited, var(--color-link, var(--color-primary-text, var(--color-primary-600))));
4820
5276
  }
4821
5277
 
4822
5278
  :where(a):focus-visible {
4823
- outline: 2px solid var(--color-primary-500);
5279
+ outline: 2px solid var(--color-focus-ring, var(--color-primary-500));
4824
5280
  outline-offset: 2px;
4825
5281
  border-radius: var(--radius-sm);
4826
5282
  }
4827
5283
 
5284
+ ::selection {
5285
+ background: var(--color-selection-bg, var(--color-primary-text, var(--color-primary-600)));
5286
+ color: var(--color-selection-text, var(--color-primary-contrast, #ffffff));
5287
+ }
5288
+
4828
5289
  /* Form primitives */
4829
5290
  :where(label) {
4830
5291
  display: block;
@@ -4882,23 +5343,23 @@ nav[data-dropdown] {
4882
5343
  }
4883
5344
 
4884
5345
  &:has(input[type="checkbox"]:checked)::before {
4885
- background: var(--color-primary-600);
4886
- border-color: var(--color-primary-600);
5346
+ background: var(--color-primary-fill);
5347
+ border-color: var(--color-primary-fill);
4887
5348
  }
4888
5349
 
4889
5350
  &:has(input[type="checkbox"]:focus)::before {
4890
- outline: 2px solid var(--color-primary-500);
5351
+ outline: 2px solid var(--color-focus-ring, var(--color-primary-500));
4891
5352
  outline-offset: 2px;
4892
5353
  }
4893
5354
 
4894
5355
  &:has(input[type="checkbox"]:not(:disabled)):hover::before {
4895
- border-color: var(--color-primary-600);
5356
+ border-color: var(--color-primary-fill);
4896
5357
  background: var(--color-surface-subtle);
4897
5358
  }
4898
5359
 
4899
5360
  &:has(input[type="checkbox"]:checked:not(:disabled)):hover::before {
4900
- background: var(--color-primary-700);
4901
- border-color: var(--color-primary-700);
5361
+ background: var(--color-primary-fill-hover);
5362
+ border-color: var(--color-primary-fill-hover);
4902
5363
  }
4903
5364
 
4904
5365
  &:has(input[type="checkbox"]:disabled) {
@@ -5189,13 +5650,13 @@ ${this.#generateBorderGradientUtilities()}
5189
5650
 
5190
5651
  /* btn-primary stays vibrant in any context */
5191
5652
  & .btn-primary {
5192
- background-color: var(--color-primary-500);
5193
- border-color: var(--color-primary-500);
5653
+ background-color: var(--color-primary-fill);
5654
+ border-color: var(--color-primary-fill);
5194
5655
  color: var(--color-primary-contrast, #ffffff);
5195
5656
 
5196
5657
  &:hover {
5197
- background-color: var(--color-primary-400);
5198
- border-color: var(--color-primary-400);
5658
+ background-color: var(--color-primary-fill-hover);
5659
+ border-color: var(--color-primary-fill-hover);
5199
5660
  }
5200
5661
  }
5201
5662
  }
@@ -5228,7 +5689,7 @@ html:not([data-theme="dark"]) .surface-inverse {
5228
5689
  }
5229
5690
 
5230
5691
  & a:not([class*="btn"]) {
5231
- color: var(--color-primary-300, #7dd3fc);
5692
+ color: var(--color-link);
5232
5693
  }
5233
5694
  }
5234
5695
 
@@ -5260,7 +5721,7 @@ html[data-theme="dark"] .surface-inverse {
5260
5721
  }
5261
5722
 
5262
5723
  & a:not([class*="btn"]) {
5263
- color: var(--color-primary-600, #0284c7);
5724
+ color: var(--color-link);
5264
5725
  }
5265
5726
  }
5266
5727
 
@@ -5650,16 +6111,43 @@ export const ${name}CSS = \`${escapedCSS}\`;
5650
6111
  * @param {object} designConfig - A full or partial PDS config object
5651
6112
  * @param {object} [options]
5652
6113
  * @param {number} [options.minContrast=4.5] - Minimum contrast ratio for normal text
5653
- * @returns {{ ok: boolean, issues: Array<{path:string, message:string, ratio:number, min:number, context?:string}> }}
6114
+ * @param {boolean} [options.warnOnHueDrift=true] - Emit non-blocking brand-identity hue drift warnings for darkMode overrides.
6115
+ * @param {number} [options.maxHueDrift=35] - Maximum recommended hue delta (degrees) before warning.
6116
+ * @returns {{ ok: boolean, issues: Array<{path:string, message:string, ratio:number, min:number, context?:string}>, warnings: Array<{path:string, message:string, context?:string}> }}
5654
6117
  */
5655
6118
  export function validateDesign(designConfig = {}, options = {}) {
5656
6119
  const MIN = Number(options.minContrast || 4.5);
5657
6120
  const MIN_MUTED = Number(options.minMutedContrast || 3.0);
5658
6121
  const EXTENDED = Boolean(options.extendedChecks);
6122
+ const ENABLE_HUE_DRIFT_WARNINGS = options.warnOnHueDrift !== false;
6123
+ const MAX_HUE_DRIFT = Number.isFinite(Number(options.maxHueDrift))
6124
+ ? Number(options.maxHueDrift)
6125
+ : 35;
6126
+ const MIN_SAT_FOR_HUE_WARNING = Number.isFinite(
6127
+ Number(options.minSaturationForHueWarning),
6128
+ )
6129
+ ? Number(options.minSaturationForHueWarning)
6130
+ : 18;
5659
6131
 
5660
6132
  // Local helpers (keep public; no dependency on private Generator methods)
6133
+ const normalizeHex = (hex) => {
6134
+ const raw = String(hex || "").trim();
6135
+ const m = raw.match(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
6136
+ if (!m) return null;
6137
+ const body = m[1];
6138
+ const full =
6139
+ body.length === 3
6140
+ ? body
6141
+ .split("")
6142
+ .map((c) => c + c)
6143
+ .join("")
6144
+ : body;
6145
+ return `#${full.toLowerCase()}`;
6146
+ };
5661
6147
  const hexToRgb = (hex) => {
5662
- const h = String(hex || "").replace("#", "");
6148
+ const normalized = normalizeHex(hex);
6149
+ if (!normalized) return null;
6150
+ const h = normalized.replace("#", "");
5663
6151
  const full =
5664
6152
  h.length === 3
5665
6153
  ? h
@@ -5671,7 +6159,9 @@ export function validateDesign(designConfig = {}, options = {}) {
5671
6159
  return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 };
5672
6160
  };
5673
6161
  const luminance = (hex) => {
5674
- const { r, g, b } = hexToRgb(hex);
6162
+ const rgb = hexToRgb(hex);
6163
+ if (!rgb) return 0;
6164
+ const { r, g, b } = rgb;
5675
6165
  const srgb = [r / 255, g / 255, b / 255].map((v) =>
5676
6166
  v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4),
5677
6167
  );
@@ -5685,8 +6175,39 @@ export function validateDesign(designConfig = {}, options = {}) {
5685
6175
  const darker = Math.min(L1, L2);
5686
6176
  return (lighter + 0.05) / (darker + 0.05);
5687
6177
  };
6178
+ const hexToHsl = (hex) => {
6179
+ const rgb = hexToRgb(hex);
6180
+ if (!rgb) return null;
6181
+ const r = rgb.r / 255;
6182
+ const g = rgb.g / 255;
6183
+ const b = rgb.b / 255;
6184
+ const max = Math.max(r, g, b);
6185
+ const min = Math.min(r, g, b);
6186
+ const delta = max - min;
6187
+
6188
+ let h = 0;
6189
+ if (delta !== 0) {
6190
+ if (max === r) h = ((g - b) / delta) % 6;
6191
+ else if (max === g) h = (b - r) / delta + 2;
6192
+ else h = (r - g) / delta + 4;
6193
+ h *= 60;
6194
+ if (h < 0) h += 360;
6195
+ }
6196
+
6197
+ const l = (max + min) / 2;
6198
+ const s =
6199
+ delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
6200
+
6201
+ return { h, s: s * 100, l: l * 100 };
6202
+ };
6203
+ const hueDelta = (h1, h2) => {
6204
+ if (!Number.isFinite(h1) || !Number.isFinite(h2)) return null;
6205
+ const diff = Math.abs(h1 - h2);
6206
+ return Math.min(diff, 360 - diff);
6207
+ };
5688
6208
 
5689
6209
  const issues = [];
6210
+ const warnings = [];
5690
6211
  try {
5691
6212
  // Build tokens from the candidate config
5692
6213
  const gen = new Generator({ design: structuredClone(designConfig) });
@@ -5889,6 +6410,45 @@ export function validateDesign(designConfig = {}, options = {}) {
5889
6410
  }
5890
6411
  }
5891
6412
  }
6413
+
6414
+ if (ENABLE_HUE_DRIFT_WARNINGS) {
6415
+ const configured = designConfig?.colors || {};
6416
+ const configuredDark = configured?.darkMode || {};
6417
+ const families = ["primary", "secondary", "accent"];
6418
+
6419
+ families.forEach((family) => {
6420
+ const lightBase = configured?.[family];
6421
+ const darkOverride = configuredDark?.[family];
6422
+ if (!lightBase || !darkOverride) return;
6423
+
6424
+ const lightHsl = hexToHsl(lightBase);
6425
+ const darkHsl = hexToHsl(darkOverride);
6426
+ if (!lightHsl || !darkHsl) return;
6427
+
6428
+ // For near-neutrals hue isn't meaningful enough to warn.
6429
+ if (
6430
+ lightHsl.s < MIN_SAT_FOR_HUE_WARNING &&
6431
+ darkHsl.s < MIN_SAT_FOR_HUE_WARNING
6432
+ ) {
6433
+ return;
6434
+ }
6435
+
6436
+ const drift = hueDelta(lightHsl.h, darkHsl.h);
6437
+ if (drift == null || drift <= MAX_HUE_DRIFT) return;
6438
+
6439
+ warnings.push({
6440
+ path: `/colors/darkMode/${family}`,
6441
+ message: `Dark mode ${family} hue drifts ${drift.toFixed(
6442
+ 1,
6443
+ )}deg from light ${family} (${lightHsl.h.toFixed(
6444
+ 1,
6445
+ )}deg -> ${darkHsl.h.toFixed(
6446
+ 1,
6447
+ )}deg). This may reduce cross-theme brand identity consistency.`,
6448
+ context: `dark/identity-hue-${family}`,
6449
+ });
6450
+ });
6451
+ }
5892
6452
  } catch (err) {
5893
6453
  issues.push({
5894
6454
  path: "/",
@@ -5898,7 +6458,7 @@ export function validateDesign(designConfig = {}, options = {}) {
5898
6458
  });
5899
6459
  }
5900
6460
 
5901
- return { ok: issues.length === 0, issues };
6461
+ return { ok: issues.length === 0, issues, warnings };
5902
6462
  }
5903
6463
 
5904
6464
  /**
@@ -5907,7 +6467,7 @@ export function validateDesign(designConfig = {}, options = {}) {
5907
6467
  *
5908
6468
  * @param {Array<object>} designs - Array of design configs; items may include an optional `name` property.
5909
6469
  * @param {object} [options] - Options forwarded to validateDesign (e.g., { minContrast })
5910
- * @returns {{ ok: boolean, results: Array<{ name?: string, ok: boolean, issues: Array<{path:string, message:string, ratio:number, min:number, context?:string}> }> }}
6470
+ * @returns {{ ok: boolean, results: Array<{ name?: string, ok: boolean, issues: Array<{path:string, message:string, ratio:number, min:number, context?:string}>, warnings?: Array<{path:string, message:string, context?:string}> }> }}
5911
6471
  */
5912
6472
  export function validateDesigns(designs = [], options = {}) {
5913
6473
  const results = [];