@tenphi/glaze 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -699,9 +699,21 @@ function defaultConfig() {
699
699
  },
700
700
  autoFlip: true,
701
701
  pastel: false,
702
- inferRole: true
702
+ inferRole: true,
703
+ contrastLevel: "auto"
703
704
  };
704
705
  }
706
+ /**
707
+ * Validate an authored `contrastLevel`. Numbers clamp to 0–100 so a slider
708
+ * can't produce an out-of-range level; a non-finite number throws rather than
709
+ * silently reading as `'auto'`, which would turn a `NaN` slider value into an
710
+ * invisible no-op.
711
+ */
712
+ function normalizeContrastLevel(value) {
713
+ if (value === void 0 || value === "auto") return "auto";
714
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`glaze: contrastLevel must be a finite number 0–100 or 'auto' (got ${String(value)}).`);
715
+ return Math.max(0, Math.min(100, value));
716
+ }
705
717
  let globalConfig = defaultConfig();
706
718
  /**
707
719
  * Monotonic counter incremented on every `configure()` / `resetConfig()`
@@ -740,7 +752,8 @@ function configure(config) {
740
752
  shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
741
753
  autoFlip: config.autoFlip ?? globalConfig.autoFlip,
742
754
  pastel: false,
743
- inferRole: config.inferRole ?? globalConfig.inferRole
755
+ inferRole: config.inferRole ?? globalConfig.inferRole,
756
+ contrastLevel: normalizeContrastLevel(config.contrastLevel ?? globalConfig.contrastLevel)
744
757
  };
745
758
  }
746
759
  function resetConfig() {
@@ -770,7 +783,8 @@ function mergeConfig(base, override) {
770
783
  shadowTuning: override.shadowTuning ?? base.shadowTuning,
771
784
  autoFlip: override.autoFlip ?? base.autoFlip,
772
785
  pastel: override.pastel ?? false,
773
- inferRole: override.inferRole ?? base.inferRole
786
+ inferRole: override.inferRole ?? base.inferRole,
787
+ contrastLevel: normalizeContrastLevel(override.contrastLevel ?? base.contrastLevel)
774
788
  };
775
789
  }
776
790
  /**
@@ -794,6 +808,8 @@ function freezeConfigForExport(instanceLocal, exportArg) {
794
808
  inferRole: effective.inferRole
795
809
  };
796
810
  if (effective.shadowTuning !== void 0) out.shadowTuning = effective.shadowTuning;
811
+ const authoredLevel = exportArg?.contrastLevel ?? instanceLocal?.contrastLevel;
812
+ if (authoredLevel !== void 0) out.contrastLevel = normalizeContrastLevel(authoredLevel);
797
813
  return structuredClone(out);
798
814
  }
799
815
 
@@ -920,6 +936,43 @@ function pairHC(p) {
920
936
  function clamp(v, min, max) {
921
937
  return Math.max(min, Math.min(max, v));
922
938
  }
939
+ function lerp(a, b, f) {
940
+ return a + (b - a) * f;
941
+ }
942
+ /**
943
+ * Blend fraction at/above which an un-interpolable HC pair switches from its
944
+ * normal entry to its high-contrast one (level 50).
945
+ */
946
+ const PAIR_SWITCH = .5;
947
+ /** Blend fraction (0–1) for an authored 0–100 contrast level. */
948
+ function levelFraction(level) {
949
+ return clamp(level, 0, 100) / 100;
950
+ }
951
+ /**
952
+ * Manual-contrast blend fraction (0–1) for a resolved config, or `undefined`
953
+ * in `'auto'` mode (the two-tier normal + high-contrast model).
954
+ *
955
+ * Note `contrastLevel: 0` yields `0`, not `undefined`: "pinned to normal
956
+ * contrast with no high-contrast tier" is a distinct state from `'auto'`.
957
+ */
958
+ function contrastFraction(config) {
959
+ const level = config.contrastLevel;
960
+ if (typeof level !== "number" || !Number.isFinite(level)) return void 0;
961
+ return levelFraction(level);
962
+ }
963
+ /**
964
+ * A numeric HC pair at a blend fraction (shadow `intensity`, mix `value`).
965
+ *
966
+ * The endpoints return the authored entry *by identity* rather than through
967
+ * float arithmetic — `a + (b - a) * 1` is not bit-exactly `b` in IEEE 754, and
968
+ * levels 0 / 100 must reproduce the classic output exactly.
969
+ */
970
+ function numberAt(p, f) {
971
+ if (!Array.isArray(p)) return p;
972
+ if (f <= 0) return p[0];
973
+ if (f >= 1) return p[1];
974
+ return lerp(p[0], p[1], f);
975
+ }
923
976
  /** Whether a tone value is an extreme keyword (`'max'` / `'min'`). */
924
977
  function isExtremeTone(value) {
925
978
  return value === "max" || value === "min";
@@ -964,6 +1017,34 @@ function parseToneValue(value) {
964
1017
  };
965
1018
  }
966
1019
  /**
1020
+ * Parse an authored tone pair at a manual-contrast blend fraction, returning
1021
+ * the same normalized shape as {@link parseToneValue}.
1022
+ *
1023
+ * Same-kind numeric pairs interpolate their magnitude — absolute tones
1024
+ * (`[30, 20]` → 25 at level 50) and relative deltas (`['+10', '+20']` → +15).
1025
+ * Everything else is un-interpolable and switches at {@link PAIR_SWITCH}:
1026
+ * extremes carry no magnitude, and a mixed-kind pair would change *which*
1027
+ * resolver branch runs mid-ramp (absolute remap vs. extreme-against-base vs.
1028
+ * base-anchored delta), putting a discontinuity at level 100 — exactly where
1029
+ * the output must stay bit-exact.
1030
+ *
1031
+ * Returns a parsed struct rather than a `ToneValue` so a blended relative
1032
+ * delta never round-trips through a `'+15.5'` string, and so `kind` — which
1033
+ * selects the resolver branch — cannot drift.
1034
+ */
1035
+ function parseToneValueAt(p, f) {
1036
+ if (!Array.isArray(p)) return parseToneValue(p);
1037
+ if (f <= 0) return parseToneValue(p[0]);
1038
+ if (f >= 1) return parseToneValue(p[1]);
1039
+ const normal = parseToneValue(p[0]);
1040
+ const hc = parseToneValue(p[1]);
1041
+ if (normal.kind === hc.kind && normal.kind !== "extreme") return {
1042
+ kind: normal.kind,
1043
+ value: lerp(normal.value, hc.value, f)
1044
+ };
1045
+ return f < PAIR_SWITCH ? normal : hc;
1046
+ }
1047
+ /**
967
1048
  * Compute the effective hue for a color, given the theme seed hue
968
1049
  * and an optional per-color hue override.
969
1050
  */
