riverbankcms 0.25.0 → 0.26.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $n as ok, An as unsafeAsEntitlementId, B as parsePublicContactDetails, Bn as unsafeAsPositiveSeconds, Dn as unsafeAsDeclaredMimeType, Et as validateBookingPaymentOptionKinds, Fn as unsafeAsMediaAssetId, Gn as unsafeAsSiteId, Hn as unsafeAsProductId, Jn as unsafeAsStorageBucketName, K as locationValueSchema, Kn as unsafeAsSiteScopedStoragePath, Ln as unsafeAsOriginalFilename, N as parseFooterCompositionV1, P as footerNavigationLayouts, Pn as unsafeAsFileChecksumSha256, Q as isPresent, Qn as err, Qt as mapReadonlyNonEmptyArray, R as isCanonicalRichTextDoc, Tn as unsafeAsBookingSessionId, Tt as parseBookingTimeZone, Un as unsafeAsSafeDownloadFilename, V as parseWeeklyOpeningHours, W as stableJsonStringify, Wn as unsafeAsSiteFontId, Xn as unsafeAsUploadSessionId, Xt as fromArray, Yn as unsafeAsStoragePath, Zn as unsafeAsUserId, Zt as mapNonEmptyArray, a as NAVIGATION_LOCATIONS, bn as parseSiteId, cn as assertNever, f as EVENT_PRESET_VALUES, gn as parseMediaAssetId, gt as localScheduleTimePattern, kn as unsafeAsDetectedMimeType, mn as parseEditorSessionGeneration, n as authoredValuesEqual, p as ORDINARY_EVENT_PRESET_VALUES, u as resolveSiteVerticalBookingConfig, wn as unsafeAsBookingRequirementId, yt as parseUtcIsoDateTimeString, zn as unsafeAsPositiveByteCount } from "./src-LhtqTjH7.mjs";
2
+ import { $n as ok, An as unsafeAsEntitlementId, B as parsePublicContactDetails, Bn as unsafeAsPositiveSeconds, Dn as unsafeAsDeclaredMimeType, Et as validateBookingPaymentOptionKinds, Fn as unsafeAsMediaAssetId, Gn as unsafeAsSiteId, Hn as unsafeAsProductId, Jn as unsafeAsStorageBucketName, K as locationValueSchema, Kn as unsafeAsSiteScopedStoragePath, Ln as unsafeAsOriginalFilename, N as parseFooterCompositionV1, P as footerNavigationLayouts, Pn as unsafeAsFileChecksumSha256, Q as isPresent, Qn as err, Qt as mapReadonlyNonEmptyArray, R as isCanonicalRichTextDoc, Tn as unsafeAsBookingSessionId, Tt as parseBookingTimeZone, Un as unsafeAsSafeDownloadFilename, V as parseWeeklyOpeningHours, W as stableJsonStringify, Wn as unsafeAsSiteFontId, Xn as unsafeAsUploadSessionId, Xt as fromArray, Yn as unsafeAsStoragePath, Zn as unsafeAsUserId, Zt as mapNonEmptyArray, a as NAVIGATION_LOCATIONS, bn as parseSiteId, cn as assertNever, f as EVENT_PRESET_VALUES, gn as parseMediaAssetId, gt as localScheduleTimePattern, kn as unsafeAsDetectedMimeType, mn as parseEditorSessionGeneration, n as authoredValuesEqual, p as ORDINARY_EVENT_PRESET_VALUES, u as resolveSiteVerticalBookingConfig, wn as unsafeAsBookingRequirementId, yt as parseUtcIsoDateTimeString, zn as unsafeAsPositiveByteCount } from "./src-DsR6-gBQ.mjs";
3
3
  import "node:module";
4
4
  import { z } from "zod";
5
5
 
@@ -771,8 +771,8 @@ function paletteColorsFromEntries(entries) {
771
771
  }
772
772
  };
773
773
  }