@@ -999,7 +1080,9 @@ function isAbsoluteTone(tone) {
999
1080
  * - the resolved-variant edge adapter (`variantToOkhsl`),
1000
1081
  * - the per-scheme tone mapping that replaced the Möbius dark curve
1001
1082
  * (`mapToneForScheme`), the dark desaturation reducer, and the solver's scheme
1002
- * tone range.
1083
+ * tone range,
1084
+ * - the scheme tone window, including the high-contrast bypass and its
1085
+ * continuous generalization under a manual contrast level.
1003
1086
  *
1004
1087
  * See `docs/okhst.md` for the full specification and the calibrated
1005
1088
  * default constants.
@@ -1098,7 +1181,12 @@ function normalizeToneWindow(win) {
1098
1181
  /**
1099
1182
  * Resolve the active tone window for a scheme as OKHSL-lightness endpoints.
1100
1183
  * - HC variants always return the full range `[0, 100]` with the mode eps.
1101
- * - `false` (= "no clamping") is treated as `[0, 100]` with the reference eps.
1184
+ * - Under a manual `contrastLevel`, each endpoint is interpolated toward that
1185
+ * full range: light `10 → 0` / `100 → 100`, dark `15 → 0` / `95 → 100`. The
1186
+ * endpoints are exact — level 0 returns the configured window and level 100
1187
+ * the same literal the HC branch returns.
1188
+ * - `false` (= "no clamping") is treated as `[0, 100]` with the reference eps,
1189
+ * and is therefore level-invariant.
1102
1190
  */
1103
1191
  function activeWindow(isHighContrast, kind, config) {
1104
1192
  const win = normalizeToneWindow(kind === "dark" ? config.darkTone : config.lightTone);
@@ -1107,7 +1195,18 @@ function activeWindow(isHighContrast, kind, config) {
1107
1195
  hi: 100,
1108
1196
  eps: win.eps
1109
1197
  };
1110
- return win;
1198
+ const f = contrastFraction(config);
1199
+ if (f === void 0 || f <= 0) return win;
1200
+ if (f >= 1) return {
1201
+ lo: 0,
1202
+ hi: 100,
1203
+ eps: win.eps
1204
+ };
1205
+ return {
1206
+ lo: lerp(win.lo, 0, f),
1207
+ hi: lerp(win.hi, 100, f),
1208
+ eps: win.eps
1209
+ };
1111
1210
  }
1112
1211
  /**
1113
1212
  * Remap an authored tone (0–100) into a scheme window and return the final
@@ -1131,6 +1230,9 @@ function remapToneToLightness(authorTone, win) {
1131
1230
  * The window remap uses the mode's render eps to land a final OKHSL
1132
1231
  * lightness; that lightness is then re-expressed as canonical tone so
1133
1232
  * relative offsets and contrast stay comparable across schemes.
1233
+ *
1234
+ * A manual `contrastLevel` widens the window continuously (see
1235
+ * `activeWindow`); inversion is unaffected.
1134
1236
  */
1135
1237
  function mapToneForScheme(authorTone, mode, isDark, isHighContrast, config) {
1136
1238
  if (mode === "static") return clamp(authorTone, 0, 100);
@@ -1145,7 +1247,8 @@ function mapSaturationDark(s, mode, config) {
1145
1247
  /**
1146
1248
  * Tone search range (0–1) for the contrast solver in a given scheme.
1147
1249
  * `static` searches the full range; otherwise the scheme window's tone
1148
- * endpoints (HC bypasses to full range).
1250
+ * endpoints (HC bypasses to full range, a manual `contrastLevel` widens
1251
+ * toward it — see `activeWindow`).
1149
1252
  */
1150
1253
  function schemeToneRange(isDark, mode, isHighContrast, config) {
1151
1254
  if (mode === "static") return [0, 1];
@@ -1164,7 +1267,8 @@ function schemeToneRange(isDark, mode, isHighContrast, config) {
1164
1267
  * converges quickly.
1165
1268
  *
1166
1269
  * Public API: `findToneForContrast`, `findValueForMixContrast`,
1167
- * `resolveMinContrast`, `resolveContrastForMode`, `apcaContrast`.
1270
+ * `resolveMinContrast`, `resolveContrastForMode`, `resolveContrastForLevel`,
1271
+ * `apcaContrast`.
1168
1272
  */
1169
1273
  /**
1170
1274
  * Luminance of a linear-sRGB color in the basis the metric expects: WCAG
@@ -1237,6 +1341,13 @@ function pickPair(p, isHighContrast) {
1237
1341
  return Array.isArray(p) ? isHighContrast ? p[1] : p[0] : p;
1238
1342
  }
1239
1343
  /**
1344
+ * The metric a single `ContrastSpec` measures in. Bare numbers and WCAG
1345
+ * presets are `'wcag'`; only an `{ apca }` object is `'apca'`.
1346
+ */
1347
+ function contrastMetricOf(spec) {
1348
+ return typeof spec === "object" && "apca" in spec ? "apca" : "wcag";
1349
+ }
1350
+ /**
1240
1351
  * Resolve a `ContrastSpec` (already selected from any outer HC pair) for a
1241
1352
  * given mode into `{ metric, target }`. Handles the inner metric HC pair and
1242
1353
  * preset resolution. `polarity` is passed through to the result for the APCA
@@ -1272,6 +1383,36 @@ function resolveContrastForMode(spec, isHighContrast, polarity, outerExplicitHC)
1272
1383
  target: resolveWcagTarget(pickPair(spec.wcag, isHighContrast), isHighContrast, !!outerExplicitHC || innerExplicitHC)
1273
1384
  };
1274
1385
  }
1386
+ /**
1387
+ * Resolve a `contrast` spec — including its outer `[normal, highContrast]`
1388
+ * pair — at a manual contrast `level` (0–100), by resolving both ends and
1389
+ * interpolating the numeric target.
1390
+ *
1391
+ * Level 0 / 100 delegate verbatim to {@link resolveContrastForMode}, so the
1392
+ * endpoints are bit-identical to classic normal / high-contrast resolution.
1393
+ * Resolving each end fully first also means every rule that governs the
1394
+ * escalation still applies at both ends for free: preset mapping, the
1395
+ * `APCA_MAX_LC` clamp, and the explicit-HC suppression that cancels the
1396
+ * `AA → AAA` promotion and the `+15 Lc` enhancement.
1397
+ *
1398
+ * Throws when the two ends measure in different metrics — a WCAG ratio and an
1399
+ * APCA Lc live on different scales, so there is no target between them. Such a
1400
+ * pair is rejected at authoring time by `validateColorDefs`; this is the guard
1401
+ * for direct callers.
1402
+ */
1403
+ function resolveContrastForLevel(spec, level, polarity) {
1404
+ const outerExplicitHC = Array.isArray(spec);
1405
+ const f = levelFraction(level);
1406
+ const normal = resolveContrastForMode(pairNormal(spec), false, polarity, outerExplicitHC);
1407
+ if (f <= 0) return normal;
1408
+ const hc = resolveContrastForMode(pairHC(spec), true, polarity, outerExplicitHC);
1409
+ if (f >= 1) return hc;
1410
+ if (normal.metric !== hc.metric) throw new Error(`glaze: a "contrast" pair cannot switch metric between its normal (${normal.metric}) and high-contrast (${hc.metric}) entry — the two targets are on different scales. Use one metric for both entries.`);
1411
+ return {
1412
+ ...normal,
1413
+ target: lerp(normal.target, hc.target, f)
1414
+ };
1415
+ }
1275
1416
  const APCA_EXPONENTS = {
1276
1417
  mainTRC: 2.4,
1277
1418
  normBG: .56,
@@ -1412,7 +1553,7 @@ function wcagToneSeed(yBase, target, darker) {
1412
1553
  return Math.max(0, Math.min(1, toneFromY(yClamped, REF_EPS) / 100));
1413
1554
  }
1414
1555
  function solveNearestContrast(opts) {
1415
- const { lum, yBase, metric, target, searchTarget, lo, hi, searchAnchor, distanceAnchor, epsilon, maxIterations, flip, initialIsLower, polarity } = opts;
1556
+ const { lum, yBase, metric, target, searchTarget, lo, hi, searchAnchor, distanceAnchor, epsilon, maxIterations, flip, preferInitial, initialIsLower, polarity } = opts;
1416
1557
  const runBranch = (lower) => lower ? searchBranch(lum, lo, searchAnchor, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor, polarity) : searchBranch(lum, searchAnchor, hi, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor, polarity);
1417
1558
  const initialResult = runBranch(initialIsLower);
1418
1559
  initialResult.met = initialResult.contrast >= target;
@@ -1423,14 +1564,20 @@ function solveNearestContrast(opts) {
1423
1564
  if (flip) {
1424
1565
  const oppositeResult = (initialIsLower ? distanceAnchor < hi : distanceAnchor > lo) ? runBranch(!initialIsLower) : null;
1425
1566
  if (oppositeResult) oppositeResult.met = oppositeResult.contrast >= target;
1426
- if (initialResult.met && oppositeResult?.met) return Math.abs(initialResult.pos - distanceAnchor) <= Math.abs(oppositeResult.pos - distanceAnchor) ? {
1427
- ...initialResult,
1428
- lower: initialIsLower
1429
- } : {
1430
- ...oppositeResult,
1431
- lower: !initialIsLower,
1432
- flipped: true
1433
- };
1567
+ if (initialResult.met && oppositeResult?.met) {
1568
+ if (preferInitial) return {
1569
+ ...initialResult,
1570
+ lower: initialIsLower
1571
+ };
1572
+ return Math.abs(initialResult.pos - distanceAnchor) <= Math.abs(oppositeResult.pos - distanceAnchor) ? {
1573
+ ...initialResult,
1574
+ lower: initialIsLower
1575
+ } : {
1576
+ ...oppositeResult,
1577
+ lower: !initialIsLower,
1578
+ flipped: true
1579
+ };
1580
+ }
1434
1581
  if (initialResult.met) return {
1435
1582
  ...initialResult,
1436
1583
  lower: initialIsLower
@@ -1493,6 +1640,7 @@ function findToneForContrast(options) {
1493
1640
  epsilon,
1494
1641
  maxIterations,
1495
1642
  flip: options.flip ?? false,
1643
+ preferInitial: options.preferInitial ?? false,
1496
1644
  initialIsLower: initialIsDarker,
1497
1645
  polarity
1498
1646
  });
@@ -1543,6 +1691,7 @@ function findValueForMixContrast(options) {
1543
1691
  epsilon,
1544
1692
  maxIterations,
1545
1693
  flip: options.flip ?? false,
1694
+ preferInitial: false,
1546
1695
  initialIsLower,
1547
1696
  polarity
1548
1697
  });
@@ -1744,6 +1893,22 @@ function computeShadow(bg, fg, intensity, tuning) {
1744
1893
  * resolver runs. `topoSort` orders defs so each color is processed after
1745
1894
  * its base / bg / fg / target dependencies.
1746
1895
  */
1896
+ /**
1897
+ * Reject a `contrast` pair whose two entries measure in different metrics.
1898
+ *
1899
+ * A WCAG ratio and an APCA Lc are different scales, so `[4.5, { apca: 75 }]`
1900
+ * asks one color to be judged two incompatible ways. There is no target between
1901
+ * them for a manual `contrastLevel` to resolve, and even in `'auto'` mode the
1902
+ * two variants become incomparable. Pick one metric and pair its values
1903
+ * instead: `{ wcag: [4.5, 7] }` or `{ apca: [60, 90] }`.
1904
+ */
1905
+ function assertConsistentContrastMetric(name, contrast) {
1906
+ if (contrast === void 0 || !Array.isArray(contrast)) return;
1907
+ const normal = contrastMetricOf(pairNormal(contrast));
1908
+ const hc = contrastMetricOf(pairHC(contrast));
1909
+ if (normal === hc) return;
1910
+ throw new Error(`glaze: color "${name}" has a "contrast" pair that switches metric (${normal} → ${hc}). A WCAG ratio and an APCA Lc are different scales; use one metric for both entries, e.g. { ${normal}: [normal, highContrast] }.`);
1911
+ }
1747
1912
  function validateColorDefs(defs, externalBases) {
1748
1913
  const localNames = new Set(Object.keys(defs));
1749
1914
  const allNames = new Set([...localNames, ...externalBases ? externalBases.keys() : []]);
@@ -1762,9 +1927,11 @@ function validateColorDefs(defs, externalBases) {
1762
1927
  if (!allNames.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
1763
1928
  if (localNames.has(def.base) && isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
1764
1929
  if (localNames.has(def.target) && isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
1930
+ assertConsistentContrastMetric(name, def.contrast);
1765
1931
  continue;
1766
1932
  }
1767
1933
  const regDef = def;
1934
+ assertConsistentContrastMetric(name, regDef.contrast);
1768
1935
  if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
1769
1936
  if (regDef.tone !== void 0 && !isAbsoluteTone(regDef.tone) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "tone" without "base".`);
1770
1937
  if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
@@ -1881,7 +2048,10 @@ function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase
1881
2048
  * Color resolution engine.
1882
2049
  *
1883
2050
  * Runs the four-pass solver (light → light-HC → dark → dark-HC) that
1884
- * turns a `ColorMap` into a fully resolved `ResolvedColor` per name.
2051
+ * turns a `ColorMap` into a fully resolved `ResolvedColor` per name — or,
2052
+ * under a manual `contrastLevel`, a two-pass solver (light → dark) whose
2053
+ * authored HC pairs, tone window, and contrast targets are interpolated at
2054
+ * that level and whose high-contrast slots mirror the normal ones.
1885
2055
  * Owns the per-scheme resolve helpers for regular, shadow, and mix
1886
2056
  * color defs.
1887
2057
  *
@@ -1970,11 +2140,27 @@ function resolveRoleInMap(name, def, defs, inferRole, roles) {
1970
2140
  function resolveRole(name, def, ctx) {
1971
2141
  return resolveRoleInMap(name, def, ctx.defs, ctx.config.inferRole, ctx.roles);
1972
2142
  }
1973
- function resolveContrastSpec(spec, isHighContrast, polarity) {
2143
+ function resolveContrastSpec(spec, isHighContrast, config, polarity) {
2144
+ if (!isHighContrast && contrastFraction(config) !== void 0) return resolveContrastForLevel(spec, config.contrastLevel, polarity);
1974
2145
  const outerExplicitHC = Array.isArray(spec);
1975
2146
  return resolveContrastForMode(isHighContrast ? pairHC(spec) : pairNormal(spec), isHighContrast, polarity, outerExplicitHC);
1976
2147
  }
1977
2148
  /**
2149
+ * The authored tone for this pass: the pair's own entry in `'auto'` mode, or
2150
+ * the manual-level interpolation. Returns the parsed struct so the `kind` —
2151
+ * which selects the branch in `resolveDependentColor` — is fixed here once.
2152
+ */
2153
+ function passTone(tone, isHighContrast, config) {
2154
+ const f = contrastFraction(config);
2155
+ return f === void 0 || isHighContrast ? parseToneValue(isHighContrast ? pairHC(tone) : pairNormal(tone)) : parseToneValueAt(tone, f);
2156
+ }
2157
+ /** The authored numeric pair for this pass (shadow `intensity`, mix `value`). */
2158
+ function passNumber(p, isHighContrast, config) {
2159
+ const f = contrastFraction(config);
2160
+ if (f === void 0 || isHighContrast) return isHighContrast ? pairHC(p) : pairNormal(p);
2161
+ return numberAt(p, f);
2162
+ }
2163
+ /**
1978
2164
  * Apply the relative-tone delta against a base, honoring `flip`.
1979
2165
  *
1980
2166
  * When `flip` is on and `base + delta` falls outside `[0, 100]`, try mirroring
@@ -1990,65 +2176,118 @@ function applyToneFlip(delta, baseTone, flip) {
1990
2176
  if (mirrored >= 0 && mirrored <= 100) return -delta;
1991
2177
  return delta;
1992
2178
  }
1993
- function resolveRootColor(def, isHighContrast) {
1994
- const rawT = def.tone;
2179
+ /**
2180
+ * Dark position for an extreme tone (`'max'` / `'min'`) anchored to a base.
2181
+ *
2182
+ * A windowed dark remap compresses the base→extreme span, so the pair loses
2183
+ * contrast in dark. Instead, measure the tone shift the light scheme applied
2184
+ * between the base and the extreme, then replay it against the base's dark
2185
+ * tone: `auto` mirrors the sign (both ends invert), `fixed` keeps it. The
2186
+ * result is clamped to `[0, 100]` only — crossing the dark tone window is
2187
+ * intended, since the author asked for the extreme.
2188
+ */
2189
+ function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, baseDarkTone, config) {
2190
+ const delta = mapToneForScheme(authorTone, mode, false, isHighContrast, config) - getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
2191
+ return clamp(baseDarkTone + (mode === "auto" ? -delta : delta), 0, 100);
2192
+ }
2193
+ function resolveRootColor(def, isHighContrast, config) {
2194
+ return clamp(passTone(def.tone, isHighContrast, config).value, 0, 100);
2195
+ }
2196
+ /**
2197
+ * Effective hue and saturation for one color in one scheme.
2198
+ *
2199
+ * Dark schemes read the dark seed pair and the color's `darkHue` /
2200
+ * `darkSaturation`, each falling back to its light counterpart. An explicit
2201
+ * dark saturation — on the seed or the def — is taken literally; the global
2202
+ * `darkDesaturation` reduction applies only when neither is authored.
2203
+ * `mode: 'static'` pins one value across every scheme, so it skips all of it.
2204
+ *
2205
+ * Shared by `resolveColorForScheme` (the emitted variant) and
2206
+ * `resolveDependentColor` (the contrast solver's inputs) so the solver always
2207
+ * measures the color it is actually going to produce.
2208
+ */
2209
+ function resolveChannels(def, ctx, isDark) {
2210
+ const mode = def.mode ?? "auto";
2211
+ const satFactor = clamp(def.saturation ?? 1, 0, 1);
2212
+ if (!isDark || mode === "static") return {
2213
+ hue: resolveEffectiveHue(ctx.hue, def.hue),
2214
+ saturation: clamp(satFactor * ctx.saturation / 100, 0, 1)
2215
+ };
2216
+ const darkSeedSaturation = ctx.darkSaturation ?? ctx.saturation;
2217
+ const darkFactor = clamp(def.darkSaturation ?? satFactor, 0, 1);
2218
+ const explicitDark = def.darkSaturation !== void 0 || ctx.darkSaturation !== void 0;
2219
+ const raw = darkFactor * darkSeedSaturation / 100;
1995
2220
  return {
1996
- authorTone: clamp(parseToneValue(isHighContrast ? pairHC(rawT) : pairNormal(rawT)).value, 0, 100),
1997
- satFactor: clamp(def.saturation ?? 1, 0, 1)
2221
+ hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
2222
+ saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
1998
2223
  };
1999
2224
  }
2000
- function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue, polarity, effectivePastel) {
2225
+ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels, polarity, effectivePastel) {
2001
2226
  const baseName = def.base;
2002
2227
  const baseResolved = ctx.resolved.get(baseName);
2003
2228
  if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
2004
2229
  const mode = def.mode ?? "auto";
2005
- const satFactor = clamp(def.saturation ?? 1, 0, 1);
2006
2230
  const flip = def.autoFlip ?? ctx.config.autoFlip;
2007
2231
  const pastel = effectivePastel;
2008
2232
  const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
2009
2233
  const baseTone = baseVariant.t * 100;
2010
2234
  let preferredTone;
2235
+ let isExtreme = false;
2011
2236
  const rawTone = def.tone;
2012
2237
  if (rawTone === void 0) preferredTone = baseTone;
2013
2238
  else {
2014
- const parsed = parseToneValue(isHighContrast ? pairHC(rawTone) : pairNormal(rawTone));
2239
+ const parsed = passTone(rawTone, isHighContrast, ctx.config);
2015
2240
  if (parsed.kind === "relative") if (isDark && mode === "auto") {
2016
2241
  const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
2017
2242
  preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, ctx.config);
2018
2243
  } else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
2019
- else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, ctx.config);
2244
+ else if (parsed.kind === "extreme" && isDark && mode !== "static") {
2245
+ isExtreme = true;
2246
+ preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, baseTone, ctx.config);
2247
+ } else {
2248
+ isExtreme = parsed.kind === "extreme";
2249
+ preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, ctx.config);
2250
+ }
2020
2251
  }
2021
2252
  const rawContrast = def.contrast;
2022
2253
  if (rawContrast !== void 0) {
2023
- const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
2024
- const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
2254
+ const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, ctx.config, polarity);
2025
2255
  const baseOkhsl = toOkhslVariant(baseVariant);
2026
2256
  const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
2027
2257
  const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
2028
2258
  let initialDirection;
2029
2259
  if (preferredTone < baseTone) initialDirection = "darker";
2030
2260
  else if (preferredTone > baseTone) initialDirection = "lighter";
2031
- const result = findToneForContrast({
2032
- hue: effectiveHue,
2033
- saturation: effectiveSat,
2034
- preferredTone: clamp(preferredTone / 100, toneRange[0], toneRange[1]),
2261
+ const seedTone = isExtreme ? clamp(preferredTone / 100, 0, 1) : clamp(preferredTone / 100, toneRange[0], toneRange[1]);
2262
+ const solve = {
2263
+ hue: channels.hue,
2264
+ saturation: channels.saturation,
2265
+ preferredTone: seedTone,
2035
2266
  baseLinearRgb,
2036
- contrast: resolvedContrast,
2037
2267
  toneRange: [0, 1],
2038
- initialDirection,
2039
2268
  flip,
2040
2269
  pastel
2270
+ };
2271
+ const level = contrastFraction(ctx.config);
2272
+ let preferInitial = false;
2273
+ if (level !== void 0 && level > 0 && level < 1) {
2274
+ initialDirection = findToneForContrast({
2275
+ ...solve,
2276
+ contrast: resolveContrastForLevel(rawContrast, level < PAIR_SWITCH ? 0 : 100, polarity),
2277
+ initialDirection
2278
+ }).tone * 100 < baseTone ? "darker" : "lighter";
2279
+ preferInitial = true;
2280
+ }
2281
+ const result = findToneForContrast({
2282
+ ...solve,
2283
+ contrast: resolvedContrast,
2284
+ initialDirection,
2285
+ preferInitial
2041
2286
  });
2042
2287
  if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
2043
- return {
2044
- tone: result.tone * 100,
2045
- satFactor
2046
- };
2288
+ return result.tone * 100;
2047
2289
  }
2048
- return {
2049
- tone: clamp(preferredTone, 0, 100),
2050
- satFactor
2051
- };
2290
+ return clamp(preferredTone, 0, 100);
2052
2291
  }
2053
2292
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
2054
2293
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
@@ -2056,27 +2295,14 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
2056
2295
  const regDef = def;
2057
2296
  const mode = regDef.mode ?? "auto";
2058
2297
  const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
2059
- const effectiveHue = resolveEffectiveHue(ctx.hue, regDef.hue);
2298
+ const channels = resolveChannels(regDef, ctx, isDark);
2060
2299
  const polarity = roleToPolarity(resolveRole(name, def, ctx));
2061
2300
  const pastel = regDef.pastel ?? ctx.config.pastel;
2062
- let finalTone;
2063
- let satFactor;
2064
- if (isRoot) {
2065
- const root = resolveRootColor(regDef, isHighContrast);
2066
- finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
2067
- satFactor = root.satFactor;
2068
- } else {
2069
- const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue, polarity, pastel);
2070
- finalTone = dep.tone;
2071
- satFactor = dep.satFactor;
2072
- }
2073
- const baseSat = satFactor * ctx.saturation / 100;
2074
- const finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
2075
- const toneFraction = clamp(finalTone / 100, 0, 1);
2301
+ const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast, ctx.config), mode, isDark, isHighContrast, ctx.config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
2076
2302
  return {
2077
- h: effectiveHue,
2078
- s: clamp(finalSat, 0, 1),
2079
- t: toneFraction,
2303
+ h: channels.hue,
2304
+ s: channels.saturation,
2305
+ t: clamp(finalTone / 100, 0, 1),
2080
2306
  alpha: regDef.opacity ?? 1,
2081
2307
  pastel
2082
2308
  };
@@ -2085,7 +2311,7 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
2085
2311
  const bgVariant = toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.bg), isDark, isHighContrast));
2086
2312
  let fgVariant;
2087
2313
  if (def.fg) fgVariant = toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast));
2088
- const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
2314
+ const intensity = passNumber(def.intensity, isHighContrast, ctx.config);
2089
2315
  const tuning = resolveShadowTuning(def.tuning, ctx.config.shadowTuning);
2090
2316
  return {
2091
2317
  ...toToneVariant(computeShadow(bgVariant, fgVariant, intensity, tuning)),
@@ -2134,7 +2360,7 @@ function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
2134
2360
  const targetResolved = ctx.resolved.get(def.target);
2135
2361
  const baseVariant = toOkhslVariant(getSchemeVariant(baseResolved, isDark, isHighContrast));
2136
2362
  const targetVariant = toOkhslVariant(getSchemeVariant(targetResolved, isDark, isHighContrast));
2137
- let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
2363
+ let t = clamp(passNumber(def.value, isHighContrast, ctx.config), 0, 100) / 100;
2138
2364
  const blend = def.blend ?? "opaque";
2139
2365
  const space = def.space ?? "okhsl";
2140
2366
  const polarity = roleToPolarity(resolveRole(name, def, ctx));
@@ -2142,7 +2368,7 @@ function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
2142
2368
  const baseLinear = okhslVariantToLinearRgb(baseVariant, baseVariant.pastel ?? ctx.config.pastel);
2143
2369
  const targetLinear = okhslVariantToLinearRgb(targetVariant, targetVariant.pastel ?? ctx.config.pastel);
2144
2370
  if (def.contrast !== void 0) {
2145
- const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast, polarity);
2371
+ const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast, ctx.config, polarity);
2146
2372
  const metric = resolvedContrast.metric;
2147
2373
  let luminanceAt;
2148
2374
  if (blend === "transparent" || space === "srgb") luminanceAt = (v) => metricLuminance(metric, linearSrgbLerp(baseLinear, targetLinear, v));
@@ -2226,9 +2452,14 @@ function seedField(order, ctx, field, source) {
2226
2452
  }
2227
2453
  }
2228
2454
  /**
2229
- * After the four passes, surface chromatic contrast drift (§10): a color
2455
+ * After the passes, surface chromatic contrast drift (§10): a color
2230
2456
  * resolved with a `base` + `contrast` may land slightly under the contrast
2231
2457
  * its tone implies because chromatic luminance drifts from the gray tone.
2458
+ *
2459
+ * Under a manual `contrastLevel` only the two emitted variants are checked
2460
+ * (the high-contrast slots are mirrors), and the spec is resolved at the level
2461
+ * so the check measures the output against the target it was actually solved
2462
+ * for.
2232
2463
  */
2233
2464
  function verifyContrastDrift(order, defs, result, config) {
2234
2465
  const roles = /* @__PURE__ */ new Map();
@@ -2241,7 +2472,7 @@ function verifyContrastDrift(order, defs, result, config) {
2241
2472
  const base = result.get(regDef.base);
2242
2473
  if (!color || !base) continue;
2243
2474
  const polarity = roleToPolarity(resolveRoleInMap(name, def, defs, config.inferRole, roles));
2244
- for (const s of [
2475
+ const schemes = [
2245
2476
  {
2246
2477
  isDark: false,
2247
2478
  isHighContrast: false,
@@ -2262,8 +2493,11 @@ function verifyContrastDrift(order, defs, result, config) {
2262
2493
  isHighContrast: true,
2263
2494
  field: "darkContrast"
2264
2495
  }
2265
- ]) {
2266
- const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast, polarity);
2496
+ ];
2497
+ const manual = contrastFraction(config) !== void 0;
2498
+ for (const s of schemes) {
2499
+ if (manual && s.isHighContrast) continue;
2500
+ const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast, config, polarity);
2267
2501
  const cVariant = color[s.field];
2268
2502
  const bVariant = base[s.field];
2269
2503
  const cOkhsl = toOkhslVariant(cVariant);
@@ -2276,26 +2510,32 @@ function verifyContrastDrift(order, defs, result, config) {
2276
2510
  }
2277
2511
  }
2278
2512
  }
2279
- function resolveAllColors(hue, saturation, defs, config, externalBases) {
2513
+ function resolveAllColors(seed, defs, config, externalBases) {
2280
2514
  validateColorDefs(defs, externalBases);
2281
2515
  const order = topoSort(defs);
2282
2516
  const ctx = {
2283
- hue,
2284
- saturation,
2517
+ ...seed,
2285
2518
  defs,
2286
2519
  resolved: /* @__PURE__ */ new Map(),
2287
2520
  config,
2288
2521
  roles: /* @__PURE__ */ new Map()
2289
2522
  };
2290
2523
  if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
2524
+ const manual = contrastFraction(config) !== void 0;
2291
2525
  const lightMap = runPass(order, defs, ctx, false, false, "light");
2292
- seedField(order, ctx, "lightContrast", lightMap);
2293
- const lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
2526
+ let lightHCMap = lightMap;
2527
+ if (!manual) {
2528
+ seedField(order, ctx, "lightContrast", lightMap);
2529
+ lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
2530
+ }
2294
2531
  seedField(order, ctx, "dark", lightMap);
2295
2532
  seedField(order, ctx, "darkContrast", lightHCMap);
2296
2533
  const darkMap = runPass(order, defs, ctx, true, false, "dark");
2297
- seedField(order, ctx, "darkContrast", darkMap);
2298
- const darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
2534
+ let darkHCMap = darkMap;
2535
+ if (!manual) {
2536
+ seedField(order, ctx, "darkContrast", darkMap);
2537
+ darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
2538
+ }
2299
2539
  const result = /* @__PURE__ */ new Map();
2300
2540
  for (const name of order) result.set(name, {
2301
2541
  name,
@@ -2314,8 +2554,12 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
2314
2554
  /**
2315
2555
  * Hue channel planning for `splitHue` exports.
2316
2556
  *
2317
- * Builds per-color hue var references and scheme-independent `--*-hue`
2318
- * declarations for oklch CSS / Tasty output when every color is pastel.
2557
+ * Builds per-color hue var references and the `--*-hue` declarations backing
2558
+ * them for oklch CSS / Tasty output when every color is pastel.
2559
+ *
2560
+ * Hue is scheme-independent unless the theme or a color authors a dark hue,
2561
+ * so declarations are collected per scheme (`'light'` / `'dark'`) and the dark
2562
+ * set is emitted only when it actually differs — see `darkHueDeclarations`.
2319
2563
  */
2320
2564
  const ACHROMATIC_EPSILON = 1e-6;
2321
2565
  function cssProp(prefix, name, suffix) {
@@ -2324,7 +2568,11 @@ function cssProp(prefix, name, suffix) {
2324
2568
  function isAchromatic(v) {
2325
2569
  return v.s <= ACHROMATIC_EPSILON;
2326
2570
  }
2327
- function themeHuePlan(name, def, variant, ctx) {
2571
+ /** Seed hue backing `--{baseName}-hue` in a given scheme. */
2572
+ function schemeSeedHue(ctx, scheme) {
2573
+ return scheme === "dark" ? ctx.darkSeedHue ?? ctx.seedHue : ctx.seedHue;
2574
+ }
2575
+ function themeHuePlan(name, def, variant, ctx, scheme) {
2328
2576
  if (def === void 0 || isShadowDef(def) || isMixDef(def) || isAchromatic(variant)) return {
2329
2577
  hueVar: "",
2330
2578
  inline: true,
@@ -2332,13 +2580,25 @@ function themeHuePlan(name, def, variant, ctx) {
2332
2580
  };
2333
2581
  const regDef = def;
2334
2582
  const baseHueVar = `var(--${ctx.baseName}-hue)`;
2335
- if (regDef.hue === void 0) return {
2583
+ const prop = cssProp(ctx.prefix, name, "-hue");
2584
+ if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) {
2585
+ const pinned = resolveEffectiveHue(ctx.seedHue, regDef.hue);
2586
+ return {
2587
+ hueVar: `var(${prop})`,
2588
+ inline: false,
2589
+ declarations: [{
2590
+ prop,
2591
+ value: String(pinned)
2592
+ }]
2593
+ };
2594
+ }
2595
+ const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
2596
+ if (authored === void 0) return {
2336
2597
  hueVar: baseHueVar,
2337
2598
  inline: false,
2338
2599
  declarations: []
2339
2600
  };
2340
- const parsed = parseRelativeOrAbsolute(regDef.hue);
2341
- const prop = cssProp(ctx.prefix, name, "-hue");
2601
+ const parsed = parseRelativeOrAbsolute(authored);
2342
2602
  if (parsed.relative) {
2343
2603
  const sign = parsed.value >= 0 ? "+" : "-";
2344
2604
  const magnitude = Math.abs(parsed.value);
@@ -2362,13 +2622,13 @@ function themeHuePlan(name, def, variant, ctx) {
2362
2622
  }]
2363
2623
  };
2364
2624
  }
2365
- function standaloneHuePlan(name, variant, ctx) {
2625
+ function standaloneHuePlan(name, variant, ctx, scheme) {
2366
2626
  if (isAchromatic(variant)) return {
2367
2627
  hueVar: "",
2368
2628
  inline: true,
2369
2629
  declarations: []
2370
2630
  };
2371
- const hue = ctx.resolvedHue ?? variant.h;
2631
+ const hue = scheme === "dark" ? variant.h : ctx.resolvedHue ?? variant.h;
2372
2632
  const prop = cssProp(ctx.prefix, name, "-hue");
2373
2633
  return {
2374
2634
  hueVar: `var(${prop})`,
@@ -2379,12 +2639,12 @@ function standaloneHuePlan(name, variant, ctx) {
2379
2639
  }]
2380
2640
  };
2381
2641
  }
2382
- function buildHuePlan(name, def, variant, ctx) {
2383
- if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx);
2384
- return themeHuePlan(name, def, variant, ctx);
2642
+ function buildHuePlan(name, def, variant, ctx, scheme = "light") {
2643
+ if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx, scheme);
2644
+ return themeHuePlan(name, def, variant, ctx, scheme);
2385
2645
  }
2386
2646
  /** Collect unique hue declarations across all colors (theme + per-color). */
2387
- function collectHueDeclarations(resolved, ctx) {
2647
+ function collectHueDeclarations(resolved, ctx, scheme = "light") {
2388
2648
  if (ctx.emitDeclarations === false) return [];
2389
2649
  const seen = /* @__PURE__ */ new Set();
2390
2650
  const out = [];
@@ -2395,15 +2655,32 @@ function collectHueDeclarations(resolved, ctx) {
2395
2655
  };
2396
2656
  if (ctx.mode === "theme") push({
2397
2657
  prop: `--${ctx.baseName}-hue`,
2398
- value: String(ctx.seedHue)
2658
+ value: String(schemeSeedHue(ctx, scheme))
2399
2659
  });
2400
2660
  for (const [name, color] of resolved) {
2401
2661
  const def = ctx.defs[name];
2402
- const plan = buildHuePlan(name, def, color.light, ctx);
2662
+ const plan = buildHuePlan(name, def, scheme === "dark" ? color.dark : color.light, ctx, scheme);
2403
2663
  for (const decl of plan.declarations) push(decl);
2404
2664
  }
2405
2665
  return out;
2406
2666
  }
2667
+ /**
2668
+ * Dark-scheme hue declarations, or `[]` when dark resolves to the same hues as
2669
+ * light (the common case — no `darkHue` anywhere).
2670
+ *
2671
+ * When they differ, the **whole** set is returned rather than only the entries
2672
+ * whose text changed. A relative declaration reads
2673
+ * `calc(var(--theme-hue) + N)` in both schemes even though its resolved value
2674
+ * moves with the seed, and custom properties substitute `var()` at
2675
+ * computed-value time on the declaring element — so re-declaring
2676
+ * `--{baseName}-hue` alone would leave inherited per-color vars still holding
2677
+ * the light-substituted value.
2678
+ */
2679
+ function darkHueDeclarations(resolved, ctx) {
2680
+ const dark = collectHueDeclarations(resolved, ctx, "dark");
2681
+ const light = collectHueDeclarations(resolved, ctx, "light");
2682
+ return dark.length === light.length && dark.every((decl, i) => decl.prop === light[i].prop && decl.value === light[i].value) ? [] : dark;
2683
+ }
2407
2684
  function buildHuePlans(resolved, ctx) {
2408
2685
  const plans = /* @__PURE__ */ new Map();
2409
2686
  for (const [name, color] of resolved) plans.set(name, buildHuePlan(name, ctx.defs[name], color.light, ctx));
@@ -2466,28 +2743,41 @@ function formatColorValue(v, format, pastel, huePlan) {
2466
2743
  if (format === "oklch" && huePlan !== void 0) return formatVariantHue(v, huePlan, pastel);
2467
2744
  return formatVariant(v, format, pastel);
2468
2745
  }
2746
+ /**
2747
+ * Resolve the effective output modes for an export.
2748
+ *
2749
+ * A global manual `contrastLevel` turns off high-contrast output outright: the
2750
+ * level *is* the contrast preference, so a separate high-contrast tier has no
2751
+ * meaning alongside it. `modes.highContrast` therefore reads as "emit a separate
2752
+ * high-contrast set **when** contrast is automatic" — it goes inert under a
2753
+ * manual level rather than fighting it, and silently, since switching a
2754
+ * preference from auto to manual is normal use and not a mistake to report.
2755
+ *
2756
+ * A level set on a single theme or token does not change which modes are
2757
+ * emitted: sibling themes in a palette may still have a real high-contrast
2758
+ * tier, and the manual one correctly reports its own resolved values there —
2759
+ * the alternative would drop its colors from that tier entirely.
2760
+ */
2469
2761
  function resolveModes(override) {
2470
2762
  const cfg = getConfig();
2471
2763
  return {
2472
2764
  dark: override?.dark ?? cfg.modes.dark,
2473
- highContrast: override?.highContrast ?? cfg.modes.highContrast
2765
+ highContrast: contrastFraction(cfg) !== void 0 ? false : override?.highContrast ?? cfg.modes.highContrast
2474
2766
  };
2475
2767
  }
2476
2768
  function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel = false, channelCtx) {
2477
2769
  const tokens = {};
2478
2770
  const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
2479
2771
  if (huePlans !== void 0 && channelCtx !== void 0) {
2480
- const emitDecls = channelCtx.emitDeclarations !== false;
2481
- if (emitDecls && channelCtx.mode === "theme") tokens[`$${channelCtx.baseName}-hue`] = { "": String(channelCtx.seedHue) };
2482
- for (const [name, color] of resolved) {
2483
- const plan = huePlans.get(name);
2484
- if (emitDecls) for (const decl of plan.declarations) {
2485
- const key = `$${decl.prop.slice(2)}`;
2486
- if (!(key in tokens)) tokens[key] = { "": decl.value };
2487
- }
2488
- const colorKey = `#${prefix}${name}`;
2489
- tokens[colorKey] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
2772
+ for (const decl of collectHueDeclarations(resolved, channelCtx)) tokens[`$${decl.prop.slice(2)}`] = { "": decl.value };
2773
+ if (modes.dark) for (const decl of darkHueDeclarations(resolved, channelCtx)) {
2774
+ const key = `$${decl.prop.slice(2)}`;
2775
+ tokens[key] = {
2776
+ ...tokens[key],
2777
+ [states.dark]: decl.value
2778
+ };
2490
2779
  }
2780
+ for (const [name, color] of resolved) tokens[`#${prefix}${name}`] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
2491
2781
  return tokens;
2492
2782
  }
2493
2783
  for (const [name, color] of resolved) {
@@ -2536,7 +2826,13 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
2536
2826
  darkContrast: []
2537
2827
  };
2538
2828
  const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
2539
- if (huePlans !== void 0 && channelCtx !== void 0) for (const decl of collectHueDeclarations(resolved, channelCtx)) lines.light.push(`${decl.prop}: ${decl.value};`);
2829
+ if (huePlans !== void 0 && channelCtx !== void 0) {
2830
+ for (const decl of collectHueDeclarations(resolved, channelCtx)) lines.light.push(`${decl.prop}: ${decl.value};`);
2831
+ for (const decl of darkHueDeclarations(resolved, channelCtx)) {
2832
+ lines.dark.push(`${decl.prop}: ${decl.value};`);
2833
+ lines.darkContrast.push(`${decl.prop}: ${decl.value};`);
2834
+ }
2835
+ }
2540
2836
  for (const [name, color] of resolved) {
2541
2837
  const prop = `--${prefix}${name}${suffix}`;
2542
2838
  const plan = huePlans?.get(name);
@@ -2973,6 +3269,13 @@ function validateStructuredInput(input) {
2973
3269
  if (input.saturationFactor !== void 0) {
2974
3270
  if (!Number.isFinite(input.saturationFactor) || input.saturationFactor < 0 || input.saturationFactor > 1) throw new Error(`glaze.color: structured saturationFactor must be a finite number in 0–1 (got ${input.saturationFactor}).`);
2975
3271
  }
3272
+ if (typeof input.darkHue === "number" && !Number.isFinite(input.darkHue)) throw new Error(`glaze.color: structured darkHue must be a finite number (got ${input.darkHue}).`);
3273
+ if (input.darkSaturation !== void 0) {
3274
+ if (!Number.isFinite(input.darkSaturation) || input.darkSaturation < 0 || input.darkSaturation > 100) throw new Error(`glaze.color: structured darkSaturation must be a finite number in 0–100 (got ${input.darkSaturation}).`);
3275
+ }
3276
+ if (input.darkSaturationFactor !== void 0) {
3277
+ if (!Number.isFinite(input.darkSaturationFactor) || input.darkSaturationFactor < 0 || input.darkSaturationFactor > 1) throw new Error(`glaze.color: structured darkSaturationFactor must be a finite number in 0–1 (got ${input.darkSaturationFactor}).`);
3278
+ }
2976
3279
  if (input.opacity !== void 0) validateStandaloneOpacity(input.opacity);
2977
3280
  }
2978
3281
  /**
@@ -3019,11 +3322,27 @@ function extractOkhslFromValue(value) {
3019
3322
  return value;
3020
3323
  }
3021
3324
  /**
3325
+ * Resolve `'max'` / `'min'` to their literal author tones (100 / 0).
3326
+ *
3327
+ * The hidden `STANDALONE_SEED` anchor exists only so `contrast` has something
3328
+ * to measure against — it is not an authored base. Pinning the extreme keeps
3329
+ * the token on the plain scheme mapping instead of the base-anchored dark
3330
+ * replay the resolver applies to real `base` references.
3331
+ */
3332
+ function pinExtremeTone(tone) {
3333
+ const pin = (value) => value === "max" ? 100 : value === "min" ? 0 : value;
3334
+ return Array.isArray(tone) ? [pin(tone[0]), pin(tone[1])] : pin(tone);
3335
+ }
3336
+ /**
3022
3337
  * Build the `ColorMap` for a value-shorthand `glaze.color()` call.
3023
3338
  *
3024
3339
  * The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
3025
3340
  * across every value-shorthand form.
3026
3341
  *
3342
+ * An absolute `hue` override becomes the seed while a relative one becomes a
3343
+ * def field; `darkHue` always lands on the def in either form, so an absolute
3344
+ * `darkHue` wins outright instead of being re-offset by a relative `hue`.
3345
+ *
3027
3346
  * When the user requests `contrast` or relative `tone`, a hidden
3028
3347
  * `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
3029
3348
  * the seed pinned to the literal user-provided color across all four
@@ -3041,10 +3360,13 @@ function buildStandaloneValueDefs(main, options) {
3041
3360
  if (userName !== void 0) validateStandaloneName(userName);
3042
3361
  const primary = userName ?? STANDALONE_VALUE;
3043
3362
  const seedTone = toTone(main.l);
3363
+ const primaryTone = toneOption ?? seedTone;
3044
3364
  const valueDef = {
3045
3365
  hue: relativeHue,
3046
3366
  saturation: options?.saturationFactor,
3047
- tone: toneOption ?? seedTone,
3367
+ darkHue: options?.darkHue,
3368
+ darkSaturation: options?.darkSaturationFactor,
3369
+ tone: needsSeedAnchor ? pinExtremeTone(primaryTone) : primaryTone,
3048
3370
  contrast: options?.contrast,
3049
3371
  mode: options?.mode ?? "auto",
3050
3372
  autoFlip: options?.autoFlip,
@@ -3061,13 +3383,16 @@ function buildStandaloneValueDefs(main, options) {
3061
3383
  mode: "static"
3062
3384
  };
3063
3385
  return {
3064
- seedHue,
3065
- seedSaturation,
3386
+ seed: {
3387
+ hue: seedHue,
3388
+ saturation: seedSaturation,
3389
+ darkSaturation: options?.darkSaturation
3390
+ },
3066
3391
  defs,
3067
3392
  primary
3068
3393
  };
3069
3394
  }
3070
- function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, configOverride, baseToken, buildExport) {
3395
+ function createColorTokenFromDefs(seed, defs, primary, configOverride, baseToken, buildExport) {
3071
3396
  let cache = null;
3072
3397
  function getEffectiveConfig() {
3073
3398
  const version = getConfigVersion();
@@ -3084,7 +3409,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
3084
3409
  const version = getConfigVersion();
3085
3410
  if (cache && cache.version === version && cache.map) return cache.map;
3086
3411
  const effectiveConfig = getEffectiveConfig();
3087
- const map = resolveAllColors(seedHue, seedSaturation, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
3412
+ const map = resolveAllColors(seed, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
3088
3413
  cache = {
3089
3414
  map,
3090
3415
  version,
@@ -3122,7 +3447,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
3122
3447
  if (options.splitHue && format === "oklch") {
3123
3448
  assertAllPastel(renamed, resolveModes());
3124
3449
  channelCtx = {
3125
- seedHue,
3450
+ seedHue: seed.hue,
3126
3451
  baseName: options.name,
3127
3452
  prefix: "",
3128
3453
  defs: { [options.name]: defs[primary] },
@@ -3206,8 +3531,10 @@ function createColorToken(input, configOverride) {
3206
3531
  const hasExternalBase = baseToken !== void 0;
3207
3532
  const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
3208
3533
  const defs = { [primary]: {
3209
- tone: input.tone,
3534
+ tone: needsSeedAnchor ? pinExtremeTone(input.tone) : input.tone,
3210
3535
  saturation: input.saturationFactor,
3536
+ darkHue: input.darkHue,
3537
+ darkSaturation: input.darkSaturationFactor,
3211
3538
  mode: input.mode ?? "auto",
3212
3539
  autoFlip: input.autoFlip,
3213
3540
  contrast: input.contrast,
@@ -3216,16 +3543,17 @@ function createColorToken(input, configOverride) {
3216
3543
  role: input.role,
3217
3544
  base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
3218
3545
  } };
3219
- if (needsSeedAnchor) {
3220
- const seedTone = pairNormal(input.tone);
3221
- defs[STANDALONE_SEED] = {
3222
- tone: seedTone === "max" ? 100 : seedTone === "min" ? 0 : seedTone,
3223
- saturation: 1,
3224
- mode: "static"
3225
- };
3226
- }
3546
+ if (needsSeedAnchor) defs[STANDALONE_SEED] = {
3547
+ tone: pinExtremeTone(pairNormal(input.tone)),
3548
+ saturation: 1,
3549
+ mode: "static"
3550
+ };
3227
3551
  const localOverride = configOverride;
3228
- return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, localOverride, baseToken, (exportArg) => ({
3552
+ return createColorTokenFromDefs({
3553
+ hue: input.hue,
3554
+ saturation: input.saturation,
3555
+ darkSaturation: input.darkSaturation
3556
+ }, defs, primary, localOverride, baseToken, (exportArg) => ({
3229
3557
  kind: "color",
3230
3558
  version: GLAZE_EXPORT_VERSION,
3231
3559
  form: "structured",
@@ -3236,9 +3564,9 @@ function createColorToken(input, configOverride) {
3236
3564
  function createColorTokenFromValue(value, options, configOverride) {
3237
3565
  const main = extractOkhslFromValue(value);
3238
3566
  const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
3239
- const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
3567
+ const { seed, defs, primary } = buildStandaloneValueDefs(main, options);
3240
3568
  const localOverride = sparseValueFormLocal(configOverride);
3241
- return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, localOverride, linkingBase, (exportArg) => ({
3569
+ return createColorTokenFromDefs(seed, defs, primary, localOverride, linkingBase, (exportArg) => ({
3242
3570
  kind: "color",
3243
3571
  version: GLAZE_EXPORT_VERSION,
3244
3572
  form: "value",
@@ -3258,6 +3586,9 @@ function buildOverridesExport(options, exportArg) {
3258
3586
  if (options.saturation !== void 0) out.saturation = options.saturation;
3259
3587
  if (options.tone !== void 0) out.tone = options.tone;
3260
3588
  if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
3589
+ if (options.darkHue !== void 0) out.darkHue = options.darkHue;
3590
+ if (options.darkSaturation !== void 0) out.darkSaturation = options.darkSaturation;
3591
+ if (options.darkSaturationFactor !== void 0) out.darkSaturationFactor = options.darkSaturationFactor;
3261
3592
  if (options.mode !== void 0) out.mode = options.mode;
3262
3593
  if (options.autoFlip !== void 0) out.autoFlip = options.autoFlip;
3263
3594
  if (options.contrast !== void 0) out.contrast = options.contrast;
@@ -3275,6 +3606,9 @@ function buildStructuredInputExport(input, exportArg) {
3275
3606
  tone: input.tone
3276
3607
  };
3277
3608
  if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
3609
+ if (input.darkHue !== void 0) out.darkHue = input.darkHue;
3610
+ if (input.darkSaturation !== void 0) out.darkSaturation = input.darkSaturation;
3611
+ if (input.darkSaturationFactor !== void 0) out.darkSaturationFactor = input.darkSaturationFactor;
3278
3612
  if (input.mode !== void 0) out.mode = input.mode;
3279
3613
  if (input.autoFlip !== void 0) out.autoFlip = input.autoFlip;
3280
3614
  if (input.opacity !== void 0) out.opacity = input.opacity;
@@ -3297,6 +3631,9 @@ function rehydrateOverrides(data) {
3297
3631
  if (data.saturation !== void 0) out.saturation = data.saturation;
3298
3632
  if (data.tone !== void 0) out.tone = data.tone;
3299
3633
  if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
3634
+ if (data.darkHue !== void 0) out.darkHue = data.darkHue;
3635
+ if (data.darkSaturation !== void 0) out.darkSaturation = data.darkSaturation;
3636
+ if (data.darkSaturationFactor !== void 0) out.darkSaturationFactor = data.darkSaturationFactor;
3300
3637
  if (data.mode !== void 0) out.mode = data.mode;
3301
3638
  if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
3302
3639
  if (data.contrast !== void 0) out.contrast = data.contrast;
@@ -3314,6 +3651,9 @@ function rehydrateStructuredInput(data) {
3314
3651
  tone: data.tone
3315
3652
  };
3316
3653
  if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
3654
+ if (data.darkHue !== void 0) out.darkHue = data.darkHue;
3655
+ if (data.darkSaturation !== void 0) out.darkSaturation = data.darkSaturation;
3656
+ if (data.darkSaturationFactor !== void 0) out.darkSaturationFactor = data.darkSaturationFactor;
3317
3657
  if (data.mode !== void 0) out.mode = data.mode;
3318
3658
  if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
3319
3659
  if (data.opacity !== void 0) out.opacity = data.opacity;
@@ -3347,7 +3687,8 @@ function colorFromExport(data) {
3347
3687
 
3348
3688
  //#endregion
3349
3689
  //#region src/theme.ts
3350
- function createTheme(hue, saturation, initialColors, configOverride) {
3690
+ function createTheme(seed, initialColors, configOverride) {
3691
+ const { hue, saturation, darkHue, darkSaturation } = seed;
3351
3692
  let colorDefs = initialColors ? { ...initialColors } : {};
3352
3693
  let cache = null;
3353
3694
  function getEffectiveConfig() {
@@ -3365,7 +3706,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3365
3706
  const version = getConfigVersion();
3366
3707
  if (cache && cache.version === version && cache.map) return cache.map;
3367
3708
  const effectiveConfig = getEffectiveConfig();
3368
- const map = resolveAllColors(hue, saturation, colorDefs, effectiveConfig);
3709
+ const map = resolveAllColors(seed, colorDefs, effectiveConfig);
3369
3710
  cache = {
3370
3711
  map,
3371
3712
  version,
@@ -3382,6 +3723,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3382
3723
  assertAllPastel(resolveCached(), resolveModes(options?.modes));
3383
3724
  return {
3384
3725
  seedHue: hue,
3726
+ darkSeedHue: darkHue,
3385
3727
  baseName: options.name ?? "theme",
3386
3728
  prefix,
3387
3729
  defs: colorDefs,
@@ -3395,6 +3737,12 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3395
3737
  get saturation() {
3396
3738
  return saturation;
3397
3739
  },
3740
+ get darkHue() {
3741
+ return darkHue;
3742
+ },
3743
+ get darkSaturation() {
3744
+ return darkSaturation;
3745
+ },
3398
3746
  getConfig() {
3399
3747
  return getEffectiveConfig();
3400
3748
  },
@@ -3431,16 +3779,22 @@ function createTheme(hue, saturation, initialColors, configOverride) {
3431
3779
  version: GLAZE_EXPORT_VERSION,
3432
3780
  hue,
3433
3781
  saturation,
3782
+ ...darkHue !== void 0 ? { darkHue } : {},
3783
+ ...darkSaturation !== void 0 ? { darkSaturation } : {},
3434
3784
  colors: structuredClone(colorDefs),
3435
3785
  config: freezeConfigForExport(configOverride, override)
3436
3786
  };
3437
3787
  },
3438
3788
  extend(options) {
3439
- const newHue = options.hue ?? hue;
3440
- const newSat = options.saturation ?? saturation;
3789
+ const childSeed = {
3790
+ hue: options.hue ?? hue,
3791
+ saturation: options.saturation ?? saturation,
3792
+ darkHue: options.darkHue ?? darkHue,
3793
+ darkSaturation: options.darkSaturation ?? darkSaturation
3794
+ };
3441
3795
  const inheritedColors = {};
3442
3796
  for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
3443
- return createTheme(newHue, newSat, options.colors ? {
3797
+ return createTheme(childSeed, options.colors ? {
3444
3798
  ...inheritedColors,
3445
3799
  ...options.colors
3446
3800
  } : { ...inheritedColors }, configOverride || options.config ? {
@@ -3550,6 +3904,7 @@ function channelCtxForTheme(theme, themeName, passPrefix, themedPrefix, splitHue
3550
3904
  assertAllPastel(filtered, modes);
3551
3905
  return {
3552
3906
  seedHue: theme.hue,
3907
+ darkSeedHue: theme.darkHue,
3553
3908
  baseName: themeName,
3554
3909
  prefix: themedPrefix,
3555
3910
  defs: colorMapFromTheme(theme),
@@ -3581,7 +3936,13 @@ function createThemeFromExport(data, factory = "glaze.themeFrom") {
3581
3936
  assertExportKind(data, "theme", factory);
3582
3937
  assertExportVersion(data, factory);
3583
3938
  if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
3584
- return createTheme(data.hue, data.saturation, data.colors, data.config);
3939
+ for (const field of ["darkHue", "darkSaturation"]) if (data[field] !== void 0 && typeof data[field] !== "number") throw new Error(`${factory}: expected "${field}" to be a number when present (got ${JSON.stringify(data[field])}).`);
3940
+ return createTheme({
3941
+ hue: data.hue,
3942
+ saturation: data.saturation,
3943
+ darkHue: data.darkHue,
3944
+ darkSaturation: data.darkSaturation
3945
+ }, data.colors, data.config);
3585
3946
  }
3586
3947
  /**
3587
3948
  * Rebuild a palette from a `palette.export()` snapshot.
@@ -3743,18 +4104,28 @@ function createPalette(themes, paletteOptions) {
3743
4104
  * the theme still reacts to later `configure()` calls for fields it
3744
4105
  * didn't override.
3745
4106
  *
4107
+ * The object form also accepts `darkHue` / `darkSaturation` — a separate
4108
+ * seed pair for the dark and dark-high-contrast schemes. Both default to
4109
+ * their light counterparts, and an explicit `darkSaturation` opts the theme
4110
+ * out of the global `darkDesaturation` reduction.
4111
+ *
3746
4112
  * @example
3747
4113
  * ```ts
3748
4114
  * const primary = glaze(280, 80);
3749
4115
  * // or shorthand:
3750
4116
  * const primary = glaze({ hue: 280, saturation: 80 });
4117
+ * // with a retuned dark seed:
4118
+ * const primary = glaze({ hue: 280, saturation: 80, darkHue: 268, darkSaturation: 65 });
3751
4119
  * // with config override:
3752
4120
  * const raw = glaze(280, 80, { lightTone: false });
3753
4121
  * ```
3754
4122
  */
3755
4123
  function glaze(hueOrOptions, saturation, config) {
3756
- if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100, void 0, config);
3757
- return createTheme(hueOrOptions.hue, hueOrOptions.saturation, void 0, config);
4124
+ if (typeof hueOrOptions === "number") return createTheme({
4125
+ hue: hueOrOptions,
4126
+ saturation: saturation ?? 100
4127
+ }, void 0, config);
4128
+ return createTheme(hueOrOptions, void 0, config);
3758
4129
  }
3759
4130
  /** Configure global glaze settings. */
3760
4131
  glaze.configure = function configure$1(config) {
@@ -3868,7 +4239,10 @@ glaze.fromHex = function fromHex(hex) {
3868
4239
  const rgb = parseHex(hex);
3869
4240
  if (!rgb) throw new Error(`glaze: invalid hex color "${hex}".`);
3870
4241
  const [h, s] = srgbToOkhsl(rgb);
3871
- return createTheme(h, s * 100);
4242
+ return createTheme({
4243
+ hue: h,
4244
+ saturation: s * 100
4245
+ });
3872
4246
  };
3873
4247
  /**
3874
4248
  * Create a theme from RGB values (0–255).
@@ -3880,7 +4254,10 @@ glaze.fromRgb = function fromRgb(r, g, b) {
3880
4254
  g / 255,
3881
4255
  b / 255
3882
4256
  ]);
3883
- return createTheme(h, s * 100);
4257
+ return createTheme({
4258
+ hue: h,
4259
+ saturation: s * 100
4260
+ });
3884
4261
  };
3885
4262
  /**
3886
4263
  * Rehydrate a `glaze.color()` token from a `.export()` snapshot.
@@ -3924,5 +4301,5 @@ glaze.resetConfig = function resetConfig$1() {
3924
4301
  };
3925
4302
 
3926
4303
  //#endregion
3927
- export { APCA_HC_ENHANCEMENT, APCA_MAX_LC, APCA_PRESETS, GLAZE_EXPORT_VERSION, REF_EPS, apcaContrast, assertAllPastel, assertNativeFormat, contrastRatioFromLuminance, cuspLightness, findToneForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOkhst, formatOklch, formatRgb, fromTone, gamutClampedLuminance, glaze, hslToSrgb, inferRoleFromName, isColorTokenExport, isPaletteExport, isThemeExport, normalizeRole, okhslToLinearSrgb, okhslToOkhst, okhslToOklab, okhslToOklch, okhslToSrgb, okhstToOkhsl, oklabToOkhsl, oppositeRole, parseHex, parseHexAlpha, relativeLuminanceFromLinearRgb, resolveApcaTarget, resolveContrastForMode, resolveMinContrast, roleToPolarity, srgbToHex, srgbToOkhsl, toTone, toneFromY, variantToOkhsl, yFromTone };
4304
+ export { APCA_HC_ENHANCEMENT, APCA_MAX_LC, APCA_PRESETS, GLAZE_EXPORT_VERSION, REF_EPS, apcaContrast, assertAllPastel, assertNativeFormat, contrastRatioFromLuminance, cuspLightness, findToneForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOkhst, formatOklch, formatRgb, fromTone, gamutClampedLuminance, glaze, hslToSrgb, inferRoleFromName, isColorTokenExport, isPaletteExport, isThemeExport, normalizeRole, okhslToLinearSrgb, okhslToOkhst, okhslToOklab, okhslToOklch, okhslToSrgb, okhstToOkhsl, oklabToOkhsl, oppositeRole, parseHex, parseHexAlpha, relativeLuminanceFromLinearRgb, resolveApcaTarget, resolveContrastForLevel, resolveContrastForMode, resolveMinContrast, roleToPolarity, srgbToHex, srgbToOkhsl, toTone, toneFromY, variantToOkhsl, yFromTone };
3928
4305
  //# sourceMappingURL=index.mjs.map