774
- const hex = normalizePaletteHex(entry.hex);
775
- if (!hex) {
774
+ const hex$1 = normalizePaletteHex(entry.hex);
775
+ if (!hex$1) {
776
776
  return {
777
777
  ok: false,
778
778
  error: {
@@ -782,7 +782,7 @@ function paletteColorsFromEntries(entries) {
782
782
  }
783
783
  };
784
784
  }
785
- values[role] = hex;
785
+ values[role] = hex$1;
786
786
  }
787
787
  const missing = PALETTE_TOKEN_NAMES.filter((role) => values[role] === undefined);
788
788
  if (missing.length > 0) {
@@ -869,8 +869,8 @@ const clamp01$2 = (x) => Math.max(0, Math.min(1, x));
869
869
  const mod = (a, n) => (a % n + n) % n;
870
870
  const deg = (x) => mod(x, 360);
871
871
  const mix$1 = (a, b, t) => a + (b - a) * clamp01$2(t);
872
- function hexToRgb(hex) {
873
- const h = hex.replace(/^#/, "");
872
+ function hexToRgb(hex$1) {
873
+ const h = hex$1.replace(/^#/, "");
874
874
  const m = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
875
875
  if (m.length !== 6) throw new Error("Bad hex");
876
876
  const int = parseInt(m, 16);
@@ -1015,8 +1015,8 @@ function oklchToHexGamut(L, C, h) {
1015
1015
  const { r, g, b: bb } = oklabToSrgb(L, a, b);
1016
1016
  return rgbToHex(r, g, bb);
1017
1017
  }
1018
- function featuresFromHex(hex) {
1019
- const { r, g, b } = hexToRgb(hex);
1018
+ function featuresFromHex(hex$1) {
1019
+ const { r, g, b } = hexToRgb(hex$1);
1020
1020
  const { h, s, l } = rgbToHsl(r, g, b);
1021
1021
  const { L, a, b: b2 } = srgbToOklab(r, g, b);
1022
1022
  const { L: okL, C: okC, h: okH } = oklabToOklch(L, a, b2);
@@ -1037,7 +1037,7 @@ function featuresFromHex(hex) {
1037
1037
  okL,
1038
1038
  okC,
1039
1039
  okH,
1040
- hex
1040
+ hex: hex$1
1041
1041
  };
1042
1042
  }
1043
1043
  function hueRotate(h, degrees) {
@@ -1130,19 +1130,19 @@ function preparePaletteColourIndex(palette) {
1130
1130
  return Object.freeze(Object.fromEntries(colours.flatMap(colourIndexEntry)));
1131
1131
  }
1132
1132
  /** An unused unsupported colour must not turn preparation into an eager validation failure. */
1133
- function colourIndexEntry(hex) {
1133
+ function colourIndexEntry(hex$1) {
1134
1134
  try {
1135
- return [[hex, colourLuminance(hex)]];
1135
+ return [[hex$1, colourLuminance(hex$1)]];
1136
1136
  } catch {
1137
1137
  return [];
1138
1138
  }
1139
1139
  }
1140
- function colourLuminance(hex) {
1141
- const { r, g, b } = hexToRgb(hex);
1140
+ function colourLuminance(hex$1) {
1141
+ const { r, g, b } = hexToRgb(hex$1);
1142
1142
  return relativeLuminance(r, g, b);
1143
1143
  }
1144
- function indexedLuminance(hex, index) {
1145
- return (index && Object.prototype.hasOwnProperty.call(index, hex) ? index[hex] : undefined) ?? colourLuminance(hex);
1144
+ function indexedLuminance(hex$1, index) {
1145
+ return (index && Object.prototype.hasOwnProperty.call(index, hex$1) ? index[hex$1] : undefined) ?? colourLuminance(hex$1);
1146
1146
  }
1147
1147
  /**
1148
1148
  * A closed luminance range with `lo <= hi`. The private field means only
@@ -1165,12 +1165,12 @@ function luminanceInterval(lo, hi = lo) {
1165
1165
  return new Interval(lo, hi);
1166
1166
  }
1167
1167
  /** The relative luminance of a hex colour, read from the prepared index when given. */
1168
- function hexLuminance(hex, index) {
1169
- return toLuminance(indexedLuminance(hex, index));
1168
+ function hexLuminance(hex$1, index) {
1169
+ return toLuminance(indexedLuminance(hex$1, index));
1170
1170
  }
1171
1171
  /** The paint of a solid hex ground: a single point. */
1172
- function solidPaint(hex, index) {
1173
- const y = hexLuminance(hex, index);
1172
+ function solidPaint(hex$1, index) {
1173
+ const y = hexLuminance(hex$1, index);
1174
1174
  return new Interval(y, y);
1175
1175
  }
1176
1176
  function luminanceRatio(a, b) {
@@ -1272,8 +1272,8 @@ function compositeHex$1(topHex, alpha, bottomHex) {
1272
1272
  function worstCaseScrimContrast(input) {
1273
1273
  const overWhite = compositeHex$1(input.scrimHex, input.alpha, WHITE);
1274
1274
  const overBlack = compositeHex$1(input.scrimHex, input.alpha, BLACK);
1275
- const luminance = (hex) => {
1276
- const { r, g, b } = hexToRgb(hex);
1275
+ const luminance = (hex$1) => {
1276
+ const { r, g, b } = hexToRgb(hex$1);
1277
1277
  return relativeLuminance(r, g, b);
1278
1278
  };
1279
1279
  const foreground = luminance(input.foregroundHex);
@@ -1303,9 +1303,9 @@ function gradientScrimSamples(gradient, alpha, palette) {
1303
1303
  alpha: alpha * (stop[4] === undefined ? 1 : Number(stop[4]))
1304
1304
  }));
1305
1305
  const tokens = [...gradient.matchAll(/var\(--tb-([\w-]+)\)/g)].flatMap((stop) => {
1306
- const hex = stop[1] ? resolveConcreteColorHex(stop[1], palette) : null;
1307
- return hex ? [{
1308
- hex,
1306
+ const hex$1 = stop[1] ? resolveConcreteColorHex(stop[1], palette) : null;
1307
+ return hex$1 ? [{
1308
+ hex: hex$1,
1309
1309
  alpha
1310
1310
  }] : [];
1311
1311
  });
@@ -1421,7 +1421,7 @@ function generateShades(hexColor, name) {
1421
1421
 
1422
1422
  //#endregion
1423
1423
  //#region ../theme-core/src/palette/neutralScale.ts
1424
- function generateNeutralScale({ background, steps = 11, warm = false }) {
1424
+ function generateNeutralScale({ background, steps: steps$1 = 11, warm = false }) {
1425
1425
  const isDark = background === "#000000" || background.startsWith("#0") || background.startsWith("#1");
1426
1426
  const shadeNames = [
1427
1427
  50,
@@ -1436,17 +1436,17 @@ function generateNeutralScale({ background, steps = 11, warm = false }) {
1436
1436
  900,
1437
1437
  950
1438
1438
  ];
1439
- const count = Math.min(steps, shadeNames.length);
1439
+ const count = Math.min(steps$1, shadeNames.length);
1440
1440
  const scale$1 = [];
1441
1441
  for (let i = 0; i < count; i++) {
1442
1442
  const shadeName = shadeNames[i] ?? shadeNames[shadeNames.length - 1] ?? 950;
1443
1443
  const t = i / (count - 1);
1444
1444
  const tone = isDark ? mix$2(.06, .92, t) : mix$2(.98, .1, t);
1445
- const hex = oklchToHexGamut(tone, .02, warm ? 85 : 250);
1445
+ const hex$1 = oklchToHexGamut(tone, .02, warm ? 85 : 250);
1446
1446
  const name = `neutral-${shadeName}`;
1447
1447
  scale$1.push({
1448
1448
  name,
1449
- hex
1449
+ hex: hex$1
1450
1450
  });
1451
1451
  }
1452
1452
  return scale$1;
@@ -1488,9 +1488,9 @@ function colorsToMap$1(palette) {
1488
1488
  }
1489
1489
  return map;
1490
1490
  }
1491
- function isBackgroundDark(hex) {
1491
+ function isBackgroundDark(hex$1) {
1492
1492
  try {
1493
- return featuresFromHex(hex).Y < .5;
1493
+ return featuresFromHex(hex$1).Y < .5;
1494
1494
  } catch {
1495
1495
  return false;
1496
1496
  }
@@ -1537,10 +1537,10 @@ function expandBasePalette(palette) {
1537
1537
  neutral$1.forEach((n) => {
1538
1538
  expanded[n.name] = n.hex;
1539
1539
  });
1540
- for (const [name, hex] of Object.entries(base)) {
1540
+ for (const [name, hex$1] of Object.entries(base)) {
1541
1541
  if (nonShadeablePaletteRoleNames.has(name)) continue;
1542
- if (hex) {
1543
- const shades = generateShades(hex, name);
1542
+ if (hex$1) {
1543
+ const shades = generateShades(hex$1, name);
1544
1544
  shades.forEach((shade) => {
1545
1545
  expanded[shade.name] = shade.hex;
1546
1546
  });
@@ -1554,11 +1554,11 @@ function expandBasePalette(palette) {
1554
1554
  const isDark = base.background ? isBackgroundDark(base.background) : false;
1555
1555
  const isDarkMode = palette.meta?.mode === "dark" || isDark;
1556
1556
  expanded.mutedText = isDarkMode ? expanded["neutral-600"] ?? "#666" : expanded["neutral-500"] ?? "#888";
1557
- for (const [token, hex] of Object.entries({ ...expanded })) {
1558
- if (!hex || !isOnColorBackgroundCandidateToken(token)) continue;
1557
+ for (const [token, hex$1] of Object.entries({ ...expanded })) {
1558
+ if (!hex$1 || !isOnColorBackgroundCandidateToken(token)) continue;
1559
1559
  const onToken = onColorTokenName(token);
1560
1560
  if (expanded[onToken]) continue;
1561
- expanded[onToken] = getTextColorForBackground(hex);
1561
+ expanded[onToken] = getTextColorForBackground(hex$1);
1562
1562
  }
1563
1563
  return expanded;
1564
1564
  }
@@ -1589,7 +1589,7 @@ function firstContrastBlend$1(from, to, ground, threshold) {
1589
1589
  return Array.from({ length: 101 }, (_, index) => {
1590
1590
  const amount = index / 100;
1591
1591
  return rgbToHex(mix$1(start.r, end.r, amount), mix$1(start.g, end.g, amount), mix$1(start.b, end.b, amount));
1592
- }).find((hex) => contrastRatio(hex, ground) >= threshold) ?? to;
1592
+ }).find((hex$1) => contrastRatio(hex$1, ground) >= threshold) ?? to;
1593
1593
  }
1594
1594
  /** `markSource` names the colour surface marks are derived from when a style marks in its brand rather than its accent. */
1595
1595
  function arrangementPaletteTokens(base, options) {
@@ -1600,100 +1600,6 @@ function arrangementPaletteTokens(base, options) {
1600
1600
  };
1601
1601
  }
1602
1602
 
1603
- //#endregion
1604
- //#region ../theme-core/src/surfaces/foregroundContract.ts
1605
- const SURFACE_FOREGROUND_CSS_VARS = {
1606
- text: "--rb-surface-text-color",
1607
- heading: "--rb-surface-heading-color",
1608
- link: "--rb-surface-link-color",
1609
- linkHover: "--rb-surface-link-hover-color",
1610
- supporting: "--rb-surface-supporting-color",
1611
- muted: "--rb-surface-muted-color",
1612
- line: "--rb-surface-line-color",
1613
- mark: "--rb-surface-mark-color"
1614
- };
1615
- const DEFAULT_SURFACE_TEXT_COLOR = "rgb(var(--tb-text, 17 24 39))";
1616
- const DEFAULT_SURFACE_LINK_COLOR = "rgb(var(--tb-primary, var(--tb-text, 17 24 39)))";
1617
- const DEFAULT_SURFACE_SUPPORTING_COLOR = "rgb(var(--tb-mutedText, 100 116 139))";
1618
- const DEFAULT_ROLE_FALLBACKS = {
1619
- text: DEFAULT_SURFACE_TEXT_COLOR,
1620
- heading: DEFAULT_SURFACE_TEXT_COLOR,
1621
- link: DEFAULT_SURFACE_LINK_COLOR,
1622
- linkHover: DEFAULT_SURFACE_LINK_COLOR,
1623
- supporting: DEFAULT_SURFACE_SUPPORTING_COLOR,
1624
- muted: DEFAULT_SURFACE_SUPPORTING_COLOR
1625
- };
1626
- function requireSurfaceColorValue(role, value) {
1627
- const normalized = value.trim();
1628
- if (normalized.length === 0) {
1629
- throw new RangeError(`Surface foreground ${role} must be a non-empty CSS color.`);
1630
- }
1631
- return normalized;
1632
- }
1633
- function completeSurfaceForegrounds(input) {
1634
- const text = requireSurfaceColorValue("text", input.text);
1635
- const link = input.link === undefined ? text : requireSurfaceColorValue("link", input.link);
1636
- return {
1637
- text,
1638
- heading: input.heading === undefined ? text : requireSurfaceColorValue("heading", input.heading),
1639
- link,
1640
- linkHover: input.linkHover === undefined ? link : requireSurfaceColorValue("linkHover", input.linkHover),
1641
- muted: input.muted === undefined ? text : requireSurfaceColorValue("muted", input.muted),
1642
- supporting: input.supporting === undefined ? text : requireSurfaceColorValue("supporting", input.supporting)
1643
- };
1644
- }
1645
- function surfaceForegroundValue(role, outsideProviderFallback = DEFAULT_ROLE_FALLBACKS[role]) {
1646
- const roleVariable = SURFACE_FOREGROUND_CSS_VARS[role];
1647
- const fallback = requireSurfaceColorValue("fallback", outsideProviderFallback);
1648
- if (role === "text") {
1649
- return `var(${roleVariable}, ${fallback})`;
1650
- }
1651
- if (role === "linkHover") {
1652
- return `var(${roleVariable}, ${surfaceForegroundValue("link", fallback)})`;
1653
- }
1654
- return `var(${roleVariable}, var(${SURFACE_FOREGROUND_CSS_VARS.text}, ${fallback}))`;
1655
- }
1656
- function surfaceForegroundValueWithOpacity(role, opacityPercent) {
1657
- if (!Number.isFinite(opacityPercent) || opacityPercent < 0 || opacityPercent > 100) {
1658
- throw new RangeError("Surface foreground opacity must be between 0 and 100.");
1659
- }
1660
- return `color-mix(in srgb, ${surfaceForegroundValue(role)} ${opacityPercent}%, transparent)`;
1661
- }
1662
- function surfaceForegroundStyle(input) {
1663
- const foregrounds = completeSurfaceForegrounds(input);
1664
- return {
1665
- color: foregrounds.text,
1666
- [SURFACE_FOREGROUND_CSS_VARS.text]: foregrounds.text,
1667
- [SURFACE_FOREGROUND_CSS_VARS.heading]: foregrounds.heading,
1668
- [SURFACE_FOREGROUND_CSS_VARS.link]: foregrounds.link,
1669
- [SURFACE_FOREGROUND_CSS_VARS.linkHover]: foregrounds.linkHover,
1670
- [SURFACE_FOREGROUND_CSS_VARS.supporting]: foregrounds.supporting,
1671
- ...input.muted === undefined ? {} : { [SURFACE_FOREGROUND_CSS_VARS.muted]: foregrounds.muted },
1672
- "--rb-surface-link-hover-decoration": hoverDecoration(input)
1673
- };
1674
- }
1675
- function surfaceForegroundDeclarations(input) {
1676
- return Object.entries(surfaceForegroundStyle(input)).map(([property, value]) => `${property}: ${value};`);
1677
- }
1678
- function surfaceForegroundInputFromResolved(foreground, color) {
1679
- const { roles } = foreground;
1680
- const value = (ink) => {
1681
- const token = parseThemeColorToken(ink);
1682
- return token ? color(token) : ink;
1683
- };
1684
- return {
1685
- text: value(roles.text),
1686
- heading: value(roles.heading),
1687
- link: value(roles.link),
1688
- linkHover: value(roles.linkHover),
1689
- supporting: value(roles.supporting),
1690
- hoverDecoration: foreground.hoverDecoration
1691
- };
1692
- }
1693
- function hoverDecoration(input) {
1694
- return input.hoverDecoration ?? "underline";
1695
- }
1696
-
1697
1603
  //#endregion
1698
1604
  //#region ../theme-core/src/tokens/tokens.ts
1699
1605
  const colorTokens = [
@@ -1886,6 +1792,209 @@ function cloneThemeTokens(source) {
1886
1792
  return JSON.parse(JSON.stringify(source));
1887
1793
  }
1888
1794
 
1795
+ //#endregion
1796
+ //#region ../theme-core/src/tokens/resolver.ts
1797
+ /**
1798
+ * Resolves theme tokens to CSS variables
1799
+ *
1800
+ * This resolver ensures all button styles use theme tokens (not hardcoded colors)
1801
+ * so that changing the palette automatically updates all buttons.
1802
+ */
1803
+ var TokenResolver = class {
1804
+ constructor(theme) {
1805
+ this.theme = theme;
1806
+ }
1807
+ /**
1808
+ * Resolve a color token to a CSS variable reference wrapped in rgb()
1809
+ *
1810
+ * @param token - Token name (e.g., 'primary-500', 'white', 'text-900')
1811
+ * @returns CSS rgb() function with var reference (e.g., 'rgb(var(--tb-primary-500))')
1812
+ *
1813
+ * Note: CSS variables are stored as RGB channels (e.g., "37 99 235") following Tailwind convention,
1814
+ * so we must wrap them with rgb() to create a valid color value.
1815
+ *
1816
+ * @example
1817
+ * resolver.getColor('primary-500') // => 'rgb(var(--tb-primary-500))'
1818
+ * resolver.getColor('white') // => 'rgb(var(--tb-white))'
1819
+ */
1820
+ getColor(token) {
1821
+ if (!this.tokenExists(token)) {
1822
+ console.warn(`[TokenResolver] Color token "${token}" not found in theme palette. Button may render incorrectly.`);
1823
+ }
1824
+ return rgbColorValue(token);
1825
+ }
1826
+ /**
1827
+ * Adjust a color token's shade by a given amount
1828
+ *
1829
+ * @param token - Base token (e.g., 'primary-500')
1830
+ * @param amount - Shade adjustment (+100 = darker, -100 = lighter)
1831
+ * @returns Adjusted token name (e.g., 'primary-600')
1832
+ *
1833
+ * @example
1834
+ * resolver.adjustShade('primary-500', 100) // => 'primary-600' (darker)
1835
+ * resolver.adjustShade('primary-500', -100) // => 'primary-400' (lighter)
1836
+ */
1837
+ adjustShade(token, amount) {
1838
+ const parts = token.split("-");
1839
+ if (parts.length < 2) {
1840
+ const newShade$1 = Math.max(100, Math.min(900, 500 + amount));
1841
+ return `${token}-${newShade$1}`;
1842
+ }
1843
+ const base = parts.slice(0, -1).join("-");
1844
+ const shade = parseInt(parts[parts.length - 1] ?? "", 10);
1845
+ if (isNaN(shade)) {
1846
+ console.warn(`[TokenResolver] Cannot adjust shade for non-shade token "${token}". Returning original token.`);
1847
+ return token;
1848
+ }
1849
+ const newShade = Math.max(100, Math.min(900, shade + amount));
1850
+ return `${base}-${newShade}`;
1851
+ }
1852
+ /**
1853
+ * Check if a token exists in the theme palette
1854
+ *
1855
+ * @param token - Token name to check
1856
+ * @returns True if token exists in theme
1857
+ */
1858
+ tokenExists(token) {
1859
+ const commonTokens = [
1860
+ "white",
1861
+ "black",
1862
+ "transparent"
1863
+ ];
1864
+ if (commonTokens.includes(token)) {
1865
+ return true;
1866
+ }
1867
+ const parts = token.split("-");
1868
+ if (parts.length >= 2) {
1869
+ const lastPart = parts[parts.length - 1] ?? "";
1870
+ const shade = parseInt(lastPart, 10);
1871
+ if (!isNaN(shade) && shade >= 100 && shade <= 900 && shade % 100 === 0) {
1872
+ return true;
1873
+ }
1874
+ }
1875
+ return true;
1876
+ }
1877
+ };
1878
+
1879
+ //#endregion
1880
+ //#region ../theme-core/src/surfaces/foregroundInteraction.ts
1881
+ /**
1882
+ * How far a hover state softens away from its resting foreground, as a
1883
+ * percentage of full opacity.
1884
+ *
1885
+ * Softening — rather than brightening — is forced by the token vocabulary
1886
+ * rather than chosen for taste. A resolved foreground is frequently an
1887
+ * `on*` token, which `expandPalette` emits as pure `#FFFFFF` or `#000000`
1888
+ * with no shade ramp: over a primary-painted section the resolved text role
1889
+ * is `onPrimary`, and white has no headroom left to move further from its
1890
+ * background. Moving *toward* the mid-range always has headroom, whatever
1891
+ * the resting colour, and it keeps the hue exactly.
1892
+ *
1893
+ * 85% is the measured floor, not a round number. Softening costs contrast, and
1894
+ * the binding case is a scrolled overlay header: `contrast-glass` paints 60%
1895
+ * black or white, which composites the backdrop into a known band whatever the
1896
+ * page content underneath happens to be. The lightest backdrop a dark glass can
1897
+ * present is 60% black over white, `#666666`; a white foreground softened to
1898
+ * 85% reads 4.69:1 there, against 4.07:1 at 75%, which fails. Nav links are
1899
+ * 18px regular (`textSize: 'lg'`), so the bar is WCAG AA's 4.5:1 rather than
1900
+ * the large-text 3:1.
1901
+ *
1902
+ * The mirror case — a light glass over dark content, `#999999` — cannot be met
1903
+ * by any softening, because the *resting* colour is already only about 4.6:1
1904
+ * there. That is a property of the glass rather than of this derivation, and
1905
+ * it wants fixing where the glass tone is chosen.
1906
+ */
1907
+ const FOREGROUND_HOVER_OPACITY_PERCENT = 85;
1908
+ /**
1909
+ * Derive the hover foreground that belongs with a resolved resting foreground.
1910
+ *
1911
+ * A resting foreground is chosen to be legible against a known surface. A hover
1912
+ * state derived from it stays within a bounded distance of that colour, so it
1913
+ * inherits most of the resting colour's contrast margin instead of asserting
1914
+ * an unrelated brand colour that no one checked against the surface.
1915
+ *
1916
+ * That bounded-distance property is what lets this be used on a transparent
1917
+ * overlay header, where the backdrop is only partly knowable: the scrolled
1918
+ * state composites a 60% glass tint over arbitrary page content, so no
1919
+ * compile-time contrast proof is available there. Staying near a colour that
1920
+ * is already trusted in both states is a weaker claim than a contrast proof,
1921
+ * and it is one this function can actually keep.
1922
+ *
1923
+ * Returns `null` when the resting token is not a plain palette token — an
1924
+ * already-faded or raw value has no well-defined softening, and the caller
1925
+ * should leave hover alone rather than guess.
1926
+ */
1927
+ function deriveForegroundHoverToken(restingToken) {
1928
+ const parts = parseColorTokenParts(restingToken);
1929
+ if (parts.kind !== "token") return null;
1930
+ if (parts.opacityPercent !== null) return null;
1931
+ return parseThemeColorToken(`${parts.token}/${FOREGROUND_HOVER_OPACITY_PERCENT}`);
1932
+ }
1933
+ /**
1934
+ * The opacity `contrast-glass` paints at. Mirrors the value in
1935
+ * `generateHeaderScrollTreatmentStyles`; the two must agree, because the whole
1936
+ * point of the bound below is that it describes what that treatment can produce.
1937
+ */
1938
+ const CONTRAST_GLASS_ALPHA = .6;
1939
+ /**
1940
+ * The extreme backdrop a glass tone can present, whatever page content happens
1941
+ * to be underneath it.
1942
+ *
1943
+ * A 60% tint clamps the composite into a known band, which is what makes a
1944
+ * conservative check possible at compile time even though the content passing
1945
+ * under a sticky header is not knowable. For a dark glass the composite is
1946
+ * lightest over white; for a light glass it is darkest over black. Those are
1947
+ * the two ends that a foreground has to survive.
1948
+ */
1949
+ function worstCaseGlassBackdrop(tone) {
1950
+ const [over, tint$1] = tone === "dark" ? ["#ffffff", "#000000"] : ["#000000", "#ffffff"];
1951
+ return compositeHex(tint$1, over, CONTRAST_GLASS_ALPHA);
1952
+ }
1953
+ /** Flatten `foreground` at `alpha` over `background` into an opaque hex. */
1954
+ function compositeHex(foreground, background, alpha) {
1955
+ const f = hexToRgb(foreground);
1956
+ const b = hexToRgb(background);
1957
+ return rgbToHex(Math.round(mix$1(b.r, f.r, alpha)), Math.round(mix$1(b.g, f.g, alpha)), Math.round(mix$1(b.b, f.b, alpha)));
1958
+ }
1959
+ /**
1960
+ * Prefer an authored hover that survives every measured backdrop, then a
1961
+ * checked softening of the resting colour. A null backdrop set represents a
1962
+ * trusted treatment: only an explicit authored hover may change the colour.
1963
+ * When neither option works, retain the resting colour and underline it.
1964
+ */
1965
+ function planForegroundHover(input) {
1966
+ const { restingToken, candidate: candidate$1, palette, backdrops } = input;
1967
+ if (candidate$1 && String(candidate$1) !== String(restingToken)) {
1968
+ if (backdrops === null || survivesEveryBackdrop(candidate$1, palette, backdrops)) {
1969
+ return {
1970
+ kind: "candidate",
1971
+ token: candidate$1,
1972
+ decoration: "initial"
1973
+ };
1974
+ }
1975
+ }
1976
+ const resting = parseThemeColorToken(restingToken);
1977
+ const softened = resting ? deriveForegroundHoverToken(resting) : null;
1978
+ if (softened && backdrops && survivesEveryBackdrop(softened, palette, backdrops)) {
1979
+ return {
1980
+ kind: "softened",
1981
+ token: softened,
1982
+ decoration: "initial"
1983
+ };
1984
+ }
1985
+ return {
1986
+ kind: "resting",
1987
+ token: restingToken,
1988
+ decoration: "underline"
1989
+ };
1990
+ }
1991
+ function survivesEveryBackdrop(token, palette, backdrops) {
1992
+ return backdrops.length > 0 && backdrops.every((backdrop) => {
1993
+ const foreground = resolveOpaqueSurfaceHex(token, palette, backdrop);
1994
+ return foreground !== null && contrastRatio(foreground, backdrop) >= ORDINARY_TEXT_MIN_CONTRAST_RATIO;
1995
+ });
1996
+ }
1997
+
1889
1998
  //#endregion
1890
1999
  //#region ../theme-core/src/surfaces/foregroundRoles.ts
1891
2000
  const SURFACE_FOREGROUND_ROLES = [
@@ -1897,6 +2006,425 @@ const SURFACE_FOREGROUND_ROLES = [
1897
2006
  "muted"
1898
2007
  ];
1899
2008
 
2009
+ //#endregion
2010
+ //#region ../theme-core/src/palette/family.ts
2011
+ /**
2012
+ * Only `colourFamily` makes one (the class is not exported), so a caller can't
2013
+ * swap in its own members and still hand the solver a valid family. That
2014
+ * matters because `unpaintable` relies on the members running from black to
2015
+ * white.
2016
+ */
2017
+ var Family = class {
2018
+ #members;
2019
+ constructor(seed, members) {
2020
+ this.seed = seed;
2021
+ this.#members = Object.freeze(members);
2022
+ }
2023
+ /** Unique by hex, ascending luminance. Includes the exact seed, black and white. */
2024
+ get members() {
2025
+ return this.#members;
2026
+ }
2027
+ };
2028
+ /** Lightness steps between black and white. */
2029
+ const FAMILY_STEPS = 512;
2030
+ const GAMUT_BISECTION_STEPS = 18;
2031
+ /** The highest chroma at or below `c` that stays inside sRGB at this lightness and hue. */
2032
+ function gamutChroma(l, c, h) {
2033
+ if (inLinearSrgbGamut(oklchToLinearSrgb(l, c, h))) return c;
2034
+ return Array.from({ length: GAMUT_BISECTION_STEPS }).reduce(({ lo, hi }) => {
2035
+ const mid = (lo + hi) / 2;
2036
+ return inLinearSrgbGamut(oklchToLinearSrgb(l, mid, h)) ? {
2037
+ lo: mid,
2038
+ hi
2039
+ } : {
2040
+ lo,
2041
+ hi: mid
2042
+ };
2043
+ }, {
2044
+ lo: 0,
2045
+ hi: c
2046
+ }).lo;
2047
+ }
2048
+ function oklabDistance(a, b) {
2049
+ return Math.hypot(a.L - b.L, a.a - b.a, a.b - b.b);
2050
+ }
2051
+ function colourFamily(seedHex) {
2052
+ if (!/^#[0-9a-f]{6}$/i.test(seedHex)) throw new Error(`Expected an opaque #rrggbb colour: ${seedHex}`);
2053
+ const features = featuresFromHex(seedHex.toLowerCase());
2054
+ const seed = member(seedHex.toLowerCase(), {
2055
+ l: features.okL,
2056
+ c: features.okC,
2057
+ h: features.okH
2058
+ });
2059
+ const seedTaper = Math.max(.001, Math.sin(Math.PI * seed.sample.l));
2060
+ const samples = Array.from({ length: FAMILY_STEPS + 1 }, (_, i) => {
2061
+ const l = i / FAMILY_STEPS;
2062
+ const c = gamutChroma(l, seed.sample.c * Math.min(1, Math.sin(Math.PI * l) / seedTaper), seed.sample.h);
2063
+ return sampleMember({
2064
+ l,
2065
+ c,
2066
+ h: seed.sample.h
2067
+ });
2068
+ });
2069
+ const unique = new Map([seed, ...samples].map((candidate$1) => [candidate$1.hex, candidate$1]).reverse());
2070
+ const members = [...unique.values()].sort((a, b) => a.y - b.y || (a.hex < b.hex ? -1 : a.hex > b.hex ? 1 : 0));
2071
+ return new Family(seed, members);
2072
+ }
2073
+ function sampleMember(sample) {
2074
+ const { r, g, b } = oklchToLinearSrgb(sample.l, sample.c, sample.h);
2075
+ return member(rgbToHex(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)), sample);
2076
+ }
2077
+ function member(hex$1, sample) {
2078
+ const { r, g, b } = hexToRgb(hex$1);
2079
+ return {
2080
+ hex: hex$1,
2081
+ y: hexLuminance(hex$1),
2082
+ oklab: srgbToOklab(r, g, b),
2083
+ sample
2084
+ };
2085
+ }
2086
+
2087
+ //#endregion
2088
+ //#region ../theme-core/src/palette/utils/inkSolver.ts
2089
+ /**
2090
+ * How far (OKLab ΔE) a brand colour drawn as ink may be nudged along its own
2091
+ * family before it stops reading as that colour. Past this the caller uses
2092
+ * neutral or palette text instead (Will, 2026-10-14).
2093
+ */
2094
+ const BRAND_INK_MAX_DELTA_E = .08;
2095
+ /**
2096
+ * The seed itself when it clears `paint`, otherwise the family member nearest
2097
+ * the seed (OKLab ΔE) that clears. On a light ground that is darker, on a dark
2098
+ * one lighter. A passing member further than `maxDistance` from the seed is
2099
+ * refused as `too-far`, which is distinct from `unpaintable`: the family runs
2100
+ * from black to white, so if no member clears, neither does black or white.
2101
+ */
2102
+ function inkFrom(family, paint, ratio$1, maxDistance = BRAND_INK_MAX_DELTA_E) {
2103
+ assertContrastRatio(ratio$1);
2104
+ if (!Number.isFinite(maxDistance) || maxDistance < 0) {
2105
+ throw new RangeError(`Maximum ink distance must be finite and not negative: ${maxDistance}`);
2106
+ }
2107
+ const seedRatio = worstContrast(family.seed.y, paint);
2108
+ if (seedRatio >= ratio$1) return {
2109
+ kind: "ink",
2110
+ hex: family.seed.hex,
2111
+ ratio: seedRatio,
2112
+ source: "exact"
2113
+ };
2114
+ const nearest = family.members.reduce((best, member$1) => {
2115
+ const achieved = worstContrast(member$1.y, paint);
2116
+ if (achieved < ratio$1) return best;
2117
+ const distance$1 = oklabDistance(member$1.oklab, family.seed.oklab);
2118
+ return best === null || distance$1 < best.distance ? {
2119
+ hex: member$1.hex,
2120
+ distance: distance$1,
2121
+ ratio: achieved
2122
+ } : best;
2123
+ }, null);
2124
+ if (nearest === null) return {
2125
+ kind: "unpaintable",
2126
+ interval: paint,
2127
+ ratio: ratio$1
2128
+ };
2129
+ if (nearest.distance > maxDistance) {
2130
+ return {
2131
+ kind: "too-far",
2132
+ nearest: {
2133
+ hex: nearest.hex,
2134
+ distance: nearest.distance
2135
+ },
2136
+ maxDistance
2137
+ };
2138
+ }
2139
+ return {
2140
+ kind: "ink",
2141
+ hex: nearest.hex,
2142
+ ratio: nearest.ratio,
2143
+ source: "family"
2144
+ };
2145
+ }
2146
+ /**
2147
+ * Neutral text for a paint. The style's preferred neutral wins when one of its
2148
+ * pair clears (the higher-contrast one, dark on a tie); otherwise literal black
2149
+ * or white by `maxContrastBlackOrWhite`; otherwise `unpaintable`.
2150
+ */
2151
+ function neutralInk(paint, ratio$1, preferred) {
2152
+ assertContrastRatio(ratio$1);
2153
+ if (preferred) {
2154
+ const dark = worstContrast(hexLuminance(preferred.dark), paint);
2155
+ const light = worstContrast(hexLuminance(preferred.light), paint);
2156
+ const [hex$1, achieved] = dark >= light ? [preferred.dark, dark] : [preferred.light, light];
2157
+ if (achieved >= ratio$1) return {
2158
+ kind: "ink",
2159
+ hex: hex$1,
2160
+ ratio: achieved,
2161
+ source: "neutral"
2162
+ };
2163
+ }
2164
+ const literal = maxContrastBlackOrWhite(paint);
2165
+ return literal.ratio >= ratio$1 ? {
2166
+ kind: "ink",
2167
+ hex: literal.ink === "black" ? "#000000" : "#ffffff",
2168
+ ratio: literal.ratio,
2169
+ source: "neutral"
2170
+ } : {
2171
+ kind: "unpaintable",
2172
+ interval: paint,
2173
+ ratio: ratio$1
2174
+ };
2175
+ }
2176
+
2177
+ //#endregion
2178
+ //#region ../theme-core/src/surfaces/surfaceInks.ts
2179
+ const SURFACE_INK_ROLES = [
2180
+ "brand",
2181
+ "accent",
2182
+ "focus"
2183
+ ];
2184
+ /** Lines and focus rings are graphics: WCAG non-text contrast. */
2185
+ const GRAPHIC_MIN_CONTRAST_RATIO = 3;
2186
+ const INK_SEEDS = {
2187
+ brand: {
2188
+ token: "primary",
2189
+ ratio: ORDINARY_TEXT_MIN_CONTRAST_RATIO
2190
+ },
2191
+ accent: {
2192
+ token: "accent",
2193
+ ratio: ORDINARY_TEXT_MIN_CONTRAST_RATIO
2194
+ },
2195
+ focus: {
2196
+ token: "primary",
2197
+ ratio: GRAPHIC_MIN_CONTRAST_RATIO
2198
+ }
2199
+ };
2200
+ /**
2201
+ * Solve the three inks for a measured solid paint. `textInk` is the surface's
2202
+ * own (already certified) text ink: it stands in when the owner's colour is
2203
+ * missing, or can only clear the paint by drifting too far from itself.
2204
+ * A block owner can supply an authored shade candidate for a local ink.
2205
+ */
2206
+ function solveSurfaceInks(backdropHex, textInk, palette, candidates = {}) {
2207
+ const paint = solidPaint(backdropHex);
2208
+ const fallback = literalInk(resolveConcreteColorHex(textInk, palette) ?? pickBlackOrWhiteTextColor(backdropHex));
2209
+ return {
2210
+ brand: solveInk({
2211
+ ...INK_SEEDS.brand,
2212
+ token: candidates.brand ?? INK_SEEDS.brand.token
2213
+ }, paint, palette, fallback),
2214
+ accent: solveInk({
2215
+ ...INK_SEEDS.accent,
2216
+ token: candidates.accent ?? INK_SEEDS.accent.token
2217
+ }, paint, palette, fallback),
2218
+ focus: solveInk({
2219
+ ...INK_SEEDS.focus,
2220
+ token: candidates.focus ?? INK_SEEDS.focus.token
2221
+ }, paint, palette, fallback)
2222
+ };
2223
+ }
2224
+ function solveInk(seed, paint, palette, fallback) {
2225
+ const token = parseThemeColorToken(seed.token);
2226
+ const seedHex = hexColorSchema.safeParse(resolveConcreteColorHex(seed.token, palette));
2227
+ if (!token || !seedHex.success) return fallback;
2228
+ const solved = inkFrom(familyOf(seedHex.data), paint, seed.ratio);
2229
+ switch (solved.kind) {
2230
+ case "ink": return solved.source === "exact" ? token : literalInk(solved.hex);
2231
+ case "too-far":
2232
+ case "unpaintable": return fallback;
2233
+ }
2234
+ }
2235
+ function literalInk(hex$1) {
2236
+ return hexColorSchema.parse(hex$1.toLowerCase());
2237
+ }
2238
+ const FAMILY_CACHE_LIMIT = 64;
2239
+ const families = new Map();
2240
+ function familyOf(seed) {
2241
+ const cached = families.get(seed);
2242
+ if (cached) return cached;
2243
+ if (families.size >= FAMILY_CACHE_LIMIT) families.clear();
2244
+ const family = colourFamily(seed);
2245
+ families.set(seed, family);
2246
+ return family;
2247
+ }
2248
+
2249
+ //#endregion
2250
+ //#region ../theme-core/src/surfaces/resolveSurfaceForeground.ts
2251
+ const FALLBACK_TEXT_TOKENS = [
2252
+ "text",
2253
+ "primaryForeground",
2254
+ "secondaryForeground",
2255
+ "accentForeground",
2256
+ "neutral-950",
2257
+ "neutral-50"
2258
+ ];
2259
+ const CANONICAL_EQUIVALENT_TOKEN_ORDER = [
2260
+ "text",
2261
+ "primaryForeground",
2262
+ "secondaryForeground",
2263
+ "accentForeground",
2264
+ "onPrimary",
2265
+ "onSecondary",
2266
+ "onAccent",
2267
+ "onSurface",
2268
+ "neutral-950",
2269
+ "neutral-50"
2270
+ ];
2271
+ function resolveSurfaceForeground(background, palette, candidates = {}, options = {}) {
2272
+ const authored = foregroundCandidates(background, candidates);
2273
+ if ("measuredBackdrop" in options) return resolveMeasuredForeground(background?.color ?? "background", options.measuredBackdrop, authored, palette);
2274
+ return resolveAuthoredBackground(background, palette, authored, options);
2275
+ }
2276
+ /** Legacy references may be measurable, inherited, or a trusted media treatment. */
2277
+ function resolveAuthoredBackground(background, palette, authored, options) {
2278
+ if (background === null) {
2279
+ return resolveDeterministicForeground("background", {
2280
+ ...authored,
2281
+ text: authored.text ?? "text"
2282
+ }, palette);
2283
+ }
2284
+ if (background.type === "color") {
2285
+ const backgroundReference = normalizedReference(background.color);
2286
+ if (!backgroundReference || backgroundReference === "transparent") {
2287
+ return { kind: "inherits" };
2288
+ }
2289
+ return resolveDeterministicForeground(backgroundReference, authored, palette, options.compositeBase);
2290
+ }
2291
+ return resolveTrustedTreatmentForeground(authored, palette);
2292
+ }
2293
+ function foregroundCandidates(background, overrides) {
2294
+ return {
2295
+ text: candidateOrBackground(overrides.text, background?.textColor),
2296
+ heading: candidateOrBackground(overrides.heading, background?.headingColor),
2297
+ link: candidateOrBackground(overrides.link, background?.linkColor),
2298
+ linkHover: candidateOrBackground(overrides.linkHover),
2299
+ supporting: candidateOrBackground(overrides.supporting, background?.supportingColor)
2300
+ };
2301
+ }
2302
+ function candidateOrBackground(candidate$1, backgroundValue) {
2303
+ return candidate$1 ?? backgroundValue ?? null;
2304
+ }
2305
+ function resolveDeterministicForeground(backgroundReference, candidates, palette, compositeBase) {
2306
+ const backgroundHex = resolveOpaqueSurfaceHex(backgroundReference, palette, compositeBase);
2307
+ if (!backgroundHex) {
2308
+ return resolveTrustedTreatmentForeground(candidates, palette);
2309
+ }
2310
+ return resolveMeasuredForeground(backgroundReference, backgroundHex, candidates, palette);
2311
+ }
2312
+ function resolveMeasuredForeground(backgroundReference, backgroundHex, candidates, palette) {
2313
+ const backgroundToken$1 = paletteToken(backgroundReference);
2314
+ const derivedOnToken = backgroundToken$1 ? onColorTokenName(backgroundToken$1) : null;
2315
+ const text = firstSafeToken([
2316
+ candidates.text,
2317
+ derivedOnToken,
2318
+ ...FALLBACK_TEXT_TOKENS
2319
+ ], backgroundHex, palette, derivedOnToken ? [derivedOnToken] : []) ?? pickBlackOrWhiteTextColor(backgroundHex);
2320
+ return knownForeground({
2321
+ text,
2322
+ heading: safeTokenOr(candidates.heading, text, backgroundHex, palette),
2323
+ link: safeTokenOr(candidates.link, text, backgroundHex, palette),
2324
+ supporting: safeTokenOr(candidates.supporting, text, backgroundHex, palette)
2325
+ }, {
2326
+ kind: "measured",
2327
+ backdropHex: backgroundHex,
2328
+ inks: solveSurfaceInks(backgroundHex, text, palette)
2329
+ }, candidates.linkHover, palette);
2330
+ }
2331
+ function resolveTrustedTreatmentForeground(candidates, palette) {
2332
+ const text = canonicalToken(candidates.text, palette);
2333
+ if (!text) return { kind: "unmeasured-paint" };
2334
+ return knownForeground({
2335
+ text,
2336
+ heading: canonicalToken(candidates.heading, palette) ?? text,
2337
+ link: canonicalToken(candidates.link, palette) ?? text,
2338
+ supporting: canonicalToken(candidates.supporting, palette) ?? text
2339
+ }, { kind: "trusted-treatment" }, candidates.linkHover, palette);
2340
+ }
2341
+ function knownForeground(roles, provenance, hoverCandidate, palette) {
2342
+ const hover = planForegroundHover({
2343
+ restingToken: roles.link,
2344
+ candidate: canonicalHoverToken(hoverCandidate, palette),
2345
+ palette,
2346
+ backdrops: provenance.kind === "measured" ? [provenance.backdropHex] : null
2347
+ });
2348
+ return {
2349
+ kind: "known",
2350
+ provenance,
2351
+ roles: {
2352
+ ...roles,
2353
+ linkHover: hover.token,
2354
+ muted: provenance.kind === "measured" ? deriveSurfaceMutedColor(provenance.backdropHex, resolveConcreteColorHex(roles.text, palette) ?? provenance.backdropHex) : roles.text
2355
+ },
2356
+ hoverDecoration: hover.decoration
2357
+ };
2358
+ }
2359
+ function canonicalHoverToken(reference, palette) {
2360
+ const token = parseThemeColorToken(reference);
2361
+ if (token) {
2362
+ const parts = parseColorTokenParts(token);
2363
+ if (parts.kind === "token" && resolveConcreteColorHex(parts.token, palette)) {
2364
+ return token;
2365
+ }
2366
+ }
2367
+ return canonicalToken(reference, palette);
2368
+ }
2369
+ /**
2370
+ * The one rule for what a resolved foreground publishes for the solved inks.
2371
+ * Only a measured paint under a publishing runtime publishes inks; any other
2372
+ * paint it owns resets them, and a paint it only inherits publishes nothing.
2373
+ */
2374
+ function surfaceInkPublication(foreground, policy) {
2375
+ switch (foreground.kind) {
2376
+ case "inherits": return { kind: "none" };
2377
+ case "unmeasured-paint": return { kind: "reset" };
2378
+ case "known": return foreground.provenance.kind === "measured" && policy === "publish" ? {
2379
+ kind: "solved",
2380
+ inks: foreground.provenance.inks
2381
+ } : { kind: "reset" };
2382
+ }
2383
+ }
2384
+ /** Keep a candidate only when it meets ordinary-text contrast on the ground. */
2385
+ function safeTokenOr(candidate$1, fallback, backgroundHex, palette) {
2386
+ return firstSafeToken([candidate$1], backgroundHex, palette) ?? fallback;
2387
+ }
2388
+ function firstSafeToken(candidates, backgroundHex, palette, preferredEquivalentTokens = []) {
2389
+ const seen = new Set();
2390
+ for (const candidate$1 of candidates) {
2391
+ const token = canonicalToken(candidate$1, palette, preferredEquivalentTokens);
2392
+ if (!token || seen.has(token)) continue;
2393
+ seen.add(token);
2394
+ const foregroundHex = resolveConcreteColorHex(token, palette);
2395
+ if (foregroundHex && contrastRatio(backgroundHex, foregroundHex) >= ORDINARY_TEXT_MIN_CONTRAST_RATIO) {
2396
+ return token;
2397
+ }
2398
+ }
2399
+ return null;
2400
+ }
2401
+ function canonicalToken(reference, palette, preferredEquivalentTokens = []) {
2402
+ const normalized = normalizedReference(reference);
2403
+ if (!normalized || normalized === "transparent") return null;
2404
+ const direct = parseThemeColorToken(normalized);
2405
+ if (direct && resolveConcreteColorHex(direct, palette)) return direct;
2406
+ const referenceHex = resolveConcreteColorHex(normalized, palette);
2407
+ if (!referenceHex) return null;
2408
+ const equivalent = [
2409
+ ...preferredEquivalentTokens,
2410
+ ...CANONICAL_EQUIVALENT_TOKEN_ORDER,
2411
+ ...Object.keys(palette).sort()
2412
+ ].find((token) => {
2413
+ const parsed = parseThemeColorToken(token);
2414
+ return parsed !== null && resolveConcreteColorHex(parsed, palette)?.toLowerCase() === referenceHex.toLowerCase();
2415
+ });
2416
+ return equivalent ? parseThemeColorToken(equivalent) : null;
2417
+ }
2418
+ function paletteToken(reference) {
2419
+ const parts = parseColorTokenParts(reference);
2420
+ return parts.kind === "token" && parts.opacityPercent === null ? parts.token : null;
2421
+ }
2422
+ function normalizedReference(value) {
2423
+ if (typeof value !== "string") return null;
2424
+ const normalized = value.trim();
2425
+ return normalized.length > 0 ? normalized : null;
2426
+ }
2427
+
1900
2428
  //#endregion
1901
2429
  //#region ../theme-core/src/surfaces/logoVariant.ts
1902
2430
  /** Concrete hex behind a plain, opaque palette token, or null otherwise. */
@@ -1914,9 +2442,9 @@ function opaquePaletteHexForToken(expandedPalette, token) {
1914
2442
  * a guess.
1915
2443
  */
1916
2444
  function surfaceToneFromForegroundToken(expandedPalette, foregroundToken) {
1917
- const hex = opaquePaletteHexForToken(expandedPalette, foregroundToken);
1918
- if (!hex) return "unknown";
1919
- return featuresFromHex(hex).Y >= .5 ? "dark" : "light";
2445
+ const hex$1 = opaquePaletteHexForToken(expandedPalette, foregroundToken);
2446
+ if (!hex$1) return "unknown";
2447
+ return featuresFromHex(hex$1).Y >= .5 ? "dark" : "light";
1920
2448
  }
1921
2449
  /**
1922
2450
  * The logo to draw on a surface of the given tone. A missing variant falls
@@ -3220,14 +3748,14 @@ function resolveSolid(input, backgroundToken$1, edge) {
3220
3748
  function resolveSolidInk(input, backgroundToken$1, backgroundHex) {
3221
3749
  const foregroundToken = unsafeAsOpaqueButtonColourToken(solidForegroundToken(backgroundToken$1));
3222
3750
  const candidate$1 = input.palette[foregroundToken];
3223
- const contrast = typeof candidate$1 === "string" ? validateButtonTextContrast({
3751
+ const contrast$1 = typeof candidate$1 === "string" ? validateButtonTextContrast({
3224
3752
  ...input,
3225
3753
  background: backgroundToken$1,
3226
3754
  backgroundHex,
3227
3755
  foreground: foregroundToken,
3228
3756
  foregroundHex: candidate$1
3229
3757
  }) : null;
3230
- if (contrast?.ok === true && typeof candidate$1 === "string") return {
3758
+ if (contrast$1?.ok === true && typeof candidate$1 === "string") return {
3231
3759
  foregroundHex: candidate$1,
3232
3760
  colour: {
3233
3761
  kind: "solid",
@@ -3287,14 +3815,14 @@ function resolveGradient(input, treatment) {
3287
3815
  for (let sample = 0; sample <= BUTTON_CONTRAST_POLICY.gradientSamplesPerSegment; sample += 1) {
3288
3816
  const position = sample / BUTTON_CONTRAST_POLICY.gradientSamplesPerSegment;
3289
3817
  const sampleHex = interpolateHex(start, end, position);
3290
- const contrast = validateButtonTextContrast({
3818
+ const contrast$1 = validateButtonTextContrast({
3291
3819
  ...input,
3292
3820
  background: `${startToken}→${endToken}@${position.toFixed(2)}`,
3293
3821
  backgroundHex: sampleHex,
3294
3822
  foreground: treatment.foregroundToken,
3295
3823
  foregroundHex: foregroundHex.hex
3296
3824
  });
3297
- if (!contrast.ok) return contrast;
3825
+ if (!contrast$1.ok) return contrast$1;
3298
3826
  if (input.surface) {
3299
3827
  const boundary = validateButtonSurfaceContrast({
3300
3828
  ...input,
@@ -3318,10 +3846,10 @@ function resolveGradient(input, treatment) {
3318
3846
  };
3319
3847
  }
3320
3848
  function paletteHex(input, token) {
3321
- const hex = input.palette[token];
3322
- return typeof hex === "string" ? {
3849
+ const hex$1 = input.palette[token];
3850
+ return typeof hex$1 === "string" ? {
3323
3851
  ok: true,
3324
- hex
3852
+ hex: hex$1
3325
3853
  } : {
3326
3854
  ok: false,
3327
3855
  error: {
@@ -3418,11 +3946,11 @@ function deriveKnownBackground(input, palette, fallbackToken) {
3418
3946
  };
3419
3947
  }
3420
3948
  function resolveKnownSurfaceColour(value, palette, underlayReference) {
3421
- const hex = resolveOpaqueSurfaceHex(value, palette, underlayReference);
3422
- if (!hex) return resolveKnownColour(value, palette);
3949
+ const hex$1 = resolveOpaqueSurfaceHex(value, palette, underlayReference);
3950
+ if (!hex$1) return resolveKnownColour(value, palette);
3423
3951
  const reference = parseThemeColorReference(value, { palette });
3424
3952
  return {
3425
- hex,
3953
+ hex: hex$1,
3426
3954
  css: resolveThemeColorReferenceValue(value, { palette }),
3427
3955
  source: reference.kind === "literal-css" ? "literal-css" : "palette-token"
3428
3956
  };
@@ -3706,10 +4234,10 @@ function resolveKnownColour(value, palette) {
3706
4234
  if (reference.kind !== "literal-css") {
3707
4235
  const token = String(reference.token);
3708
4236
  const paletteHex$1 = palette[token];
3709
- const hex$1 = paletteHex$1 ? normalizeOpaqueHex(paletteHex$1) : null;
3710
- if (hex$1) {
4237
+ const hex$2 = paletteHex$1 ? normalizeOpaqueHex(paletteHex$1) : null;
4238
+ if (hex$2) {
3711
4239
  return {
3712
- hex: hex$1,
4240
+ hex: hex$2,
3713
4241
  css: resolveThemeColorReferenceValue(value, { palette }),
3714
4242
  source: "palette-token"
3715
4243
  };
@@ -3721,9 +4249,9 @@ function resolveKnownColour(value, palette) {
3721
4249
  source: "literal-css"
3722
4250
  } : null;
3723
4251
  }
3724
- const hex = normalizeOpaqueCssColour(reference.value);
3725
- return hex ? {
3726
- hex,
4252
+ const hex$1 = normalizeOpaqueCssColour(reference.value);
4253
+ return hex$1 ? {
4254
+ hex: hex$1,
3727
4255
  css: reference.value,
3728
4256
  source: "literal-css"
3729
4257
  } : null;
@@ -3743,8 +4271,8 @@ const NAMED_OPAQUE_COLOURS = {
3743
4271
  };
3744
4272
  function normalizeOpaqueCssColour(value) {
3745
4273
  const trimmed = value.trim();
3746
- const hex = normalizeOpaqueHex(trimmed);
3747
- if (hex) return hex;
4274
+ const hex$1 = normalizeOpaqueHex(trimmed);
4275
+ if (hex$1) return hex$1;
3748
4276
  const named = NAMED_OPAQUE_COLOURS[trimmed.toLowerCase()];
3749
4277
  return named ?? normalizeOpaqueRgb(trimmed) ?? normalizeOpaqueOklch(trimmed);
3750
4278
  }
@@ -3803,7 +4331,8 @@ function opaqueSurfaceContextFromTheme(theme) {
3803
4331
  palette,
3804
4332
  colourIndex: preparePaletteColourIndex(palette),
3805
4333
  buttonSystem: theme.buttons,
3806
- linkHoverToken: theme.typography.proseLinkStyle?.hoverColorToken
4334
+ linkHoverToken: theme.typography.proseLinkStyle?.hoverColorToken,
4335
+ solvedInks: "publish"
3807
4336
  };
3808
4337
  }
3809
4338
  /**
@@ -3826,6 +4355,7 @@ function opaqueSurfaceDeclarations(surface$1, arrangement = false) {
3826
4355
  ...arrangement ? { muted: colorValue(foreground.muted) } : {},
3827
4356
  hoverDecoration: surface$1.hoverDecoration
3828
4357
  }),
4358
+ ...surfaceInkDeclarations(surface$1.inks),
3829
4359
  ...arrangement && surface$1.decorations !== undefined ? [`${SURFACE_FOREGROUND_CSS_VARS.line}: ${surface$1.decorations.line};`, `${SURFACE_FOREGROUND_CSS_VARS.mark}: ${surface$1.decorations.mark};`] : [],
3830
4360
  ...Object.entries(surface$1.buttonContext.styles).map(([property, value]) => `${property}: ${value};`)
3831
4361
  ];
@@ -3839,6 +4369,7 @@ function inheritedSurfaceDeclarations(arrangement) {
3839
4369
  return [
3840
4370
  "color",
3841
4371
  ...Object.entries(SURFACE_FOREGROUND_CSS_VARS).filter(([role]) => arrangement || role !== "line" && role !== "mark" && role !== "muted").map(([, variable]) => variable),
4372
+ ...Object.values(SURFACE_INK_CSS_VARS),
3842
4373
  "--rb-surface-link-hover-decoration",
3843
4374
  ...Object.keys(contextualButtonSurfaceResetStyles())
3844
4375
  ].map((property) => `${property}: inherit;`);
@@ -4063,8 +4594,8 @@ const mapDrawingTreatments = {
4063
4594
  function mapDrawingTreatmentFor(skin) {
4064
4595
  return (skin === undefined ? undefined : mapDrawingTreatments[skin]) ?? defaultMapDrawingTreatment;
4065
4596
  }
4066
- const toLab = (hex) => {
4067
- const { r, g, b } = hexToRgb(hex);
4597
+ const toLab = (hex$1) => {
4598
+ const { r, g, b } = hexToRgb(hex$1);
4068
4599
  return srgbToOklab(r, g, b);
4069
4600
  };
4070
4601
  const toHex = ({ L, a, b }) => {
@@ -4084,14 +4615,14 @@ function pole(ground, L) {
4084
4615
  }
4085
4616
  /** The first blend from `from` toward `to` that reaches the contrast floor against every backdrop. */
4086
4617
  function firstContrastBlend(from, to, backdrops, floor) {
4087
- const steps = Array.from({ length: 41 }, (_, index) => toHex(blend(from, to, index / 40)));
4088
- return steps.find((hex) => backdrops.every((backdrop) => contrastRatio(hex, backdrop) >= floor)) ?? toHex(to);
4618
+ const steps$1 = Array.from({ length: 41 }, (_, index) => toHex(blend(from, to, index / 40)));
4619
+ return steps$1.find((hex$1) => backdrops.every((backdrop) => contrastRatio(hex$1, backdrop) >= floor)) ?? toHex(to);
4089
4620
  }
4090
4621
  /** A tint must stand apart from what it is drawn on (a brand river on a brand band would vanish); else a step toward the ink. */
4091
4622
  function tint(base, spec, palette, ink) {
4092
4623
  const candidates = spec.roles.flatMap((role) => {
4093
- const hex = resolveConcreteColorHex(role, palette);
4094
- return hex ? [blend(base, toLab(hex), spec.amount)] : [];
4624
+ const hex$1 = resolveConcreteColorHex(role, palette);
4625
+ return hex$1 ? [blend(base, toLab(hex$1), spec.amount)] : [];
4095
4626
  });
4096
4627
  return candidates.find((candidate$1) => distance(candidate$1, base) >= .025) ?? blend(base, ink, Math.max(spec.amount * .6, .06));
4097
4628
  }
@@ -4116,15 +4647,15 @@ function resolveMapDrawingColours(input) {
4116
4647
  toHex(building)
4117
4648
  ];
4118
4649
  const pinRoles = t.pin.flatMap((role) => {
4119
- const hex = resolveConcreteColorHex(role, palette);
4120
- return hex ? [hex] : [];
4650
+ const hex$1 = resolveConcreteColorHex(role, palette);
4651
+ return hex$1 ? [hex$1] : [];
4121
4652
  });
4122
- const pin = [...pinRoles, input.ink].find((hex) => pinBackdrops.every((backdrop) => contrastRatio(hex, backdrop) >= 3)) ?? firstContrastBlend(toLab(pinRoles[0] ?? input.ink), ink, pinBackdrops, 3);
4653
+ const pin = [...pinRoles, input.ink].find((hex$1) => pinBackdrops.every((backdrop) => contrastRatio(hex$1, backdrop) >= 3)) ?? firstContrastBlend(toLab(pinRoles[0] ?? input.ink), ink, pinBackdrops, 3);
4123
4654
  const pinEdge = [
4124
4655
  toHex(light),
4125
4656
  toHex(dark),
4126
4657
  landHex
4127
- ].reduce((best, hex) => contrastRatio(hex, pin) > contrastRatio(best, pin) ? hex : best);
4658
+ ].reduce((best, hex$1) => contrastRatio(hex$1, pin) > contrastRatio(best, pin) ? hex$1 : best);
4128
4659
  return {
4129
4660
  land: landHex,
4130
4661
  water: toHex(tint(land, t.water, palette, ink)),
@@ -4330,369 +4861,138 @@ function mergeStyles(...styles) {
4330
4861
  }
4331
4862
 
4332
4863
  //#endregion
4333
- //#region ../theme-core/src/tokens/resolver.ts
4334
- /**
4335
- * Resolves theme tokens to CSS variables
4336
- *
4337
- * This resolver ensures all button styles use theme tokens (not hardcoded colors)
4338
- * so that changing the palette automatically updates all buttons.
4339
- */
4340
- var TokenResolver = class {
4341
- constructor(theme) {
4342
- this.theme = theme;
4343
- }
4344
- /**
4345
- * Resolve a color token to a CSS variable reference wrapped in rgb()
4346
- *
4347
- * @param token - Token name (e.g., 'primary-500', 'white', 'text-900')
4348
- * @returns CSS rgb() function with var reference (e.g., 'rgb(var(--tb-primary-500))')
4349
- *
4350
- * Note: CSS variables are stored as RGB channels (e.g., "37 99 235") following Tailwind convention,
4351
- * so we must wrap them with rgb() to create a valid color value.
4352
- *
4353
- * @example
4354
- * resolver.getColor('primary-500') // => 'rgb(var(--tb-primary-500))'
4355
- * resolver.getColor('white') // => 'rgb(var(--tb-white))'
4356
- */
4357
- getColor(token) {
4358
- if (!this.tokenExists(token)) {
4359
- console.warn(`[TokenResolver] Color token "${token}" not found in theme palette. Button may render incorrectly.`);
4360
- }
4361
- return rgbColorValue(token);
4864
+ //#region ../theme-core/src/surfaces/foregroundContract.ts
4865
+ const SURFACE_FOREGROUND_CSS_VARS = {
4866
+ text: "--rb-surface-text-color",
4867
+ heading: "--rb-surface-heading-color",
4868
+ link: "--rb-surface-link-color",
4869
+ linkHover: "--rb-surface-link-hover-color",
4870
+ supporting: "--rb-surface-supporting-color",
4871
+ muted: "--rb-surface-muted-color",
4872
+ line: "--rb-surface-line-color",
4873
+ mark: "--rb-surface-mark-color"
4874
+ };
4875
+ const DEFAULT_SURFACE_TEXT_COLOR = "rgb(var(--tb-text, 17 24 39))";
4876
+ const DEFAULT_SURFACE_LINK_COLOR = "rgb(var(--tb-primary, var(--tb-text, 17 24 39)))";
4877
+ const DEFAULT_SURFACE_SUPPORTING_COLOR = "rgb(var(--tb-mutedText, 100 116 139))";
4878
+ const DEFAULT_ROLE_FALLBACKS = {
4879
+ text: DEFAULT_SURFACE_TEXT_COLOR,
4880
+ heading: DEFAULT_SURFACE_TEXT_COLOR,
4881
+ link: DEFAULT_SURFACE_LINK_COLOR,
4882
+ linkHover: DEFAULT_SURFACE_LINK_COLOR,
4883
+ supporting: DEFAULT_SURFACE_SUPPORTING_COLOR,
4884
+ muted: DEFAULT_SURFACE_SUPPORTING_COLOR
4885
+ };
4886
+ function requireSurfaceColorValue(role, value) {
4887
+ const normalized = value.trim();
4888
+ if (normalized.length === 0) {
4889
+ throw new RangeError(`Surface foreground ${role} must be a non-empty CSS color.`);
4362
4890
  }
4363
- /**
4364
- * Adjust a color token's shade by a given amount
4365
- *
4366
- * @param token - Base token (e.g., 'primary-500')
4367
- * @param amount - Shade adjustment (+100 = darker, -100 = lighter)
4368
- * @returns Adjusted token name (e.g., 'primary-600')
4369
- *
4370
- * @example
4371
- * resolver.adjustShade('primary-500', 100) // => 'primary-600' (darker)
4372
- * resolver.adjustShade('primary-500', -100) // => 'primary-400' (lighter)
4373
- */
4374
- adjustShade(token, amount) {
4375
- const parts = token.split("-");
4376
- if (parts.length < 2) {
4377
- const newShade$1 = Math.max(100, Math.min(900, 500 + amount));
4378
- return `${token}-${newShade$1}`;
4379
- }
4380
- const base = parts.slice(0, -1).join("-");
4381
- const shade = parseInt(parts[parts.length - 1] ?? "", 10);
4382
- if (isNaN(shade)) {
4383
- console.warn(`[TokenResolver] Cannot adjust shade for non-shade token "${token}". Returning original token.`);
4384
- return token;
4385
- }
4386
- const newShade = Math.max(100, Math.min(900, shade + amount));
4387
- return `${base}-${newShade}`;
4891
+ return normalized;
4892
+ }
4893
+ function completeSurfaceForegrounds(input) {
4894
+ const text = requireSurfaceColorValue("text", input.text);
4895
+ const link = input.link === undefined ? text : requireSurfaceColorValue("link", input.link);
4896
+ return {
4897
+ text,
4898
+ heading: input.heading === undefined ? text : requireSurfaceColorValue("heading", input.heading),
4899
+ link,
4900
+ linkHover: input.linkHover === undefined ? link : requireSurfaceColorValue("linkHover", input.linkHover),
4901
+ muted: input.muted === undefined ? text : requireSurfaceColorValue("muted", input.muted),
4902
+ supporting: input.supporting === undefined ? text : requireSurfaceColorValue("supporting", input.supporting)
4903
+ };
4904
+ }
4905
+ function surfaceForegroundValue(role, outsideProviderFallback = DEFAULT_ROLE_FALLBACKS[role]) {
4906
+ const roleVariable = SURFACE_FOREGROUND_CSS_VARS[role];
4907
+ const fallback = requireSurfaceColorValue("fallback", outsideProviderFallback);
4908
+ if (role === "text") {
4909
+ return `var(${roleVariable}, ${fallback})`;
4388
4910
  }
4389
- /**
4390
- * Check if a token exists in the theme palette
4391
- *
4392
- * @param token - Token name to check
4393
- * @returns True if token exists in theme
4394
- */
4395
- tokenExists(token) {
4396
- const commonTokens = [
4397
- "white",
4398
- "black",
4399
- "transparent"
4400
- ];
4401
- if (commonTokens.includes(token)) {
4402
- return true;
4403
- }
4404
- const parts = token.split("-");
4405
- if (parts.length >= 2) {
4406
- const lastPart = parts[parts.length - 1] ?? "";
4407
- const shade = parseInt(lastPart, 10);
4408
- if (!isNaN(shade) && shade >= 100 && shade <= 900 && shade % 100 === 0) {
4409
- return true;
4410
- }
4411
- }
4412
- return true;
4911
+ if (role === "linkHover") {
4912
+ return `var(${roleVariable}, ${surfaceForegroundValue("link", fallback)})`;
4413
4913
  }
4914
+ return `var(${roleVariable}, var(${SURFACE_FOREGROUND_CSS_VARS.text}, ${fallback}))`;
4915
+ }
4916
+ /** The three solved inks, published beside the foreground roles by the same surfaces. */
4917
+ const SURFACE_INK_CSS_VARS = {
4918
+ brand: "--rb-surface-brand-color",
4919
+ accent: "--rb-surface-accent-color",
4920
+ focus: "--rb-surface-focus-color"
4414
4921
  };
4415
-
4416
- //#endregion
4417
- //#region ../theme-core/src/surfaces/foregroundInteraction.ts
4418
4922
  /**
4419
- * How far a hover state softens away from its resting foreground, as a
4420
- * percentage of full opacity.
4421
- *
4422
- * Softening — rather than brightening — is forced by the token vocabulary
4423
- * rather than chosen for taste. A resolved foreground is frequently an
4424
- * `on*` token, which `expandPalette` emits as pure `#FFFFFF` or `#000000`
4425
- * with no shade ramp: over a primary-painted section the resolved text role
4426
- * is `onPrimary`, and white has no headroom left to move further from its
4427
- * background. Moving *toward* the mid-range always has headroom, whatever
4428
- * the resting colour, and it keeps the hue exactly.
4429
- *
4430
- * 85% is the measured floor, not a round number. Softening costs contrast, and
4431
- * the binding case is a scrolled overlay header: `contrast-glass` paints 60%
4432
- * black or white, which composites the backdrop into a known band whatever the
4433
- * page content underneath happens to be. The lightest backdrop a dark glass can
4434
- * present is 60% black over white, `#666666`; a white foreground softened to
4435
- * 85% reads 4.69:1 there, against 4.07:1 at 75%, which fails. Nav links are
4436
- * 18px regular (`textSize: 'lg'`), so the bar is WCAG AA's 4.5:1 rather than
4437
- * the large-text 3:1.
4438
- *
4439
- * The mirror case — a light glass over dark content, `#999999` — cannot be met
4440
- * by any softening, because the *resting* colour is already only about 4.6:1
4441
- * there. That is a property of the glass rather than of this derivation, and
4442
- * it wants fixing where the glass tone is chosen.
4923
+ * Read a solved ink, falling back straight to the declaration's own previous
4924
+ * expression. Unlike `surfaceForegroundValue` this never falls through to
4925
+ * `--rb-surface-text-color`: where no ink is published (bridge pages, unmeasured
4926
+ * paints) brand text must keep its brand colour, not turn into the text ink.
4443
4927
  */
4444
- const FOREGROUND_HOVER_OPACITY_PERCENT = 85;
4445
- /**
4446
- * Derive the hover foreground that belongs with a resolved resting foreground.
4447
- *
4448
- * A resting foreground is chosen to be legible against a known surface. A hover
4449
- * state derived from it stays within a bounded distance of that colour, so it
4450
- * inherits most of the resting colour's contrast margin instead of asserting
4451
- * an unrelated brand colour that no one checked against the surface.
4452
- *
4453
- * That bounded-distance property is what lets this be used on a transparent
4454
- * overlay header, where the backdrop is only partly knowable: the scrolled
4455
- * state composites a 60% glass tint over arbitrary page content, so no
4456
- * compile-time contrast proof is available there. Staying near a colour that
4457
- * is already trusted in both states is a weaker claim than a contrast proof,
4458
- * and it is one this function can actually keep.
4459
- *
4460
- * Returns `null` when the resting token is not a plain palette token — an
4461
- * already-faded or raw value has no well-defined softening, and the caller
4462
- * should leave hover alone rather than guess.
4463
- */
4464
- function deriveForegroundHoverToken(restingToken) {
4465
- const parts = parseColorTokenParts(restingToken);
4466
- if (parts.kind !== "token") return null;
4467
- if (parts.opacityPercent !== null) return null;
4468
- return parseThemeColorToken(`${parts.token}/${FOREGROUND_HOVER_OPACITY_PERCENT}`);
4928
+ function surfaceInkValue(role, previousExpression) {
4929
+ return `var(${SURFACE_INK_CSS_VARS[role]}, ${requireSurfaceColorValue("fallback", previousExpression)})`;
4469
4930
  }
4470
4931
  /**
4471
- * The opacity `contrast-glass` paints at. Mirrors the value in
4472
- * `generateHeaderScrollTreatmentStyles`; the two must agree, because the whole
4473
- * point of the bound below is that it describes what that treatment can produce.
4932
+ * The declarations for a surface's ink publication. An exact ink is a token
4933
+ * reference (`rgb(var(--tb-primary))`), so a site's own token value wins; a
4934
+ * nudged or replaced ink is a literal colour.
4474
4935
  */
4475
- const CONTRAST_GLASS_ALPHA = .6;
4476
- /**
4477
- * The extreme backdrop a glass tone can present, whatever page content happens
4478
- * to be underneath it.
4479
- *
4480
- * A 60% tint clamps the composite into a known band, which is what makes a
4481
- * conservative check possible at compile time even though the content passing
4482
- * under a sticky header is not knowable. For a dark glass the composite is
4483
- * lightest over white; for a light glass it is darkest over black. Those are
4484
- * the two ends that a foreground has to survive.
4485
- */
4486
- function worstCaseGlassBackdrop(tone) {
4487
- const [over, tint$1] = tone === "dark" ? ["#ffffff", "#000000"] : ["#000000", "#ffffff"];
4488
- return compositeHex(tint$1, over, CONTRAST_GLASS_ALPHA);
4936
+ function surfaceInkStyle(publication) {
4937
+ switch (publication.kind) {
4938
+ case "none": return {};
4939
+ case "reset": return inkVariables(() => "initial");
4940
+ case "solved": return inkVariables((role) => {
4941
+ const ink = publication.inks[role];
4942
+ const token = parseThemeColorToken(ink);
4943
+ return token ? rgbColorValue(token) : ink;
4944
+ });
4945
+ }
4489
4946
  }
4490
- /** Flatten `foreground` at `alpha` over `background` into an opaque hex. */
4491
- function compositeHex(foreground, background, alpha) {
4492
- const f = hexToRgb(foreground);
4493
- const b = hexToRgb(background);
4494
- return rgbToHex(Math.round(mix$1(b.r, f.r, alpha)), Math.round(mix$1(b.g, f.g, alpha)), Math.round(mix$1(b.b, f.b, alpha)));
4947
+ function surfaceInkDeclarations(publication) {
4948
+ return Object.entries(surfaceInkStyle(publication)).map(([property, value]) => `${property}: ${value};`);
4495
4949
  }
4496
- /**
4497
- * Prefer an authored hover that survives every measured backdrop, then a
4498
- * checked softening of the resting colour. A null backdrop set represents a
4499
- * trusted treatment: only an explicit authored hover may change the colour.
4500
- * When neither option works, retain the resting colour and underline it.
4501
- */
4502
- function planForegroundHover(input) {
4503
- const { restingToken, candidate: candidate$1, palette, backdrops } = input;
4504
- if (candidate$1 && String(candidate$1) !== String(restingToken)) {
4505
- if (backdrops === null || survivesEveryBackdrop(candidate$1, palette, backdrops)) {
4506
- return {
4507
- kind: "candidate",
4508
- token: candidate$1,
4509
- decoration: "initial"
4510
- };
4511
- }
4512
- }
4513
- const resting = parseThemeColorToken(restingToken);
4514
- const softened = resting ? deriveForegroundHoverToken(resting) : null;
4515
- if (softened && backdrops && survivesEveryBackdrop(softened, palette, backdrops)) {
4516
- return {
4517
- kind: "softened",
4518
- token: softened,
4519
- decoration: "initial"
4520
- };
4521
- }
4950
+ function inkVariables(value) {
4522
4951
  return {
4523
- kind: "resting",
4524
- token: restingToken,
4525
- decoration: "underline"
4952
+ [SURFACE_INK_CSS_VARS.brand]: value("brand"),
4953
+ [SURFACE_INK_CSS_VARS.accent]: value("accent"),
4954
+ [SURFACE_INK_CSS_VARS.focus]: value("focus")
4526
4955
  };
4527
4956
  }
4528
- function survivesEveryBackdrop(token, palette, backdrops) {
4529
- return backdrops.length > 0 && backdrops.every((backdrop) => {
4530
- const foreground = resolveOpaqueSurfaceHex(token, palette, backdrop);
4531
- return foreground !== null && contrastRatio(foreground, backdrop) >= ORDINARY_TEXT_MIN_CONTRAST_RATIO;
4532
- });
4533
- }
4534
-
4535
- //#endregion
4536
- //#region ../theme-core/src/surfaces/resolveSurfaceForeground.ts
4537
- const FALLBACK_TEXT_TOKENS = [
4538
- "text",
4539
- "primaryForeground",
4540
- "secondaryForeground",
4541
- "accentForeground",
4542
- "neutral-950",
4543
- "neutral-50"
4544
- ];
4545
- const CANONICAL_EQUIVALENT_TOKEN_ORDER = [
4546
- "text",
4547
- "primaryForeground",
4548
- "secondaryForeground",
4549
- "accentForeground",
4550
- "onPrimary",
4551
- "onSecondary",
4552
- "onAccent",
4553
- "onSurface",
4554
- "neutral-950",
4555
- "neutral-50"
4556
- ];
4557
- function resolveSurfaceForeground(background, palette, candidates = {}, options = {}) {
4558
- const authored = foregroundCandidates(background, candidates);
4559
- if ("measuredBackdrop" in options) return resolveMeasuredForeground(background?.color ?? "background", options.measuredBackdrop, authored, palette);
4560
- return resolveAuthoredBackground(background, palette, authored, options);
4561
- }
4562
- /** Legacy references may be measurable, inherited, or a trusted media treatment. */
4563
- function resolveAuthoredBackground(background, palette, authored, options) {
4564
- if (background === null) {
4565
- return resolveDeterministicForeground("background", {
4566
- ...authored,
4567
- text: authored.text ?? "text"
4568
- }, palette);
4569
- }
4570
- if (background.type === "color") {
4571
- const backgroundReference = normalizedReference(background.color);
4572
- if (!backgroundReference || backgroundReference === "transparent") {
4573
- return { kind: "unknown" };
4574
- }
4575
- return resolveDeterministicForeground(backgroundReference, authored, palette, options.compositeBase);
4957
+ function surfaceForegroundValueWithOpacity(role, opacityPercent) {
4958
+ if (!Number.isFinite(opacityPercent) || opacityPercent < 0 || opacityPercent > 100) {
4959
+ throw new RangeError("Surface foreground opacity must be between 0 and 100.");
4576
4960
  }
4577
- return resolveTrustedTreatmentForeground(authored, palette);
4961
+ return `color-mix(in srgb, ${surfaceForegroundValue(role)} ${opacityPercent}%, transparent)`;
4578
4962
  }
4579
- function foregroundCandidates(background, overrides) {
4963
+ function surfaceForegroundStyle(input) {
4964
+ const foregrounds = completeSurfaceForegrounds(input);
4580
4965
  return {
4581
- text: candidateOrBackground(overrides.text, background?.textColor),
4582
- heading: candidateOrBackground(overrides.heading, background?.headingColor),
4583
- link: candidateOrBackground(overrides.link, background?.linkColor),
4584
- linkHover: candidateOrBackground(overrides.linkHover),
4585
- supporting: candidateOrBackground(overrides.supporting, background?.supportingColor)
4966
+ color: foregrounds.text,
4967
+ [SURFACE_FOREGROUND_CSS_VARS.text]: foregrounds.text,
4968
+ [SURFACE_FOREGROUND_CSS_VARS.heading]: foregrounds.heading,
4969
+ [SURFACE_FOREGROUND_CSS_VARS.link]: foregrounds.link,
4970
+ [SURFACE_FOREGROUND_CSS_VARS.linkHover]: foregrounds.linkHover,
4971
+ [SURFACE_FOREGROUND_CSS_VARS.supporting]: foregrounds.supporting,
4972
+ ...input.muted === undefined ? {} : { [SURFACE_FOREGROUND_CSS_VARS.muted]: foregrounds.muted },
4973
+ "--rb-surface-link-hover-decoration": hoverDecoration(input)
4586
4974
  };
4587
4975
  }
4588
- function candidateOrBackground(candidate$1, backgroundValue) {
4589
- return candidate$1 ?? backgroundValue ?? null;
4590
- }
4591
- function resolveDeterministicForeground(backgroundReference, candidates, palette, compositeBase) {
4592
- const backgroundHex = resolveOpaqueSurfaceHex(backgroundReference, palette, compositeBase);
4593
- if (!backgroundHex) {
4594
- return resolveTrustedTreatmentForeground(candidates, palette);
4595
- }
4596
- return resolveMeasuredForeground(backgroundReference, backgroundHex, candidates, palette);
4597
- }
4598
- function resolveMeasuredForeground(backgroundReference, backgroundHex, candidates, palette) {
4599
- const backgroundToken$1 = paletteToken(backgroundReference);
4600
- const derivedOnToken = backgroundToken$1 ? onColorTokenName(backgroundToken$1) : null;
4601
- const text = firstSafeToken([
4602
- candidates.text,
4603
- derivedOnToken,
4604
- ...FALLBACK_TEXT_TOKENS
4605
- ], backgroundHex, palette, derivedOnToken ? [derivedOnToken] : []) ?? pickBlackOrWhiteTextColor(backgroundHex);
4606
- return knownForeground({
4607
- text,
4608
- heading: safeTokenOr(candidates.heading, text, backgroundHex, palette),
4609
- link: safeTokenOr(candidates.link, text, backgroundHex, palette),
4610
- supporting: safeTokenOr(candidates.supporting, text, backgroundHex, palette)
4611
- }, {
4612
- kind: "measured",
4613
- backdropHex: backgroundHex
4614
- }, candidates.linkHover, palette);
4615
- }
4616
- function resolveTrustedTreatmentForeground(candidates, palette) {
4617
- const text = canonicalToken(candidates.text, palette);
4618
- if (!text) return { kind: "unknown" };
4619
- return knownForeground({
4620
- text,
4621
- heading: canonicalToken(candidates.heading, palette) ?? text,
4622
- link: canonicalToken(candidates.link, palette) ?? text,
4623
- supporting: canonicalToken(candidates.supporting, palette) ?? text
4624
- }, { kind: "trusted-treatment" }, candidates.linkHover, palette);
4976
+ function surfaceForegroundDeclarations(input) {
4977
+ return Object.entries(surfaceForegroundStyle(input)).map(([property, value]) => `${property}: ${value};`);
4625
4978
  }
4626
- function knownForeground(roles, provenance, hoverCandidate, palette) {
4627
- const hover = planForegroundHover({
4628
- restingToken: roles.link,
4629
- candidate: canonicalHoverToken(hoverCandidate, palette),
4630
- palette,
4631
- backdrops: provenance.kind === "measured" ? [provenance.backdropHex] : null
4632
- });
4979
+ function surfaceForegroundInputFromResolved(foreground, color) {
4980
+ const { roles } = foreground;
4981
+ const value = (ink) => {
4982
+ const token = parseThemeColorToken(ink);
4983
+ return token ? color(token) : ink;
4984
+ };
4633
4985
  return {
4634
- kind: "known",
4635
- provenance,
4636
- roles: {
4637
- ...roles,
4638
- linkHover: hover.token,
4639
- muted: provenance.kind === "measured" ? deriveSurfaceMutedColor(provenance.backdropHex, resolveConcreteColorHex(roles.text, palette) ?? provenance.backdropHex) : roles.text
4640
- },
4641
- hoverDecoration: hover.decoration
4986
+ text: value(roles.text),
4987
+ heading: value(roles.heading),
4988
+ link: value(roles.link),
4989
+ linkHover: value(roles.linkHover),
4990
+ supporting: value(roles.supporting),
4991
+ hoverDecoration: foreground.hoverDecoration
4642
4992
  };
4643
4993
  }
4644
- function canonicalHoverToken(reference, palette) {
4645
- const token = parseThemeColorToken(reference);
4646
- if (token) {
4647
- const parts = parseColorTokenParts(token);
4648
- if (parts.kind === "token" && resolveConcreteColorHex(parts.token, palette)) {
4649
- return token;
4650
- }
4651
- }
4652
- return canonicalToken(reference, palette);
4653
- }
4654
- /** Keep a candidate only when it meets ordinary-text contrast on the ground. */
4655
- function safeTokenOr(candidate$1, fallback, backgroundHex, palette) {
4656
- return firstSafeToken([candidate$1], backgroundHex, palette) ?? fallback;
4657
- }
4658
- function firstSafeToken(candidates, backgroundHex, palette, preferredEquivalentTokens = []) {
4659
- const seen = new Set();
4660
- for (const candidate$1 of candidates) {
4661
- const token = canonicalToken(candidate$1, palette, preferredEquivalentTokens);
4662
- if (!token || seen.has(token)) continue;
4663
- seen.add(token);
4664
- const foregroundHex = resolveConcreteColorHex(token, palette);
4665
- if (foregroundHex && contrastRatio(backgroundHex, foregroundHex) >= ORDINARY_TEXT_MIN_CONTRAST_RATIO) {
4666
- return token;
4667
- }
4668
- }
4669
- return null;
4670
- }
4671
- function canonicalToken(reference, palette, preferredEquivalentTokens = []) {
4672
- const normalized = normalizedReference(reference);
4673
- if (!normalized || normalized === "transparent") return null;
4674
- const direct = parseThemeColorToken(normalized);
4675
- if (direct && resolveConcreteColorHex(direct, palette)) return direct;
4676
- const referenceHex = resolveConcreteColorHex(normalized, palette);
4677
- if (!referenceHex) return null;
4678
- const equivalent = [
4679
- ...preferredEquivalentTokens,
4680
- ...CANONICAL_EQUIVALENT_TOKEN_ORDER,
4681
- ...Object.keys(palette).sort()
4682
- ].find((token) => {
4683
- const parsed = parseThemeColorToken(token);
4684
- return parsed !== null && resolveConcreteColorHex(parsed, palette)?.toLowerCase() === referenceHex.toLowerCase();
4685
- });
4686
- return equivalent ? parseThemeColorToken(equivalent) : null;
4687
- }
4688
- function paletteToken(reference) {
4689
- const parts = parseColorTokenParts(reference);
4690
- return parts.kind === "token" && parts.opacityPercent === null ? parts.token : null;
4691
- }
4692
- function normalizedReference(value) {
4693
- if (typeof value !== "string") return null;
4694
- const normalized = value.trim();
4695
- return normalized.length > 0 ? normalized : null;
4994
+ function hoverDecoration(input) {
4995
+ return input.hoverDecoration ?? "underline";
4696
4996
  }
4697
4997
 
4698
4998
  //#endregion
@@ -4862,22 +5162,22 @@ function resolveAccessibleSurfaceRecipe(input) {
4862
5162
  if (result.kind === "resolved-accessible-surface") return result;
4863
5163
  const recipe = surfaceRecipes[input.surface];
4864
5164
  const token = recipe.backgroundCandidates.find((candidate$1) => resolveConcreteColorHex(candidate$1, input.palette) !== null);
4865
- const hex = token ? resolveConcreteColorHex(token, input.palette) : null;
4866
- if (!token || !hex) return result;
4867
- const ink = pickBlackOrWhiteTextColor(hex);
5165
+ const hex$1 = token ? resolveConcreteColorHex(token, input.palette) : null;
5166
+ if (!token || !hex$1) return result;
5167
+ const ink = pickBlackOrWhiteTextColor(hex$1);
4868
5168
  const foreground = (role) => ({
4869
5169
  kind: "safe-token",
4870
5170
  role,
4871
5171
  token: asAccessibleSurfaceToken(ink),
4872
5172
  hex: ink,
4873
- contrastRatio: contrastRatio(ink, hex)
5173
+ contrastRatio: contrastRatio(ink, hex$1)
4874
5174
  });
4875
5175
  return {
4876
5176
  kind: "resolved-accessible-surface",
4877
5177
  surface: input.surface,
4878
5178
  background: {
4879
5179
  token: asAccessibleSurfaceToken(token),
4880
- hex,
5180
+ hex: hex$1,
4881
5181
  inherited: recipe.inheritedBackground
4882
5182
  },
4883
5183
  heading: {
@@ -5066,12 +5366,12 @@ function surfaceResolutionFailureReason(rejectedCandidates) {
5066
5366
  return rejectedCandidates.some((candidate$1) => candidate$1.kind === "foreground-candidate") ? "no-safe-foreground-candidate" : "no-background-candidate";
5067
5367
  }
5068
5368
  function missingBackgroundSurface(input) {
5069
- const contrast = contrastRatio(missingBackgroundTokens.foreground, missingBackgroundTokens.background);
5369
+ const contrast$1 = contrastRatio(missingBackgroundTokens.foreground, missingBackgroundTokens.background);
5070
5370
  const foreground = {
5071
5371
  kind: "safe-token",
5072
5372
  token: asAccessibleSurfaceToken(missingBackgroundTokens.foreground),
5073
5373
  hex: missingBackgroundTokens.foreground,
5074
- contrastRatio: contrast
5374
+ contrastRatio: contrast$1
5075
5375
  };
5076
5376
  return {
5077
5377
  kind: "resolved-accessible-surface",
@@ -5146,7 +5446,8 @@ function surfaceTransportContext(context) {
5146
5446
  const revision = ownSurfaceRevision(context);
5147
5447
  const result = Object.freeze({
5148
5448
  ...revision,
5149
- colourIndex: context.colourIndex
5449
+ colourIndex: context.colourIndex,
5450
+ solvedInks: context.solvedInks
5150
5451
  });
5151
5452
  surfaceRevisionOwners.set(result, revision);
5152
5453
  return result;
@@ -5172,7 +5473,7 @@ function resolveSurface(spec, context) {
5172
5473
  return spec.kind === "section" ? resolveSectionSurface(spec.background, context) : preparedSurfaceValue(spec, context) ?? certifySurface(spec, context);
5173
5474
  }
5174
5475
  /** Own one theme revision; derive its surface decisions without copying its inputs again. */
5175
- function prepareThemeSurfaceRevision(inputTheme, selectSurfaces = () => []) {
5476
+ function prepareThemeSurfaceRevision(inputTheme, selectSurfaces = () => [], solvedInks) {
5176
5477
  const theme = structuredClone(inputTheme);
5177
5478
  const snapshot = freezeOwnedThemeValue(theme);
5178
5479
  const declarations = ownedThemeSnapshot(selectSurfaces(snapshot));
@@ -5184,7 +5485,8 @@ function prepareThemeSurfaceRevision(inputTheme, selectSurfaces = () => []) {
5184
5485
  kind: "concrete-palette",
5185
5486
  palette,
5186
5487
  colourIndex: preparePaletteColourIndex(palette),
5187
- buttonSystem: snapshot.buttons
5488
+ buttonSystem: snapshot.buttons,
5489
+ solvedInks
5188
5490
  });
5189
5491
  registerOwnedRevisionInputs(context);
5190
5492
  const preparedSurfaces = declarations.length === 0 ? undefined : surfaceIndexFromOwnedInputs(declarations, context);
@@ -5204,7 +5506,8 @@ function prepareSurfaceIndex(specs, context) {
5204
5506
  linkHoverToken: context.linkHoverToken,
5205
5507
  foreground: context.foreground,
5206
5508
  compositeBase: context.compositeBase,
5207
- buttonContext: context.buttonContext
5509
+ buttonContext: context.buttonContext,
5510
+ solvedInks: context.solvedInks
5208
5511
  });
5209
5512
  return surfaceIndexFromOwnedInputs(ownedThemeSnapshot(specs), snapshot);
5210
5513
  }
@@ -5244,7 +5547,8 @@ function surfaceResolutionKey(spec, context) {
5244
5547
  context.linkHoverToken,
5245
5548
  context.foreground,
5246
5549
  context.compositeBase,
5247
- context.buttonContext
5550
+ context.buttonContext,
5551
+ context.solvedInks
5248
5552
  ]);
5249
5553
  }
5250
5554
  function resolveSurfaceSpec(spec, context) {
@@ -5318,6 +5622,7 @@ function resolveOwnedSurface(paint, context, measuredBackdrop) {
5318
5622
  const decision = {
5319
5623
  kind: "owned",
5320
5624
  foreground,
5625
+ inks: ownedInkPublication(resolved, context),
5321
5626
  decorations: surfaceDecorationColors(resolved, context.palette),
5322
5627
  hoverDecoration: resolved.kind === "known" ? resolved.hoverDecoration : "underline",
5323
5628
  buttonContext: resolveSurfaceButtons(paint, resolved, context)
@@ -5327,11 +5632,11 @@ function resolveOwnedSurface(paint, context, measuredBackdrop) {
5327
5632
  paint,
5328
5633
  contrast: { kind: "unmeasured" }
5329
5634
  };
5330
- const contrast = surfaceContrast(resolved, context.palette, context.colourIndex);
5331
- return contrast.kind === "measured" ? {
5635
+ const contrast$1 = surfaceContrast(resolved, context.palette, context.colourIndex);
5636
+ return contrast$1.kind === "measured" ? {
5332
5637
  ...decision,
5333
5638
  paint,
5334
- contrast
5639
+ contrast: contrast$1
5335
5640
  } : {
5336
5641
  ...decision,
5337
5642
  paint,
@@ -5342,6 +5647,11 @@ function resolveOwnedSurface(paint, context, measuredBackdrop) {
5342
5647
  }
5343
5648
  };
5344
5649
  }
5650
+ /** A paint the element owns but that resolves as inherited (an empty token) still blocks outer inks. */
5651
+ function ownedInkPublication(resolved, context) {
5652
+ const publication = surfaceInkPublication(resolved, context.solvedInks ?? "withhold");
5653
+ return publication.kind === "none" ? { kind: "reset" } : publication;
5654
+ }
5345
5655
  /** Unprovable legacy paint retains its declared foreground instead of inventing one. */
5346
5656
  function ownedForegroundRoles(resolved, text, candidates) {
5347
5657
  if (resolved.kind === "known") return resolved.roles;
@@ -5390,7 +5700,7 @@ function surfaceContrast(foreground, palette, colourIndex) {
5390
5700
  }
5391
5701
  /** Runtime backgrounds retain authored media, compositing and unknown foregrounds. */
5392
5702
  function resolveSectionSurface(background, context) {
5393
- const foreground = background === null ? { kind: "unknown" } : resolveSurfaceForeground(background, context.palette, {
5703
+ const foreground = background === null ? { kind: "inherits" } : resolveSurfaceForeground(background, context.palette, {
5394
5704
  linkHover: context.linkHoverToken,
5395
5705
  ...context.foreground
5396
5706
  }, { compositeBase: context.compositeBase });
@@ -5404,6 +5714,7 @@ function resolveSectionSurface(background, context) {
5404
5714
  const result = {
5405
5715
  kind: "section",
5406
5716
  foreground,
5717
+ inks: surfaceInkPublication(foreground, context.solvedInks ?? "withhold"),
5407
5718
  buttonStyles: resolveSurfaceButtonContext({
5408
5719
  surface: surface$1,
5409
5720
  palette: revision.palette,
@@ -5442,8 +5753,8 @@ function resolveTintSurface(spec, context) {
5442
5753
  }, context);
5443
5754
  const source = spec.source === "text" ? base.foreground.text : base.decorations?.mark;
5444
5755
  const token = source ? resolveOpaqueSurfaceHex(`${source}/${spec.opacity}`, context.palette, spec.base) : null;
5445
- const hex = source ? resolveConcreteColorHex(source, context.palette) : null;
5446
- const tinted = token ?? (hex ? resolveOpaqueSurfaceHex(hexToRgba(hex, spec.opacity / 100), context.palette, spec.base) : null);
5756
+ const hex$1 = source ? resolveConcreteColorHex(source, context.palette) : null;
5757
+ const tinted = token ?? (hex$1 ? resolveOpaqueSurfaceHex(hexToRgba(hex$1, spec.opacity / 100), context.palette, spec.base) : null);
5447
5758
  if (!tinted) throw new Error(`Cannot derive tint on ${spec.base}`);
5448
5759
  const solid = resolveDeclaredSolidSurface({
5449
5760
  kind: "solid",
@@ -5456,6 +5767,7 @@ function resolveTintSurface(spec, context) {
5456
5767
  assertTintSamples(solid, spec.base, context.palette);
5457
5768
  return {
5458
5769
  ...solid,
5770
+ inks: { kind: "reset" },
5459
5771
  tintBase: spec.base
5460
5772
  };
5461
5773
  }
@@ -5479,8 +5791,8 @@ function assertTintSamples(surface$1, base, palette) {
5479
5791
  if (!safe) throw new RangeError(`No checked tint gradient for ${role} on ${base}`);
5480
5792
  });
5481
5793
  }
5482
- function hexToRgba(hex, alpha) {
5483
- const { r, g, b } = hexToRgb(hex);
5794
+ function hexToRgba(hex$1, alpha) {
5795
+ const { r, g, b } = hexToRgb(hex$1);
5484
5796
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
5485
5797
  }
5486
5798
  const foregroundInputSchema = z.object({
@@ -5503,6 +5815,7 @@ const paintInputSchema = z.discriminatedUnion("kind", [
5503
5815
  z.object({ kind: z.literal("transparent") })
5504
5816
  ]);
5505
5817
  const buttonInputSchema = z.object({ primary: opaqueButtonColourTokenSchema });
5818
+ const solvedInksSchema = z.enum(["publish", "withhold"]);
5506
5819
  const surfaceSpecSchema = z.discriminatedUnion("kind", [
5507
5820
  z.object({
5508
5821
  kind: z.literal("recipe"),
@@ -5539,16 +5852,21 @@ const surfaceTransportSchema = z.object({ $surface: z.object({
5539
5852
  foreground: foregroundInputSchema.optional(),
5540
5853
  linkHoverToken: z.string().optional(),
5541
5854
  compositeBase: z.string().optional(),
5542
- buttonContext: buttonInputSchema.optional()
5855
+ buttonContext: buttonInputSchema.optional(),
5856
+ solvedInks: solvedInksSchema.optional()
5543
5857
  })
5544
5858
  }) });
5545
5859
  /** Deserialised output is never evidence: parse the inputs and rerun their owner. */
5546
5860
  function parseResolvedSurface(value) {
5547
- if (isResolvedSurface(value)) return value;
5548
- const { spec, context } = surfaceTransportSchema.parse(value).$surface;
5861
+ return resolveTransportedSurface(value);
5862
+ }
5863
+ function resolveTransportedSurface(value, solvedInks) {
5864
+ if (isResolvedSurface(value) && solvedInks === undefined) return value;
5865
+ const { spec, context } = isResolvedSurface(value) ? serializeResolvedSurface(value).$surface : surfaceTransportSchema.parse(value).$surface;
5549
5866
  return resolveSurface(spec, {
5550
5867
  ...context,
5551
- buttonSystem: context.buttonSystem
5868
+ buttonSystem: context.buttonSystem,
5869
+ solvedInks: solvedInks ?? context.solvedInks
5552
5870
  });
5553
5871
  }
5554
5872
  /** Wire only the owner inputs; parsed CSS/results are recomputed, never trusted. */
@@ -5568,7 +5886,8 @@ function certifySurface(spec, context) {
5568
5886
  foreground: context.foreground,
5569
5887
  linkHoverToken: context.linkHoverToken,
5570
5888
  compositeBase: context.compositeBase,
5571
- buttonContext: context.buttonContext
5889
+ buttonContext: context.buttonContext,
5890
+ solvedInks: context.solvedInks
5572
5891
  })
5573
5892
  })
5574
5893
  };
@@ -5614,7 +5933,10 @@ function serializeSurfaceTree(value) {
5614
5933
  };
5615
5934
  const encode = (item) => {
5616
5935
  const revision = isSurfaceTreeRecord(item) ? surfaceRevisionOwners.get(item) : undefined;
5617
- if (revision) return { $surfaceRevision: revisionIndex(revision) };
5936
+ if (revision) return {
5937
+ $surfaceRevision: revisionIndex(revision),
5938
+ solvedInks: isSurfaceTreeRecord(item) ? item.solvedInks : undefined
5939
+ };
5618
5940
  if (isResolvedSurface(item)) {
5619
5941
  const input = serializeResolvedSurface(item).$surface;
5620
5942
  const index = revisionIndex(item.revision);
@@ -5647,19 +5969,28 @@ const surfaceReferenceSchema = z.object({ $surface: z.object({
5647
5969
  foreground: foregroundInputSchema.optional(),
5648
5970
  linkHoverToken: z.string().optional(),
5649
5971
  compositeBase: z.string().optional(),
5650
- buttonContext: buttonInputSchema.optional()
5972
+ buttonContext: buttonInputSchema.optional(),
5973
+ solvedInks: solvedInksSchema.optional()
5651
5974
  })
5652
5975
  }) });
5653
- const surfaceRevisionReferenceSchema = z.object({ $surfaceRevision: z.number().int().nonnegative() });
5976
+ const surfaceRevisionReferenceSchema = z.object({
5977
+ $surfaceRevision: z.number().int().nonnegative(),
5978
+ solvedInks: solvedInksSchema.optional()
5979
+ });
5654
5980
  /** Recreate owner results before any island's ordinary schema consumes them. */
5655
- function parseSurfaceTree(value) {
5656
- if (isSurfaceTransportLeaf(value)) return value;
5657
- if (!isSurfaceTreeRecord(value) || !("$surfaceTransport" in value)) return mapSurfaceTreeChildren(value, parseSurfaceTree);
5981
+ function parseSurfaceTree(value, solvedInks) {
5982
+ if (isResolvedSurface(value)) return resolveTransportedSurface(value, solvedInks);
5983
+ const revision = isSurfaceTreeRecord(value) ? surfaceRevisionOwners.get(value) : undefined;
5984
+ if (revision) return solvedInks === undefined ? value : surfaceTransportContext({
5985
+ ...revision,
5986
+ solvedInks
5987
+ });
5988
+ if (!isSurfaceTreeRecord(value) || !("$surfaceTransport" in value)) return mapSurfaceTreeChildren(value, (item) => parseSurfaceTree(item, solvedInks));
5658
5989
  const envelope = surfaceEnvelopeSchema.parse(value).$surfaceTransport;
5659
- const revisions = envelope.revisions.map((revision) => {
5990
+ const revisions = envelope.revisions.map((revision$1) => {
5660
5991
  const owned = ownedThemeSnapshot({
5661
- palette: revision.palette,
5662
- buttonSystem: revision.buttonSystem
5992
+ palette: revision$1.palette,
5993
+ buttonSystem: revision$1.buttonSystem
5663
5994
  });
5664
5995
  registerOwnedRevisionInputs(owned);
5665
5996
  return ownSurfaceRevision(owned);
@@ -5667,18 +5998,23 @@ function parseSurfaceTree(value) {
5667
5998
  const decode = (item) => {
5668
5999
  if (isSurfaceTreeRecord(item) && "$surfaceRevision" in item) {
5669
6000
  const reference = surfaceRevisionReferenceSchema.parse(item);
5670
- const revision = revisions[reference.$surfaceRevision];
5671
- if (!revision) throw new Error("Unknown surface revision");
5672
- return revision;
6001
+ const revision$1 = revisions[reference.$surfaceRevision];
6002
+ if (!revision$1) throw new Error("Unknown surface revision");
6003
+ const policy = solvedInks ?? reference.solvedInks;
6004
+ return policy === undefined ? revision$1 : surfaceTransportContext({
6005
+ ...revision$1,
6006
+ solvedInks: policy
6007
+ });
5673
6008
  }
5674
6009
  if (isSurfaceTreeRecord(item) && "$surface" in item) {
5675
6010
  const request = surfaceReferenceSchema.parse(item).$surface;
5676
- const revision = revisions[request.revision];
5677
- if (!revision) throw new Error("Unknown surface revision");
6011
+ const revision$1 = revisions[request.revision];
6012
+ if (!revision$1) throw new Error("Unknown surface revision");
5678
6013
  return resolveSurface(request.spec, {
5679
- ...revision,
6014
+ ...revision$1,
5680
6015
  ...request.context,
5681
- buttonSystem: revision.buttonSystem
6016
+ buttonSystem: revision$1.buttonSystem,
6017
+ solvedInks: solvedInks ?? request.context.solvedInks
5682
6018
  });
5683
6019
  }
5684
6020
  return mapSurfaceTreeChildren(item, decode);
@@ -5783,9 +6119,9 @@ function makeBoundaryId(previousBlockId, nextBlockId) {
5783
6119
  function assertNever$1(value, message = "Unhandled Theme V2 variant") {
5784
6120
  throw new Error(`${message}: ${String(value)}`);
5785
6121
  }
5786
- function brandNonEmptyString(value, label) {
6122
+ function brandNonEmptyString(value, label$1) {
5787
6123
  if (value.length === 0) {
5788
- throw new Error(`${label} must be a non-empty string.`);
6124
+ throw new Error(`${label$1} must be a non-empty string.`);
5789
6125
  }
5790
6126
  return value;
5791
6127
  }
@@ -7229,6 +7565,14 @@ const blockAppearancePresets = [
7229
7565
  slotRoles: { copy: "card" },
7230
7566
  styleFallback: [id$1("hero-photo-beside-text")]
7231
7567
  }),
7568
+ defineArrangementLook("hero-text-over-photo", "hero", "Text over photo", "classic", [], {
7569
+ photoTreatment: "text-overlay",
7570
+ whenMissing: [{
7571
+ fact: "photo",
7572
+ look: id$1("hero-page-head")
7573
+ }],
7574
+ styleFallback: [id$1("hero-photo-behind-text")]
7575
+ }),
7232
7576
  defineArrangementLook("hero-short-photo-banner", "hero", "Short photo banner", "classic", [], {
7233
7577
  slotRoles: { copy: "card" },
7234
7578
  styleFallback: [id$1("hero-page-head")]
@@ -11897,8 +12241,8 @@ function getRasterAssetIds(assets) {
11897
12241
  function getMissingAssetUsageIds(input) {
11898
12242
  return input.rasterAssetIds.filter((assetId) => {
11899
12243
  if (input.inFlightAssetIds.has(assetId)) return false;
11900
- const status = input.storedStatusByAssetId[assetId];
11901
- return status === undefined || status.kind === "unknown";
12244
+ const status$1 = input.storedStatusByAssetId[assetId];
12245
+ return status$1 === undefined || status$1.kind === "unknown";
11902
12246
  });
11903
12247
  }
11904
12248
  function markAssetUsageChecking(statusByAssetId, assetIds) {
@@ -13953,12 +14297,31 @@ function parseCanonicalRuntimeBackground(content) {
13953
14297
  if (!background) return { kind: "none" };
13954
14298
  const parsed = runtimeBoxBackgroundSchema.safeParse(background);
13955
14299
  if (!parsed.success) return { kind: "none" };
13956
- return parsed.data.contentOwner === pageDesignEditorBackgroundOverrideOwner ? {
14300
+ const painted = applyLookPhotoTreatment(content, parsed.data);
14301
+ return painted.contentOwner === pageDesignEditorBackgroundOverrideOwner ? {
13957
14302
  kind: "page-design-override",
13958
- background: parsed.data
14303
+ background: painted
13959
14304
  } : {
13960
14305
  kind: "block-authored",
13961
- background: parsed.data
14306
+ background: painted
14307
+ };
14308
+ }
14309
+ /** The Look owns shading and ink together; authored image/crop stay intact. */
14310
+ function applyLookPhotoTreatment(content, background) {
14311
+ const look$1 = readRecord(readRecord(content._effectiveLook)?.look);
14312
+ const structure = readRecord(look$1?.structure);
14313
+ if (background.type !== "image" || structure?.photoTreatment !== "text-overlay") return background;
14314
+ return {
14315
+ ...background,
14316
+ overlay: {
14317
+ type: "gradient",
14318
+ opacity: 1,
14319
+ gradient: "var(--rb-opening-photo-shade, linear-gradient(transparent, transparent)), linear-gradient(to top, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.76) 25%, rgba(0,0,0,0.4) 46%, transparent 72%)"
14320
+ },
14321
+ textColor: "neutral-50",
14322
+ headingColor: "neutral-50",
14323
+ linkColor: "neutral-50",
14324
+ supportingColor: "neutral-50"
13962
14325
  };
13963
14326
  }
13964
14327
  function resolveSectionBackground(source, lookDefault, rhythm, arrangementHeroPhoto) {
@@ -14015,8 +14378,15 @@ function resolveSectionForeground(background, palette) {
14015
14378
  * `resolveSectionBackground` instead.
14016
14379
  */
14017
14380
  function resolveCompiledSectionForegroundFromContent(content, palette, photo) {
14018
- if (!content) return resolveSurfaceForeground(null, palette);
14019
- return resolveSectionForeground(resolveSectionBackground(parseCanonicalRuntimeBackground(content), null, compiledOpeningRhythm(content), photo), palette);
14381
+ return resolveCompiledSectionContextFromContent(content, palette, photo).openingForeground;
14382
+ }
14383
+ /** Resolve the compiled ground once for every opening appearance consumer. */
14384
+ function resolveCompiledSectionContextFromContent(content, palette, photo) {
14385
+ const background = content ? resolveSectionBackground(parseCanonicalRuntimeBackground(content), null, compiledOpeningRhythm(content), photo) : { kind: "base" };
14386
+ return {
14387
+ openingForeground: resolveSectionForeground(background, palette),
14388
+ openingPhotoBehindText: resolveSectionPhotoPlacement(background, photo) === "behind-text"
14389
+ };
14020
14390
  }
14021
14391
  /** Legacy detached photos keep already-legible authored ink and its exact CSS treatment. */
14022
14392
  function sectionForegroundBackground(resolved, palette) {
@@ -14024,7 +14394,7 @@ function sectionForegroundBackground(resolved, palette) {
14024
14394
  if (resolved.kind !== "content-media") return resolved.background;
14025
14395
  const original = resolveSurfaceForeground(resolved.background, palette);
14026
14396
  const measured = resolveSurfaceForeground(resolved.ground, palette);
14027
- if (original.kind === "unknown" || measured.kind === "unknown") return resolved.background;
14397
+ if (original.kind !== "known" || measured.kind !== "known") return resolved.background;
14028
14398
  const roles = [
14029
14399
  "text",
14030
14400
  "heading",
@@ -14129,9 +14499,9 @@ function sectionScrimSamples(background, palette) {
14129
14499
  const overlay = sectionVeil(background);
14130
14500
  if (overlay?.type === "gradient") return gradientScrimSamples(overlay.gradient ?? "", overlay.opacity ?? 1, palette);
14131
14501
  if (overlay?.type !== "color") return [];
14132
- const hex = resolveConcreteColorHex(overlay.color ?? "", palette);
14133
- return hex ? [{
14134
- hex,
14502
+ const hex$1 = resolveConcreteColorHex(overlay.color ?? "", palette);
14503
+ return hex$1 ? [{
14504
+ hex: hex$1,
14135
14505
  alpha: overlay.opacity ?? 1
14136
14506
  }] : [];
14137
14507
  }
@@ -14860,6 +15230,10 @@ function isRecord$1(value) {
14860
15230
 
14861
15231
  //#endregion
14862
15232
  //#region ../theme-core/src/header/rootPaint.ts
15233
+ /** Detached branding needs protection even when the navigation has its own rail. */
15234
+ function headerNeedsPhotoShade(header) {
15235
+ return header.variant === "floating" && header.background?.type === "color" && header.background.color === "transparent" && (!header.container || (header.container.opacity ?? 1) < 1);
15236
+ }
14863
15237
  /** The nav container takes precedence when a floating header also has one. */
14864
15238
  function headerRootPaint(header) {
14865
15239
  const navContainerType = header?.navContainer?.type ?? "none";
@@ -14992,7 +15366,7 @@ function applyFooterBackgroundOverride(theme, footer, override) {
14992
15366
  }
14993
15367
  function assertChromeBackgroundHasForeground(theme, background) {
14994
15368
  const resolved = resolveSurfaceForeground(background, expandPalette(theme.palette));
14995
- if (resolved.kind === "unknown") {
15369
+ if (resolved.kind !== "known") {
14996
15370
  throw new Error(`Site chrome background "${background?.type === "color" ? background.color ?? "unknown" : "unknown"}" has no resolvable AA foreground; the theme palette is incomplete`);
14997
15371
  }
14998
15372
  }
@@ -15803,6 +16177,730 @@ function resolveCardRole(theme, cardSystem, role) {
15803
16177
  }
15804
16178
  }
15805
16179
 
16180
+ //#endregion
16181
+ //#region ../theme-core/src/site-styles/headerDropdownRecipe.ts
16182
+ const neutralHeaderDropdown = {
16183
+ surface: {
16184
+ kind: "solid",
16185
+ background: "surface"
16186
+ },
16187
+ textColor: "text",
16188
+ borderColor: "border",
16189
+ shadow: "md",
16190
+ borderRadius: "md",
16191
+ textTransform: "none",
16192
+ letterSpacing: "normal"
16193
+ };
16194
+ const brandHeaderDropdown = {
16195
+ surface: {
16196
+ kind: "solid",
16197
+ background: "primary-700"
16198
+ },
16199
+ textColor: "primaryForeground",
16200
+ borderColor: null,
16201
+ shadow: "md",
16202
+ borderRadius: "md",
16203
+ hoverBackground: "primary-900",
16204
+ hoverTextColor: "secondary",
16205
+ textTransform: "uppercase",
16206
+ letterSpacing: "wider",
16207
+ fontWeight: "bold",
16208
+ textSize: "sm"
16209
+ };
16210
+ const clearSplitHeaderDropdown = {
16211
+ ...neutralHeaderDropdown,
16212
+ surface: {
16213
+ kind: "frosted",
16214
+ background: "surface",
16215
+ tone: "opening",
16216
+ opacity: .45,
16217
+ blur: "md"
16218
+ },
16219
+ textColor: "text",
16220
+ shadow: "sm",
16221
+ borderRadius: "sm",
16222
+ textSize: "base",
16223
+ fontWeight: "medium",
16224
+ hoverBackground: "text"
16225
+ };
16226
+ /** Solid menus retain their existing compiled bytes; no second runtime authority. */
16227
+ function compileHeaderDropdownRecipe(recipe) {
16228
+ const { surface: surface$1, ...style } = recipe;
16229
+ return {
16230
+ ...style,
16231
+ background: surface$1.background,
16232
+ ...surface$1.kind === "frosted" ? { surfaceTreatment: {
16233
+ kind: surface$1.kind,
16234
+ tone: surface$1.tone,
16235
+ opacity: surface$1.opacity,
16236
+ blur: surface$1.blur
16237
+ } } : {}
16238
+ };
16239
+ }
16240
+
16241
+ //#endregion
16242
+ //#region ../theme-core/src/site-styles/bookingPageLookIds.ts
16243
+ /** Deterministic fallback order; Fulcrum is the default. */
16244
+ const bookingPageLookIdValues = [
16245
+ "site-style:fulcrum",
16246
+ "site-style:folio",
16247
+ "site-style:measure",
16248
+ "site-style:grounded-practice",
16249
+ "site-style:lantern",
16250
+ "site-style:poster"
16251
+ ];
16252
+ /** Boundary admission for draft and suggestion inputs. */
16253
+ function parseBookingPageLookId(value) {
16254
+ const id$2 = bookingPageLookIdValues.find((candidate$1) => candidate$1 === value);
16255
+ return id$2 ? asSiteStyleId(id$2) : null;
16256
+ }
16257
+
16258
+ //#endregion
16259
+ //#region ../theme-core/src/site-styles/ownColours/bookingLookColourSpecs.ts
16260
+ const bookingLookGroundLimits = {
16261
+ "site-style:poster": {
16262
+ lightness: [.9, 1],
16263
+ maxChroma: .04
16264
+ },
16265
+ "site-style:fulcrum": {
16266
+ lightness: [.9, 1],
16267
+ maxChroma: .03
16268
+ },
16269
+ "site-style:folio": {
16270
+ lightness: [.88, 1],
16271
+ maxChroma: .05
16272
+ },
16273
+ "site-style:measure": {
16274
+ lightness: [.9, 1],
16275
+ maxChroma: .02
16276
+ },
16277
+ "site-style:grounded-practice": {
16278
+ lightness: [.85, 1],
16279
+ maxChroma: .06
16280
+ },
16281
+ "site-style:lantern": {
16282
+ lightness: [.12, .34],
16283
+ maxChroma: .07
16284
+ }
16285
+ };
16286
+ const contrast = (against, ratio$1 = 3) => ({
16287
+ kind: "contrast",
16288
+ against,
16289
+ ratio: ratio$1
16290
+ });
16291
+ const distinct = (from, deltaE) => ({
16292
+ kind: "distinct",
16293
+ from,
16294
+ deltaE
16295
+ });
16296
+ const label = { kind: "label" };
16297
+ /** The quiet light grounds the five light looks share: faint tints of the leading colour, inside the look's limit. */
16298
+ function lightGrounds(limits, tint$1) {
16299
+ const page = Math.min(tint$1.page, limits.maxChroma);
16300
+ const band = Math.min(tint$1.band, limits.maxChroma);
16301
+ return {
16302
+ background: {
16303
+ l: [.955, .985],
16304
+ c: [0, page],
16305
+ chromaShare: .12,
16306
+ target: .972,
16307
+ name: "the page"
16308
+ },
16309
+ surface: {
16310
+ l: [.99, 1],
16311
+ c: [0, Math.min(page, .004)],
16312
+ chromaShare: .04,
16313
+ target: .997,
16314
+ name: "the cards"
16315
+ },
16316
+ surfaceAlt: {
16317
+ l: [.93, .965],
16318
+ c: [0, band],
16319
+ chromaShare: .25,
16320
+ target: .948,
16321
+ name: "the quieter panels",
16322
+ needs: [distinct("background", .015)]
16323
+ },
16324
+ muted: {
16325
+ l: tint$1.bandL,
16326
+ c: [0, band],
16327
+ chromaShare: .3,
16328
+ target: (tint$1.bandL[0] + tint$1.bandL[1]) / 2,
16329
+ name: "the band",
16330
+ needs: [distinct("background", .03)]
16331
+ },
16332
+ border: {
16333
+ l: [.8, .88],
16334
+ c: [0, band],
16335
+ chromaShare: .25,
16336
+ target: .85,
16337
+ name: "the rules"
16338
+ }
16339
+ };
16340
+ }
16341
+ const groundOrder = [
16342
+ "background",
16343
+ "surface",
16344
+ "muted",
16345
+ "surfaceAlt",
16346
+ "border"
16347
+ ];
16348
+ const lightReading = [
16349
+ "background",
16350
+ "surface",
16351
+ "surfaceAlt",
16352
+ "muted"
16353
+ ];
16354
+ /** Fulcrum: a quiet stone page, white sheets, a strong mid-tone on the buttons, a deep plate behind the opening panel. */
16355
+ const fulcrum = {
16356
+ roles: {
16357
+ ...lightGrounds(bookingLookGroundLimits["site-style:fulcrum"], {
16358
+ page: .008,
16359
+ band: .022,
16360
+ bandL: [.9, .94]
16361
+ }),
16362
+ primary: {
16363
+ l: [.3, .6],
16364
+ c: [.04, .16],
16365
+ target: .45,
16366
+ name: "the buttons",
16367
+ mention: true,
16368
+ needs: [
16369
+ contrast("background"),
16370
+ contrast("surface"),
16371
+ contrast("muted"),
16372
+ label
16373
+ ]
16374
+ },
16375
+ secondary: {
16376
+ l: [.18, .36],
16377
+ c: [.02, .11],
16378
+ target: .28,
16379
+ name: "the plate",
16380
+ mention: true,
16381
+ needs: [contrast("surface"), distinct("primary", .08)]
16382
+ },
16383
+ accent: {
16384
+ l: [.38, .72],
16385
+ c: [.06, .17],
16386
+ target: .62,
16387
+ name: "the small marks",
16388
+ analogous: true,
16389
+ needs: [
16390
+ contrast("background"),
16391
+ contrast("surface"),
16392
+ contrast("muted"),
16393
+ distinct("primary", .1)
16394
+ ]
16395
+ }
16396
+ },
16397
+ slots: [
16398
+ {
16399
+ roles: ["muted"],
16400
+ name: "the band"
16401
+ },
16402
+ {
16403
+ roles: ["secondary"],
16404
+ name: "the plate"
16405
+ },
16406
+ {
16407
+ roles: ["primary"],
16408
+ name: "the buttons"
16409
+ },
16410
+ {
16411
+ roles: ["accent"],
16412
+ name: "the small marks"
16413
+ }
16414
+ ],
16415
+ order: [
16416
+ ...groundOrder,
16417
+ "primary",
16418
+ "secondary",
16419
+ "accent"
16420
+ ],
16421
+ neutrals: {
16422
+ dark: [.24, .02],
16423
+ light: [.995, .005]
16424
+ },
16425
+ reading: lightReading,
16426
+ groundLimit: bookingLookGroundLimits["site-style:fulcrum"].maxChroma,
16427
+ contrast: [{
16428
+ label: "a muted complement on the plate",
16429
+ hue: "complement",
16430
+ second: "secondary",
16431
+ lead: "primary",
16432
+ l: .3,
16433
+ c: [.04, .07]
16434
+ }, {
16435
+ label: "a warm colour on the buttons",
16436
+ hue: "split-warm",
16437
+ second: "primary",
16438
+ lead: "secondary",
16439
+ l: .45,
16440
+ c: [.08, .13]
16441
+ }]
16442
+ };
16443
+ /** Folio: warm paper, sheets and a band; buttons and links share the primary (hovering to its 800 shade); small rules and marks. */
16444
+ const folio = {
16445
+ roles: {
16446
+ ...lightGrounds(bookingLookGroundLimits["site-style:folio"], {
16447
+ page: .012,
16448
+ band: .03,
16449
+ bandL: [.9, .94]
16450
+ }),
16451
+ primary: {
16452
+ l: [.3, .55],
16453
+ c: [.03, .14],
16454
+ target: .42,
16455
+ name: "the buttons and links",
16456
+ mention: true,
16457
+ link: true,
16458
+ needs: [
16459
+ contrast("background"),
16460
+ contrast("surface"),
16461
+ contrast("muted"),
16462
+ label
16463
+ ]
16464
+ },
16465
+ secondary: {
16466
+ l: [.45, .62],
16467
+ c: [.01, .04],
16468
+ target: .54,
16469
+ name: "the quiet details"
16470
+ },
16471
+ accent: {
16472
+ l: [.38, .68],
16473
+ c: [.05, .15],
16474
+ target: .6,
16475
+ name: "the marks",
16476
+ mention: true,
16477
+ analogous: true,
16478
+ needs: [
16479
+ contrast("background"),
16480
+ contrast("surface"),
16481
+ contrast("muted"),
16482
+ distinct("primary", .1)
16483
+ ]
16484
+ }
16485
+ },
16486
+ slots: [
16487
+ {
16488
+ roles: ["muted"],
16489
+ name: "the band"
16490
+ },
16491
+ {
16492
+ roles: ["primary"],
16493
+ name: "the buttons and links"
16494
+ },
16495
+ {
16496
+ roles: ["accent"],
16497
+ name: "the marks"
16498
+ }
16499
+ ],
16500
+ order: [
16501
+ ...groundOrder,
16502
+ "primary",
16503
+ "accent",
16504
+ "secondary"
16505
+ ],
16506
+ neutrals: {
16507
+ dark: [.27, .015],
16508
+ light: [.985, .01]
16509
+ },
16510
+ reading: lightReading,
16511
+ groundLimit: bookingLookGroundLimits["site-style:folio"].maxChroma,
16512
+ contrast: [{
16513
+ label: "the complement on the buttons and links",
16514
+ hue: "complement",
16515
+ second: "primary",
16516
+ lead: "accent",
16517
+ l: .42,
16518
+ c: [.08, .13]
16519
+ }, {
16520
+ label: "a warm colour on the buttons and links",
16521
+ hue: "split-warm",
16522
+ second: "primary",
16523
+ lead: "accent",
16524
+ l: .42,
16525
+ c: [.08, .13]
16526
+ }]
16527
+ };
16528
+ /** Measure: one plain page. Flat buttons that fill with the secondary on hover, and a small square. */
16529
+ const measure = {
16530
+ roles: {
16531
+ ...lightGrounds(bookingLookGroundLimits["site-style:measure"], {
16532
+ page: .008,
16533
+ band: .012,
16534
+ bandL: [.91, .94]
16535
+ }),
16536
+ primary: {
16537
+ l: [.18, .55],
16538
+ c: [0, .16],
16539
+ target: .26,
16540
+ derivedChroma: .035,
16541
+ name: "the buttons",
16542
+ needs: [
16543
+ contrast("background"),
16544
+ contrast("surface"),
16545
+ label
16546
+ ]
16547
+ },
16548
+ secondary: {
16549
+ l: [.12, .5],
16550
+ c: [0, .16],
16551
+ target: {
16552
+ from: "primary",
16553
+ dl: -.06
16554
+ },
16555
+ hueFrom: "primary",
16556
+ name: "the button hover",
16557
+ needs: [contrast("background"), label]
16558
+ },
16559
+ accent: {
16560
+ l: [.4, .7],
16561
+ c: [.08, .2],
16562
+ target: .55,
16563
+ name: "the small square",
16564
+ mention: true,
16565
+ analogous: true,
16566
+ needs: [
16567
+ contrast("background"),
16568
+ contrast("surface"),
16569
+ distinct("primary", .1)
16570
+ ]
16571
+ }
16572
+ },
16573
+ slots: [{
16574
+ roles: ["primary"],
16575
+ name: "the buttons"
16576
+ }, {
16577
+ roles: ["accent"],
16578
+ name: "the small square"
16579
+ }],
16580
+ order: [
16581
+ ...groundOrder,
16582
+ "primary",
16583
+ "secondary",
16584
+ "accent"
16585
+ ],
16586
+ neutrals: {
16587
+ dark: [.22, .012],
16588
+ light: [.995, .004]
16589
+ },
16590
+ reading: lightReading,
16591
+ groundLimit: bookingLookGroundLimits["site-style:measure"].maxChroma,
16592
+ contrast: [{
16593
+ label: "the complement on the square and the button hover",
16594
+ hue: "complement",
16595
+ second: "accent",
16596
+ lead: "primary",
16597
+ alsoTints: {
16598
+ roles: ["secondary"],
16599
+ name: "the button hover"
16600
+ },
16601
+ l: .55,
16602
+ c: [.1, .17]
16603
+ }, {
16604
+ label: "a warm colour on the buttons",
16605
+ hue: "split-warm",
16606
+ second: "primary",
16607
+ lead: "accent",
16608
+ l: .38,
16609
+ c: [.08, .13]
16610
+ }]
16611
+ };
16612
+ /** Grounded Practice: an inset panel on the muted colour opens the page; links and buttons share the primary. */
16613
+ const grounded = {
16614
+ roles: {
16615
+ ...lightGrounds(bookingLookGroundLimits["site-style:grounded-practice"], {
16616
+ page: .012,
16617
+ band: .04,
16618
+ bandL: [.9, .945]
16619
+ }),
16620
+ primary: {
16621
+ l: [.3, .55],
16622
+ c: [.03, .14],
16623
+ target: .42,
16624
+ name: "the buttons and links",
16625
+ mention: true,
16626
+ link: true,
16627
+ needs: [
16628
+ contrast("background"),
16629
+ contrast("muted"),
16630
+ label,
16631
+ {
16632
+ kind: "ladder",
16633
+ step: 600,
16634
+ against: "muted",
16635
+ ratio: 3
16636
+ }
16637
+ ]
16638
+ },
16639
+ secondary: {
16640
+ l: [.45, .62],
16641
+ c: [.01, .04],
16642
+ target: .54,
16643
+ name: "the quiet details"
16644
+ },
16645
+ accent: {
16646
+ l: [.38, .68],
16647
+ c: [.05, .15],
16648
+ target: .6,
16649
+ name: "the small marks",
16650
+ analogous: true,
16651
+ needs: [contrast("background"), distinct("primary", .1)]
16652
+ }
16653
+ },
16654
+ slots: [{
16655
+ roles: ["muted"],
16656
+ name: "the opening panel"
16657
+ }, {
16658
+ roles: ["primary"],
16659
+ name: "the buttons and links"
16660
+ }],
16661
+ order: [
16662
+ ...groundOrder,
16663
+ "primary",
16664
+ "accent",
16665
+ "secondary"
16666
+ ],
16667
+ neutrals: {
16668
+ dark: [.27, .015],
16669
+ light: [.985, .01]
16670
+ },
16671
+ reading: lightReading,
16672
+ groundLimit: bookingLookGroundLimits["site-style:grounded-practice"].maxChroma,
16673
+ contrast: [{
16674
+ label: "the complement on the buttons and links",
16675
+ hue: "complement",
16676
+ second: "primary",
16677
+ lead: "muted",
16678
+ l: .42,
16679
+ c: [.08, .13]
16680
+ }, {
16681
+ label: "a warm colour on the buttons and links",
16682
+ hue: "split-warm",
16683
+ second: "primary",
16684
+ lead: "muted",
16685
+ l: .42,
16686
+ c: [.08, .13]
16687
+ }]
16688
+ };
16689
+ /**
16690
+ * Lantern: always dark. A very dark page, a deep band, gilt buttons running
16691
+ * from the accent into the primary (one colour family, light into deeper),
16692
+ * and the accent on every ornament and the candle glow.
16693
+ */
16694
+ const lanternLimits = bookingLookGroundLimits["site-style:lantern"];
16695
+ const lantern = {
16696
+ roles: {
16697
+ background: {
16698
+ l: [.14, .22],
16699
+ c: [0, Math.min(.03, lanternLimits.maxChroma)],
16700
+ chromaShare: .3,
16701
+ target: .18,
16702
+ name: "the page"
16703
+ },
16704
+ surface: {
16705
+ l: [.24, .3],
16706
+ c: [0, Math.min(.035, lanternLimits.maxChroma)],
16707
+ chromaShare: .3,
16708
+ target: .27,
16709
+ name: "the cards",
16710
+ needs: [distinct("background", .05)]
16711
+ },
16712
+ surfaceAlt: {
16713
+ l: [.28, .34],
16714
+ c: [0, Math.min(.035, lanternLimits.maxChroma)],
16715
+ chromaShare: .3,
16716
+ target: .31,
16717
+ name: "the quieter panels"
16718
+ },
16719
+ muted: {
16720
+ l: [.2, .32],
16721
+ c: [0, lanternLimits.maxChroma],
16722
+ chromaShare: .45,
16723
+ target: .235,
16724
+ name: "the band",
16725
+ needs: [distinct("background", .04)]
16726
+ },
16727
+ border: {
16728
+ l: [.35, .42],
16729
+ c: [0, .035],
16730
+ chromaShare: .3,
16731
+ target: .38,
16732
+ name: "the rules"
16733
+ },
16734
+ accent: {
16735
+ l: [.76, .9],
16736
+ c: [.03, .13],
16737
+ target: .86,
16738
+ name: "the ornaments and the light end of the gilt",
16739
+ mention: true,
16740
+ giltEnd: true,
16741
+ hueFrom: "primary",
16742
+ needs: [
16743
+ contrast("background"),
16744
+ contrast("muted"),
16745
+ label
16746
+ ]
16747
+ },
16748
+ primary: {
16749
+ l: [.62, .82],
16750
+ c: [.03, .15],
16751
+ target: {
16752
+ from: "accent",
16753
+ dl: -.11
16754
+ },
16755
+ hueFrom: "accent",
16756
+ name: "the deep end of the gilt",
16757
+ giltEnd: true,
16758
+ needs: [
16759
+ contrast("background"),
16760
+ contrast("muted"),
16761
+ label,
16762
+ distinct("accent", .06)
16763
+ ]
16764
+ },
16765
+ secondary: {
16766
+ l: [.24, .38],
16767
+ c: [.04, .11],
16768
+ target: .3,
16769
+ name: "the deep details"
16770
+ }
16771
+ },
16772
+ slots: [{
16773
+ roles: ["muted"],
16774
+ name: "the band"
16775
+ }, {
16776
+ roles: ["accent", "primary"],
16777
+ name: "the gilt buttons"
16778
+ }],
16779
+ order: [
16780
+ ...groundOrder,
16781
+ "accent",
16782
+ "primary",
16783
+ "secondary"
16784
+ ],
16785
+ neutrals: {
16786
+ dark: [.2, .03],
16787
+ light: [.95, .02]
16788
+ },
16789
+ reading: [
16790
+ "background",
16791
+ "surface",
16792
+ "surfaceAlt",
16793
+ "muted"
16794
+ ],
16795
+ groundLimit: lanternLimits.maxChroma,
16796
+ contrast: [{
16797
+ label: "a warm complement on the gilt",
16798
+ hue: "warm-complement",
16799
+ second: "accent",
16800
+ lead: "muted",
16801
+ l: .84,
16802
+ c: [.08, .12]
16803
+ }, {
16804
+ label: "a second warm colour on the gilt",
16805
+ hue: "warm-other",
16806
+ second: "accent",
16807
+ lead: "muted",
16808
+ l: .84,
16809
+ c: [.08, .12]
16810
+ }]
16811
+ };
16812
+ /** Poster: the brand block is the one large area meant to be loud. Its buttons are the pop colour; elsewhere buttons are the block colour. */
16813
+ const posterBand = {
16814
+ roles: ["muted", "surfaceAlt"],
16815
+ name: "a faint tint of it on the band"
16816
+ };
16817
+ const poster = {
16818
+ roles: {
16819
+ ...lightGrounds(bookingLookGroundLimits["site-style:poster"], {
16820
+ page: .008,
16821
+ band: .04,
16822
+ bandL: [.92, .96]
16823
+ }),
16824
+ primary: {
16825
+ l: [.3, .62],
16826
+ c: [.08, .25],
16827
+ target: .48,
16828
+ name: "the big block",
16829
+ mention: true,
16830
+ needs: [
16831
+ contrast("surface", 3.6),
16832
+ contrast("background"),
16833
+ label
16834
+ ]
16835
+ },
16836
+ accent: {
16837
+ l: [.62, .95],
16838
+ c: [.08, .25],
16839
+ target: .86,
16840
+ name: "the pop buttons and stickers",
16841
+ mention: true,
16842
+ analogous: true,
16843
+ needs: [
16844
+ contrast("primary"),
16845
+ label,
16846
+ distinct("primary", .15)
16847
+ ]
16848
+ },
16849
+ secondary: {
16850
+ l: [.15, .32],
16851
+ c: [.03, .15],
16852
+ target: .24,
16853
+ name: "the hard shadows",
16854
+ needs: [distinct("primary", .1)]
16855
+ }
16856
+ },
16857
+ slots: [{
16858
+ roles: ["primary"],
16859
+ name: "the big block"
16860
+ }, {
16861
+ roles: ["accent"],
16862
+ name: "the pop buttons and stickers"
16863
+ }],
16864
+ order: [
16865
+ ...groundOrder,
16866
+ "primary",
16867
+ "accent",
16868
+ "secondary"
16869
+ ],
16870
+ neutrals: {
16871
+ dark: [.2, .04],
16872
+ light: [1, 0]
16873
+ },
16874
+ reading: lightReading,
16875
+ groundLimit: bookingLookGroundLimits["site-style:poster"].maxChroma,
16876
+ contrast: [{
16877
+ label: "the complement on the pop buttons",
16878
+ hue: "complement",
16879
+ second: "accent",
16880
+ lead: "primary",
16881
+ alsoTints: posterBand,
16882
+ l: .82,
16883
+ c: [.12, .15]
16884
+ }, {
16885
+ label: "a warm triadic partner on the pop buttons",
16886
+ hue: "triadic-warm",
16887
+ second: "accent",
16888
+ lead: "primary",
16889
+ alsoTints: posterBand,
16890
+ l: .82,
16891
+ c: [.12, .15]
16892
+ }]
16893
+ };
16894
+ /** Keyed by the offered looks, so a booking look without a colour spec doesn't compile. */
16895
+ const bookingLookColourSpecs = {
16896
+ "site-style:fulcrum": fulcrum,
16897
+ "site-style:folio": folio,
16898
+ "site-style:measure": measure,
16899
+ "site-style:grounded-practice": grounded,
16900
+ "site-style:lantern": lantern,
16901
+ "site-style:poster": poster
16902
+ };
16903
+
15806
16904
  //#endregion
15807
16905
  //#region ../theme-core/src/site-styles/footerLookDefaults.ts
15808
16906
  const defaultFooterLookRecipes = {
@@ -17932,10 +19030,10 @@ function buildTextContrastShadow(input) {
17932
19030
  return isLikelyLightColorToken(input.theme, input.colorToken) ? "0 1px 3px rgb(0 0 0 / 0.55), 0 0 18px rgb(0 0 0 / 0.38)" : "0 1px 3px rgb(255 255 255 / 0.72), 0 0 18px rgb(255 255 255 / 0.5)";
17933
19031
  }
17934
19032
  function isLikelyLightColorToken(theme, colorToken) {
17935
- const hex = resolveColorTokenHex(theme, colorToken);
17936
- if (hex) {
19033
+ const hex$1 = resolveColorTokenHex(theme, colorToken);
19034
+ if (hex$1) {
17937
19035
  try {
17938
- return featuresFromHex(hex).Y >= .5;
19036
+ return featuresFromHex(hex$1).Y >= .5;
17939
19037
  } catch {}
17940
19038
  }
17941
19039
  const normalizedToken = colorToken?.toLowerCase() ?? "";
@@ -27434,6 +28532,7 @@ function resolveHeaderStyleRecipe(siteStyle, styleId) {
27434
28532
  const defaultHeaderStyleRecipes = {
27435
28533
  minimal: {
27436
28534
  tone: "neutral",
28535
+ dropdown: neutralHeaderDropdown,
27437
28536
  background: {
27438
28537
  kind: "token",
27439
28538
  token: "surface"
@@ -27452,6 +28551,7 @@ const defaultHeaderStyleRecipes = {
27452
28551
  },
27453
28552
  "fine-line": {
27454
28553
  tone: "neutral",
28554
+ dropdown: neutralHeaderDropdown,
27455
28555
  background: {
27456
28556
  kind: "token",
27457
28557
  token: "surface"
@@ -27470,6 +28570,7 @@ const defaultHeaderStyleRecipes = {
27470
28570
  },
27471
28571
  "soft-bar": {
27472
28572
  tone: "neutral",
28573
+ dropdown: neutralHeaderDropdown,
27473
28574
  background: {
27474
28575
  kind: "token",
27475
28576
  token: "surfaceAlt"
@@ -27490,6 +28591,7 @@ const defaultHeaderStyleRecipes = {
27490
28591
  },
27491
28592
  "pill-nav": {
27492
28593
  tone: "neutral",
28594
+ dropdown: neutralHeaderDropdown,
27493
28595
  background: {
27494
28596
  kind: "token",
27495
28597
  token: "surface"
@@ -27515,6 +28617,7 @@ const defaultHeaderStyleRecipes = {
27515
28617
  },
27516
28618
  "brand-bar": {
27517
28619
  tone: "brand",
28620
+ dropdown: brandHeaderDropdown,
27518
28621
  background: {
27519
28622
  kind: "token",
27520
28623
  token: "primary"
@@ -27533,6 +28636,7 @@ const defaultHeaderStyleRecipes = {
27533
28636
  },
27534
28637
  "brand-underline": {
27535
28638
  tone: "brand",
28639
+ dropdown: brandHeaderDropdown,
27536
28640
  background: {
27537
28641
  kind: "token",
27538
28642
  token: "primary"
@@ -27556,6 +28660,7 @@ const defaultHeaderStyleRecipes = {
27556
28660
  },
27557
28661
  "brand-pill": {
27558
28662
  tone: "brand",
28663
+ dropdown: brandHeaderDropdown,
27559
28664
  background: {
27560
28665
  kind: "token",
27561
28666
  token: "primary"
@@ -27586,6 +28691,7 @@ const defaultHeaderStyleRecipes = {
27586
28691
  },
27587
28692
  "brand-capsule": {
27588
28693
  tone: "brand",
28694
+ dropdown: brandHeaderDropdown,
27589
28695
  background: {
27590
28696
  kind: "token",
27591
28697
  token: "primary"
@@ -27609,6 +28715,7 @@ const defaultHeaderStyleRecipes = {
27609
28715
  },
27610
28716
  "serif-line": {
27611
28717
  tone: "neutral",
28718
+ dropdown: neutralHeaderDropdown,
27612
28719
  background: {
27613
28720
  kind: "token",
27614
28721
  token: "background"
@@ -27635,6 +28742,7 @@ const defaultHeaderStyleRecipes = {
27635
28742
  },
27636
28743
  "flat-tabs": {
27637
28744
  tone: "neutral",
28745
+ dropdown: neutralHeaderDropdown,
27638
28746
  background: {
27639
28747
  kind: "token",
27640
28748
  token: "surface"
@@ -27653,6 +28761,7 @@ const defaultHeaderStyleRecipes = {
27653
28761
  },
27654
28762
  glass: {
27655
28763
  tone: "neutral",
28764
+ dropdown: neutralHeaderDropdown,
27656
28765
  background: { kind: "transparent" },
27657
28766
  nav: {
27658
28767
  kind: "pill",
@@ -27676,6 +28785,7 @@ const defaultHeaderStyleRecipes = {
27676
28785
  },
27677
28786
  "minimal-surface": {
27678
28787
  tone: "neutral",
28788
+ dropdown: clearSplitHeaderDropdown,
27679
28789
  background: { kind: "transparent" },
27680
28790
  nav: {
27681
28791
  kind: "minimal",
@@ -27693,11 +28803,13 @@ const defaultHeaderStyleRecipes = {
27693
28803
  contrastTreatment: "soft-glow"
27694
28804
  },
27695
28805
  openingForeground: "all",
28806
+ openingPlacement: "photo-behind-text",
27696
28807
  scrollTreatment: "contrast-glass",
27697
28808
  interactionPalette: clearSplitInteractionPalette
27698
28809
  },
27699
28810
  "soft-shell": {
27700
28811
  tone: "neutral",
28812
+ dropdown: neutralHeaderDropdown,
27701
28813
  background: { kind: "transparent" },
27702
28814
  nav: {
27703
28815
  kind: "pill",
@@ -27721,6 +28833,7 @@ const defaultHeaderStyleRecipes = {
27721
28833
  },
27722
28834
  "split-glass": {
27723
28835
  tone: "neutral",
28836
+ dropdown: neutralHeaderDropdown,
27724
28837
  background: { kind: "transparent" },
27725
28838
  nav: {
27726
28839
  kind: "pill",
@@ -27744,6 +28857,7 @@ const defaultHeaderStyleRecipes = {
27744
28857
  },
27745
28858
  "split-pill": {
27746
28859
  tone: "neutral",
28860
+ dropdown: neutralHeaderDropdown,
27747
28861
  background: { kind: "transparent" },
27748
28862
  nav: {
27749
28863
  kind: "pill",
@@ -27908,33 +29022,6 @@ function toneColorToken(tone) {
27908
29022
  default: return assertNever$1(tone, "Unhandled header tone recipe");
27909
29023
  }
27910
29024
  }
27911
- function toneDropdownStyle(tone) {
27912
- switch (tone) {
27913
- case "neutral": return {
27914
- background: "surface",
27915
- textColor: "text",
27916
- borderColor: "border",
27917
- shadow: "md",
27918
- borderRadius: "md",
27919
- textTransform: "none",
27920
- letterSpacing: "normal"
27921
- };
27922
- case "brand": return {
27923
- background: "primary-700",
27924
- textColor: "primaryForeground",
27925
- borderColor: null,
27926
- shadow: "md",
27927
- borderRadius: "md",
27928
- hoverBackground: "primary-900",
27929
- hoverTextColor: "secondary",
27930
- textTransform: "uppercase",
27931
- letterSpacing: "wider",
27932
- fontWeight: "bold",
27933
- textSize: "sm"
27934
- };
27935
- default: return assertNever$1(tone, "Unhandled header tone recipe");
27936
- }
27937
- }
27938
29025
  function recipeBackground(background) {
27939
29026
  switch (background.kind) {
27940
29027
  case "token": return {
@@ -28138,7 +29225,7 @@ function compileHeaderRecipe(theme, layout, recipe) {
28138
29225
  ...headerForLayout(layout),
28139
29226
  textColor: toneColorToken(effective.tone),
28140
29227
  navColor: toneColorToken(effective.tone),
28141
- dropdownStyle: toneDropdownStyle(effective.tone),
29228
+ dropdownStyle: compileHeaderDropdownRecipe(effective.dropdown),
28142
29229
  background: recipeBackground(effective.background),
28143
29230
  border: recipeBorder(effective.border),
28144
29231
  navStyle: recipeNavStyle(effective.nav),
@@ -28670,7 +29757,7 @@ function resolveCornerRadiiCss$1(cornerRadii, fallback) {
28670
29757
  return `${at(cornerRadii.topLeft)} ${at(cornerRadii.topRight)} ${at(cornerRadii.bottomRight)} ${at(cornerRadii.bottomLeft)}`;
28671
29758
  }
28672
29759
  function generateCardCss(options) {
28673
- const { themeId, theme, cardSystem: providedCardSystem } = options;
29760
+ const { themeId, theme, cardSystem: providedCardSystem, solvedInks } = options;
28674
29761
  const tokens = new TokenResolver(theme);
28675
29762
  const palette = expandPalette(theme.palette);
28676
29763
  const cardSystem = providedCardSystem ?? generateDefaultCardSystem(theme);
@@ -28683,11 +29770,11 @@ function generateCardCss(options) {
28683
29770
  const groupKey = roleAliasGroupKey(source, variant.id);
28684
29771
  emittedGroupKeys.add(groupKey);
28685
29772
  const aliasClasses = aliasGroups.get(groupKey)?.classes ?? [];
28686
- cssChunks.push(generateVariantCSS(variant, themeId, [`card-${variant.id}`, ...aliasClasses], tokens, theme, palette));
29773
+ cssChunks.push(generateVariantCSS(variant, themeId, [`card-${variant.id}`, ...aliasClasses], tokens, theme, palette, solvedInks));
28687
29774
  }
28688
29775
  for (const [groupKey, group] of aliasGroups) {
28689
29776
  if (emittedGroupKeys.has(groupKey)) continue;
28690
- cssChunks.push(generateVariantCSS(group.variant, themeId, group.classes, tokens, theme, palette));
29777
+ cssChunks.push(generateVariantCSS(group.variant, themeId, group.classes, tokens, theme, palette, solvedInks));
28691
29778
  }
28692
29779
  return cssChunks.filter(Boolean).join("\n\n");
28693
29780
  }
@@ -28750,7 +29837,7 @@ const MEDIA_CORNER_MAP = {
28750
29837
  * This CSS-first approach eliminates runtime transforms for styling,
28751
29838
  * keeping transforms only for data normalization and dynamic layout.
28752
29839
  */
28753
- function generateVariantCSS(variant, themeId, cardClasses, tokens, theme, palette) {
29840
+ function generateVariantCSS(variant, themeId, cardClasses, tokens, theme, palette, solvedInks) {
28754
29841
  const selectorsFor = (suffix) => cardClasses.map((cardClass) => `:where([data-theme-scope="${themeId}"]) .${cardClass}.${cardClass}${suffix}`).join(",\n");
28755
29842
  const containerSelectors = cardClasses.map((cardClass) => `:where([data-theme-scope="${themeId}"]) .${cardClass}.${cardClass}`);
28756
29843
  const containerSelector = containerSelectors.join(",\n");
@@ -28758,7 +29845,7 @@ function generateVariantCSS(variant, themeId, cardClasses, tokens, theme, palett
28758
29845
  const contentSelector = selectorsFor(" .card-content");
28759
29846
  const fallbackBackgroundSelector = selectorsFor(" > .rb-bg-surface");
28760
29847
  const rules = [];
28761
- rules.push(...cardSurfaceRules(variant, tokens, theme, palette));
29848
+ rules.push(...cardSurfaceRules(variant, tokens, theme, palette, solvedInks));
28762
29849
  const resolveColor = (token) => tokens.getColor(token);
28763
29850
  rules.push(...cardBorderDeclarations(variant.border ? borderDeclaration(variant.border, tokens) : null, variant.decorations, resolveColor));
28764
29851
  const corners$1 = variant.corners ?? theme.corners;
@@ -28782,7 +29869,7 @@ function generateVariantCSS(variant, themeId, cardClasses, tokens, theme, palett
28782
29869
  `${contentSelector} {\n ${contentRules.join(";\n ")};\n}`
28783
29870
  ].join("\n\n");
28784
29871
  }
28785
- function cardSurfaceRules(variant, tokens, theme, palette) {
29872
+ function cardSurfaceRules(variant, tokens, theme, palette, solvedInks) {
28786
29873
  if (variant.background.type === "transparent") return ["background: transparent"];
28787
29874
  const surface$1 = resolveSurface({
28788
29875
  kind: "paint",
@@ -28790,7 +29877,8 @@ function cardSurfaceRules(variant, tokens, theme, palette) {
28790
29877
  }, {
28791
29878
  palette,
28792
29879
  buttonSystem: theme.buttons,
28793
- linkHoverToken: theme.typography.proseLinkStyle?.hoverColorToken
29880
+ linkHoverToken: theme.typography.proseLinkStyle?.hoverColorToken,
29881
+ solvedInks
28794
29882
  });
28795
29883
  return [cardBackgroundRule(variant.background, tokens), ...opaqueSurfaceDeclarations(surface$1)];
28796
29884
  }
@@ -30215,9 +31303,9 @@ const paletteSchema = z.object({
30215
31303
  return z.NEVER;
30216
31304
  }
30217
31305
  return {
30218
- colors: paletteColorEntries(colors.colors).map(({ name, hex }) => ({
31306
+ colors: paletteColorEntries(colors.colors).map(({ name, hex: hex$1 }) => ({
30219
31307
  name: String(name),
30220
- hex: String(hex)
31308
+ hex: String(hex$1)
30221
31309
  })),
30222
31310
  meta: palette.meta
30223
31311
  };
@@ -31877,38 +32965,49 @@ function renderHeaderPaintTargetRule(target, rules) {
31877
32965
  "border-radius: inherit;",
31878
32966
  "z-index: -1;"
31879
32967
  ] : [];
31880
- return `${target.selector} {\n ${[...pseudoElementSetup, ...rules].join("\n ")}\n}`;
31881
- }
31882
- case "viewport-band": {
31883
- const { top, bottom } = target.extent;
31884
- const ownerRules = [
31885
- `${top.customProperty}: ${top.value};`,
31886
- `${bottom.customProperty}: ${bottom.value};`,
31887
- `${top.ownerProperty}: var(${top.customProperty});`,
31888
- ...target.ownerNeedsPositioning ? ["position: relative;"] : [],
31889
- "isolation: isolate;",
31890
- "background: none;",
31891
- "backdrop-filter: none;",
31892
- "-webkit-backdrop-filter: none;",
31893
- "border: 0;",
31894
- "box-shadow: none;"
31895
- ];
31896
- const bandRules = [
31897
- "content: \"\";",
31898
- "position: absolute;",
31899
- `top: calc(-1 * var(${top.customProperty}));`,
31900
- `bottom: calc(-1 * var(${bottom.customProperty}));`,
31901
- "left: 50%;",
31902
- "width: 100vw;",
31903
- "transform: translateX(-50%);",
31904
- "pointer-events: none;",
31905
- "border-radius: 0;",
31906
- "z-index: -1;",
31907
- ...rules
31908
- ];
31909
- return `${target.clipSelector} {\n overflow-x: clip;\n}\n${target.ownerSelector} {\n ${ownerRules.join("\n ")}\n}\n${target.selector} {\n ${bandRules.join("\n ")}\n}`;
31910
- }
31911
- }
32968
+ const ownerInks = target.pseudoElement === "inset" ? headerPaintOwnerInks(target.selector.replace(/::before$/, ""), rules) : "";
32969
+ return `${ownerInks}${target.selector} {\n ${[...pseudoElementSetup, ...rules].join("\n ")}\n}`;
32970
+ }
32971
+ case "viewport-band": return renderViewportBandPaint(target, rules);
32972
+ }
32973
+ }
32974
+ function headerInkRules(rules) {
32975
+ return rules.filter((rule$1) => Object.values(SURFACE_INK_CSS_VARS).some((variable) => rule$1.startsWith(`${variable}:`)));
32976
+ }
32977
+ function headerPaintOwnerInks(selector, rules) {
32978
+ const inks = headerInkRules(rules);
32979
+ return inks.length === 0 ? "" : `${selector} {\n ${inks.join("\n ")}\n}\n`;
32980
+ }
32981
+ /** Viewport paint stays on its pseudo-element; inherited ink belongs to the content owner. */
32982
+ function renderViewportBandPaint(target, rules) {
32983
+ const { top, bottom } = target.extent;
32984
+ const ownerRules = [
32985
+ `${top.customProperty}: ${top.value};`,
32986
+ `${bottom.customProperty}: ${bottom.value};`,
32987
+ `${top.ownerProperty}: var(${top.customProperty});`,
32988
+ ...target.ownerNeedsPositioning ? ["position: relative;"] : [],
32989
+ "isolation: isolate;",
32990
+ "background: none;",
32991
+ "backdrop-filter: none;",
32992
+ "-webkit-backdrop-filter: none;",
32993
+ "border: 0;",
32994
+ "box-shadow: none;",
32995
+ ...headerInkRules(rules)
32996
+ ];
32997
+ const bandRules = [
32998
+ "content: \"\";",
32999
+ "position: absolute;",
33000
+ `top: calc(-1 * var(${top.customProperty}));`,
33001
+ `bottom: calc(-1 * var(${bottom.customProperty}));`,
33002
+ "left: 50%;",
33003
+ "width: 100vw;",
33004
+ "transform: translateX(-50%);",
33005
+ "pointer-events: none;",
33006
+ "border-radius: 0;",
33007
+ "z-index: -1;",
33008
+ ...rules
33009
+ ];
33010
+ return `${target.clipSelector} {\n overflow-x: clip;\n}\n${target.ownerSelector} {\n ${ownerRules.join("\n ")}\n}\n${target.selector} {\n ${bandRules.join("\n ")}\n}`;
31912
33011
  }
31913
33012
 
31914
33013
  //#endregion
@@ -31932,7 +33031,8 @@ function generateHeaderScrollTreatmentStyles(treatment, themeId, variant, surfac
31932
33031
  `background-color: rgb(${backgroundChannels} / 0.60);`,
31933
33032
  `backdrop-filter: ${CONTRAST_GLASS_BLUR};`,
31934
33033
  `-webkit-backdrop-filter: ${CONTRAST_GLASS_BLUR};`,
31935
- ...surfaceRules
33034
+ ...surfaceRules,
33035
+ ...surfaceInkDeclarations({ kind: "reset" }).map((rule$1) => rule$1.replace(";", " !important;"))
31936
33036
  ]);
31937
33037
  }
31938
33038
  }
@@ -32200,6 +33300,12 @@ const dropdownStyleSchema = z.object({
32200
33300
  "md",
32201
33301
  "lg"
32202
33302
  ]).default("md"),
33303
+ surfaceTreatment: z.discriminatedUnion("kind", [z.object({ kind: z.literal("solid") }), z.object({
33304
+ kind: z.literal("frosted"),
33305
+ tone: z.enum(["fixed", "opening"]),
33306
+ opacity: z.number().min(0).max(1),
33307
+ blur: z.enum(["sm", "md"])
33308
+ })]).optional(),
32203
33309
  hoverBackground: themeColorTokenSchema.nullable().optional(),
32204
33310
  hoverTextColor: themeColorTokenSchema.nullable().optional(),
32205
33311
  textTransform: z.enum([
@@ -33239,8 +34345,8 @@ function getGradientPreset(id$2) {
33239
34345
  */
33240
34346
  function gradientToCss(config, tokens) {
33241
34347
  const stops = config.stops.map((stop) => {
33242
- const hex = tokens[stop.color] ?? "#000000";
33243
- return `${hex} ${stop.position}%`;
34348
+ const hex$1 = tokens[stop.color] ?? "#000000";
34349
+ return `${hex$1} ${stop.position}%`;
33244
34350
  }).join(", ");
33245
34351
  const type = config.type ?? "linear";
33246
34352
  if (type === "radial") {
@@ -33366,8 +34472,8 @@ function gradientMediaScrim(input) {
33366
34472
  function scrimOpacityFor(scrimHex, foreground, palette) {
33367
34473
  if (typeof scrimHex !== "string") return { kind: "unreadable" };
33368
34474
  const foregroundHexes = Object.values(foreground).flatMap((token) => {
33369
- const hex = typeof token === "string" ? palette[token] : undefined;
33370
- return typeof hex === "string" ? [hex] : [];
34475
+ const hex$1 = typeof token === "string" ? palette[token] : undefined;
34476
+ return typeof hex$1 === "string" ? [hex$1] : [];
33371
34477
  });
33372
34478
  if (foregroundHexes.length === 0) return { kind: "unreadable" };
33373
34479
  const opacity = minimumScrimOpacity({
@@ -33513,13 +34619,13 @@ function surfaceResolutionContextFromPalette(palette) {
33513
34619
  buttonSystem: undefined
33514
34620
  });
33515
34621
  }
33516
- function surfaceResolutionContextFromTheme(theme, declaredSurfaces = []) {
34622
+ function surfaceResolutionContextFromTheme(theme, declaredSurfaces = [], solvedInks) {
33517
34623
  return prepareThemeSurfaceRevision({
33518
34624
  palette: theme.palette,
33519
34625
  buttons: theme.buttons,
33520
34626
  skin: theme.skin,
33521
34627
  skinMarks: theme.skinMarks
33522
- }, () => declaredSurfaces).context;
34628
+ }, () => declaredSurfaces, solvedInks).context;
33523
34629
  }
33524
34630
  function defineDesignRuntimeRules(rules) {
33525
34631
  return rules;
@@ -34194,8 +35300,11 @@ const bookingPageNoPhotoHeroLookIds = {
34194
35300
  "site-style:poster": "hero-page-head"
34195
35301
  };
34196
35302
  function bookingPageOfferingFor(siteStyleId) {
34197
- const entry = Object.entries(bookingPageNoPhotoHeroLookIds).find(([id$2]) => id$2 === siteStyleId);
34198
- return entry ? { noPhotoHeroLookId: asAppearancePresetId(entry[1]) } : undefined;
35303
+ const lookId = bookingPageLookIdValues.find((id$2) => id$2 === siteStyleId);
35304
+ return lookId ? {
35305
+ noPhotoHeroLookId: asAppearancePresetId(bookingPageNoPhotoHeroLookIds[lookId]),
35306
+ colourSpec: bookingLookColourSpecs[lookId]
35307
+ } : undefined;
34199
35308
  }
34200
35309
  function clientStyleMatchesSite(siteSlugs, target) {
34201
35310
  if (target.audience !== "site-picker" && target.audience !== "local-demo") return false;
@@ -34555,7 +35664,11 @@ const headerCurationBySiteStyle = {
34555
35664
  "site-style:folio": defineHeaderCuration([
34556
35665
  {
34557
35666
  layoutId: "floating",
34558
- styleIds: ["minimal-surface", "glass"]
35667
+ styleIds: [
35668
+ "minimal-surface",
35669
+ "soft-shell",
35670
+ "glass"
35671
+ ]
34559
35672
  },
34560
35673
  {
34561
35674
  layoutId: "classic",
@@ -35239,6 +36352,11 @@ const folioLooks = {
35239
36352
  treatment: "hero-short-panel",
35240
36353
  roleMap: folioRoles
35241
36354
  },
36355
+ {
36356
+ id: id("hero-text-over-photo"),
36357
+ treatment: "hero-photo-overlay",
36358
+ roleMap: folioRoles
36359
+ },
35242
36360
  {
35243
36361
  id: id("hero-page-head"),
35244
36362
  treatment: "hero-ruled",
@@ -35289,10 +36407,7 @@ const folioLooks = {
35289
36407
  id: id("event-calendar-ruled-rows"),
35290
36408
  treatment: "calendar-card",
35291
36409
  itemRoles: { container: "card" },
35292
- surfaceTints: { date: {
35293
- source: "text",
35294
- opacity: 8
35295
- } },
36410
+ knobValues: { actionStyle: ["button"] },
35296
36411
  roleMap: folioRoles
35297
36412
  },
35298
36413
  {
@@ -35303,6 +36418,7 @@ const folioLooks = {
35303
36418
  {
35304
36419
  id: id("event-calendar-next-three"),
35305
36420
  treatment: "calendar-compact",
36421
+ knobValues: { actionStyle: ["button"] },
35306
36422
  roleMap: folioRoles
35307
36423
  },
35308
36424
  {
@@ -37708,12 +38824,15 @@ const curatedSiteStyles = [
37708
38824
  accordions: editorialDividedAccordionSystem,
37709
38825
  testimonialQuote: serifDisplayQuote
37710
38826
  },
37711
- headerStyleRecipes: { "minimal-surface": {
38827
+ headerStyleRecipes: { "soft-shell": {
37712
38828
  tone: "neutral",
38829
+ dropdown: neutralHeaderDropdown,
37713
38830
  background: { kind: "transparent" },
37714
38831
  nav: {
37715
- kind: "minimal",
37716
- hoverColorToken: "primary"
38832
+ kind: "pill",
38833
+ backing: "capsule",
38834
+ hoverColorToken: "primary",
38835
+ hoverAlpha: 8
37717
38836
  },
37718
38837
  navWeight: "medium",
37719
38838
  navLinkWeight: "medium",
@@ -37805,6 +38924,7 @@ const curatedSiteStyles = [
37805
38924
  },
37806
38925
  headerStyleRecipes: { "serif-line": {
37807
38926
  tone: "neutral",
38927
+ dropdown: neutralHeaderDropdown,
37808
38928
  background: {
37809
38929
  kind: "token",
37810
38930
  token: "background"
@@ -38554,8 +39674,8 @@ function validateLookFallbacks(style, catalogue = appearancePresets.filter(isArr
38554
39674
  function validateCalendarModeOffers(style, catalogue = appearancePresets.filter(isArrangementAppearancePreset)) {
38555
39675
  const calendarLooks = catalogue.filter((look$1) => look$1.blockKinds.includes("block.event-calendar"));
38556
39676
  arrangementModes.forEach((mode) => {
38557
- const offered = calendarLooks.some((look$1) => lookModes(look$1).includes(mode) && style.offers.some((offer) => offer.id === look$1.id));
38558
- if (!offered) throw new Error(`Missing automatic calendar Look for ${style.skin}: ${mode}`);
39677
+ const offered$1 = calendarLooks.some((look$1) => lookModes(look$1).includes(mode) && style.offers.some((offer) => offer.id === look$1.id));
39678
+ if (!offered$1) throw new Error(`Missing automatic calendar Look for ${style.skin}: ${mode}`);
38559
39679
  });
38560
39680
  }
38561
39681
  function fallbackEndsAtOffer(entry, mode, style, catalogue, visited) {
@@ -38977,12 +40097,12 @@ function autoEditorChoice(effectiveValue) {
38977
40097
  };
38978
40098
  }
38979
40099
  function userPinnedEditorChoice(value, input = {}) {
38980
- const status = input.status ?? "supported";
40100
+ const status$1 = input.status ?? "supported";
38981
40101
  return {
38982
40102
  kind: "user-pinned",
38983
40103
  value,
38984
- status,
38985
- effectiveValue: input.effectiveValue !== undefined ? input.effectiveValue : status === "supported" ? value : null
40104
+ status: status$1,
40105
+ effectiveValue: input.effectiveValue !== undefined ? input.effectiveValue : status$1 === "supported" ? value : null
38986
40106
  };
38987
40107
  }
38988
40108
  const backgroundImagePromotedFieldPath = "_sectionStyles.background.image";
@@ -39450,8 +40570,8 @@ function compileFooterAgainstRow(input) {
39450
40570
  }
39451
40571
  /** Style switches retain an offered choice; otherwise use the style's first choice. */
39452
40572
  function resolveFooterLookIdForStyle(siteStyle, current) {
39453
- const offered = siteStyle?.recommendedFooterLookIds ?? [guaranteedFallbackFooterLookId];
39454
- return current !== null && offered.includes(current) ? current : offered[0] ?? guaranteedFallbackFooterLookId;
40573
+ const offered$1 = siteStyle?.recommendedFooterLookIds ?? [guaranteedFallbackFooterLookId];
40574
+ return current !== null && offered$1.includes(current) ? current : offered$1[0] ?? guaranteedFallbackFooterLookId;
39455
40575
  }
39456
40576
 
39457
40577
  //#endregion
@@ -40775,7 +41895,35 @@ function mergeBodyTextOverride(theme, override) {
40775
41895
  ...intro ? { intro } : {}
40776
41896
  });
40777
41897
  }
40778
- function withSiteStyleMetadata({ skin: _previousSkin, skinTokens: _previousTokens, fontLoading: _previousFonts, skinMarks: _previousMarks, ...theme }, siteStyle) {
41898
+ function siteStyleLookMetadata(siteStyle) {
41899
+ const looks = siteStyle.looks;
41900
+ return looks.kind === "offers" ? {
41901
+ skin: looks.skin,
41902
+ skinTokens: looks.skinTokens,
41903
+ fontLoading: looks.fontLoading,
41904
+ ...looks.marksFrom === undefined ? {} : { skinMarks: looks.marksFrom }
41905
+ } : {};
41906
+ }
41907
+ function withoutLookMetadata({ skin: _previousSkin, skinTokens: _previousTokens, fontLoading: _previousFonts, skinMarks: _previousMarks, ...theme }) {
41908
+ return theme;
41909
+ }
41910
+ /** Repair absent applied skin fields without refreshing an existing design from the catalogue. */
41911
+ function fillMissingSiteStyleLookMetadata(theme) {
41912
+ const siteStyle = theme.siteStyleId === undefined ? null : getCuratedSiteStyle(theme.siteStyleId);
41913
+ if (siteStyle === null) return theme;
41914
+ const looks = siteStyle.looks;
41915
+ if (looks.kind !== "offers") return withoutLookMetadata(theme);
41916
+ return {
41917
+ ...theme,
41918
+ skin: theme.skin ?? looks.skin,
41919
+ skinTokens: theme.skinTokens ?? looks.skinTokens,
41920
+ fontLoading: theme.fontLoading ?? looks.fontLoading,
41921
+ ...theme.skinMarks === undefined && looks.marksFrom !== undefined ? { skinMarks: looks.marksFrom } : {}
41922
+ };
41923
+ }
41924
+ /** Full style Apply deliberately replaces applied skin output with the current catalogue. */
41925
+ function withSiteStyleMetadata(previous, siteStyle) {
41926
+ const theme = withoutLookMetadata(previous);
40779
41927
  const surfaceContexts = siteStyle.looks.kind === "offers" ? Object.fromEntries(Object.values(siteStyle.looks.surfaces).flatMap((spec) => spec.kind === "token" && spec.buttonContext ? [[spec.token, spec.buttonContext]] : [])) : {};
40780
41928
  return {
40781
41929
  ...theme,
@@ -40787,12 +41935,7 @@ function withSiteStyleMetadata({ skin: _previousSkin, skinTokens: _previousToken
40787
41935
  description: siteStyle.description,
40788
41936
  rationale: `Compiled from curated Theme V2 site style ${siteStyle.id}.`,
40789
41937
  siteStyleId: siteStyle.id,
40790
- ...siteStyle.looks.kind === "offers" ? {
40791
- skin: siteStyle.looks.skin,
40792
- skinTokens: siteStyle.looks.skinTokens,
40793
- fontLoading: siteStyle.looks.fontLoading,
40794
- ...siteStyle.looks.marksFrom === undefined ? {} : { skinMarks: siteStyle.looks.marksFrom }
40795
- } : {},
41938
+ ...siteStyleLookMetadata(siteStyle),
40796
41939
  headingDecoration: siteStyle.headingDecoration
40797
41940
  };
40798
41941
  }
@@ -42304,8 +43447,8 @@ function headerStyleAvailabilityReasonLabel(reason) {
42304
43447
  }
42305
43448
  function applyOpeningForeground(theme, header, recipe, context) {
42306
43449
  if (!recipe.openingForeground) return header;
42307
- const foreground = context?.openingForeground ?? { kind: "unknown" };
42308
- if (foreground.kind === "unknown") return withOpeningForegroundProjection(applyUnknownOpeningForegroundFallback(theme, header), foreground);
43450
+ const foreground = context?.openingForeground ?? { kind: "unmeasured-paint" };
43451
+ if (foreground.kind !== "known") return withOpeningForegroundProjection(applyUnknownOpeningForegroundFallback(theme, header), foreground);
42309
43452
  return projectKnownOpeningForeground(theme, header, recipe, foreground);
42310
43453
  }
42311
43454
  function projectKnownOpeningForeground(theme, header, recipe, foreground) {
@@ -43292,23 +44435,6 @@ const decodedPageDesignIntentSchema = z.union([z.nullish(z.never()).transform(()
43292
44435
  return typeof value === "object" && value !== null && "schemaVersion" in value && value.schemaVersion !== 2 ? "Unsupported page design decisions schemaVersion." : "Invalid page design decisions shape.";
43293
44436
  } });
43294
44437
 
43295
- //#endregion
43296
- //#region ../theme-core/src/site-styles/bookingPageLookIds.ts
43297
- /** Deterministic fallback order; Fulcrum is the default. */
43298
- const bookingPageLookIdValues = [
43299
- "site-style:fulcrum",
43300
- "site-style:folio",
43301
- "site-style:measure",
43302
- "site-style:grounded-practice",
43303
- "site-style:lantern",
43304
- "site-style:poster"
43305
- ];
43306
- /** Boundary admission for draft and suggestion inputs. */
43307
- function parseBookingPageLookId(value) {
43308
- const id$2 = bookingPageLookIdValues.find((candidate$1) => candidate$1 === value);
43309
- return id$2 ? asSiteStyleId(id$2) : null;
43310
- }
43311
-
43312
44438
  //#endregion
43313
44439
  //#region ../theme-core/src/site-styles/appearance.ts
43314
44440
  const bookingPageLooks = bookingPageLookIdValues.map((value) => {
@@ -43433,6 +44559,907 @@ function describeAppearance(value) {
43433
44559
  };
43434
44560
  }
43435
44561
 
44562
+ //#endregion
44563
+ //#region ../theme-core/src/site-styles/ownColours/types.ts
44564
+ const groundRoles = [
44565
+ "background",
44566
+ "surface",
44567
+ "surfaceAlt",
44568
+ "muted",
44569
+ "border"
44570
+ ];
44571
+ const paintedRoles = [
44572
+ ...groundRoles,
44573
+ "primary",
44574
+ "secondary",
44575
+ "accent"
44576
+ ];
44577
+
44578
+ //#endregion
44579
+ //#region ../theme-core/src/site-styles/ownColours/colourSpace.ts
44580
+ /** Below this OKLCH chroma a colour has no hue worth carrying (greys, black, white). */
44581
+ const ACHROMATIC_CHROMA = .02;
44582
+ const wrapHue = (degrees) => (degrees % 360 + 360) % 360;
44583
+ /** The angle between two hues, 0–180. */
44584
+ const hueGap = (a, b) => Math.abs((a - b + 540) % 360 - 180);
44585
+ function lchOf(hex$1) {
44586
+ const { okL, okC, okH } = featuresFromHex(hex$1);
44587
+ return {
44588
+ l: okL,
44589
+ c: okC,
44590
+ h: okH
44591
+ };
44592
+ }
44593
+ /** The in-gamut sRGB colour nearest this OKLCH, holding lightness and hue and reducing chroma. */
44594
+ function fromLch(l, c, h) {
44595
+ const lightness = Math.min(1, Math.max(0, l));
44596
+ const chroma = gamutChroma(lightness, Math.max(0, c), wrapHue(h));
44597
+ const { r, g, b } = oklchToLinearSrgb(lightness, chroma, wrapHue(h));
44598
+ return hexColorSchema.parse(rgbToHex(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)));
44599
+ }
44600
+ /**
44601
+ * A shade of a hue at another lightness. Yellows and olives turn toward
44602
+ * orange as they darken, as a painter would shade them, so a deep mustard
44603
+ * reads as ochre rather than khaki.
44604
+ */
44605
+ function shadeAt(l, c, h, fromL) {
44606
+ const turn = h >= 55 && h <= 115 && l < fromL ? Math.min(25, (fromL - l) * 50) : 0;
44607
+ return fromLch(l, c, h - turn);
44608
+ }
44609
+ function oklabOf(hex$1) {
44610
+ const { r, g, b } = hexToRgb(hex$1);
44611
+ return srgbToOklab(r, g, b);
44612
+ }
44613
+ /** OKLab ΔE between two colours. */
44614
+ function colourDistance(a, b) {
44615
+ return oklabDistance(oklabOf(a), oklabOf(b));
44616
+ }
44617
+
44618
+ //#endregion
44619
+ //#region ../theme-core/src/site-styles/ownColours/engine.ts
44620
+ const hex = (value) => hexColorSchema.parse(value.toLowerCase());
44621
+ const clamp = (value, [lo, hi]) => Math.min(hi, Math.max(lo, value));
44622
+ const isGround = (role) => groundRoles.includes(role);
44623
+ const STEP = .005;
44624
+ function lightnessSteps([lo, hi]) {
44625
+ const count = Math.floor((hi - lo + 1e-9) / STEP) + 1;
44626
+ return Array.from({ length: count }, (_, i) => Math.round((lo + i * STEP) * 1e3) / 1e3);
44627
+ }
44628
+ /** The first of the cheapest, so ties keep the search's own order. */
44629
+ function cheapest(options) {
44630
+ return options.reduce((best, next) => !best || next.cost < best.cost ? next : best, undefined);
44631
+ }
44632
+ const labelReads = (fill) => maxContrastBlackOrWhite(solidPaint(fill)).ratio >= 4.5;
44633
+ function meets(candidate$1, needs, placed) {
44634
+ return needs.every((need) => {
44635
+ switch (need.kind) {
44636
+ case "label": return labelReads(candidate$1);
44637
+ case "ladder": {
44638
+ const ground = placed[need.against];
44639
+ const step = generateShades(candidate$1, "x").find((entry) => entry.name === `x-${need.step}`);
44640
+ return !ground || !step || contrastRatio(step.hex, ground) >= need.ratio;
44641
+ }
44642
+ case "contrast": {
44643
+ const other = placed[need.against];
44644
+ return !other || contrastRatio(candidate$1, other) >= need.ratio;
44645
+ }
44646
+ case "distinct": {
44647
+ const other = placed[need.from];
44648
+ return !other || colourDistance(candidate$1, other) >= need.deltaE;
44649
+ }
44650
+ }
44651
+ });
44652
+ }
44653
+ /** A role's own needs, plus every painted role's needs that point back at it, turned round. */
44654
+ function needsFor(spec, role, placed) {
44655
+ const reverse = paintedRoles.flatMap((other) => {
44656
+ if (!placed[other]) return [];
44657
+ return (spec.roles[other].needs ?? []).flatMap((need) => {
44658
+ if (need.kind === "contrast" && need.against === role) return [{
44659
+ kind: "contrast",
44660
+ against: other,
44661
+ ratio: need.ratio
44662
+ }];
44663
+ if (need.kind === "distinct" && need.from === role) return [{
44664
+ kind: "distinct",
44665
+ from: other,
44666
+ deltaE: need.deltaE
44667
+ }];
44668
+ return [];
44669
+ });
44670
+ });
44671
+ return [...spec.roles[role].needs ?? [], ...reverse];
44672
+ }
44673
+ const linkReads = (candidate$1, placed) => ["background", "surface"].every((ground) => {
44674
+ const paint = placed[ground];
44675
+ return !paint || contrastRatio(candidate$1, paint) >= 4.5;
44676
+ });
44677
+ /** The solver's family ink for a link role: the owner's colour moved at most ΔE 0.08 to read on the page and cards. */
44678
+ function linkInk(colour, placed) {
44679
+ const { background, surface: surface$1 } = placed;
44680
+ if (!background || !surface$1) return [];
44681
+ const lo = Math.min(hexLuminance(background), hexLuminance(surface$1));
44682
+ const hi = Math.max(hexLuminance(background), hexLuminance(surface$1));
44683
+ const ink = inkFrom(colourFamily(colour), luminanceInterval(lo, hi), 4.5);
44684
+ return ink.kind === "ink" ? [hex(ink.hex)] : [];
44685
+ }
44686
+ function inRange(roleSpec, candidate$1) {
44687
+ const { l, c } = lchOf(candidate$1);
44688
+ return l >= roleSpec.l[0] - .001 && l <= roleSpec.l[1] + .001 && c <= roleSpec.c[1] + .004;
44689
+ }
44690
+ /**
44691
+ * A held colour in a role: exactly when it fits, otherwise its nearest shade
44692
+ * inside the role's range that meets its needs. A link role first offers the
44693
+ * solver's family ink within ΔE 0.08; a link that still misses 4.5:1 ranks as
44694
+ * if it had moved that far, so a close shade that reads wins.
44695
+ */
44696
+ function hold(spec, role, colour, placed) {
44697
+ const roleSpec = spec.roles[role];
44698
+ const needs = needsFor(spec, role, placed);
44699
+ const s = lchOf(colour);
44700
+ const linkPenalty = (candidate$1) => roleSpec.link && !linkReads(candidate$1, placed) ? BRAND_INK_MAX_DELTA_E : 0;
44701
+ const chroma = clamp(s.c, [Math.min(roleSpec.c[0], s.c), roleSpec.c[1]]);
44702
+ const candidates = [
44703
+ colour,
44704
+ ...roleSpec.link ? linkInk(colour, placed) : [],
44705
+ ...lightnessSteps(roleSpec.l).map((l) => shadeAt(l, chroma, s.h, s.l))
44706
+ ];
44707
+ const best = cheapest(candidates.filter((candidate$1) => inRange(roleSpec, candidate$1) && meets(candidate$1, needs, placed)).map((candidate$1) => ({
44708
+ hex: candidate$1,
44709
+ cost: colourDistance(candidate$1, colour) + linkPenalty(candidate$1)
44710
+ })));
44711
+ return best ? {
44712
+ kind: "fitted",
44713
+ hex: best.hex
44714
+ } : {
44715
+ kind: "unfit",
44716
+ reason: {
44717
+ kind: "role-unmet",
44718
+ role
44719
+ }
44720
+ };
44721
+ }
44722
+ /** The chroma a derived role takes from its source: a share of it, inside the role's range; none from a grey. */
44723
+ function derivedChroma(roleSpec, sourceChroma) {
44724
+ if (sourceChroma < ACHROMATIC_CHROMA) return 0;
44725
+ const wanted = Math.min(sourceChroma * (roleSpec.chromaShare ?? 1), roleSpec.derivedChroma ?? Infinity);
44726
+ return clamp(wanted, [Math.min(roleSpec.c[0], sourceChroma), roleSpec.c[1]]);
44727
+ }
44728
+ function targetLightness(roleSpec, source, placed) {
44729
+ if (typeof roleSpec.target === "number") return roleSpec.target;
44730
+ return clamp(lchOf(placed[roleSpec.target.from] ?? source).l + roleSpec.target.dl, roleSpec.l);
44731
+ }
44732
+ /** An unheld role, as a shade or tint of its source's hue inside the role's range, as near its target lightness as its needs allow. */
44733
+ function derive(spec, role, source, placed) {
44734
+ const roleSpec = spec.roles[role];
44735
+ const needs = needsFor(spec, role, placed);
44736
+ const s = lchOf(source);
44737
+ const target = targetLightness(roleSpec, source, placed);
44738
+ const chroma = derivedChroma(roleSpec, s.c);
44739
+ const turns = roleSpec.analogous ? [
44740
+ 0,
44741
+ 20,
44742
+ -20,
44743
+ 40,
44744
+ -40
44745
+ ] : [0];
44746
+ const best = cheapest(turns.flatMap((turn) => lightnessSteps(roleSpec.l).flatMap((l) => {
44747
+ const candidate$1 = shadeAt(l, chroma, s.h + turn, s.l);
44748
+ return inRange(roleSpec, candidate$1) && meets(candidate$1, needs, placed) ? [{
44749
+ hex: candidate$1,
44750
+ cost: Math.abs(l - target) + .004 * Math.abs(turn)
44751
+ }] : [];
44752
+ })));
44753
+ return best ? {
44754
+ kind: "fitted",
44755
+ hex: best.hex
44756
+ } : {
44757
+ kind: "unfit",
44758
+ reason: {
44759
+ kind: "role-unmet",
44760
+ role
44761
+ }
44762
+ };
44763
+ }
44764
+ const holdsGround = (entry) => entry.slot.roles.some(isGround);
44765
+ /** Each held colour, in the slot role it fits with the smallest move. */
44766
+ function holdPhase(spec, holds, start) {
44767
+ return holds.reduce((result, entry) => {
44768
+ if (result.kind === "unfit") return result;
44769
+ const { placed, held } = result.painting;
44770
+ const fits = entry.slot.roles.flatMap((role) => {
44771
+ const fitted = hold(spec, role, entry.hex, placed);
44772
+ return fitted.kind === "fitted" ? [{
44773
+ role,
44774
+ hex: fitted.hex,
44775
+ cost: colourDistance(fitted.hex, entry.hex)
44776
+ }] : [];
44777
+ });
44778
+ const best = cheapest(fits);
44779
+ if (!best) return {
44780
+ kind: "unfit",
44781
+ reason: {
44782
+ kind: "role-unmet",
44783
+ role: entry.slot.roles[0]
44784
+ }
44785
+ };
44786
+ const landed = {
44787
+ source: entry.source,
44788
+ slot: entry.slot,
44789
+ role: best.role,
44790
+ hex: best.hex,
44791
+ asked: entry.hex,
44792
+ deltaE: best.cost
44793
+ };
44794
+ return {
44795
+ kind: "painting",
44796
+ painting: {
44797
+ placed: {
44798
+ ...placed,
44799
+ [best.role]: best.hex
44800
+ },
44801
+ held: [...held, landed]
44802
+ }
44803
+ };
44804
+ }, {
44805
+ kind: "painting",
44806
+ painting: start
44807
+ });
44808
+ }
44809
+ /** The colour an unheld role takes its hue from. Grounds follow a colour already held on a ground, so the page and a held band agree. */
44810
+ function sourceFor(spec, role, request, painting) {
44811
+ const given = request.hueSources?.[role];
44812
+ if (given) return given;
44813
+ const linked = spec.roles[role].hueFrom;
44814
+ const fromRole = linked ? painting.placed[linked] : undefined;
44815
+ if (fromRole) return fromRole;
44816
+ return isGround(role) ? groundTint(painting, request.lead) : request.lead;
44817
+ }
44818
+ function groundTint(painting, lead) {
44819
+ return painting.held.find((entry) => isGround(entry.role))?.asked ?? lead;
44820
+ }
44821
+ function derivePhase(spec, roles, request, start) {
44822
+ return roles.reduce((result, role) => {
44823
+ if (result.kind === "unfit" || result.painting.placed[role]) return result;
44824
+ const { painting } = result;
44825
+ const fitted = derive(spec, role, sourceFor(spec, role, request, painting), painting.placed);
44826
+ if (fitted.kind === "unfit") return fitted;
44827
+ return {
44828
+ kind: "painting",
44829
+ painting: {
44830
+ ...painting,
44831
+ placed: {
44832
+ ...painting.placed,
44833
+ [role]: fitted.hex
44834
+ }
44835
+ }
44836
+ };
44837
+ }, start);
44838
+ }
44839
+ /** Every painted role, or the first one missing. */
44840
+ function complete(placed) {
44841
+ const { background, surface: surface$1, surfaceAlt, muted, border: border$1, primary, secondary, accent } = placed;
44842
+ if (background && surface$1 && surfaceAlt && muted && border$1 && primary && secondary && accent) {
44843
+ return {
44844
+ kind: "complete",
44845
+ roles: {
44846
+ background,
44847
+ surface: surface$1,
44848
+ surfaceAlt,
44849
+ muted,
44850
+ border: border$1,
44851
+ primary,
44852
+ secondary,
44853
+ accent
44854
+ }
44855
+ };
44856
+ }
44857
+ const missing = paintedRoles.find((role) => !placed[role]) ?? "primary";
44858
+ return {
44859
+ kind: "unfit",
44860
+ reason: {
44861
+ kind: "role-unmet",
44862
+ role: missing
44863
+ }
44864
+ };
44865
+ }
44866
+ /** Status colours keep their meaning (green, amber, red); they are semantic, not part of the look. */
44867
+ const statusHues = {
44868
+ success: 150,
44869
+ warning: 70,
44870
+ destructive: 27
44871
+ };
44872
+ function status(hue, background, dark) {
44873
+ const fits = (l$1) => contrastRatio(fromLch(l$1, .13, hue), background) >= 3;
44874
+ const l = (dark ? lightnessSteps([.6, .95]) : lightnessSteps([.2, .6]).reverse()).find(fits) ?? (dark ? .95 : .2);
44875
+ return fromLch(l, .13, hue);
44876
+ }
44877
+ /** The inks and status colours, solved for the painted roles. */
44878
+ function finish(spec, roles, tintToward) {
44879
+ const { c, h } = lchOf(tintToward);
44880
+ const tint$1 = Math.min(c, .03);
44881
+ const preferred = {
44882
+ dark: fromLch(spec.neutrals.dark[0], Math.min(tint$1, spec.neutrals.dark[1]), h),
44883
+ light: fromLch(spec.neutrals.light[0], Math.min(tint$1, spec.neutrals.light[1]), h)
44884
+ };
44885
+ const ys = spec.reading.map((role) => hexLuminance(roles[role]));
44886
+ const text = neutralInk(luminanceInterval(Math.min(...ys), Math.max(...ys)), 4.5, preferred);
44887
+ const onPrimary = neutralInk(solidPaint(roles.primary), 4.5, preferred);
44888
+ if (text.kind !== "ink" || onPrimary.kind !== "ink") return {
44889
+ kind: "unfit",
44890
+ reason: { kind: "no-readable-text" }
44891
+ };
44892
+ const dark = maxContrastBlackOrWhite(solidPaint(roles.background)).ink === "white";
44893
+ return {
44894
+ kind: "finished",
44895
+ colors: {
44896
+ ...roles,
44897
+ success: status(statusHues.success, roles.background, dark),
44898
+ warning: status(statusHues.warning, roles.background, dark),
44899
+ destructive: status(statusHues.destructive, roles.background, dark),
44900
+ text: hex(text.hex),
44901
+ primaryForeground: hex(onPrimary.hex)
44902
+ },
44903
+ mode: dark ? "dark" : "light"
44904
+ };
44905
+ }
44906
+ /**
44907
+ * A band or panel is always a pale or deep tint of a colour, so moving a
44908
+ * colour there counts for less than shading a button colour.
44909
+ */
44910
+ const GROUND_MOVE_WEIGHT = .3;
44911
+ /** One complete palette for the look: held grounds, derived grounds, held fills, derived fills, then inks. */
44912
+ function paintPalette(spec, request) {
44913
+ const start = {
44914
+ placed: {},
44915
+ held: []
44916
+ };
44917
+ const grounds = derivePhase(spec, spec.order.filter(isGround), request, holdPhase(spec, request.holds.filter(holdsGround), start));
44918
+ if (grounds.kind === "unfit") return grounds;
44919
+ const fills = derivePhase(spec, spec.order.filter((role) => !isGround(role)), request, holdPhase(spec, request.holds.filter((entry) => !holdsGround(entry)), grounds.painting));
44920
+ if (fills.kind === "unfit") return fills;
44921
+ const roles = complete(fills.painting.placed);
44922
+ if (roles.kind === "unfit") return roles;
44923
+ const finished = finish(spec, roles.roles, groundTint(fills.painting, request.lead));
44924
+ if (finished.kind === "unfit") return finished;
44925
+ const { held } = fills.painting;
44926
+ return {
44927
+ kind: "painted",
44928
+ colors: finished.colors,
44929
+ mode: finished.mode,
44930
+ held,
44931
+ cost: held.reduce((sum, entry) => sum + entry.deltaE * (isGround(entry.role) ? GROUND_MOVE_WEIGHT : 1), 0)
44932
+ };
44933
+ }
44934
+
44935
+ //#endregion
44936
+ //#region ../theme-core/src/site-styles/ownColours/ownColourPalettes.ts
44937
+ /** Amber in OKLCH; warmth is closeness to it. */
44938
+ const WARM_CENTRE = 60;
44939
+ const warmth = (h) => hueGap(h, WARM_CENTRE);
44940
+ /** Rose round to gold (OKLCH 340° to 110°). */
44941
+ const isWarm = (h) => h >= 340 || h <= 110;
44942
+ /**
44943
+ * Hues that look muddy or sweet as a contrast (olive and forest greens,
44944
+ * orchids and magentas) move to their warmer neighbour: plum's complement
44945
+ * becomes gold, sage's becomes rose.
44946
+ */
44947
+ function outOfMud(h) {
44948
+ if (h > 110 && h < 165) return 105;
44949
+ if (h > 295 && h < 350) return 355;
44950
+ return h;
44951
+ }
44952
+ /** Steps round the wheel, 15° apart, from straight across inwards. */
44953
+ const steps = [180, ...[
44954
+ 165,
44955
+ 150,
44956
+ 135,
44957
+ 120,
44958
+ 105,
44959
+ 90,
44960
+ 75,
44961
+ 60,
44962
+ 45,
44963
+ 30
44964
+ ].flatMap((o) => [o, -o])];
44965
+ const warmChoices = [
44966
+ "warm-complement",
44967
+ "split-warm",
44968
+ "triadic-warm",
44969
+ "warm-other"
44970
+ ];
44971
+ function preferredHues(choice, lead) {
44972
+ const byWarmth = (offsets) => offsets.map((o) => wrapHue(lead + o)).sort((a, b) => warmth(a) - warmth(b));
44973
+ const warmest = () => byWarmth(steps).filter(isWarm);
44974
+ switch (choice) {
44975
+ case "complement": return byWarmth([
44976
+ 165,
44977
+ 180,
44978
+ 195
44979
+ ]);
44980
+ case "warm-complement": {
44981
+ const across = byWarmth([
44982
+ 165,
44983
+ 180,
44984
+ 195
44985
+ ])[0] ?? wrapHue(lead + 180);
44986
+ return isWarm(across) ? [across, ...warmest()] : warmest();
44987
+ }
44988
+ case "split-warm": return [...byWarmth([150, -150]).filter(isWarm), ...warmest()];
44989
+ case "triadic-warm": return [...byWarmth([120, -120]).filter(isWarm), ...warmest()];
44990
+ case "warm-other": return warmest();
44991
+ }
44992
+ }
44993
+ /**
44994
+ * The hues a rule tries, in tiers, best first. Warm rules keep 45° from a
44995
+ * warm brand's own hue, and only when nothing warm is left (terracotta,
44996
+ * mustard) turn to the cool complement (deep teal, ink blue). Every hue is
44997
+ * kept out of the muddy zones and 40° clear of the other rule's colour. The
44998
+ * last tier is any step round the wheel, for when nothing earlier looks
44999
+ * different enough from the palettes before it.
45000
+ */
45001
+ function candidateHues(rule$1, lead, avoid) {
45002
+ const warmRule = warmChoices.includes(rule$1.hue);
45003
+ const leadGap = warmRule ? 45 : 30;
45004
+ const clear = (h) => hueGap(h, lead) >= leadGap && (avoid === null || hueGap(h, avoid) >= 40);
45005
+ const unique = (hues) => hues.filter((h, i) => hues.findIndex((other) => Math.abs(other - h) < 1) === i);
45006
+ const own = unique(preferredHues(rule$1.hue, lead).map(outOfMud).filter(clear));
45007
+ const cool = own.length > 0 ? [] : unique(preferredHues("complement", lead).map(outOfMud).filter(clear));
45008
+ const anyStep = unique(steps.map((o) => outOfMud(wrapHue(lead + o))).filter((h) => hueGap(h, lead) >= 30));
45009
+ return [
45010
+ own,
45011
+ cool,
45012
+ anyStep
45013
+ ];
45014
+ }
45015
+ /**
45016
+ * The rule's second colour at a hue, before it is fitted to its role. Deep
45017
+ * yellows would read as olive and brown, so a deep rule paints amber and
45018
+ * ochre a little lighter, turned toward orange.
45019
+ */
45020
+ function madeColour(rule$1, lead, hue) {
45021
+ const s = lchOf(lead);
45022
+ const yellow = hue >= 55 && hue <= 105;
45023
+ const l = rule$1.l < .5 && yellow ? rule$1.l + .12 : rule$1.l;
45024
+ const turn = hue >= 55 && hue <= 115 && l < .85 ? Math.min(35, (.85 - l) * 70) : 0;
45025
+ return fromLch(l, Math.min(rule$1.c[1], Math.max(rule$1.c[0], s.c)), hue - turn);
45026
+ }
45027
+ /** [hue upper bound, dark, mid, light] in OKLCH hue. */
45028
+ const names = [
45029
+ [
45030
+ 20,
45031
+ "burgundy",
45032
+ "rose",
45033
+ "pink"
45034
+ ],
45035
+ [
45036
+ 38,
45037
+ "oxblood",
45038
+ "coral",
45039
+ "pale coral"
45040
+ ],
45041
+ [
45042
+ 60,
45043
+ "rust",
45044
+ "orange",
45045
+ "peach"
45046
+ ],
45047
+ [
45048
+ 78,
45049
+ "bronze",
45050
+ "amber",
45051
+ "apricot"
45052
+ ],
45053
+ [
45054
+ 100,
45055
+ "ochre",
45056
+ "gold",
45057
+ "pale gold"
45058
+ ],
45059
+ [
45060
+ 122,
45061
+ "olive",
45062
+ "yellow",
45063
+ "pale yellow"
45064
+ ],
45065
+ [
45066
+ 160,
45067
+ "forest green",
45068
+ "green",
45069
+ "pale green"
45070
+ ],
45071
+ [
45072
+ 182,
45073
+ "deep jade",
45074
+ "jade",
45075
+ "mint"
45076
+ ],
45077
+ [
45078
+ 228,
45079
+ "deep teal",
45080
+ "teal",
45081
+ "aqua"
45082
+ ],
45083
+ [
45084
+ 258,
45085
+ "deep blue",
45086
+ "blue",
45087
+ "pale blue"
45088
+ ],
45089
+ [
45090
+ 288,
45091
+ "navy",
45092
+ "blue",
45093
+ "periwinkle"
45094
+ ],
45095
+ [
45096
+ 318,
45097
+ "indigo",
45098
+ "violet",
45099
+ "lavender"
45100
+ ],
45101
+ [
45102
+ 348,
45103
+ "plum",
45104
+ "magenta",
45105
+ "orchid"
45106
+ ],
45107
+ [
45108
+ 361,
45109
+ "burgundy",
45110
+ "rose",
45111
+ "pink"
45112
+ ]
45113
+ ];
45114
+ /** A plain name for a colour the look made, as an owner would say it. */
45115
+ function colourName(hex$1) {
45116
+ const { l, c, h } = lchOf(hex$1);
45117
+ if (c < .025) return l > .9 ? "off-white" : l < .3 ? "near-black" : l > .7 ? "pale grey" : "grey";
45118
+ const [, dark, mid, light] = names.find(([upTo]) => h < upTo) ?? names[names.length - 1] ?? [
45119
+ 361,
45120
+ "grey",
45121
+ "grey",
45122
+ "grey"
45123
+ ];
45124
+ if (l < .42) return dark;
45125
+ if (l > .8) return light;
45126
+ return c < .06 ? `muted ${mid}` : mid;
45127
+ }
45128
+ const sentence = (text) => `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
45129
+ function shadeWords(asked, drawn) {
45130
+ const dl = lchOf(drawn).l - lchOf(asked).l;
45131
+ return dl < -.25 ? "a deep shade of" : dl < 0 ? "a deeper shade of" : dl > .25 ? "a pale shade of" : "a lighter shade of";
45132
+ }
45133
+ function ownerWords(entry, whose) {
45134
+ return entry.hex === entry.asked ? `${whose} on ${entry.slot.name}` : `${shadeWords(entry.asked, entry.hex)} ${whose} on ${entry.slot.name}`;
45135
+ }
45136
+ const isGiltEnd = (spec, role) => spec.roles[role].giltEnd === true;
45137
+ /** Two owner colours this close count as one. */
45138
+ const SAME_COLOUR = .05;
45139
+ /** Palettes for the same look keep at least this OKLab ΔE apart on one of their coloured parts. */
45140
+ const OWN_PALETTE_MIN_DIFFERENCE = .06;
45141
+ const colouredRoles = [
45142
+ "primary",
45143
+ "secondary",
45144
+ "accent",
45145
+ "muted"
45146
+ ];
45147
+ /** How far apart two palettes are on the parts that carry colour. */
45148
+ function paletteDifference(a, b) {
45149
+ return Math.max(...colouredRoles.map((role) => colourDistance(a[role], b[role])));
45150
+ }
45151
+ function slotFor(spec, role) {
45152
+ return spec.slots.find((entry) => entry.roles.includes(role)) ?? {
45153
+ roles: [role],
45154
+ name: spec.roles[role].name
45155
+ };
45156
+ }
45157
+ function placementsOf(painted) {
45158
+ const owned = painted.held.flatMap((entry) => entry.source === "made" ? [] : [{
45159
+ colour: entry.source,
45160
+ role: entry.role,
45161
+ place: entry.slot.name,
45162
+ drawn: entry.hex === entry.asked ? { kind: "exact" } : {
45163
+ kind: "nearest-shade",
45164
+ hex: entry.hex,
45165
+ deltaE: entry.deltaE
45166
+ }
45167
+ }]);
45168
+ const [first, ...rest] = owned;
45169
+ return first ? [first, ...rest] : null;
45170
+ }
45171
+ function offered(painted, palette, admit) {
45172
+ return admit(painted) ? {
45173
+ kind: "offered",
45174
+ painted,
45175
+ palette
45176
+ } : {
45177
+ kind: "unfit",
45178
+ reason: { kind: "buttons-replaced" }
45179
+ };
45180
+ }
45181
+ /** The cheapest offered candidate, or the first reason none was. */
45182
+ function bestOffered(candidates) {
45183
+ const offers = candidates.flatMap((candidate$1) => candidate$1.kind === "offered" ? [candidate$1] : []);
45184
+ const best = offers.reduce((chosen, next) => !chosen || next.painted.cost < chosen.painted.cost ? next : chosen, undefined);
45185
+ return best ?? candidates.find((candidate$1) => candidate$1.kind === "unfit") ?? {
45186
+ kind: "unfit",
45187
+ reason: { kind: "buttons-replaced" }
45188
+ };
45189
+ }
45190
+ function singleCandidate(spec, first, slot, admit) {
45191
+ const painted = paintPalette(spec, {
45192
+ lead: first,
45193
+ holds: [{
45194
+ source: "first",
45195
+ hex: first,
45196
+ slot
45197
+ }]
45198
+ });
45199
+ if (painted.kind === "unfit") return painted;
45200
+ const lead = painted.held[0];
45201
+ const placements = placementsOf(painted);
45202
+ if (!lead || !placements) return {
45203
+ kind: "unfit",
45204
+ reason: {
45205
+ kind: "role-unmet",
45206
+ role: slot.roles[0]
45207
+ }
45208
+ };
45209
+ const mentions = spec.order.filter((role) => role !== lead.role && spec.roles[role].mention && !(isGiltEnd(spec, lead.role) && isGiltEnd(spec, role))).map((role) => `${spec.roles[role].name} in ${shadeWords(first, painted.colors[role])} it`);
45210
+ const describe = sentence(`${ownerWords(lead, "your colour")}${mentions.length ? `, with ${mentions.join(" and ")}` : ""}`);
45211
+ return offered(painted, {
45212
+ kind: "single",
45213
+ colors: painted.colors,
45214
+ mode: painted.mode,
45215
+ describe,
45216
+ placements,
45217
+ made: null
45218
+ }, admit);
45219
+ }
45220
+ /** The owner's colour alone, in the place it suits best of those the look keeps its buttons in. */
45221
+ function singlePalette(spec, first, admit) {
45222
+ return bestOffered(spec.slots.map((slot) => singleCandidate(spec, first, slot, admit)));
45223
+ }
45224
+ /** Where the second colour shows, in words: the owner's is "your second colour", a made one is named. */
45225
+ function secondWords(spec, rule$1, painted, placed, whose) {
45226
+ if (isGiltEnd(spec, placed.role)) {
45227
+ return `gilt buttons running from ${colourName(painted.colors.accent)} into ${colourName(painted.colors.primary)}`;
45228
+ }
45229
+ const name = placed.source !== "made" ? placed.hex === placed.asked ? whose : `${shadeWords(placed.asked, placed.hex)} ${whose}` : colourName(placed.hex);
45230
+ return `${name} on ${placed.slot.name}${rule$1.alsoTints ? ` and ${rule$1.alsoTints.name}` : ""}`;
45231
+ }
45232
+ function withSecond(spec, rule$1, first, second, admit) {
45233
+ const holds = [{
45234
+ source: "first",
45235
+ hex: first,
45236
+ slot: slotFor(spec, rule$1.lead)
45237
+ }, {
45238
+ source: second.source,
45239
+ hex: second.hex,
45240
+ slot: slotFor(spec, rule$1.second)
45241
+ }];
45242
+ const hueSources = Object.fromEntries((rule$1.alsoTints?.roles ?? []).map((role) => [role, second.hex]));
45243
+ const painted = paintPalette(spec, {
45244
+ lead: first,
45245
+ holds,
45246
+ hueSources
45247
+ });
45248
+ if (painted.kind === "unfit") return painted;
45249
+ const lead = painted.held.find((entry) => entry.source === "first");
45250
+ const placedSecond = painted.held.find((entry) => entry.source === second.source);
45251
+ const placements = placementsOf(painted);
45252
+ if (!lead || !placedSecond || !placements) return {
45253
+ kind: "unfit",
45254
+ reason: {
45255
+ kind: "role-unmet",
45256
+ role: rule$1.second
45257
+ }
45258
+ };
45259
+ const owner = second.source === "second";
45260
+ const describe = sentence(`${ownerWords(lead, owner ? "your first colour" : "your colour")}, with ${secondWords(spec, rule$1, painted, placedSecond, "your second colour")}`);
45261
+ const body = {
45262
+ colors: painted.colors,
45263
+ mode: painted.mode,
45264
+ describe,
45265
+ placements
45266
+ };
45267
+ return offered(painted, owner ? {
45268
+ ...body,
45269
+ kind: "owner-contrast",
45270
+ made: null
45271
+ } : {
45272
+ ...body,
45273
+ kind: "made-contrast",
45274
+ made: {
45275
+ rule: rule$1.label,
45276
+ hex: placedSecond.hex,
45277
+ role: placedSecond.role,
45278
+ place: placedSecond.slot.name
45279
+ }
45280
+ }, admit);
45281
+ }
45282
+ const differenceFrom = (before, palette) => Math.min(...before.map((other) => paletteDifference(other.colors, palette.colors)));
45283
+ /**
45284
+ * The rule's contrast: its best hue whose palette the look can offer and
45285
+ * that looks visibly different from the palettes before it, otherwise the
45286
+ * offered one that differs most. Unfit only when no hue gives an offered palette.
45287
+ */
45288
+ function madeContrast(spec, rule$1, first, avoidHue, before, admit) {
45289
+ const offers = [];
45290
+ const failures = [];
45291
+ for (const hue of candidateHues(rule$1, lchOf(first).h, avoidHue).flat()) {
45292
+ const candidate$1 = withSecond(spec, rule$1, first, {
45293
+ source: "made",
45294
+ hex: madeColour(rule$1, first, hue)
45295
+ }, admit);
45296
+ if (candidate$1.kind === "unfit") {
45297
+ failures.push(candidate$1.reason);
45298
+ continue;
45299
+ }
45300
+ const offer = {
45301
+ palette: candidate$1.palette,
45302
+ hue: lchOf(candidate$1.palette.made?.hex ?? first).h,
45303
+ difference: differenceFrom(before, candidate$1.palette)
45304
+ };
45305
+ if (offer.difference >= OWN_PALETTE_MIN_DIFFERENCE) return offer;
45306
+ offers.push(offer);
45307
+ }
45308
+ const best = offers.reduce((chosen, next) => !chosen || next.difference > chosen.difference ? next : chosen, undefined);
45309
+ if (best) return best;
45310
+ return {
45311
+ kind: "unfit",
45312
+ reason: failures[0] ?? { kind: "buttons-replaced" }
45313
+ };
45314
+ }
45315
+ /**
45316
+ * The owner's own second colour as palette 2, in whichever of the look's two
45317
+ * contrast places needs it moved least among those that look visibly
45318
+ * different from the single-colour palette. Null when neither does.
45319
+ */
45320
+ function ownerContrastFor(spec, first, second, single, admit) {
45321
+ const distinct$1 = spec.contrast.map((rule$1) => withSecond(spec, rule$1, first, {
45322
+ source: "second",
45323
+ hex: second
45324
+ }, admit)).filter((candidate$1) => candidate$1.kind === "offered" && paletteDifference(candidate$1.palette.colors, single.colors) >= OWN_PALETTE_MIN_DIFFERENCE);
45325
+ const chosen = bestOffered(distinct$1);
45326
+ return chosen.kind === "offered" ? chosen.palette : null;
45327
+ }
45328
+ const isMade = (result) => "palette" in result;
45329
+ /**
45330
+ * Three palettes for a booking look from one or two owner colours (fewer only
45331
+ * when the first colour has no hue to make a contrast from). `admit` is the
45332
+ * surface owner's decision on whether the look keeps its own buttons; see
45333
+ * `bookingLookButtonAdmission`. Deterministic, so a stored palette can be
45334
+ * matched back to the palette it came from.
45335
+ */
45336
+ function ownColourPalettes(spec, colours, admit) {
45337
+ const [first, maybeSecond] = colours;
45338
+ const tooAlike = maybeSecond !== undefined && colourDistance(first, maybeSecond) < SAME_COLOUR;
45339
+ const second = tooAlike ? undefined : maybeSecond;
45340
+ const singleResult = singlePalette(spec, first, admit);
45341
+ if (singleResult.kind === "unfit") return singleResult;
45342
+ const single = singleResult.palette;
45343
+ const ownerContrast = second ? ownerContrastFor(spec, first, second, single, admit) : null;
45344
+ if (lchOf(first).c < ACHROMATIC_CHROMA) {
45345
+ return {
45346
+ kind: "fewer",
45347
+ palettes: ownerContrast ? [single, ownerContrast] : [single],
45348
+ reason: "no-hue"
45349
+ };
45350
+ }
45351
+ if (second && ownerContrast) return withOwnerContrast(spec, first, second, single, ownerContrast, admit);
45352
+ return withMadeContrasts(spec, first, single, maybeSecond === undefined ? "none" : "too-alike", admit);
45353
+ }
45354
+ function withOwnerContrast(spec, first, second, single, ownerContrast, admit) {
45355
+ const made = madeContrast(spec, spec.contrast[0], first, lchOf(second).h, [single, ownerContrast], admit);
45356
+ if (!isMade(made)) return made;
45357
+ return {
45358
+ kind: "three",
45359
+ palettes: [
45360
+ single,
45361
+ ownerContrast,
45362
+ made.palette
45363
+ ],
45364
+ secondColour: "used"
45365
+ };
45366
+ }
45367
+ /** Both of the look's made contrasts. A second colour given but not used was too close to the first, or to the single-colour palette. */
45368
+ function withMadeContrasts(spec, first, single, secondColour, admit) {
45369
+ const [ruleA, ruleB] = spec.contrast;
45370
+ const a = madeContrast(spec, ruleA, first, null, [single], admit);
45371
+ if (!isMade(a)) return a;
45372
+ const b = madeContrast(spec, ruleB, first, a.hue, [single, a.palette], admit);
45373
+ if (!isMade(b)) return b;
45374
+ return {
45375
+ kind: "three",
45376
+ palettes: [
45377
+ single,
45378
+ a.palette,
45379
+ b.palette
45380
+ ],
45381
+ secondColour
45382
+ };
45383
+ }
45384
+
45385
+ //#endregion
45386
+ //#region ../theme-core/src/site-styles/ownColours/bookingLookButtonAdmission.ts
45387
+ /**
45388
+ * Where each look's most prominent buttons sit on a booking page, per the
45389
+ * surface table (apps/frontend hostedBookingSurfaceTable.test.ts): the
45390
+ * opening, and the offerings section's cards or section.
45391
+ */
45392
+ const bookingLookProminentButtonSurfaces = {
45393
+ "site-style:fulcrum": ["base", "sheet"],
45394
+ "site-style:folio": ["base", "sheet"],
45395
+ "site-style:measure": ["base", "base"],
45396
+ "site-style:grounded-practice": ["band", "base"],
45397
+ "site-style:lantern": ["base", "band"],
45398
+ "site-style:poster": ["brand", "sheet"]
45399
+ };
45400
+ /** Grounded Practice's legacy sections carry only resolved styles: the look's button fills from a palette token. */
45401
+ function legacyKeepsButton(theme, surface$1) {
45402
+ const color = surface$1 === "band" ? "muted" : "background";
45403
+ const resolved = resolveSurface({
45404
+ kind: "section",
45405
+ background: {
45406
+ type: "color",
45407
+ color
45408
+ }
45409
+ }, opaqueSurfaceContextFromTheme(theme));
45410
+ const styles = resolved.buttonStyles;
45411
+ return `${styles["--rb-action-primary-base-background-color"] ?? ""}${styles["--rb-action-primary-base-background-image"] ?? ""}`.includes("var(--tb-");
45412
+ }
45413
+ function keptPrimary(primary) {
45414
+ return primary?.kind === "preferred" || primary?.kind === "repaired" && primary.candidate === "contextual-hover";
45415
+ }
45416
+ /**
45417
+ * Whether the surface owner keeps the look's own primary button on this
45418
+ * surface, in every state, rather than replacing it with an outline.
45419
+ */
45420
+ function keepsLookButton(style, theme, surface$1) {
45421
+ if (style.looks.kind !== "offers") return legacyKeepsButton(theme, surface$1);
45422
+ const declared = style.looks.surfaces[surface$1];
45423
+ if (declared?.kind !== "token") return false;
45424
+ const resolved = resolveSurface({
45425
+ kind: "token",
45426
+ token: declared.token,
45427
+ ...declared.buttonContext ? { buttonContext: declared.buttonContext } : {},
45428
+ ...declared.foreground ? { foreground: declared.foreground } : {}
45429
+ }, opaqueSurfaceContextFromTheme(theme));
45430
+ return keptPrimary(resolved.buttonContext.roles?.primary);
45431
+ }
45432
+ /** The look's theme with this palette, compiled as a booking page would be. */
45433
+ function bookingLookThemeFor(style, palette) {
45434
+ return compileReplacementSiteThemeFromSelections({
45435
+ siteStyle: style,
45436
+ palette: {
45437
+ colors: Object.entries(palette.colors).map(([name, hex$1]) => ({
45438
+ name,
45439
+ hex: hex$1
45440
+ })),
45441
+ meta: { mode: palette.mode }
45442
+ },
45443
+ compilationPlan: replaceSiteStyleCompilationPlan({
45444
+ headerSelection: defaultHeaderSelectionForSiteStyle(style),
45445
+ buttonStyleSelection: null
45446
+ })
45447
+ }).theme;
45448
+ }
45449
+ /** The surface owner's decision, for one booking look, on whether a palette keeps the look's buttons where they matter most. */
45450
+ function bookingLookButtonAdmission(lookId) {
45451
+ const style = getCuratedSiteStyle(asSiteStyleId(lookId));
45452
+ if (!style) throw new Error(`Missing booking look ${lookId}`);
45453
+ return (palette) => {
45454
+ const theme = bookingLookThemeFor(style, palette);
45455
+ return bookingLookProminentButtonSurfaces[lookId].every((surface$1) => keepsLookButton(style, theme, surface$1));
45456
+ };
45457
+ }
45458
+ /** A booking look's own-colour palettes, admitted through the surface owner. */
45459
+ function bookingLookOwnColourPalettes(lookId, colours) {
45460
+ return ownColourPalettes(bookingLookColourSpecs[lookId], colours, bookingLookButtonAdmission(lookId));
45461
+ }
45462
+
43436
45463
  //#endregion
43437
45464
  //#region ../site-commands/src/application.ts
43438
45465
  const commandEffectKinds = [
@@ -44095,174 +46122,6 @@ function generateCssVars(palette) {
44095
46122
  return cssVars;
44096
46123
  }
44097
46124
 
44098
- //#endregion
44099
- //#region ../theme-core/src/palette/family.ts
44100
- /**
44101
- * Only `colourFamily` makes one (the class is not exported), so a caller can't
44102
- * swap in its own members and still hand the solver a valid family. That
44103
- * matters because `unpaintable` relies on the members running from black to
44104
- * white.
44105
- */
44106
- var Family = class {
44107
- #members;
44108
- constructor(seed, members) {
44109
- this.seed = seed;
44110
- this.#members = Object.freeze(members);
44111
- }
44112
- /** Unique by hex, ascending luminance. Includes the exact seed, black and white. */
44113
- get members() {
44114
- return this.#members;
44115
- }
44116
- };
44117
- /** Lightness steps between black and white. */
44118
- const FAMILY_STEPS = 512;
44119
- const GAMUT_BISECTION_STEPS = 18;
44120
- /** The highest chroma at or below `c` that stays inside sRGB at this lightness and hue. */
44121
- function gamutChroma(l, c, h) {
44122
- if (inLinearSrgbGamut(oklchToLinearSrgb(l, c, h))) return c;
44123
- return Array.from({ length: GAMUT_BISECTION_STEPS }).reduce(({ lo, hi }) => {
44124
- const mid = (lo + hi) / 2;
44125
- return inLinearSrgbGamut(oklchToLinearSrgb(l, mid, h)) ? {
44126
- lo: mid,
44127
- hi
44128
- } : {
44129
- lo,
44130
- hi: mid
44131
- };
44132
- }, {
44133
- lo: 0,
44134
- hi: c
44135
- }).lo;
44136
- }
44137
- function oklabDistance(a, b) {
44138
- return Math.hypot(a.L - b.L, a.a - b.a, a.b - b.b);
44139
- }
44140
- function colourFamily(seedHex) {
44141
- if (!/^#[0-9a-f]{6}$/i.test(seedHex)) throw new Error(`Expected an opaque #rrggbb colour: ${seedHex}`);
44142
- const features = featuresFromHex(seedHex.toLowerCase());
44143
- const seed = member(seedHex.toLowerCase(), {
44144
- l: features.okL,
44145
- c: features.okC,
44146
- h: features.okH
44147
- });
44148
- const seedTaper = Math.max(.001, Math.sin(Math.PI * seed.sample.l));
44149
- const samples = Array.from({ length: FAMILY_STEPS + 1 }, (_, i) => {
44150
- const l = i / FAMILY_STEPS;
44151
- const c = gamutChroma(l, seed.sample.c * Math.min(1, Math.sin(Math.PI * l) / seedTaper), seed.sample.h);
44152
- return sampleMember({
44153
- l,
44154
- c,
44155
- h: seed.sample.h
44156
- });
44157
- });
44158
- const unique = new Map([seed, ...samples].map((candidate$1) => [candidate$1.hex, candidate$1]).reverse());
44159
- const members = [...unique.values()].sort((a, b) => a.y - b.y || (a.hex < b.hex ? -1 : a.hex > b.hex ? 1 : 0));
44160
- return new Family(seed, members);
44161
- }
44162
- function sampleMember(sample) {
44163
- const { r, g, b } = oklchToLinearSrgb(sample.l, sample.c, sample.h);
44164
- return member(rgbToHex(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)), sample);
44165
- }
44166
- function member(hex, sample) {
44167
- const { r, g, b } = hexToRgb(hex);
44168
- return {
44169
- hex,
44170
- y: hexLuminance(hex),
44171
- oklab: srgbToOklab(r, g, b),
44172
- sample
44173
- };
44174
- }
44175
-
44176
- //#endregion
44177
- //#region ../theme-core/src/palette/utils/inkSolver.ts
44178
- /**
44179
- * How far (OKLab ΔE) a brand colour drawn as ink may be nudged along its own
44180
- * family before it stops reading as that colour. Past this the caller uses
44181
- * neutral or palette text instead (Will, 2026-10-14).
44182
- */
44183
- const BRAND_INK_MAX_DELTA_E = .08;
44184
- /**
44185
- * The seed itself when it clears `paint`, otherwise the family member nearest
44186
- * the seed (OKLab ΔE) that clears. On a light ground that is darker, on a dark
44187
- * one lighter. A passing member further than `maxDistance` from the seed is
44188
- * refused as `too-far`, which is distinct from `unpaintable`: the family runs
44189
- * from black to white, so if no member clears, neither does black or white.
44190
- */
44191
- function inkFrom(family, paint, ratio$1, maxDistance = BRAND_INK_MAX_DELTA_E) {
44192
- assertContrastRatio(ratio$1);
44193
- if (!Number.isFinite(maxDistance) || maxDistance < 0) {
44194
- throw new RangeError(`Maximum ink distance must be finite and not negative: ${maxDistance}`);
44195
- }
44196
- const seedRatio = worstContrast(family.seed.y, paint);
44197
- if (seedRatio >= ratio$1) return {
44198
- kind: "ink",
44199
- hex: family.seed.hex,
44200
- ratio: seedRatio,
44201
- source: "exact"
44202
- };
44203
- const nearest = family.members.reduce((best, member$1) => {
44204
- const achieved = worstContrast(member$1.y, paint);
44205
- if (achieved < ratio$1) return best;
44206
- const distance$1 = oklabDistance(member$1.oklab, family.seed.oklab);
44207
- return best === null || distance$1 < best.distance ? {
44208
- hex: member$1.hex,
44209
- distance: distance$1,
44210
- ratio: achieved
44211
- } : best;
44212
- }, null);
44213
- if (nearest === null) return {
44214
- kind: "unpaintable",
44215
- interval: paint,
44216
- ratio: ratio$1
44217
- };
44218
- if (nearest.distance > maxDistance) {
44219
- return {
44220
- kind: "too-far",
44221
- nearest: {
44222
- hex: nearest.hex,
44223
- distance: nearest.distance
44224
- },
44225
- maxDistance
44226
- };
44227
- }
44228
- return {
44229
- kind: "ink",
44230
- hex: nearest.hex,
44231
- ratio: nearest.ratio,
44232
- source: "family"
44233
- };
44234
- }
44235
- /**
44236
- * Neutral text for a paint. The style's preferred neutral wins when one of its
44237
- * pair clears (the higher-contrast one, dark on a tie); otherwise literal black
44238
- * or white by `maxContrastBlackOrWhite`; otherwise `unpaintable`.
44239
- */
44240
- function neutralInk(paint, ratio$1, preferred) {
44241
- assertContrastRatio(ratio$1);
44242
- if (preferred) {
44243
- const dark = worstContrast(hexLuminance(preferred.dark), paint);
44244
- const light = worstContrast(hexLuminance(preferred.light), paint);
44245
- const [hex, achieved] = dark >= light ? [preferred.dark, dark] : [preferred.light, light];
44246
- if (achieved >= ratio$1) return {
44247
- kind: "ink",
44248
- hex,
44249
- ratio: achieved,
44250
- source: "neutral"
44251
- };
44252
- }
44253
- const literal = maxContrastBlackOrWhite(paint);
44254
- return literal.ratio >= ratio$1 ? {
44255
- kind: "ink",
44256
- hex: literal.ink === "black" ? "#000000" : "#ffffff",
44257
- ratio: literal.ratio,
44258
- source: "neutral"
44259
- } : {
44260
- kind: "unpaintable",
44261
- interval: paint,
44262
- ratio: ratio$1
44263
- };
44264
- }
44265
-
44266
46125
  //#endregion
44267
46126
  //#region ../theme-core/src/palette/utils/backgroundGeneration.ts
44268
46127
  /**
@@ -44757,7 +46616,7 @@ const allRecipes = [
44757
46616
  //#region ../theme-core/src/palette/utils/legs.ts
44758
46617
  const clamp01$1 = (x) => Math.max(0, Math.min(1, x));
44759
46618
  function nudgeToneForContrast(color, againstHex, target, prefer) {
44760
- const steps = [
46619
+ const steps$1 = [
44761
46620
  0,
44762
46621
  .02,
44763
46622
  .04,
@@ -44772,18 +46631,18 @@ function nudgeToneForContrast(color, againstHex, target, prefer) {
44772
46631
  L: color.L
44773
46632
  };
44774
46633
  let bestMiss = Infinity;
44775
- for (const dir of dirs) for (const s of steps) {
46634
+ for (const dir of dirs) for (const s of steps$1) {
44776
46635
  const L = clamp01$1(color.L + dir * s);
44777
- const hex = oklchToHexGamut(L, color.C, color.h);
44778
- const cr = contrastRatio(hex, againstHex);
46636
+ const hex$1 = oklchToHexGamut(L, color.C, color.h);
46637
+ const cr = contrastRatio(hex$1, againstHex);
44779
46638
  const miss = Math.max(0, target - cr);
44780
46639
  if (miss === 0) return {
44781
- hex,
46640
+ hex: hex$1,
44782
46641
  L
44783
46642
  };
44784
46643
  if (miss < bestMiss) {
44785
46644
  best = {
44786
- hex,
46645
+ hex: hex$1,
44787
46646
  L
44788
46647
  };
44789
46648
  bestMiss = miss;
@@ -44791,8 +46650,8 @@ function nudgeToneForContrast(color, againstHex, target, prefer) {
44791
46650
  }
44792
46651
  return best;
44793
46652
  }
44794
- function oklchFromHex(hex) {
44795
- const f = featuresFromHex(hex);
46653
+ function oklchFromHex(hex$1) {
46654
+ const f = featuresFromHex(hex$1);
44796
46655
  return {
44797
46656
  L: f.okL,
44798
46657
  C: f.okC,
@@ -44816,16 +46675,16 @@ function ensureHueLeg(primaryHex, legs, targetL, targetCmid, backgroundHex, pref
44816
46675
  const L = targetL;
44817
46676
  const C = targetCmid;
44818
46677
  const h = wrapDeg(p.h + offset);
44819
- let hex = oklchToHexGamut(L, C, h);
44820
- hex = nudgeToneForContrast({
46678
+ let hex$1 = oklchToHexGamut(L, C, h);
46679
+ hex$1 = nudgeToneForContrast({
44821
46680
  L,
44822
46681
  C,
44823
46682
  h
44824
46683
  }, backgroundHex, 3, prefer).hex;
44825
- const a = oklchFromHex(hex);
44826
- const score = .7 * dOKLCH(a, p) + .3 * (contrastRatio(hex, backgroundHex) / 7);
46684
+ const a = oklchFromHex(hex$1);
46685
+ const score = .7 * dOKLCH(a, p) + .3 * (contrastRatio(hex$1, backgroundHex) / 7);
44827
46686
  return {
44828
- hex,
46687
+ hex: hex$1,
44829
46688
  h,
44830
46689
  score
44831
46690
  };
@@ -44897,12 +46756,12 @@ function generateSupportRole(targetHue, theme, labels, backgroundHex, avoidHexes
44897
46756
  }
44898
46757
  const preferDir = theme === "dark" ? "lighter" : "darker";
44899
46758
  const _hex0 = oklchToHexGamut(Ltarget, Cmid, h);
44900
- const { hex } = nudgeToneForContrast({
46759
+ const { hex: hex$1 } = nudgeToneForContrast({
44901
46760
  L: Ltarget,
44902
46761
  C: Cmid,
44903
46762
  h
44904
46763
  }, backgroundHex, 3, preferDir);
44905
- return hex;
46764
+ return hex$1;
44906
46765
  }
44907
46766
 
44908
46767
  //#endregion
@@ -44930,18 +46789,18 @@ function legibleTextOn(bgHex, _prefer, min = 4.5, opts) {
44930
46789
  const startL = bgIsLight ? .14 : .97;
44931
46790
  const preferDir = bgIsLight ? "darker" : "lighter";
44932
46791
  const target = Math.max(3, min + cushion);
44933
- const { hex } = nudgeToneForContrast({
46792
+ const { hex: hex$1 } = nudgeToneForContrast({
44934
46793
  L: startL,
44935
46794
  C,
44936
46795
  h: neutralH
44937
46796
  }, bgHex, target, preferDir);
44938
- const test = contrastRatio(hex, bgHex);
46797
+ const test = contrastRatio(hex$1, bgHex);
44939
46798
  if (test >= min) {
44940
46799
  const Lclip = (L) => {
44941
46800
  if (bgIsLight) return Math.max(.1, Math.min(.3, L));
44942
46801
  return Math.max(.85, Math.min(.98, L));
44943
46802
  };
44944
- const f = featuresFromHex(hex);
46803
+ const f = featuresFromHex(hex$1);
44945
46804
  return oklchToHexGamut(Lclip(f.okL), C, neutralH);
44946
46805
  }
44947
46806
  return bgIsLight ? "#000000" : "#FFFFFF";
@@ -45287,16 +47146,16 @@ const RULES = [
45287
47146
  }
45288
47147
  }
45289
47148
  ];
45290
- function classifyColor(hex, rules = RULES) {
45291
- const f = featuresFromHex(hex);
47149
+ function classifyColor(hex$1, rules = RULES) {
47150
+ const f = featuresFromHex(hex$1);
45292
47151
  const labels = rules.map((r) => ({
45293
47152
  label: r.label,
45294
47153
  score: clamp01(r.score(f))
45295
47154
  })).filter((x) => x.score > 0).sort((a, b) => b.score - a.score);
45296
47155
  return labels;
45297
47156
  }
45298
- function topLabel(hex, rules = RULES) {
45299
- const list = classifyColor(hex, rules);
47157
+ function topLabel(hex$1, rules = RULES) {
47158
+ const list = classifyColor(hex$1, rules);
45300
47159
  return list[0] ?? null;
45301
47160
  }
45302
47161
 
@@ -45382,8 +47241,8 @@ function suggestRecipes(labels, fallback = [
45382
47241
  const ranked = Array.from(scores.entries()).sort((a, b) => b[1] - a[1]).map(([id$2]) => id$2);
45383
47242
  return ranked.length ? ranked : fallback;
45384
47243
  }
45385
- function generateAllPalettes(hex, _labels) {
45386
- return allRecipes.map((recipe) => buildPaletteRespectingSeed(recipe, featuresFromHex(hex), hex));
47244
+ function generateAllPalettes(hex$1, _labels) {
47245
+ return allRecipes.map((recipe) => buildPaletteRespectingSeed(recipe, featuresFromHex(hex$1), hex$1));
45387
47246
  }
45388
47247
  function paletteToRecord(p) {
45389
47248
  const out = {
@@ -45400,10 +47259,10 @@ function paletteToRecord(p) {
45400
47259
  }
45401
47260
  return out;
45402
47261
  }
45403
- function ensureMinContrast(hex, bg, min = 2.5) {
45404
- let cr = contrastRatio(hex, bg);
45405
- if (cr >= min) return hex;
45406
- const f = featuresFromHex(hex);
47262
+ function ensureMinContrast(hex$1, bg, min = 2.5) {
47263
+ let cr = contrastRatio(hex$1, bg);
47264
+ if (cr >= min) return hex$1;
47265
+ const f = featuresFromHex(hex$1);
45407
47266
  const bgL = featuresFromHex(bg).okL;
45408
47267
  let L = f.okL;
45409
47268
  const dir = bgL >= .5 ? -1 : 1;
@@ -45413,10 +47272,10 @@ function ensureMinContrast(hex, bg, min = 2.5) {
45413
47272
  const ncr = contrastRatio(cand, bg);
45414
47273
  if (ncr > cr) {
45415
47274
  cr = ncr;
45416
- hex = cand;
47275
+ hex$1 = cand;
45417
47276
  }
45418
47277
  }
45419
- return hex;
47278
+ return hex$1;
45420
47279
  }
45421
47280
  function blendOKLCH(a, b, alpha = .25) {
45422
47281
  const A = featuresFromHex(a), B = featuresFromHex(b);
@@ -47038,13 +48897,13 @@ var require_jsx_runtime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
47038
48897
  //#endregion
47039
48898
  //#region ../theme-core/src/mock-themes/components/swatch.tsx
47040
48899
  var import_jsx_runtime = /* @__PURE__ */ __toESM(require_jsx_runtime());
47041
- function Swatch({ name, hex }) {
48900
+ function Swatch({ name, hex: hex$1 }) {
47042
48901
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
47043
48902
  className: "rb-rounded-xl rb-overflow-hidden rb-shadow-sm rb-border",
47044
48903
  style: { borderColor: "rgba(0, 0, 0, 0.05)" },
47045
48904
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
47046
48905
  className: "rb-h-12",
47047
- style: { backgroundColor: hex }
48906
+ style: { backgroundColor: hex$1 }
47048
48907
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
47049
48908
  className: "rb-px-3 rb-py-2 rb-text-xs rb-flex rb-items-center rb-justify-between",
47050
48909
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
@@ -47054,7 +48913,7 @@ function Swatch({ name, hex }) {
47054
48913
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
47055
48914
  className: "rb-font-mono",
47056
48915
  style: { color: "#71717a" },
47057
- children: hex
48916
+ children: hex$1
47058
48917
  })]
47059
48918
  })]
47060
48919
  });
@@ -47646,8 +49505,8 @@ const ordinaryEventDashboard = [{
47646
49505
  surface: "Dashboard",
47647
49506
  scope: "Events booked by date"
47648
49507
  }];
47649
- const meaning = (label, description) => ({
47650
- label,
49508
+ const meaning = (label$1, description) => ({
49509
+ label: label$1,
47651
49510
  description,
47652
49511
  availability: ordinaryEventDashboard
47653
49512
  });
@@ -51407,4 +53266,4 @@ function parseSdkEventRunUpsertInput(input, runIndex, options) {
51407
53266
  }
51408
53267
 
51409
53268
  //#endregion
51410
- export { createFormRef as $, deriveVideoSlotPolicy as $n, resolveTokenOrCssColorValue as $r, asFooterLookId as $t, getSiteCommandMetadata as A, deleteEnvSpecificKeys as An, makeBoundaryId as Ar, generateHeaderScrollTreatmentStyles as At, eventBookingOfferIntentSchema as B, isSiteScopedDirectImageStoragePath as Bn, surfaceDecorationColors as Br, getComponentBorderWidthPx as Bt, sortRecord as C, boxBackgroundSchema as Cn, defineFrameCapability as Cr, deriveSiteStyleBlockPolicy as Ct, mapEventCommandContent as D, collectPortableMediaReferencesFromContent as Dn, featureCardCompositionTreatmentValues as Dr, getGradientPreset as Dt, parseAndPlanCommandBatchForTypes as E, stripStaleTransitionStyleGroup as En, eyebrowTreatmentValues as Er, gradientToCss as Et, pageIdentifierForBlockCommand as F, parseAspectRatioValue as Fn, visibleHeadingDecorationFamilyValues as Fr, BREAKPOINT_MD as Ft, ianaTimezoneSchema as G, resolveManagedMediaReference as Gn, mergeStyles as Gr, themeButtonExtraClassesWithTrailingIcon as Gt, bookingPaymentTermsSchema as H, persistableMediaSchema as Hn, TokenResolver as Hr, ICON_CONFIG as Ht, pageIdentifierForCommand as I, resolveMediaTransformForAspect as In, parseResolvedSurface as Ir, BREAKPOINT_SM as It, createCommandRef as J, buildMediaIdentifierFromFilename as Jn, mapDrawingStyle as Jr, parseThemeForPersistence as Jt, createBlockRef as K, mediaTypeDiscriminants as Kn, rgbColorValue as Kr, themeButtonSizeClassName as Kt, pagePathForCommand as L, getDirectImageUrlForRelativePath as Ln, parseSurfaceTree as Lr, BREAKPOINT_XL as Lt, blockIdentifierForCommand as M, rotateNormalizedPoint as Mn, testimonialsCompositionTreatmentValues as Mr, getHeaderPaintTargets as Mt, blockOrderIdentifiersForCommand as N, normalizeWidthCandidates as Nn, tileTreatmentValues as Nr, renderHeaderPaintTargetRules as Nt, evaluateSiteCommandBatchExposure as O, summarizePortableMediaReferences as On, headingDecorationColorTokenValues as Or, parseGradientPresetId as Ot, entryIdentifierForCommand as P, isExplicitRectCrop as Pn, toNonEmptyReadonlyArray as Pr, BREAKPOINT_LG as Pt, createFooterRef as Q, slugifyMediaIdentifier as Qn, resolveThemeColorReferenceValue as Qr, normalizeCardRole as Qt, hasConfiguredBookingVertical as R, getDirectImageUrlForSiteScopedPath as Rn, resolveSurface as Rr, MQ_MD_DOWN as Rt, venueManagementFields as S, boxBackgroundOverlaySchema as Sn, defineBlockDesignCapability as Sr, resolvePublishedLook as St, createDryRunReport as T, siteChromeBackgroundOverrideOwner as Tn, eventListTreatmentValues as Tr, compiledRuntimeStyleFieldNames as Tt, require_jsx_runtime as U, runtimeMediaSchema as Un, backgroundColorStyle as Ur, generateDefaultButtonSystem as Ut, bookingPaymentCollectionOptionSchema as V, mediaSchema as Vn, resolveSurfaceForeground as Vr, getComponentShadowVar as Vt, require_react as W, buildRenderableMediaAssetIndex as Wn, borderColorStyle as Wr, parseThemeButtonVariantId as Wt, createEventCategoryRef as X, parseMediaIdentifier as Xn, isCssColorLiteral as Xr, cardRoleSchema as Xt, createContentEntryRef as Y, parseCanonicalMediaIdentifier as Yn, opaqueSurfaceDeclarations as Yr, renderCssDeclarations as Yt, createEventSeriesRef as Z, resolveMediaIdentifier as Zn, resolveOnColorForThemeToken as Zr, cardVariantRoleValues as Zt, normalizeLinkValue as _, heroPhotoPaintsGround as _n, asDesignBlockId as _r, resolveLookSlotSurfaces as _t, classifySdkEventSchedule as a, surfaceForegroundInputFromResolved as ai, buttonStyleSelectionSchema as an, parseRelativeStoragePath as ar, slugPart as at, isEventVisibility as b, resolveSectionPhotoPlacement as bn, asSiteStyleId as br, resolveEffectiveArrangement as bt, customLinkSchema as c, expandPalette as ci, generateNavLinkCSS as cn, classifyKnownMimeType as cr, headerLeafTextColor as ct, internalLinkSchema as d, parseBlockColorToken as di, footerPreferencesSchema as dn, defineBlockDesignMetadata as dr, headerPreferencesSchema as dt, PUBLIC_BUTTON_ROLES as ei, footerLookIdValues as en, resolveVideoSelectionIntentForMediaField as er, createNavigationRef as et, internalResolvedLinkSchema as f, __commonJSMin as fi, headerRootPaint as fn, resolveEffectiveKnobs as fr, mergeHeaderNavTypographyPreference as ft, linkSchema as g, deriveSectionGround as gn, asAppearancePresetId as gr, resolveItemSurface as gt, isResolvedInternalLinkEntityType as h, sectionFactsForContent as hn, arrangementModes as hr, arrangementSlotNames as ht, sdkEventUpsertOptionalFieldSchemas as i, surfaceForegroundDeclarations as ii, headerStyleIdValues as in, normalizeAmbiguousStoragePathForSite as ir, refSlug as it, eventManagementTiming as j, looksLikeUuid as jn, offeringsPreviewCompositionTreatmentValues as jr, generateMobileOverlayBackdropFilterFix as jt, evaluateSiteCommandExposure as k, walkAndCollectImageAttrs as kn, introCompositionTreatmentValues as kr, themeSchema as kt, entryLinkSchema as l, getTextColorForBackground as li, pageDesignBoundaryTreatmentSchema as ln, managementMediaLibraryUploadLimitForMimeType as lr, transparentHeaderForegroundInput as lt, isInternalResolvedLinkValue as m, composeBoundary as mn, cardListKnobBlocks as mr, arrangementLookHooks as mt, sdkCourseBookingSettingsSchema as n, surfaceToneFromForegroundToken as ni, asHeaderStyleId as nn, getMimeType as nr, createSiteCommandSiteRef as nt, classifySdkEventBookingOffer as o, surfaceForegroundStyle as oi, footerForegroundCandidates as on, FILE_SIZE_LIMITS as or, effectiveHeaderNavLinkStyle as ot, internalRouteOnlyLinkSchema as p, __toESM as pi, boundaryLayout as pn, cardListFamilyBlocks as pr, readHeaderNavFontSizePreference as pt, createBlocksRef as q, mediaIdentifierSchema as qn, textColorStyle as qr, themeButtonVariantClassNames as qt, sdkEventOccurrenceInputSchema as r, defaultThemeTokens as ri, headerLayoutIdValues as rn, asRecord as rr, createVenueRef as rt, RESOLVED_INTERNAL_LINK_ENTITY_TYPES as s, surfaceForegroundValue as si, buildTextContrastShadow as sn, MAX_MANAGEMENT_MEDIA_UPLOAD_BYTES as sr, headerForegroundCandidates as st, classifySdkEventTiming as t, selectLogoVariant as ti, asHeaderLayoutId as tn, getExtensionForMimeType as tr, createPageRef as tt, externalLinkSchema as u, parseThemeColorToken as ui, pageDesignIntentSchemaVersion as un, parseSafeDownloadFilename as ur, readOpeningForegroundProjection as ut, pageLinkSchema as v, parseCanonicalRuntimeBackground as vn, asLayoutVariantKey as vr, arrangementManifestFromVariants as vt, stableStringify as w, runtimeBoxBackgroundSchema as wn, defineNoBlockDesignCapability as wr, requireCuratedSiteStyle as wt, resolvePageUpsertSeo as x, sectionForegroundBackground as xn, assertNever$1 as xr, resolveModeArrangement as xt, eventVisibilitySchema as y, resolveSectionBackground as yn, asPageFlowPresetId as yr, readStoredMode as yt, eventCommandScheduleSchema as z, getImageUrl as zn, serializeSurfaceTree as zr, headerVariantValues as zt };
53269
+ export { createFormRef as $, slugifyMediaIdentifier as $n, mapDrawingStyle as $r, asFooterLookId as $t, getSiteCommandMetadata as A, walkAndCollectImageAttrs as An, introCompositionTreatmentValues as Ar, generateHeaderScrollTreatmentStyles as At, eventBookingOfferIntentSchema as B, getImageUrl as Bn, serializeSurfaceTree as Br, getComponentBorderWidthPx as Bt, sortRecord as C, boxBackgroundOverlaySchema as Cn, defineBlockDesignCapability as Cr, deriveSiteStyleBlockPolicy as Ct, mapEventCommandContent as D, stripStaleTransitionStyleGroup as Dn, eyebrowTreatmentValues as Dr, getGradientPreset as Dt, parseAndPlanCommandBatchForTypes as E, siteChromeBackgroundOverrideOwner as En, eventListTreatmentValues as Er, gradientToCss as Et, pageIdentifierForBlockCommand as F, isExplicitRectCrop as Fn, toNonEmptyReadonlyArray as Fr, BREAKPOINT_MD as Ft, ianaTimezoneSchema as G, buildRenderableMediaAssetIndex as Gn, surfaceForegroundValue as Gr, themeButtonExtraClassesWithTrailingIcon as Gt, bookingPaymentTermsSchema as H, mediaSchema as Hn, surfaceForegroundDeclarations as Hr, ICON_CONFIG as Ht, pageIdentifierForCommand as I, parseAspectRatioValue as In, visibleHeadingDecorationFamilyValues as Ir, BREAKPOINT_SM as It, createCommandRef as J, mediaIdentifierSchema as Jn, backgroundColorStyle as Jr, parseThemeForPersistence as Jt, createBlockRef as K, resolveManagedMediaReference as Kn, surfaceInkDeclarations as Kr, themeButtonSizeClassName as Kt, pagePathForCommand as L, resolveMediaTransformForAspect as Ln, parseResolvedSurface as Lr, BREAKPOINT_XL as Lt, blockIdentifierForCommand as M, looksLikeUuid as Mn, offeringsPreviewCompositionTreatmentValues as Mr, getHeaderPaintTargets as Mt, blockOrderIdentifiersForCommand as N, rotateNormalizedPoint as Nn, testimonialsCompositionTreatmentValues as Nr, renderHeaderPaintTargetRules as Nt, evaluateSiteCommandBatchExposure as O, collectPortableMediaReferencesFromContent as On, featureCardCompositionTreatmentValues as Or, parseGradientPresetId as Ot, entryIdentifierForCommand as P, normalizeWidthCandidates as Pn, tileTreatmentValues as Pr, BREAKPOINT_LG as Pt, createFooterRef as Q, resolveMediaIdentifier as Qn, textColorStyle as Qr, normalizeCardRole as Qt, hasConfiguredBookingVertical as R, getDirectImageUrlForRelativePath as Rn, parseSurfaceTree as Rr, MQ_MD_DOWN as Rt, venueManagementFields as S, sectionForegroundBackground as Sn, assertNever$1 as Sr, resolvePublishedLook as St, createDryRunReport as T, runtimeBoxBackgroundSchema as Tn, defineNoBlockDesignCapability as Tr, compiledRuntimeStyleFieldNames as Tt, require_jsx_runtime as U, persistableMediaSchema as Un, surfaceForegroundInputFromResolved as Ur, generateDefaultButtonSystem as Ut, bookingPaymentCollectionOptionSchema as V, isSiteScopedDirectImageStoragePath as Vn, surfaceDecorationColors as Vr, getComponentShadowVar as Vt, require_react as W, runtimeMediaSchema as Wn, surfaceForegroundStyle as Wr, parseThemeButtonVariantId as Wt, createEventCategoryRef as X, parseCanonicalMediaIdentifier as Xn, mergeStyles as Xr, cardRoleSchema as Xt, createContentEntryRef as Y, buildMediaIdentifierFromFilename as Yn, borderColorStyle as Yr, renderCssDeclarations as Yt, createEventSeriesRef as Z, parseMediaIdentifier as Zn, rgbColorValue as Zr, cardVariantRoleValues as Zt, normalizeLinkValue as _, __toESM as _i, deriveSectionGround as _n, asAppearancePresetId as _r, resolveLookSlotSurfaces as _t, classifySdkEventSchedule as a, PUBLIC_BUTTON_ROLES as ai, buttonStyleSelectionSchema as an, normalizeAmbiguousStoragePathForSite as ar, slugPart as at, isEventVisibility as b, resolveSectionBackground as bn, asPageFlowPresetId as br, resolveEffectiveArrangement as bt, customLinkSchema as c, resolveSurfaceForeground as ci, generateNavLinkCSS as cn, MAX_MANAGEMENT_MEDIA_UPLOAD_BYTES as cr, headerLeafTextColor as ct, internalLinkSchema as d, defaultThemeTokens as di, footerPreferencesSchema as dn, parseSafeDownloadFilename as dr, headerPreferencesSchema as dt, opaqueSurfaceDeclarations as ei, footerLookIdValues as en, deriveVideoSlotPolicy as er, createNavigationRef as et, internalResolvedLinkSchema as f, expandPalette as fi, headerNeedsPhotoShade as fn, defineBlockDesignMetadata as fr, mergeHeaderNavTypographyPreference as ft, linkSchema as g, __commonJSMin as gi, sectionFactsForContent as gn, arrangementModes as gr, resolveItemSurface as gt, isResolvedInternalLinkEntityType as h, parseBlockColorToken as hi, composeBoundary as hn, cardListKnobBlocks as hr, arrangementSlotNames as ht, sdkEventUpsertOptionalFieldSchemas as i, resolveTokenOrCssColorValue as ii, headerStyleIdValues as in, asRecord as ir, refSlug as it, eventManagementTiming as j, deleteEnvSpecificKeys as jn, makeBoundaryId as jr, generateMobileOverlayBackdropFilterFix as jt, evaluateSiteCommandExposure as k, summarizePortableMediaReferences as kn, headingDecorationColorTokenValues as kr, themeSchema as kt, entryLinkSchema as l, surfaceInkPublication as li, pageDesignBoundaryTreatmentSchema as ln, classifyKnownMimeType as lr, transparentHeaderForegroundInput as lt, isInternalResolvedLinkValue as m, parseThemeColorToken as mi, boundaryLayout as mn, cardListFamilyBlocks as mr, arrangementLookHooks as mt, sdkCourseBookingSettingsSchema as n, resolveOnColorForThemeToken as ni, asHeaderStyleId as nn, getExtensionForMimeType as nr, createSiteCommandSiteRef as nt, classifySdkEventBookingOffer as o, selectLogoVariant as oi, footerForegroundCandidates as on, parseRelativeStoragePath as or, effectiveHeaderNavLinkStyle as ot, internalRouteOnlyLinkSchema as p, getTextColorForBackground as pi, headerRootPaint as pn, resolveEffectiveKnobs as pr, readHeaderNavFontSizePreference as pt, createBlocksRef as q, mediaTypeDiscriminants as qn, surfaceInkStyle as qr, themeButtonVariantClassNames as qt, sdkEventOccurrenceInputSchema as r, resolveThemeColorReferenceValue as ri, headerLayoutIdValues as rn, getMimeType as rr, createVenueRef as rt, RESOLVED_INTERNAL_LINK_ENTITY_TYPES as s, surfaceToneFromForegroundToken as si, buildTextContrastShadow as sn, FILE_SIZE_LIMITS as sr, headerForegroundCandidates as st, classifySdkEventTiming as t, isCssColorLiteral as ti, asHeaderLayoutId as tn, resolveVideoSelectionIntentForMediaField as tr, createPageRef as tt, externalLinkSchema as u, TokenResolver as ui, pageDesignIntentSchemaVersion as un, managementMediaLibraryUploadLimitForMimeType as ur, readOpeningForegroundProjection as ut, pageLinkSchema as v, heroPhotoPaintsGround as vn, asDesignBlockId as vr, arrangementManifestFromVariants as vt, stableStringify as w, boxBackgroundSchema as wn, defineFrameCapability as wr, requireCuratedSiteStyle as wt, resolvePageUpsertSeo as x, resolveSectionPhotoPlacement as xn, asSiteStyleId as xr, resolveModeArrangement as xt, eventVisibilitySchema as y, parseCanonicalRuntimeBackground as yn, asLayoutVariantKey as yr, readStoredMode as yt, eventCommandScheduleSchema as z, getDirectImageUrlForSiteScopedPath as zn, resolveSurface as zr, headerVariantValues as zt };