riverbankcms 0.7.0 → 0.8.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 { D as parseFooterCompositionV1, E as footerNavigationLayouts, Rt as err, U as parseUtcIsoDateTimeString, b as stableJsonStringify, j as isPresent, k as isCanonicalRichTextDoc, l as EVENT_PRESET_VALUES, ot as assertNever, q as parseBookingTimeZone, s as resolveSiteVerticalBookingConfig, t as NAVIGATION_LOCATIONS, u as ORDINARY_EVENT_PRESET_VALUES, zt as ok } from "./src-DT7DBZtQ.mjs";
2
+ import { A as parseFooterCompositionV1, Ht as ok, K as parseUtcIsoDateTimeString, M as isCanonicalRichTextDoc, P as isPresent, Vt as err, X as parseBookingTimeZone, b as stableJsonStringify, k as footerNavigationLayouts, l as EVENT_PRESET_VALUES, lt as assertNever, s as resolveSiteVerticalBookingConfig, t as NAVIGATION_LOCATIONS, u as ORDINARY_EVENT_PRESET_VALUES } from "./src-B8fOSKJ6.mjs";
3
3
  import "node:module";
4
4
  import { z } from "zod";
5
5
 
@@ -869,6 +869,66 @@ const buttonSystemSchema = z.strictObject({
869
869
  });
870
870
  });
871
871
 
872
+ //#endregion
873
+ //#region ../theme-core/src/footer/mode.ts
874
+ const footerModeSchema = z.enum([
875
+ "default",
876
+ "blocks",
877
+ "default+blocks",
878
+ "none"
879
+ ]);
880
+ /** Reads only SDK footer intent; unrelated config and theme fields are not authorities. */
881
+ function resolveSdkFooterMode(config) {
882
+ const parsed = z.object({ layout: z.object({ footerMode: footerModeSchema.optional() }).optional() }).safeParse(config);
883
+ return parsed.success ? parsed.data.layout?.footerMode ?? "default" : "default";
884
+ }
885
+ /** Mirrors the incumbent footer renderer's variant-specific identity rules. */
886
+ function resolveFooterBrandVisibility(footer) {
887
+ return {
888
+ showLogo: footer.variant === "simple" ? footer.logo?.showLogo === true : footer.logo?.showLogo !== false,
889
+ showTitle: footer.logo?.showLogoText ?? footer.showLogoText === true
890
+ };
891
+ }
892
+ /**
893
+ * Resolve what to render for the footer given the current mode and whether the
894
+ * site is SDK-managed.
895
+ *
896
+ * Backwards compatible: missing/undefined mode is treated as 'default'.
897
+ */
898
+ function resolveFooterRenderPlan(params) {
899
+ if (!params.isSdkSite) {
900
+ return {
901
+ mode: "default",
902
+ renderDefaultFooter: true,
903
+ renderFooterBlocks: false
904
+ };
905
+ }
906
+ const mode = params.mode ?? "default";
907
+ switch (mode) {
908
+ case "blocks": return {
909
+ mode: "blocks",
910
+ renderDefaultFooter: false,
911
+ renderFooterBlocks: true
912
+ };
913
+ case "default+blocks": return {
914
+ mode: "default+blocks",
915
+ renderDefaultFooter: true,
916
+ renderFooterBlocks: true
917
+ };
918
+ case "none": return {
919
+ mode: "none",
920
+ renderDefaultFooter: false,
921
+ renderFooterBlocks: false
922
+ };
923
+ case "default":
924
+ default: return {
925
+ mode: "default",
926
+ renderDefaultFooter: true,
927
+ renderFooterBlocks: false
928
+ };
929
+ }
930
+ }
931
+
872
932
  //#endregion
873
933
  //#region ../theme-core/src/lib/media.ts
874
934
  const mediaTypeDiscriminants = [
@@ -1805,90 +1865,6 @@ function resolveCardRole(theme, cardSystem, role) {
1805
1865
  }
1806
1866
  }
1807
1867
 
1808
- //#endregion
1809
- //#region ../theme-core/src/tokens/resolver.ts
1810
- /**
1811
- * Resolves theme tokens to CSS variables
1812
- *
1813
- * This resolver ensures all button styles use theme tokens (not hardcoded colors)
1814
- * so that changing the palette automatically updates all buttons.
1815
- */
1816
- var TokenResolver = class {
1817
- constructor(theme) {
1818
- this.theme = theme;
1819
- }
1820
- /**
1821
- * Resolve a color token to a CSS variable reference wrapped in rgb()
1822
- *
1823
- * @param token - Token name (e.g., 'primary-500', 'white', 'text-900')
1824
- * @returns CSS rgb() function with var reference (e.g., 'rgb(var(--tb-primary-500))')
1825
- *
1826
- * Note: CSS variables are stored as RGB channels (e.g., "37 99 235") following Tailwind convention,
1827
- * so we must wrap them with rgb() to create a valid color value.
1828
- *
1829
- * @example
1830
- * resolver.getColor('primary-500') // => 'rgb(var(--tb-primary-500))'
1831
- * resolver.getColor('white') // => 'rgb(var(--tb-white))'
1832
- */
1833
- getColor(token) {
1834
- if (!this.tokenExists(token)) {
1835
- console.warn(`[TokenResolver] Color token "${token}" not found in theme palette. Button may render incorrectly.`);
1836
- }
1837
- return `rgb(var(--tb-${token}))`;
1838
- }
1839
- /**
1840
- * Adjust a color token's shade by a given amount
1841
- *
1842
- * @param token - Base token (e.g., 'primary-500')
1843
- * @param amount - Shade adjustment (+100 = darker, -100 = lighter)
1844
- * @returns Adjusted token name (e.g., 'primary-600')
1845
- *
1846
- * @example
1847
- * resolver.adjustShade('primary-500', 100) // => 'primary-600' (darker)
1848
- * resolver.adjustShade('primary-500', -100) // => 'primary-400' (lighter)
1849
- */
1850
- adjustShade(token, amount) {
1851
- const parts = token.split("-");
1852
- if (parts.length < 2) {
1853
- const newShade$1 = Math.max(100, Math.min(900, 500 + amount));
1854
- return `${token}-${newShade$1}`;
1855
- }
1856
- const base = parts.slice(0, -1).join("-");
1857
- const shade = parseInt(parts[parts.length - 1] ?? "", 10);
1858
- if (isNaN(shade)) {
1859
- console.warn(`[TokenResolver] Cannot adjust shade for non-shade token "${token}". Returning original token.`);
1860
- return token;
1861
- }
1862
- const newShade = Math.max(100, Math.min(900, shade + amount));
1863
- return `${base}-${newShade}`;
1864
- }
1865
- /**
1866
- * Check if a token exists in the theme palette
1867
- *
1868
- * @param token - Token name to check
1869
- * @returns True if token exists in theme
1870
- */
1871
- tokenExists(token) {
1872
- const commonTokens = [
1873
- "white",
1874
- "black",
1875
- "transparent"
1876
- ];
1877
- if (commonTokens.includes(token)) {
1878
- return true;
1879
- }
1880
- const parts = token.split("-");
1881
- if (parts.length >= 2) {
1882
- const lastPart = parts[parts.length - 1] ?? "";
1883
- const shade = parseInt(lastPart, 10);
1884
- if (!isNaN(shade) && shade >= 100 && shade <= 900 && shade % 100 === 0) {
1885
- return true;
1886
- }
1887
- }
1888
- return true;
1889
- }
1890
- };
1891
-
1892
1868
  //#endregion
1893
1869
  //#region ../theme-core/src/surfaces/foregroundRoles.ts
1894
1870
  const SURFACE_FOREGROUND_ROLES = [
@@ -2162,135 +2138,431 @@ function raiseContrastText(bgHex, preferred) {
2162
2138
  }
2163
2139
 
2164
2140
  //#endregion
2165
- //#region ../theme-core/src/palette/utils/onColor.ts
2166
- const BACKGROUND_ROLE_NAMES = new Set([
2167
- "primary",
2168
- "secondary",
2169
- "accent",
2170
- "success",
2171
- "warning",
2172
- "destructive",
2173
- "info",
2174
- "background",
2175
- "surface",
2176
- "surfaceAlt",
2177
- "muted",
2178
- "neutral"
2179
- ]);
2180
- function onColorTokenName(backgroundToken) {
2181
- return `on${backgroundToken.split("-").filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}`;
2182
- }
2183
- function isOnColorBackgroundCandidateToken(token) {
2184
- const role = token.match(/^(.*?)(?:-\d{2,3})?$/)?.[1] ?? token;
2185
- return BACKGROUND_ROLE_NAMES.has(role);
2186
- }
2187
-
2188
- //#endregion
2189
- //#region ../theme-core/src/palette/utils/opaqueSurfaceColor.ts
2190
- /** Resolve an opaque colour, compositing alpha against an explicit underlay. */
2191
- function resolveOpaqueSurfaceHex(reference, palette, underlayReference) {
2192
- const opaque = resolveConcreteColorHex(reference, palette);
2193
- if (opaque) return opaque;
2194
- if (!underlayReference) return null;
2195
- const underlayHex = resolveConcreteColorHex(underlayReference, palette);
2196
- if (!underlayHex) return null;
2197
- const parts = parseColorTokenParts(reference);
2198
- if (parts.kind === "token" && parts.opacityPercent !== null) {
2199
- const foregroundHex = normalizePaletteHex(palette[parts.token] ?? "");
2200
- return foregroundHex ? compositeHex(foregroundHex, underlayHex, parts.opacityPercent / 100) : null;
2201
- }
2202
- const raw = parts.kind === "raw" ? parts.value : reference;
2203
- const rgba = parseRgba(raw);
2204
- return rgba ? compositeHex(rgbToHex(rgba.r, rgba.g, rgba.b), underlayHex, rgba.alpha) : null;
2205
- }
2206
- /** Resolve a colour reference only when it is already fully opaque. */
2207
- function resolveConcreteColorHex(reference, palette) {
2208
- const literalHex = normalizePaletteHex(reference);
2209
- if (literalHex) return literalHex;
2210
- const parts = parseColorTokenParts(reference);
2211
- if (parts.kind === "token") {
2212
- if (parts.opacityPercent !== null) return null;
2213
- return normalizePaletteHex(palette[parts.token] ?? "");
2214
- }
2215
- const literal = parts.kind === "raw" ? parts.value : reference;
2216
- if (literal.toLowerCase() === "black") return "#000000";
2217
- if (literal.toLowerCase() === "white") return "#ffffff";
2218
- return normalizePaletteHex(literal);
2219
- }
2220
- function compositeHex(foregroundHex, underlayHex, alpha) {
2221
- const foreground = hexToRgb(foregroundHex);
2222
- const underlay = hexToRgb(underlayHex);
2223
- const channel = (foregroundValue, underlayValue) => Math.round(foregroundValue * alpha + underlayValue * (1 - alpha));
2224
- return rgbToHex(channel(foreground.r, underlay.r), channel(foreground.g, underlay.g), channel(foreground.b, underlay.b));
2225
- }
2226
- function parseRgba(value) {
2227
- const match = value.trim().match(/^rgba?\(\s*([\d.]+)(?:\s*,\s*|\s+)([\d.]+)(?:\s*,\s*|\s+)([\d.]+)(?:\s*(?:,|\/)\s*([\d.]+%?))?\s*\)$/i);
2228
- if (!match) return null;
2229
- const [, rSource, gSource, bSource] = match;
2230
- if (!rSource || !gSource || !bSource) return null;
2231
- const r = Number(rSource);
2232
- const g = Number(gSource);
2233
- const b = Number(bSource);
2234
- const alpha = parseAlpha(match[4]);
2235
- if (alpha === null || !isRgbChannel(r) || !isRgbChannel(g) || !isRgbChannel(b)) {
2236
- return null;
2237
- }
2238
- return {
2239
- r,
2240
- g,
2241
- b,
2242
- alpha
2243
- };
2244
- }
2245
- function parseAlpha(source) {
2246
- if (!source) return 1;
2247
- const value = source.endsWith("%") ? Number(source.slice(0, -1)) / 100 : Number(source);
2248
- return Number.isFinite(value) && value >= 0 && value <= 1 ? value : null;
2249
- }
2250
- function isRgbChannel(value) {
2251
- return Number.isFinite(value) && value >= 0 && value <= 255;
2252
- }
2253
-
2254
- //#endregion
2255
- //#region ../theme-core/src/surfaces/resolveSurfaceForeground.ts
2256
- const FALLBACK_TEXT_TOKENS = [
2257
- "text",
2258
- "primaryForeground",
2259
- "secondaryForeground",
2260
- "accentForeground",
2261
- "neutral-950",
2262
- "neutral-50"
2141
+ //#region ../theme-core/src/tokens/tokens.ts
2142
+ const colorTokens = [
2143
+ "background/base",
2144
+ "background/elevated",
2145
+ "surface/base",
2146
+ "surface/subtle",
2147
+ "surface/inverted",
2148
+ "content/primary",
2149
+ "content/secondary",
2150
+ "accent/primary",
2151
+ "accent/secondary",
2152
+ "border/base"
2263
2153
  ];
2264
- const CANONICAL_EQUIVALENT_TOKEN_ORDER = [
2265
- "text",
2266
- "primaryForeground",
2267
- "secondaryForeground",
2268
- "accentForeground",
2269
- "onPrimary",
2270
- "onSecondary",
2271
- "onAccent",
2272
- "onSurface",
2273
- "neutral-950",
2274
- "neutral-50"
2154
+ const typographyTokens = [
2155
+ "display/hero",
2156
+ "display/section",
2157
+ "heading/large",
2158
+ "heading/medium",
2159
+ "heading/small",
2160
+ "body/normal",
2161
+ "body/small",
2162
+ "caption"
2275
2163
  ];
2276
- /**
2277
- * Resolve the complete foreground contract for an effective page/section or
2278
- * site-chrome background.
2279
- *
2280
- * Deterministic colour backgrounds are validated at ordinary-text WCAG AA.
2281
- * Existing image/video/gradient treatments are trusted only when they already
2282
- * carry a usable foreground token; this function deliberately does not claim
2283
- * pixel-level contrast proof for media.
2284
- */
2285
- function resolveSurfaceForeground(background, palette, candidates = {}, options = {}) {
2286
- const authored = foregroundCandidates(background, candidates);
2287
- if (background === null) {
2288
- return resolveDeterministicForeground("background", {
2289
- ...authored,
2290
- text: authored.text ?? "text"
2291
- }, palette);
2292
- }
2293
- if (background.type === "color") {
2164
+ const spacingTokens = [
2165
+ "none",
2166
+ "3xs",
2167
+ "2xs",
2168
+ "xs",
2169
+ "sm",
2170
+ "md",
2171
+ "lg",
2172
+ "xl",
2173
+ "2xl",
2174
+ "3xl"
2175
+ ];
2176
+ const radiusTokens = [
2177
+ "none",
2178
+ "xs",
2179
+ "sm",
2180
+ "md",
2181
+ "lg",
2182
+ "pill",
2183
+ "full"
2184
+ ];
2185
+ const shadowTokens = [
2186
+ "none",
2187
+ "soft",
2188
+ "medium",
2189
+ "strong"
2190
+ ];
2191
+ const defaultThemeTokens = {
2192
+ color: {
2193
+ "background/base": "#ffffff",
2194
+ "background/elevated": "#f5f5f5",
2195
+ "surface/base": "#ffffff",
2196
+ "surface/subtle": "#f9f9fb",
2197
+ "surface/inverted": "#111111",
2198
+ "content/primary": "#111111",
2199
+ "content/secondary": "#3f3f46",
2200
+ "accent/primary": "#2563eb",
2201
+ "accent/secondary": "#1d4ed8",
2202
+ "border/base": "#e5e7eb"
2203
+ },
2204
+ typography: {
2205
+ "display/hero": {
2206
+ fontSize: "clamp(48px, 8vw, 96px)",
2207
+ lineHeight: "1.05",
2208
+ fontWeight: "700"
2209
+ },
2210
+ "display/section": {
2211
+ fontSize: "clamp(40px, 6vw, 72px)",
2212
+ lineHeight: "1.1",
2213
+ fontWeight: "700"
2214
+ },
2215
+ "heading/large": {
2216
+ fontSize: "clamp(32px, 4vw, 48px)",
2217
+ lineHeight: "1.2",
2218
+ fontWeight: "600"
2219
+ },
2220
+ "heading/medium": {
2221
+ fontSize: "clamp(24px, 3vw, 32px)",
2222
+ lineHeight: "1.25",
2223
+ fontWeight: "600"
2224
+ },
2225
+ "heading/small": {
2226
+ fontSize: "20px",
2227
+ lineHeight: "1.3",
2228
+ fontWeight: "600"
2229
+ },
2230
+ "body/normal": {
2231
+ fontSize: "16px",
2232
+ lineHeight: "1.6",
2233
+ fontWeight: "400"
2234
+ },
2235
+ "body/small": {
2236
+ fontSize: "14px",
2237
+ lineHeight: "1.5",
2238
+ fontWeight: "400"
2239
+ },
2240
+ caption: {
2241
+ fontSize: "12px",
2242
+ lineHeight: "1.4",
2243
+ fontWeight: "500"
2244
+ }
2245
+ },
2246
+ spacing: {
2247
+ none: "0px",
2248
+ "3xs": "4px",
2249
+ "2xs": "8px",
2250
+ xs: "12px",
2251
+ sm: "16px",
2252
+ md: "24px",
2253
+ lg: "32px",
2254
+ xl: "40px",
2255
+ "2xl": "56px",
2256
+ "3xl": "72px"
2257
+ },
2258
+ radius: {
2259
+ none: "0px",
2260
+ xs: "2px",
2261
+ sm: "4px",
2262
+ md: "8px",
2263
+ lg: "12px",
2264
+ pill: "999px",
2265
+ full: "9999px"
2266
+ },
2267
+ shadow: {
2268
+ none: "none",
2269
+ soft: "0px 2px 8px rgba(15, 23, 42, 0.08)",
2270
+ medium: "0px 6px 18px rgba(15, 23, 42, 0.12)",
2271
+ strong: "0px 12px 36px rgba(15, 23, 42, 0.16)"
2272
+ },
2273
+ palette: {
2274
+ surface: "#ffffff",
2275
+ "neutral-50": "#f9fafb",
2276
+ "neutral-900": "#111827",
2277
+ primary: "#2563eb",
2278
+ "primary-50": "#eff6ff",
2279
+ "primary-500": "#2563eb",
2280
+ "primary-900": "#1d4ed8",
2281
+ secondary: "#9333ea",
2282
+ "secondary-50": "#f3e8ff",
2283
+ "secondary-500": "#9333ea",
2284
+ "secondary-900": "#4c1d95",
2285
+ accent: "#f97316",
2286
+ "accent-50": "#fff7ed",
2287
+ "accent-500": "#f97316",
2288
+ "accent-900": "#7c2d12"
2289
+ }
2290
+ };
2291
+ /**
2292
+ * Project the expanded semantic palette into the token buckets consumed by
2293
+ * runtime transforms. Keep this mapping shared by full themes and the
2294
+ * tokens-only hosted-booking runtime so the two paths cannot drift.
2295
+ */
2296
+ function buildThemeTokens(palette) {
2297
+ const tokens = cloneThemeTokens(defaultThemeTokens);
2298
+ const paletteStrings = {};
2299
+ for (const [key, value] of Object.entries(palette)) {
2300
+ if (typeof value === "string" && value.trim().length > 0) {
2301
+ paletteStrings[key] = value;
2302
+ }
2303
+ }
2304
+ tokens.palette = paletteStrings;
2305
+ const resolveColor = (...keys) => {
2306
+ for (const key of keys) {
2307
+ const value = paletteStrings[key];
2308
+ if (value) return value;
2309
+ }
2310
+ return undefined;
2311
+ };
2312
+ const assignColor = (token, ...keys) => {
2313
+ const value = resolveColor(...keys);
2314
+ if (value) tokens.color[token] = value;
2315
+ };
2316
+ assignColor("background/base", "background", "surface", "primary-50");
2317
+ assignColor("background/elevated", "surface", "background");
2318
+ assignColor("surface/base", "surface", "background");
2319
+ assignColor("surface/subtle", "neutral-100", "surface", "background");
2320
+ assignColor("surface/inverted", "neutral-900", "text", "primary-900");
2321
+ assignColor("content/primary", "text", "body", "neutral-900");
2322
+ assignColor("content/secondary", "mutedText", "secondary", "neutral-600");
2323
+ assignColor("accent/primary", "primary", "accent");
2324
+ assignColor("accent/secondary", "accent", "secondary", "primary-400");
2325
+ assignColor("border/base", "border", "neutral-300");
2326
+ return tokens;
2327
+ }
2328
+ function cloneThemeTokens(source) {
2329
+ return JSON.parse(JSON.stringify(source));
2330
+ }
2331
+
2332
+ //#endregion
2333
+ //#region ../theme-core/src/surfaces/foregroundInteraction.ts
2334
+ /**
2335
+ * How far a hover state softens away from its resting foreground, as a
2336
+ * percentage of full opacity.
2337
+ *
2338
+ * Softening — rather than brightening — is forced by the token vocabulary
2339
+ * rather than chosen for taste. A resolved foreground is frequently an
2340
+ * `on*` token, which `expandPalette` emits as pure `#FFFFFF` or `#000000`
2341
+ * with no shade ramp: over a primary-painted section the resolved text role
2342
+ * is `onPrimary`, and white has no headroom left to move further from its
2343
+ * background. Moving *toward* the mid-range always has headroom, whatever
2344
+ * the resting colour, and it keeps the hue exactly.
2345
+ *
2346
+ * 85% is the measured floor, not a round number. Softening costs contrast, and
2347
+ * the binding case is a scrolled overlay header: `contrast-glass` paints 60%
2348
+ * black or white, which composites the backdrop into a known band whatever the
2349
+ * page content underneath happens to be. The lightest backdrop a dark glass can
2350
+ * present is 60% black over white, `#666666`; a white foreground softened to
2351
+ * 85% reads 4.69:1 there, against 4.07:1 at 75%, which fails. Nav links are
2352
+ * 18px regular (`textSize: 'lg'`), so the bar is WCAG AA's 4.5:1 rather than
2353
+ * the large-text 3:1.
2354
+ *
2355
+ * The mirror case — a light glass over dark content, `#999999` — cannot be met
2356
+ * by any softening, because the *resting* colour is already only about 4.6:1
2357
+ * there. That is a property of the glass rather than of this derivation, and
2358
+ * it wants fixing where the glass tone is chosen.
2359
+ */
2360
+ const FOREGROUND_HOVER_OPACITY_PERCENT = 85;
2361
+ /**
2362
+ * Derive the hover foreground that belongs with a resolved resting foreground.
2363
+ *
2364
+ * A resting foreground is chosen to be legible against a known surface. A hover
2365
+ * state derived from it stays within a bounded distance of that colour, so it
2366
+ * inherits most of the resting colour's contrast margin instead of asserting
2367
+ * an unrelated brand colour that no one checked against the surface.
2368
+ *
2369
+ * That bounded-distance property is what lets this be used on a transparent
2370
+ * overlay header, where the backdrop is only partly knowable: the scrolled
2371
+ * state composites a 60% glass tint over arbitrary page content, so no
2372
+ * compile-time contrast proof is available there. Staying near a colour that
2373
+ * is already trusted in both states is a weaker claim than a contrast proof,
2374
+ * and it is one this function can actually keep.
2375
+ *
2376
+ * Returns `null` when the resting token is not a plain palette token — an
2377
+ * already-faded or raw value has no well-defined softening, and the caller
2378
+ * should leave hover alone rather than guess.
2379
+ */
2380
+ function deriveForegroundHoverToken(restingToken) {
2381
+ const parts = parseColorTokenParts(restingToken);
2382
+ if (parts.kind !== "token") return null;
2383
+ if (parts.opacityPercent !== null) return null;
2384
+ return parseThemeColorToken(`${parts.token}/${FOREGROUND_HOVER_OPACITY_PERCENT}`);
2385
+ }
2386
+ /**
2387
+ * The opacity `contrast-glass` paints at. Mirrors the value in
2388
+ * `generateHeaderScrollTreatmentStyles`; the two must agree, because the whole
2389
+ * point of the bound below is that it describes what that treatment can produce.
2390
+ */
2391
+ const CONTRAST_GLASS_ALPHA = .6;
2392
+ /**
2393
+ * The extreme backdrop a glass tone can present, whatever page content happens
2394
+ * to be underneath it.
2395
+ *
2396
+ * A 60% tint clamps the composite into a known band, which is what makes a
2397
+ * conservative check possible at compile time even though the content passing
2398
+ * under a sticky header is not knowable. For a dark glass the composite is
2399
+ * lightest over white; for a light glass it is darkest over black. Those are
2400
+ * the two ends that a foreground has to survive.
2401
+ */
2402
+ function worstCaseGlassBackdrop(tone) {
2403
+ const [over, tint] = tone === "dark" ? ["#ffffff", "#000000"] : ["#000000", "#ffffff"];
2404
+ return compositeHex$1(tint, over, CONTRAST_GLASS_ALPHA);
2405
+ }
2406
+ /** Flatten `foreground` at `alpha` over `background` into an opaque hex. */
2407
+ function compositeHex$1(foreground, background, alpha) {
2408
+ const f = hexToRgb(foreground);
2409
+ const b = hexToRgb(background);
2410
+ return rgbToHex(Math.round(mix$2(b.r, f.r, alpha)), Math.round(mix$2(b.g, f.g, alpha)), Math.round(mix$2(b.b, f.b, alpha)));
2411
+ }
2412
+ /**
2413
+ * Decide whether the resting foreground can safely be softened for hover.
2414
+ *
2415
+ * `glassTone` is the scroll treatment's compiled tone, or `null` when the style
2416
+ * has no glass. Without one there is no bound on what the header can end up
2417
+ * over, so this declines to soften rather than guess.
2418
+ *
2419
+ * The light-glass end cannot be satisfied by any softening: against `#999999`
2420
+ * a dark resting foreground is already close to the 4.5:1 line, so every step
2421
+ * away from it fails. That is why this returns a plan rather than a colour —
2422
+ * the caller has to have a non-colour affordance to fall back on.
2423
+ */
2424
+ function planForegroundHover(restingToken, restingHex, glassTone) {
2425
+ const softened = deriveForegroundHoverToken(restingToken);
2426
+ if (!softened) return { kind: "resting" };
2427
+ if (!restingHex || !glassTone) return { kind: "resting" };
2428
+ const backdrop = worstCaseGlassBackdrop(glassTone);
2429
+ const softenedHex = compositeHex$1(restingHex, backdrop, FOREGROUND_HOVER_OPACITY_PERCENT / 100);
2430
+ return contrastRatio(softenedHex, backdrop) >= ORDINARY_TEXT_MIN_CONTRAST_RATIO ? {
2431
+ kind: "softened",
2432
+ token: softened
2433
+ } : { kind: "resting" };
2434
+ }
2435
+
2436
+ //#endregion
2437
+ //#region ../theme-core/src/palette/utils/onColor.ts
2438
+ const BACKGROUND_ROLE_NAMES = new Set([
2439
+ "primary",
2440
+ "secondary",
2441
+ "accent",
2442
+ "success",
2443
+ "warning",
2444
+ "destructive",
2445
+ "info",
2446
+ "background",
2447
+ "surface",
2448
+ "surfaceAlt",
2449
+ "muted",
2450
+ "neutral"
2451
+ ]);
2452
+ function onColorTokenName(backgroundToken) {
2453
+ return `on${backgroundToken.split("-").filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}`;
2454
+ }
2455
+ function isOnColorBackgroundCandidateToken(token) {
2456
+ const role = token.match(/^(.*?)(?:-\d{2,3})?$/)?.[1] ?? token;
2457
+ return BACKGROUND_ROLE_NAMES.has(role);
2458
+ }
2459
+
2460
+ //#endregion
2461
+ //#region ../theme-core/src/palette/utils/opaqueSurfaceColor.ts
2462
+ /** Resolve an opaque colour, compositing alpha against an explicit underlay. */
2463
+ function resolveOpaqueSurfaceHex(reference, palette, underlayReference) {
2464
+ const opaque = resolveConcreteColorHex(reference, palette);
2465
+ if (opaque) return opaque;
2466
+ if (!underlayReference) return null;
2467
+ const underlayHex = resolveConcreteColorHex(underlayReference, palette);
2468
+ if (!underlayHex) return null;
2469
+ const parts = parseColorTokenParts(reference);
2470
+ if (parts.kind === "token" && parts.opacityPercent !== null) {
2471
+ const foregroundHex = normalizePaletteHex(palette[parts.token] ?? "");
2472
+ return foregroundHex ? compositeHex(foregroundHex, underlayHex, parts.opacityPercent / 100) : null;
2473
+ }
2474
+ const raw = parts.kind === "raw" ? parts.value : reference;
2475
+ const rgba = parseRgba(raw);
2476
+ return rgba ? compositeHex(rgbToHex(rgba.r, rgba.g, rgba.b), underlayHex, rgba.alpha) : null;
2477
+ }
2478
+ /** Resolve a colour reference only when it is already fully opaque. */
2479
+ function resolveConcreteColorHex(reference, palette) {
2480
+ const literalHex = normalizePaletteHex(reference);
2481
+ if (literalHex) return literalHex;
2482
+ const parts = parseColorTokenParts(reference);
2483
+ if (parts.kind === "token") {
2484
+ if (parts.opacityPercent !== null) return null;
2485
+ return normalizePaletteHex(palette[parts.token] ?? "");
2486
+ }
2487
+ const literal = parts.kind === "raw" ? parts.value : reference;
2488
+ if (literal.toLowerCase() === "black") return "#000000";
2489
+ if (literal.toLowerCase() === "white") return "#ffffff";
2490
+ return normalizePaletteHex(literal);
2491
+ }
2492
+ function compositeHex(foregroundHex, underlayHex, alpha) {
2493
+ const foreground = hexToRgb(foregroundHex);
2494
+ const underlay = hexToRgb(underlayHex);
2495
+ const channel = (foregroundValue, underlayValue) => Math.round(foregroundValue * alpha + underlayValue * (1 - alpha));
2496
+ return rgbToHex(channel(foreground.r, underlay.r), channel(foreground.g, underlay.g), channel(foreground.b, underlay.b));
2497
+ }
2498
+ function parseRgba(value) {
2499
+ const match = value.trim().match(/^rgba?\(\s*([\d.]+)(?:\s*,\s*|\s+)([\d.]+)(?:\s*,\s*|\s+)([\d.]+)(?:\s*(?:,|\/)\s*([\d.]+%?))?\s*\)$/i);
2500
+ if (!match) return null;
2501
+ const [, rSource, gSource, bSource] = match;
2502
+ if (!rSource || !gSource || !bSource) return null;
2503
+ const r = Number(rSource);
2504
+ const g = Number(gSource);
2505
+ const b = Number(bSource);
2506
+ const alpha = parseAlpha(match[4]);
2507
+ if (alpha === null || !isRgbChannel(r) || !isRgbChannel(g) || !isRgbChannel(b)) {
2508
+ return null;
2509
+ }
2510
+ return {
2511
+ r,
2512
+ g,
2513
+ b,
2514
+ alpha
2515
+ };
2516
+ }
2517
+ function parseAlpha(source) {
2518
+ if (!source) return 1;
2519
+ const value = source.endsWith("%") ? Number(source.slice(0, -1)) / 100 : Number(source);
2520
+ return Number.isFinite(value) && value >= 0 && value <= 1 ? value : null;
2521
+ }
2522
+ function isRgbChannel(value) {
2523
+ return Number.isFinite(value) && value >= 0 && value <= 255;
2524
+ }
2525
+
2526
+ //#endregion
2527
+ //#region ../theme-core/src/surfaces/resolveSurfaceForeground.ts
2528
+ const FALLBACK_TEXT_TOKENS = [
2529
+ "text",
2530
+ "primaryForeground",
2531
+ "secondaryForeground",
2532
+ "accentForeground",
2533
+ "neutral-950",
2534
+ "neutral-50"
2535
+ ];
2536
+ const CANONICAL_EQUIVALENT_TOKEN_ORDER = [
2537
+ "text",
2538
+ "primaryForeground",
2539
+ "secondaryForeground",
2540
+ "accentForeground",
2541
+ "onPrimary",
2542
+ "onSecondary",
2543
+ "onAccent",
2544
+ "onSurface",
2545
+ "neutral-950",
2546
+ "neutral-50"
2547
+ ];
2548
+ /**
2549
+ * Resolve the complete foreground contract for an effective page/section or
2550
+ * site-chrome background.
2551
+ *
2552
+ * Deterministic colour backgrounds are validated at ordinary-text WCAG AA.
2553
+ * Existing image/video/gradient treatments are trusted only when they already
2554
+ * carry a usable foreground token; this function deliberately does not claim
2555
+ * pixel-level contrast proof for media.
2556
+ */
2557
+ function resolveSurfaceForeground(background, palette, candidates = {}, options = {}) {
2558
+ const authored = foregroundCandidates(background, candidates);
2559
+ if (background === null) {
2560
+ return resolveDeterministicForeground("background", {
2561
+ ...authored,
2562
+ text: authored.text ?? "text"
2563
+ }, palette);
2564
+ }
2565
+ if (background.type === "color") {
2294
2566
  const backgroundReference = normalizedReference(background.color);
2295
2567
  if (!backgroundReference || backgroundReference === "transparent") {
2296
2568
  return { kind: "unknown" };
@@ -2328,63 +2600,288 @@ function resolveDeterministicForeground(backgroundReference, candidates, palette
2328
2600
  link: safeTokenOr(candidates.link, text, backgroundHex, palette),
2329
2601
  supporting: safeTokenOr(candidates.supporting, text, backgroundHex, palette)
2330
2602
  }
2331
- };
2332
- }
2333
- function resolveTrustedTreatmentForeground(candidates, palette) {
2334
- const text = canonicalToken(candidates.text, palette);
2335
- if (!text) return { kind: "unknown" };
2336
- return {
2337
- kind: "known",
2338
- roles: {
2339
- text,
2340
- heading: canonicalToken(candidates.heading, palette) ?? text,
2341
- link: canonicalToken(candidates.link, palette) ?? text,
2342
- supporting: canonicalToken(candidates.supporting, palette) ?? text
2603
+ };
2604
+ }
2605
+ function resolveTrustedTreatmentForeground(candidates, palette) {
2606
+ const text = canonicalToken(candidates.text, palette);
2607
+ if (!text) return { kind: "unknown" };
2608
+ return {
2609
+ kind: "known",
2610
+ roles: {
2611
+ text,
2612
+ heading: canonicalToken(candidates.heading, palette) ?? text,
2613
+ link: canonicalToken(candidates.link, palette) ?? text,
2614
+ supporting: canonicalToken(candidates.supporting, palette) ?? text
2615
+ }
2616
+ };
2617
+ }
2618
+ function safeTokenOr(candidate$1, fallback, backgroundHex, palette) {
2619
+ return firstSafeToken([candidate$1], backgroundHex, palette) ?? fallback;
2620
+ }
2621
+ function firstSafeToken(candidates, backgroundHex, palette, preferredEquivalentTokens = []) {
2622
+ const seen = new Set();
2623
+ for (const candidate$1 of candidates) {
2624
+ const token = canonicalToken(candidate$1, palette, preferredEquivalentTokens);
2625
+ if (!token || seen.has(token)) continue;
2626
+ seen.add(token);
2627
+ const foregroundHex = resolveConcreteColorHex(token, palette);
2628
+ if (foregroundHex && contrastRatio(backgroundHex, foregroundHex) >= ORDINARY_TEXT_MIN_CONTRAST_RATIO) {
2629
+ return token;
2630
+ }
2631
+ }
2632
+ return null;
2633
+ }
2634
+ function canonicalToken(reference, palette, preferredEquivalentTokens = []) {
2635
+ const normalized = normalizedReference(reference);
2636
+ if (!normalized || normalized === "transparent") return null;
2637
+ const direct = parseThemeColorToken(normalized);
2638
+ if (direct && resolveConcreteColorHex(direct, palette)) return direct;
2639
+ const referenceHex = resolveConcreteColorHex(normalized, palette);
2640
+ if (!referenceHex) return null;
2641
+ const equivalent = [
2642
+ ...preferredEquivalentTokens,
2643
+ ...CANONICAL_EQUIVALENT_TOKEN_ORDER,
2644
+ ...Object.keys(palette).sort()
2645
+ ].find((token) => {
2646
+ const parsed = parseThemeColorToken(token);
2647
+ return parsed !== null && resolveConcreteColorHex(parsed, palette)?.toLowerCase() === referenceHex.toLowerCase();
2648
+ });
2649
+ return equivalent ? parseThemeColorToken(equivalent) : null;
2650
+ }
2651
+ function paletteToken(reference) {
2652
+ const parts = parseColorTokenParts(reference);
2653
+ return parts.kind === "token" && parts.opacityPercent === null ? parts.token : null;
2654
+ }
2655
+ function normalizedReference(value) {
2656
+ if (typeof value !== "string") return null;
2657
+ const normalized = value.trim();
2658
+ return normalized.length > 0 ? normalized : null;
2659
+ }
2660
+
2661
+ //#endregion
2662
+ //#region ../theme-core/src/utils/colorStyles.ts
2663
+ const COLOR_VAR_PREFIX = "--tb-";
2664
+ function parseToken(source) {
2665
+ const parts = parseColorTokenParts(source);
2666
+ if (parts.kind === "raw") {
2667
+ return {
2668
+ token: source,
2669
+ raw: parts.value
2670
+ };
2671
+ }
2672
+ if (parts.kind === "invalid") {
2673
+ return { token: source };
2674
+ }
2675
+ return {
2676
+ token: parts.token,
2677
+ opacityPercent: parts.opacityPercent === null ? undefined : parts.opacityPercent
2678
+ };
2679
+ }
2680
+ const SURFACE_TEXT_ROLE_BY_TOKEN = {
2681
+ text: "text",
2682
+ heading: "heading",
2683
+ mutedText: "supporting"
2684
+ };
2685
+ function surfaceRoleForTextToken(token) {
2686
+ return Object.prototype.hasOwnProperty.call(SURFACE_TEXT_ROLE_BY_TOKEN, token) ? SURFACE_TEXT_ROLE_BY_TOKEN[token] : null;
2687
+ }
2688
+ /**
2689
+ * Converts a theme color token to a CSS rgb value.
2690
+ * This is the primary function for applying theme colors to CSS properties.
2691
+ *
2692
+ * @param token - Theme color token (semantic, with opacity, or raw)
2693
+ * @returns CSS color value (rgb with optional alpha, or raw CSS value)
2694
+ *
2695
+ * @example
2696
+ * rgbColorValue('primary') // => 'rgb(var(--tb-primary))'
2697
+ * rgbColorValue('surface/95') // => 'rgb(var(--tb-surface) / 0.95)'
2698
+ * rgbColorValue('raw:transparent') // => 'transparent'
2699
+ */
2700
+ function rgbColorValue(token) {
2701
+ const { token: baseToken, opacityPercent, raw } = parseToken(token);
2702
+ if (raw) {
2703
+ return raw;
2704
+ }
2705
+ const cssVar = `${COLOR_VAR_PREFIX}${baseToken}`;
2706
+ if (opacityPercent === undefined) {
2707
+ return `rgb(var(${cssVar}))`;
2708
+ }
2709
+ return `rgb(var(${cssVar}) / ${opacityPercent / 100})`;
2710
+ }
2711
+ /**
2712
+ * Converts a theme color token to a CSS variable reference.
2713
+ * Note: This returns the base variable without rgb() wrapper, ignoring opacity.
2714
+ *
2715
+ * @param token - Theme color token
2716
+ * @returns CSS variable reference
2717
+ *
2718
+ * @example
2719
+ * cssVarValue('primary') // => 'var(--tb-primary)'
2720
+ * cssVarValue('surface/95') // => 'var(--tb-surface)' (opacity ignored)
2721
+ */
2722
+ function cssVarValue(token) {
2723
+ const { token: baseToken } = parseToken(token);
2724
+ return `var(${COLOR_VAR_PREFIX}${baseToken})`;
2725
+ }
2726
+ /**
2727
+ * Creates a backgroundColor style object from a theme token.
2728
+ *
2729
+ * @param token - Theme color token
2730
+ * @returns CSSProperties with backgroundColor
2731
+ */
2732
+ function backgroundColorStyle(token) {
2733
+ return { backgroundColor: rgbColorValue(token) };
2734
+ }
2735
+ /**
2736
+ * Creates a text color style object from a theme token.
2737
+ *
2738
+ * @param token - Theme color token
2739
+ * @returns CSSProperties with color
2740
+ */
2741
+ function textColorStyle(token) {
2742
+ const parsed = parseToken(token);
2743
+ const surfaceRole = parsed.raw ? null : surfaceRoleForTextToken(parsed.token);
2744
+ if (surfaceRole) {
2745
+ return { color: parsed.opacityPercent === undefined ? surfaceForegroundValue(surfaceRole) : surfaceForegroundValueWithOpacity(surfaceRole, parsed.opacityPercent) };
2746
+ }
2747
+ return { color: rgbColorValue(token) };
2748
+ }
2749
+ /**
2750
+ * Creates a border color style object from a theme token.
2751
+ *
2752
+ * @param token - Theme color token
2753
+ * @returns CSSProperties with borderColor
2754
+ */
2755
+ function borderColorStyle(token) {
2756
+ return { borderColor: rgbColorValue(token) };
2757
+ }
2758
+ /**
2759
+ * Creates a linear gradient background from theme tokens.
2760
+ *
2761
+ * @param tokens - Array of theme color tokens for gradient stops
2762
+ * @param direction - CSS gradient direction (default: 'to bottom')
2763
+ * @returns CSSProperties with backgroundImage
2764
+ *
2765
+ * @example
2766
+ * gradientBackgroundStyle(['primary', 'secondary'])
2767
+ * // => { backgroundImage: 'linear-gradient(to bottom, rgb(var(--tb-primary)), rgb(var(--tb-secondary)))' }
2768
+ *
2769
+ * @example
2770
+ * gradientBackgroundStyle(['primary/80', 'secondary/60'], '45deg')
2771
+ * // => { backgroundImage: 'linear-gradient(45deg, rgb(var(--tb-primary) / 0.8), rgb(var(--tb-secondary) / 0.6))' }
2772
+ */
2773
+ function gradientBackgroundStyle(tokens, direction = "to bottom") {
2774
+ const stops = tokens.map((stop) => rgbColorValue(stop)).join(", ");
2775
+ return { backgroundImage: `linear-gradient(${direction}, ${stops})` };
2776
+ }
2777
+ /**
2778
+ * Merges multiple CSSProperties objects into a single object.
2779
+ * Later styles override earlier ones. Undefined values are filtered out.
2780
+ * Returns undefined if the merged result is empty.
2781
+ *
2782
+ * @param styles - Variable number of CSSProperties objects (can be undefined)
2783
+ * @returns Merged CSSProperties object, or undefined if empty
2784
+ *
2785
+ * @example
2786
+ * mergeStyles(
2787
+ * backgroundColorStyle('surface'),
2788
+ * textColorStyle('text'),
2789
+ * borderColorStyle('border')
2790
+ * )
2791
+ * // => { backgroundColor: '...', color: '...', borderColor: '...' }
2792
+ */
2793
+ function mergeStyles(...styles) {
2794
+ const merged = {};
2795
+ for (const style of styles) {
2796
+ if (!style) continue;
2797
+ Object.assign(merged, style);
2798
+ }
2799
+ return Object.keys(merged).length ? merged : undefined;
2800
+ }
2801
+
2802
+ //#endregion
2803
+ //#region ../theme-core/src/tokens/resolver.ts
2804
+ /**
2805
+ * Resolves theme tokens to CSS variables
2806
+ *
2807
+ * This resolver ensures all button styles use theme tokens (not hardcoded colors)
2808
+ * so that changing the palette automatically updates all buttons.
2809
+ */
2810
+ var TokenResolver = class {
2811
+ constructor(theme) {
2812
+ this.theme = theme;
2813
+ }
2814
+ /**
2815
+ * Resolve a color token to a CSS variable reference wrapped in rgb()
2816
+ *
2817
+ * @param token - Token name (e.g., 'primary-500', 'white', 'text-900')
2818
+ * @returns CSS rgb() function with var reference (e.g., 'rgb(var(--tb-primary-500))')
2819
+ *
2820
+ * Note: CSS variables are stored as RGB channels (e.g., "37 99 235") following Tailwind convention,
2821
+ * so we must wrap them with rgb() to create a valid color value.
2822
+ *
2823
+ * @example
2824
+ * resolver.getColor('primary-500') // => 'rgb(var(--tb-primary-500))'
2825
+ * resolver.getColor('white') // => 'rgb(var(--tb-white))'
2826
+ */
2827
+ getColor(token) {
2828
+ if (!this.tokenExists(token)) {
2829
+ console.warn(`[TokenResolver] Color token "${token}" not found in theme palette. Button may render incorrectly.`);
2830
+ }
2831
+ return rgbColorValue(token);
2832
+ }
2833
+ /**
2834
+ * Adjust a color token's shade by a given amount
2835
+ *
2836
+ * @param token - Base token (e.g., 'primary-500')
2837
+ * @param amount - Shade adjustment (+100 = darker, -100 = lighter)
2838
+ * @returns Adjusted token name (e.g., 'primary-600')
2839
+ *
2840
+ * @example
2841
+ * resolver.adjustShade('primary-500', 100) // => 'primary-600' (darker)
2842
+ * resolver.adjustShade('primary-500', -100) // => 'primary-400' (lighter)
2843
+ */
2844
+ adjustShade(token, amount) {
2845
+ const parts = token.split("-");
2846
+ if (parts.length < 2) {
2847
+ const newShade$1 = Math.max(100, Math.min(900, 500 + amount));
2848
+ return `${token}-${newShade$1}`;
2343
2849
  }
2344
- };
2345
- }
2346
- function safeTokenOr(candidate$1, fallback, backgroundHex, palette) {
2347
- return firstSafeToken([candidate$1], backgroundHex, palette) ?? fallback;
2348
- }
2349
- function firstSafeToken(candidates, backgroundHex, palette, preferredEquivalentTokens = []) {
2350
- const seen = new Set();
2351
- for (const candidate$1 of candidates) {
2352
- const token = canonicalToken(candidate$1, palette, preferredEquivalentTokens);
2353
- if (!token || seen.has(token)) continue;
2354
- seen.add(token);
2355
- const foregroundHex = resolveConcreteColorHex(token, palette);
2356
- if (foregroundHex && contrastRatio(backgroundHex, foregroundHex) >= ORDINARY_TEXT_MIN_CONTRAST_RATIO) {
2850
+ const base = parts.slice(0, -1).join("-");
2851
+ const shade = parseInt(parts[parts.length - 1] ?? "", 10);
2852
+ if (isNaN(shade)) {
2853
+ console.warn(`[TokenResolver] Cannot adjust shade for non-shade token "${token}". Returning original token.`);
2357
2854
  return token;
2358
2855
  }
2856
+ const newShade = Math.max(100, Math.min(900, shade + amount));
2857
+ return `${base}-${newShade}`;
2359
2858
  }
2360
- return null;
2361
- }
2362
- function canonicalToken(reference, palette, preferredEquivalentTokens = []) {
2363
- const normalized = normalizedReference(reference);
2364
- if (!normalized || normalized === "transparent") return null;
2365
- const direct = parseThemeColorToken(normalized);
2366
- if (direct && resolveConcreteColorHex(direct, palette)) return direct;
2367
- const referenceHex = resolveConcreteColorHex(normalized, palette);
2368
- if (!referenceHex) return null;
2369
- const equivalent = [
2370
- ...preferredEquivalentTokens,
2371
- ...CANONICAL_EQUIVALENT_TOKEN_ORDER,
2372
- ...Object.keys(palette).sort()
2373
- ].find((token) => {
2374
- const parsed = parseThemeColorToken(token);
2375
- return parsed !== null && resolveConcreteColorHex(parsed, palette)?.toLowerCase() === referenceHex.toLowerCase();
2376
- });
2377
- return equivalent ? parseThemeColorToken(equivalent) : null;
2378
- }
2379
- function paletteToken(reference) {
2380
- const parts = parseColorTokenParts(reference);
2381
- return parts.kind === "token" && parts.opacityPercent === null ? parts.token : null;
2382
- }
2383
- function normalizedReference(value) {
2384
- if (typeof value !== "string") return null;
2385
- const normalized = value.trim();
2386
- return normalized.length > 0 ? normalized : null;
2387
- }
2859
+ /**
2860
+ * Check if a token exists in the theme palette
2861
+ *
2862
+ * @param token - Token name to check
2863
+ * @returns True if token exists in theme
2864
+ */
2865
+ tokenExists(token) {
2866
+ const commonTokens = [
2867
+ "white",
2868
+ "black",
2869
+ "transparent"
2870
+ ];
2871
+ if (commonTokens.includes(token)) {
2872
+ return true;
2873
+ }
2874
+ const parts = token.split("-");
2875
+ if (parts.length >= 2) {
2876
+ const lastPart = parts[parts.length - 1] ?? "";
2877
+ const shade = parseInt(lastPart, 10);
2878
+ if (!isNaN(shade) && shade >= 100 && shade <= 900 && shade % 100 === 0) {
2879
+ return true;
2880
+ }
2881
+ }
2882
+ return true;
2883
+ }
2884
+ };
2388
2885
 
2389
2886
  //#endregion
2390
2887
  //#region ../theme-core/src/cards/generateCardCss.ts
@@ -2513,14 +3010,12 @@ function generateVariantCSS(variant, themeId, cardClasses, tokens, theme) {
2513
3010
  const shadowValue = generateShadowValue$1(shadow$1);
2514
3011
  rules.push(`box-shadow: ${shadowValue}`);
2515
3012
  }
2516
- rules.push(`display: flex`);
2517
- rules.push(`flex-direction: column`);
2518
- rules.push(`position: relative`);
2519
- rules.push(`overflow: visible`);
3013
+ const structureSelector = cardClasses.map((cardClass) => `:where([data-theme-scope="${themeId}"]) :where(.${cardClass})`).join(",\n");
2520
3014
  const mediaRadius = MEDIA_CORNER_MAP[corners$1];
2521
3015
  const mediaRules = [`border-top-left-radius: ${mediaRadius}`, `border-top-right-radius: ${mediaRadius}`];
2522
3016
  const contentRules = [`padding: ${resolveContentPadding(variant)}`];
2523
3017
  return [
3018
+ `${structureSelector} {\n display: flex;\n flex-direction: column;\n position: relative;\n overflow: visible;\n}`,
2524
3019
  `${containerSelector} {\n ${rules.join(";\n ")};\n}`,
2525
3020
  ...cardDecorationPseudoRules(containerSelectors, variant.decorations, resolveColor),
2526
3021
  `${fallbackBackgroundSelector} {\n background: transparent;\n}`,
@@ -4096,149 +4591,8 @@ function isThemeV2NormalEditorRawStyleFieldName(fieldName) {
4096
4591
  return themeV2NormalEditorRawStyleFieldNameSet.has(fieldName);
4097
4592
  }
4098
4593
  function isThemeV2NormalEditorRawStylePath(fieldPath) {
4099
- const rootField = fieldPath.split(".")[0] ?? "";
4100
- return isThemeV2NormalEditorRawStyleFieldName(rootField);
4101
- }
4102
-
4103
- //#endregion
4104
- //#region ../theme-core/src/utils/colorStyles.ts
4105
- const COLOR_VAR_PREFIX = "--tb-";
4106
- function parseToken(source) {
4107
- const parts = parseColorTokenParts(source);
4108
- if (parts.kind === "raw") {
4109
- return {
4110
- token: source,
4111
- raw: parts.value
4112
- };
4113
- }
4114
- if (parts.kind === "invalid") {
4115
- return { token: source };
4116
- }
4117
- return {
4118
- token: parts.token,
4119
- opacityPercent: parts.opacityPercent === null ? undefined : parts.opacityPercent
4120
- };
4121
- }
4122
- const SURFACE_TEXT_ROLE_BY_TOKEN = {
4123
- text: "text",
4124
- heading: "heading",
4125
- mutedText: "supporting"
4126
- };
4127
- function surfaceRoleForTextToken(token) {
4128
- return Object.prototype.hasOwnProperty.call(SURFACE_TEXT_ROLE_BY_TOKEN, token) ? SURFACE_TEXT_ROLE_BY_TOKEN[token] : null;
4129
- }
4130
- /**
4131
- * Converts a theme color token to a CSS rgb value.
4132
- * This is the primary function for applying theme colors to CSS properties.
4133
- *
4134
- * @param token - Theme color token (semantic, with opacity, or raw)
4135
- * @returns CSS color value (rgb with optional alpha, or raw CSS value)
4136
- *
4137
- * @example
4138
- * rgbColorValue('primary') // => 'rgb(var(--tb-primary))'
4139
- * rgbColorValue('surface/95') // => 'rgb(var(--tb-surface) / 0.95)'
4140
- * rgbColorValue('raw:transparent') // => 'transparent'
4141
- */
4142
- function rgbColorValue(token) {
4143
- const { token: baseToken, opacityPercent, raw } = parseToken(token);
4144
- if (raw) {
4145
- return raw;
4146
- }
4147
- const cssVar = `${COLOR_VAR_PREFIX}${baseToken}`;
4148
- if (opacityPercent === undefined) {
4149
- return `rgb(var(${cssVar}))`;
4150
- }
4151
- return `rgb(var(${cssVar}) / ${opacityPercent / 100})`;
4152
- }
4153
- /**
4154
- * Converts a theme color token to a CSS variable reference.
4155
- * Note: This returns the base variable without rgb() wrapper, ignoring opacity.
4156
- *
4157
- * @param token - Theme color token
4158
- * @returns CSS variable reference
4159
- *
4160
- * @example
4161
- * cssVarValue('primary') // => 'var(--tb-primary)'
4162
- * cssVarValue('surface/95') // => 'var(--tb-surface)' (opacity ignored)
4163
- */
4164
- function cssVarValue(token) {
4165
- const { token: baseToken } = parseToken(token);
4166
- return `var(${COLOR_VAR_PREFIX}${baseToken})`;
4167
- }
4168
- /**
4169
- * Creates a backgroundColor style object from a theme token.
4170
- *
4171
- * @param token - Theme color token
4172
- * @returns CSSProperties with backgroundColor
4173
- */
4174
- function backgroundColorStyle(token) {
4175
- return { backgroundColor: rgbColorValue(token) };
4176
- }
4177
- /**
4178
- * Creates a text color style object from a theme token.
4179
- *
4180
- * @param token - Theme color token
4181
- * @returns CSSProperties with color
4182
- */
4183
- function textColorStyle(token) {
4184
- const parsed = parseToken(token);
4185
- const surfaceRole = parsed.raw ? null : surfaceRoleForTextToken(parsed.token);
4186
- if (surfaceRole) {
4187
- return { color: parsed.opacityPercent === undefined ? surfaceForegroundValue(surfaceRole) : surfaceForegroundValueWithOpacity(surfaceRole, parsed.opacityPercent) };
4188
- }
4189
- return { color: rgbColorValue(token) };
4190
- }
4191
- /**
4192
- * Creates a border color style object from a theme token.
4193
- *
4194
- * @param token - Theme color token
4195
- * @returns CSSProperties with borderColor
4196
- */
4197
- function borderColorStyle(token) {
4198
- return { borderColor: rgbColorValue(token) };
4199
- }
4200
- /**
4201
- * Creates a linear gradient background from theme tokens.
4202
- *
4203
- * @param tokens - Array of theme color tokens for gradient stops
4204
- * @param direction - CSS gradient direction (default: 'to bottom')
4205
- * @returns CSSProperties with backgroundImage
4206
- *
4207
- * @example
4208
- * gradientBackgroundStyle(['primary', 'secondary'])
4209
- * // => { backgroundImage: 'linear-gradient(to bottom, rgb(var(--tb-primary)), rgb(var(--tb-secondary)))' }
4210
- *
4211
- * @example
4212
- * gradientBackgroundStyle(['primary/80', 'secondary/60'], '45deg')
4213
- * // => { backgroundImage: 'linear-gradient(45deg, rgb(var(--tb-primary) / 0.8), rgb(var(--tb-secondary) / 0.6))' }
4214
- */
4215
- function gradientBackgroundStyle(tokens, direction = "to bottom") {
4216
- const stops = tokens.map((stop) => rgbColorValue(stop)).join(", ");
4217
- return { backgroundImage: `linear-gradient(${direction}, ${stops})` };
4218
- }
4219
- /**
4220
- * Merges multiple CSSProperties objects into a single object.
4221
- * Later styles override earlier ones. Undefined values are filtered out.
4222
- * Returns undefined if the merged result is empty.
4223
- *
4224
- * @param styles - Variable number of CSSProperties objects (can be undefined)
4225
- * @returns Merged CSSProperties object, or undefined if empty
4226
- *
4227
- * @example
4228
- * mergeStyles(
4229
- * backgroundColorStyle('surface'),
4230
- * textColorStyle('text'),
4231
- * borderColorStyle('border')
4232
- * )
4233
- * // => { backgroundColor: '...', color: '...', borderColor: '...' }
4234
- */
4235
- function mergeStyles(...styles) {
4236
- const merged = {};
4237
- for (const style of styles) {
4238
- if (!style) continue;
4239
- Object.assign(merged, style);
4240
- }
4241
- return Object.keys(merged).length ? merged : undefined;
4594
+ const rootField = fieldPath.split(".")[0] ?? "";
4595
+ return isThemeV2NormalEditorRawStyleFieldName(rootField);
4242
4596
  }
4243
4597
 
4244
4598
  //#endregion
@@ -4634,8 +4988,14 @@ const headerInteractionPillPaletteValues = [
4634
4988
  "accent"
4635
4989
  ];
4636
4990
  /**
4637
- * "No pill": the nav link keeps whatever hover treatment its recipe compiled,
4638
- * and the palette step writes nothing at all (#1318 Phase 4c decision 2).
4991
+ * "No pill": a colour change on hover rather than a filled background.
4992
+ *
4993
+ * The palette step writes nothing (#1318 Phase 4c decision 2), but on a
4994
+ * foreground-adaptive style that is not the end of the story: the hover colour
4995
+ * is derived from the resolved opening foreground at render time, paired with
4996
+ * the resting colour, so `quiet` resolves to a definite colour rather than to
4997
+ * whatever brand token the recipe happened to name. See
4998
+ * `applyOpeningForeground`.
4639
4999
  *
4640
5000
  * It exists because "a colour change on hover, no filled background" was not
4641
5001
  * expressible in the vocabulary, and it is what
@@ -5221,12 +5581,6 @@ const footerVariant = z.enum([
5221
5581
  "split"
5222
5582
  ]);
5223
5583
  const footerMaxWidth = z.enum(["container", "full"]);
5224
- const footerMode = z.enum([
5225
- "default",
5226
- "blocks",
5227
- "default+blocks",
5228
- "none"
5229
- ]);
5230
5584
  const footerNavLayoutMode = z.enum([
5231
5585
  "stack",
5232
5586
  "inline",
@@ -5312,7 +5666,7 @@ const footerLogoSchema = z.object({
5312
5666
  ]).optional()
5313
5667
  });
5314
5668
  const footerSchema = z.object({
5315
- mode: footerMode.optional(),
5669
+ mode: footerModeSchema.optional(),
5316
5670
  variant: footerVariant,
5317
5671
  maxWidth: footerMaxWidth,
5318
5672
  showLogoText: z.boolean().optional(),
@@ -5935,7 +6289,7 @@ const BUTTON_CONTRAST_POLICY = {
5935
6289
  nonTextMinimumRatio: 3,
5936
6290
  gradientSamplesPerSegment: 20,
5937
6291
  alphaBackgrounds: "reject-without-known-surface",
5938
- disabled: "retain-full-colour-contract"
6292
+ disabled: "validated-colour-pair-with-muted-control-opacity"
5939
6293
  };
5940
6294
  function formatButtonColourContractError(error) {
5941
6295
  switch (error.tag) {
@@ -6560,6 +6914,26 @@ function parseThemeForPersistence(value) {
6560
6914
  };
6561
6915
  }
6562
6916
 
6917
+ //#endregion
6918
+ //#region ../theme-core/src/buttons/affordanceSuppression.ts
6919
+ /**
6920
+ * Internal: the opt-out the generated affordance rule is written against.
6921
+ *
6922
+ * A button style may declare a trailing `affordance`, which the theme renders as
6923
+ * a CSS `::after` on every button of that variant. An authored button that
6924
+ * renders its own trailing icon must not also receive the theme's, or it shows
6925
+ * two.
6926
+ *
6927
+ * Deliberately not exported from `./index`. This used to be public, and two
6928
+ * block components reached past the theme to draw arrows of their own — one of
6929
+ * them doubling up on the styles that declare an affordance, both of them
6930
+ * overriding the twelve styles that deliberately declare none. Whether a button
6931
+ * carries a trailing affordance is the style's decision; the only caller that
6932
+ * legitimately opts out is the authored-button renderer, which reaches it
6933
+ * through `themeButtonClassNameWithTrailingIcon`.
6934
+ */
6935
+ const AFFORDANCE_SUPPRESS_CLASS = "rb-affordance-off";
6936
+
6563
6937
  //#endregion
6564
6938
  //#region ../theme-core/src/buttons/classNames.ts
6565
6939
  /**
@@ -6604,6 +6978,27 @@ function themeButtonClassName(spec) {
6604
6978
  ];
6605
6979
  return classes.filter(Boolean).join(" ");
6606
6980
  }
6981
+ /**
6982
+ * Button classes for an authored button that renders its own trailing icon.
6983
+ *
6984
+ * A separate function rather than a flag on `themeButtonClassName`, because
6985
+ * there is exactly one caller and it should stay visible in a diff. The name
6986
+ * states what the caller knows to be true of its own markup — that it renders a
6987
+ * trailing icon — rather than what the theme does about it. The consequence is
6988
+ * the theme's to decide and to change; a caller naming the consequence would be
6989
+ * asserting a policy it does not own, and would quietly become a lie the moment
6990
+ * that policy moved.
6991
+ *
6992
+ * Do not reach for this to remove an arrow you simply do not want. A trailing
6993
+ * affordance is declared by the button style, and a block overriding that is the
6994
+ * defect this replaced.
6995
+ */
6996
+ function themeButtonClassNameWithTrailingIcon(spec) {
6997
+ return themeButtonClassName({
6998
+ ...spec,
6999
+ extraClassName: [spec.extraClassName, AFFORDANCE_SUPPRESS_CLASS].filter(Boolean).join(" ")
7000
+ });
7001
+ }
6607
7002
 
6608
7003
  //#endregion
6609
7004
  //#region ../theme-core/src/interactive/generateBaseCSS.ts
@@ -8856,27 +9251,20 @@ function resolvedButtonFocusDeclarations(indicator, role) {
8856
9251
  ] : [["outline", value("outline", `3px solid ${indicator.colorCss}`)], ["outline-offset", value("outline-offset", "3px")]];
8857
9252
  }
8858
9253
  /**
8859
- * Disabled buttons retain their validated colour pair and add a non-layout
8860
- * visual affordance. `currentColor` keeps the cue inside the same contrast
8861
- * relationship, while retaining pointer events lets the not-allowed cursor
8862
- * remain observable.
9254
+ * Disabled buttons are deliberately muted as unavailable controls. Native
9255
+ * disabled controls are exempt from active-control contrast requirements;
9256
+ * opacity distinguishes the state without introducing a focus-like outline.
9257
+ * Retaining pointer events lets the not-allowed cursor remain observable.
8863
9258
  */
8864
9259
  const RESOLVED_BUTTON_DISABLED_DECLARATIONS = [
8865
9260
  ["cursor", "not-allowed"],
8866
9261
  ["transform", "none"],
8867
9262
  ["box-shadow", "none"],
8868
- ["outline", "2px dashed currentColor"],
8869
- ["outline-offset", "-4px"]
9263
+ ["opacity", "0.55"]
8870
9264
  ];
8871
9265
 
8872
9266
  //#endregion
8873
9267
  //#region ../theme-core/src/buttons/generateButtonCss.ts
8874
- /**
8875
- * Class the `button` runtime node adds when it renders its own trailing icon,
8876
- * so a node-level icon suppresses the theme's CSS `::after` affordance.
8877
- * Shared so generator and renderer can't drift.
8878
- */
8879
- const AFFORDANCE_SUPPRESS_CLASS = "rb-affordance-off";
8880
9268
  const BUTTON_SIZE_NAMES = buttonSizeNameSchema.options;
8881
9269
  /**
8882
9270
  * Letter spacing token → CSS value.
@@ -15999,198 +16387,6 @@ function assertUniqueLayoutProfileKeys(profiles) {
15999
16387
  }
16000
16388
  }
16001
16389
 
16002
- //#endregion
16003
- //#region ../theme-core/src/tokens/tokens.ts
16004
- const colorTokens = [
16005
- "background/base",
16006
- "background/elevated",
16007
- "surface/base",
16008
- "surface/subtle",
16009
- "surface/inverted",
16010
- "content/primary",
16011
- "content/secondary",
16012
- "accent/primary",
16013
- "accent/secondary",
16014
- "border/base"
16015
- ];
16016
- const typographyTokens = [
16017
- "display/hero",
16018
- "display/section",
16019
- "heading/large",
16020
- "heading/medium",
16021
- "heading/small",
16022
- "body/normal",
16023
- "body/small",
16024
- "caption"
16025
- ];
16026
- const spacingTokens = [
16027
- "none",
16028
- "3xs",
16029
- "2xs",
16030
- "xs",
16031
- "sm",
16032
- "md",
16033
- "lg",
16034
- "xl",
16035
- "2xl",
16036
- "3xl"
16037
- ];
16038
- const radiusTokens = [
16039
- "none",
16040
- "xs",
16041
- "sm",
16042
- "md",
16043
- "lg",
16044
- "pill",
16045
- "full"
16046
- ];
16047
- const shadowTokens = [
16048
- "none",
16049
- "soft",
16050
- "medium",
16051
- "strong"
16052
- ];
16053
- const defaultThemeTokens = {
16054
- color: {
16055
- "background/base": "#ffffff",
16056
- "background/elevated": "#f5f5f5",
16057
- "surface/base": "#ffffff",
16058
- "surface/subtle": "#f9f9fb",
16059
- "surface/inverted": "#111111",
16060
- "content/primary": "#111111",
16061
- "content/secondary": "#3f3f46",
16062
- "accent/primary": "#2563eb",
16063
- "accent/secondary": "#1d4ed8",
16064
- "border/base": "#e5e7eb"
16065
- },
16066
- typography: {
16067
- "display/hero": {
16068
- fontSize: "clamp(48px, 8vw, 96px)",
16069
- lineHeight: "1.05",
16070
- fontWeight: "700"
16071
- },
16072
- "display/section": {
16073
- fontSize: "clamp(40px, 6vw, 72px)",
16074
- lineHeight: "1.1",
16075
- fontWeight: "700"
16076
- },
16077
- "heading/large": {
16078
- fontSize: "clamp(32px, 4vw, 48px)",
16079
- lineHeight: "1.2",
16080
- fontWeight: "600"
16081
- },
16082
- "heading/medium": {
16083
- fontSize: "clamp(24px, 3vw, 32px)",
16084
- lineHeight: "1.25",
16085
- fontWeight: "600"
16086
- },
16087
- "heading/small": {
16088
- fontSize: "20px",
16089
- lineHeight: "1.3",
16090
- fontWeight: "600"
16091
- },
16092
- "body/normal": {
16093
- fontSize: "16px",
16094
- lineHeight: "1.6",
16095
- fontWeight: "400"
16096
- },
16097
- "body/small": {
16098
- fontSize: "14px",
16099
- lineHeight: "1.5",
16100
- fontWeight: "400"
16101
- },
16102
- caption: {
16103
- fontSize: "12px",
16104
- lineHeight: "1.4",
16105
- fontWeight: "500"
16106
- }
16107
- },
16108
- spacing: {
16109
- none: "0px",
16110
- "3xs": "4px",
16111
- "2xs": "8px",
16112
- xs: "12px",
16113
- sm: "16px",
16114
- md: "24px",
16115
- lg: "32px",
16116
- xl: "40px",
16117
- "2xl": "56px",
16118
- "3xl": "72px"
16119
- },
16120
- radius: {
16121
- none: "0px",
16122
- xs: "2px",
16123
- sm: "4px",
16124
- md: "8px",
16125
- lg: "12px",
16126
- pill: "999px",
16127
- full: "9999px"
16128
- },
16129
- shadow: {
16130
- none: "none",
16131
- soft: "0px 2px 8px rgba(15, 23, 42, 0.08)",
16132
- medium: "0px 6px 18px rgba(15, 23, 42, 0.12)",
16133
- strong: "0px 12px 36px rgba(15, 23, 42, 0.16)"
16134
- },
16135
- palette: {
16136
- surface: "#ffffff",
16137
- "neutral-50": "#f9fafb",
16138
- "neutral-900": "#111827",
16139
- primary: "#2563eb",
16140
- "primary-50": "#eff6ff",
16141
- "primary-500": "#2563eb",
16142
- "primary-900": "#1d4ed8",
16143
- secondary: "#9333ea",
16144
- "secondary-50": "#f3e8ff",
16145
- "secondary-500": "#9333ea",
16146
- "secondary-900": "#4c1d95",
16147
- accent: "#f97316",
16148
- "accent-50": "#fff7ed",
16149
- "accent-500": "#f97316",
16150
- "accent-900": "#7c2d12"
16151
- }
16152
- };
16153
- /**
16154
- * Project the expanded semantic palette into the token buckets consumed by
16155
- * runtime transforms. Keep this mapping shared by full themes and the
16156
- * tokens-only hosted-booking runtime so the two paths cannot drift.
16157
- */
16158
- function buildThemeTokens(palette) {
16159
- const tokens = cloneThemeTokens(defaultThemeTokens);
16160
- const paletteStrings = {};
16161
- for (const [key, value] of Object.entries(palette)) {
16162
- if (typeof value === "string" && value.trim().length > 0) {
16163
- paletteStrings[key] = value;
16164
- }
16165
- }
16166
- tokens.palette = paletteStrings;
16167
- const resolveColor = (...keys) => {
16168
- for (const key of keys) {
16169
- const value = paletteStrings[key];
16170
- if (value) return value;
16171
- }
16172
- return undefined;
16173
- };
16174
- const assignColor = (token, ...keys) => {
16175
- const value = resolveColor(...keys);
16176
- if (value) tokens.color[token] = value;
16177
- };
16178
- assignColor("background/base", "background", "surface", "primary-50");
16179
- assignColor("background/elevated", "surface", "background");
16180
- assignColor("surface/base", "surface", "background");
16181
- assignColor("surface/subtle", "neutral-100", "surface", "background");
16182
- assignColor("surface/inverted", "neutral-900", "text", "primary-900");
16183
- assignColor("content/primary", "text", "body", "neutral-900");
16184
- assignColor("content/secondary", "mutedText", "secondary", "neutral-600");
16185
- assignColor("accent/primary", "primary", "accent");
16186
- assignColor("accent/secondary", "accent", "secondary", "primary-400");
16187
- assignColor("border/base", "border", "neutral-300");
16188
- return tokens;
16189
- }
16190
- function cloneThemeTokens(source) {
16191
- return JSON.parse(JSON.stringify(source));
16192
- }
16193
-
16194
16390
  //#endregion
16195
16391
  //#region ../theme-core/src/site-styles/backgroundAppearance.ts
16196
16392
  const backgroundAppearanceQuickColorTokenNames = [
@@ -28373,6 +28569,10 @@ function compareCompiledHeaders(left, right) {
28373
28569
 
28374
28570
  //#endregion
28375
28571
  //#region ../theme-core/src/site-styles/headerOpeningForeground.ts
28572
+ /** The nav-link hover effect that is a plain colour change. */
28573
+ const TEXT_COLOR_CHANGE_EFFECT_ID = "text-color-change";
28574
+ /** The nav-link hover affordance that does not depend on a colour delta. */
28575
+ const NAV_UNDERLINE_EFFECT_ID = "nav-underline";
28376
28576
  function headerStyleAvailability(recipe, context) {
28377
28577
  if (!recipe.openingForeground) return { kind: "available" };
28378
28578
  return context?.openingForeground?.kind === "known" ? { kind: "available" } : {
@@ -28399,10 +28599,7 @@ function applyOpeningForeground(theme, header, recipe, context) {
28399
28599
  return {
28400
28600
  ...withBrandForeground,
28401
28601
  navColor: foreground.roles.text,
28402
- navLinkStyle: header.navLinkStyle ? {
28403
- ...header.navLinkStyle,
28404
- colorToken: foreground.roles.link
28405
- } : header.navLinkStyle,
28602
+ navLinkStyle: header.navLinkStyle ? adaptNavLinkStyleToForeground(header.navLinkStyle, foreground.roles.link, planForegroundHover(foreground.roles.link, paletteHexForToken(theme, foreground.roles.link), scrollTreatment?.kind === "contrast-glass" ? scrollTreatment.backgroundTone : null)) : header.navLinkStyle,
28406
28603
  border: {
28407
28604
  style: "solid",
28408
28605
  width: "thin",
@@ -28412,6 +28609,79 @@ function applyOpeningForeground(theme, header, recipe, context) {
28412
28609
  ...scrollTreatment ? { scrollTreatment } : {}
28413
28610
  };
28414
28611
  }
28612
+ /**
28613
+ * Give the resolved foreground both the resting and the hover colour.
28614
+ *
28615
+ * Adapting the resting colour alone left hover as whatever the recipe wrote —
28616
+ * for `minimal-surface` a bare `primary`, which is a brand colour no one had
28617
+ * checked against the section the header floats over. On a primary-painted
28618
+ * opening section that resolves to the section's own colour, and the link
28619
+ * disappears on hover.
28620
+ *
28621
+ * The two colours are a pair: the scroll treatment picks its glass tone from
28622
+ * the resting foreground precisely so one colour reads both against the opening
28623
+ * section and against the glass. A hover colour outside that pairing is not
28624
+ * covered by either half of it.
28625
+ *
28626
+ * A pill palette is left alone. It owns hover through `background-on-hover`,
28627
+ * which carries its own paired background and foreground tokens, so hover
28628
+ * legibility there does not depend on the surface underneath at all.
28629
+ */
28630
+ function adaptNavLinkStyleToForeground(navLinkStyle, linkToken, hoverPlan) {
28631
+ const adapted = {
28632
+ ...navLinkStyle,
28633
+ colorToken: linkToken
28634
+ };
28635
+ const hoverEffects = navLinkStyle.effects?.hover ?? [];
28636
+ const hasColorChange = hoverEffects.some(isTextColorChangeEffect);
28637
+ if (!hasColorChange && hoverEffects.length > 0) return adapted;
28638
+ if (!hasColorChange && !navLinkStyle.hoverColorToken) return adapted;
28639
+ const underline = {
28640
+ effectId: NAV_UNDERLINE_EFFECT_ID,
28641
+ options: {
28642
+ style: "grow",
28643
+ colorToken: linkToken
28644
+ }
28645
+ };
28646
+ const withoutColorChange = hoverEffects.filter((effect) => !isTextColorChangeEffect(effect) && !isNavUnderlineEffect(effect));
28647
+ if (hoverPlan.kind === "resting") {
28648
+ return {
28649
+ ...adapted,
28650
+ hoverColorToken: null,
28651
+ effects: {
28652
+ ...adapted.effects,
28653
+ hover: [...withoutColorChange, underline]
28654
+ }
28655
+ };
28656
+ }
28657
+ const hoverColorToken = hoverPlan.token;
28658
+ return {
28659
+ ...adapted,
28660
+ hoverColorToken,
28661
+ effects: {
28662
+ ...adapted.effects,
28663
+ hover: [...hoverEffects.map((effect) => isTextColorChangeEffect(effect) ? {
28664
+ ...effect,
28665
+ options: {
28666
+ ...effect.options,
28667
+ hoverColorToken
28668
+ }
28669
+ } : effect).filter((effect) => !isNavUnderlineEffect(effect)), underline]
28670
+ }
28671
+ };
28672
+ }
28673
+ function isTextColorChangeEffect(effect) {
28674
+ return effect.effectId === TEXT_COLOR_CHANGE_EFFECT_ID;
28675
+ }
28676
+ function isNavUnderlineEffect(effect) {
28677
+ return effect.effectId === NAV_UNDERLINE_EFFECT_ID;
28678
+ }
28679
+ /** Concrete hex behind a plain palette token, or null when it is not one. */
28680
+ function paletteHexForToken(theme, token) {
28681
+ const parts = parseColorTokenParts(token);
28682
+ if (parts.kind !== "token" || parts.opacityPercent !== null) return null;
28683
+ return expandPalette(theme.palette)[parts.token] ?? null;
28684
+ }
28415
28685
  function compileHeaderScrollTreatment(theme, recipe, foregroundToken) {
28416
28686
  if (recipe.scrollTreatment !== "contrast-glass") return undefined;
28417
28687
  const tokenParts = parseColorTokenParts(foregroundToken);
@@ -28458,6 +28728,31 @@ function applyPageOpeningForegroundToTheme(theme, recipe, context) {
28458
28728
  };
28459
28729
  }
28460
28730
 
28731
+ //#endregion
28732
+ //#region ../theme-core/src/site-styles/effectivePageDesign.ts
28733
+ /**
28734
+ * Every page with an applicable site style has an effective design, whether or
28735
+ * not a design version was ever persisted. Missing persistence is ordinary
28736
+ * default resolution, not a failure.
28737
+ *
28738
+ * Corrupt persisted state never reaches this function: callers keep their own
28739
+ * invalid state untouched and only resolve `stored` values they could parse.
28740
+ */
28741
+ function resolveEffectivePageDesignIntent(stored, currentSiteStyleId) {
28742
+ if (stored) {
28743
+ return {
28744
+ kind: "resolved",
28745
+ intent: currentSiteStyleId ? rebasePageDesignIntentToSiteStyle(stored, currentSiteStyleId) : stored,
28746
+ source: "stored"
28747
+ };
28748
+ }
28749
+ return currentSiteStyleId ? {
28750
+ kind: "resolved",
28751
+ intent: buildInitialPolicyPageDesignIntent(currentSiteStyleId),
28752
+ source: "default"
28753
+ } : { kind: "no-site-style" };
28754
+ }
28755
+
28461
28756
  //#endregion
28462
28757
  //#region ../theme-core/src/site-styles/pageDesignAuthority.ts
28463
28758
  const nonEmptyStringSchema = z.string().min(1);
@@ -32553,4 +32848,4 @@ function parseSdkEventRunUpsertInput(input, runIndex, options) {
32553
32848
  }
32554
32849
 
32555
32850
  //#endregion
32556
- export { defineBlockDesignMetadata as $, surfaceForegroundDeclarations as $n, borderColorStyle as $t, defaultPaletteForSiteStyle as A, NAV_LINK_TEXT_SIZE_MAP as An, __toESM as Ar, themeSchema as At, parsePersistedHeaderLayoutId as B, ICON_SIZE_PX as Bn, BREAKPOINT_SM as Bt, retargetCompiledBoundaryRuntimeForContentAdjacency as C, getComponentShadowVar as Cn, createFormRef as Cr, themeButtonClassName as Ct, curatedSiteStyles as D, NAV_LINK_LETTER_SPACING_MAP as Dn, createVenueRef as Dr, generateShadowValue as Dt, concretePaletteToTheme as E, NAV_LINK_BORDER_RADIUS_MAP as En, createSiteCommandSiteRef as Er, formatButtonColourContractError as Et, findFooterLook as F, ACCORDION_OPEN_ACCENT_THICKNESS as Fn, generateMobileOverlayBackdropFilterFix as Ft, parseGradientPresetId as G, isOnColorBackgroundCandidateToken as Gn, MQ_SM_UP as Gt, gradientToCss as H, getDefaultCardVariants as Hn, MQ_LG_UP as Ht, guaranteedFallbackFooterLookId as I, ACCORDION_SPACING_MAP as In, getHeaderPaintTargets as It, backgroundAppearanceColorTokenNames as J, contrastRatio as Jn, isCssColorLiteral as Jt, findCuratedPalette as K, onColorTokenName as Kn, MQ_TABLET_ONLY as Kt, parsePersistedFooterLookId as L, ACCORDION_TRIGGER_FONT_SCALE as Ln, renderHeaderPaintTargetRules as Lt, headerLayoutChoicesForSiteStyle as M, INPUT_FONT_SIZE_MAP as Mn, boxBackgroundOverlaySchema as Mt, headerStyleChoicesForLayout as N, INPUT_SPACING_MAP as Nn, boxBackgroundSchema as Nt, defaultButtonStyleSelectionForSiteStyle as O, NAV_LINK_PADDING_MAP as On, refSlug as Or, expandPalette as Ot, compileTransitionRuntime as P, LABEL_FONT_SIZE_MAP as Pn, generateHeaderScrollTreatmentStyles as Pt, defaultThemeTokens as Q, DEFAULT_SURFACE_TEXT_COLOR as Qn, backgroundColorStyle as Qt, getHeaderLayout as R, ACCORDION_TRIGGER_LETTER_SPACING as Rn, BREAKPOINT_LG as Rt, eventBookingOfferIntentSchema as S, getComponentBorderWidthPx as Sn, createFooterRef as Sr, parseThemeButtonVariantId as St, readHeaderNavFontSizePreference as T, parseCustomCssRules as Tn, createPageRef as Tr, compileButtonColourStates as Tt, GRADIENT_PRESET_IDS as U, generateCardCss as Un, MQ_MD_DOWN as Ut, parsePersistedHeaderStyleId as V, generateCardCoreCss as Vn, BREAKPOINT_XL as Vt, getGradientPreset as W, resolveSurfaceForeground as Wn, MQ_SM_DOWN as Wt, parseBackgroundAppearanceColorToken as X, hexToRgb as Xn, resolveThemeColorReferenceValue as Xt, backgroundAppearanceQuickColorTokenNames as Y, featuresFromHex as Yn, resolveOnColorForThemeToken as Yt, buildThemeTokens as Z, oklchToHexGamut as Zn, resolveTokenOrCssColorValue as Zt, entryIdentifierForCommand as _, makeBoundaryId as _n, createBlocksRef as _r, resolveContextualButtonRole as _t, classifySdkEventBookingOffer as a, asAppearancePresetId as an, cardVariantRoleValues as ar, buttonStyleSelectionSchema as at, pagePathForCommand as b, tileTreatmentValues as bn, createEventCategoryRef as br, generateBaseInteractiveCSS as bt, stableStringify as c, assertNever$1 as cn, mediaIdentifierSchema as cr, PUBLIC_BUTTON_ROLES as ct, mapEventCommandContent as d, defineNoBlockDesignCapability as dn, persistableMediaSchema as dr, RESOLVED_BUTTON_DISABLED_DECLARATIONS as dt, cssVarValue as en, surfaceForegroundStyle as er, require_jsx_runtime as et, evaluateSiteCommandBatchExposure as f, eventListTreatmentValues as fn, VARIANT_ROLES as fr, resolvedButtonColourDeclarations as ft, blockOrderIdentifiersForCommand as g, introCompositionTreatmentValues as gn, createBlockRef as gr, deriveButtonSurfaceContext as gt, blockIdentifierForCommand as h, headingDecorationColorTokenValues as hn, parseBlockColorToken as hr, contextualButtonSurfaceStyles as ht, classifySdkEventSchedule as i, pageDesignEditorBackgroundOverrideOwner as in, cardRoleSchema as ir, findButtonStyleRecipe as it, getCuratedSiteStyle as j, FONT_WEIGHT_MAP$1 as jn, typographySchema as jt, defaultHeaderSelectionForSiteStyle as k, NAV_LINK_PADDING_X_MAP as kn, slugPart as kr, getTextColorForBackground as kt, createDryRunReport as l, defineBlockDesignCapability as ln, mediaSchema as lr, AFFORDANCE_SUPPRESS_CLASS as lt, getSiteCommandMetadata as m, featureCardCompositionTreatmentValues as mn, unsafeAsOpaqueButtonColourToken as mr, contextualButtonSurfaceResetStyles as mt, sdkEventOccurrenceInputSchema as n, rgbColorValue as nn, TokenResolver as nr, pickShadeTowardBackground as nt, venueManagementFields as o, asDesignBlockId as on, normalizeCardRole as or, createDefaultButtonStyleSelection as ot, evaluateSiteCommandExposure as p, eyebrowTreatmentValues as pn, buttonVariantIdSchema as pr, resolvedButtonFocusDeclarations as pt, boundaryRuntimeMetricsForTransition as q, ORDINARY_TEXT_MIN_CONTRAST_RATIO as qn, headerVariantValues as qt, sdkEventUpsertOptionalFieldSchemas as r, textColorStyle as rn, generateShadowValue$1 as rr, generateCssVars as rt, sortRecord as s, asSiteStyleId as sn, paletteSchema as sr, isButtonStyleId as st, classifySdkEventTiming as t, mergeStyles as tn, surfaceForegroundValue as tr, require_react as tt, parseAndPlanCommandBatchForTypes as u, defineFrameCapability as un, mediaTypeDiscriminants as ur, generateButtonCss as ut, pageIdentifierForBlockCommand as v, offeringsPreviewCompositionTreatmentValues as vn, createCommandRef as vr, generateDefaultButtonSystem as vt, mergeHeaderNavTypographyPreference as w, parseBlockCustomCssRules as wn, createNavigationRef as wr, parseThemeForPersistence as wt, hasConfiguredBookingVertical as x, visibleHeadingDecorationFamilyValues as xn, createEventSeriesRef as xr, generateInteractiveChildInheritanceCSS as xt, pageIdentifierForCommand as y, testimonialsCompositionTreatmentValues as yn, createContentEntryRef as yr, generateEffectsCSS as yt, getHeaderStyle as z, ICON_CONFIG as zn, BREAKPOINT_MD as zt };
32851
+ export { generateBaseInteractiveCSS as $, createContentEntryRef as $n, NAV_LINK_LETTER_SPACING_MAP as $t, getGradientPreset as A, featuresFromHex as An, resolveTokenOrCssColorValue as At, generateCssVars as B, normalizeCardRole as Bn, featureCardCompositionTreatmentValues as Bt, retargetCompiledBoundaryRuntimeForContentAdjacency as C, resolveSurfaceForeground as Cn, MQ_SM_DOWN as Ct, compileTransitionRuntime as D, defaultThemeTokens as Dn, isCssColorLiteral as Dt, getCuratedSiteStyle as E, buildThemeTokens as En, headerVariantValues as Et, parseBackgroundAppearanceColorToken as F, surfaceForegroundStyle as Fn, defineBlockDesignCapability as Ft, resolvedButtonColourDeclarations as G, footerModeSchema as Gn, testimonialsCompositionTreatmentValues as Gt, PUBLIC_BUTTON_ROLES as H, mediaSchema as Hn, introCompositionTreatmentValues as Ht, defineBlockDesignMetadata as I, surfaceForegroundValue as In, defineFrameCapability as It, contextualButtonSurfaceStyles as J, unsafeAsOpaqueButtonColourToken as Jn, getComponentBorderWidthPx as Jt, resolvedButtonFocusDeclarations as K, VARIANT_ROLES as Kn, tileTreatmentValues as Kt, require_jsx_runtime as L, generateShadowValue$1 as Ln, defineNoBlockDesignCapability as Lt, boundaryRuntimeMetricsForTransition as M, oklchToHexGamut as Mn, asAppearancePresetId as Mt, backgroundAppearanceColorTokenNames as N, DEFAULT_SURFACE_TEXT_COLOR as Nn, asDesignBlockId as Nt, gradientToCss as O, ORDINARY_TEXT_MIN_CONTRAST_RATIO as On, resolveOnColorForThemeToken as Ot, backgroundAppearanceQuickColorTokenNames as P, surfaceForegroundDeclarations as Pn, assertNever$1 as Pt, generateEffectsCSS as Q, createCommandRef as Qn, NAV_LINK_BORDER_RADIUS_MAP as Qt, require_react as R, cardRoleSchema as Rn, eventListTreatmentValues as Rt, eventBookingOfferIntentSchema as S, textColorStyle as Sn, MQ_MD_DOWN as St, readHeaderNavFontSizePreference as T, onColorTokenName as Tn, MQ_TABLET_ONLY as Tt, generateButtonCss as U, mediaTypeDiscriminants as Un, makeBoundaryId as Ut, buttonStyleSelectionSchema as V, mediaIdentifierSchema as Vn, headingDecorationColorTokenValues as Vt, RESOLVED_BUTTON_DISABLED_DECLARATIONS as W, persistableMediaSchema as Wn, offeringsPreviewCompositionTreatmentValues as Wt, resolveContextualButtonRole as X, createBlockRef as Xn, parseBlockCustomCssRules as Xt, deriveButtonSurfaceContext as Y, parseBlockColorToken as Yn, getComponentShadowVar as Yt, generateDefaultButtonSystem as Z, createBlocksRef as Zn, parseCustomCssRules as Zt, entryIdentifierForCommand as _, backgroundColorStyle as _n, BREAKPOINT_LG as _t, classifySdkEventBookingOffer as a, INPUT_SPACING_MAP as an, createPageRef as ar, compileButtonColourStates as at, pagePathForCommand as b, mergeStyles as bn, BREAKPOINT_XL as bt, stableStringify as c, ACCORDION_SPACING_MAP as cn, refSlug as cr, expandPalette as ct, mapEventCommandContent as d, ICON_CONFIG as dn, boxBackgroundOverlaySchema as dt, NAV_LINK_PADDING_MAP as en, createEventCategoryRef as er, generateInteractiveChildInheritanceCSS as et, evaluateSiteCommandBatchExposure as f, ICON_SIZE_PX as fn, boxBackgroundSchema as ft, blockOrderIdentifiersForCommand as g, TokenResolver as gn, renderHeaderPaintTargetRules as gt, blockIdentifierForCommand as h, generateCardCss as hn, getHeaderPaintTargets as ht, classifySdkEventSchedule as i, INPUT_FONT_SIZE_MAP as in, createNavigationRef as ir, parseThemeForPersistence as it, parseGradientPresetId as j, hexToRgb as jn, pageDesignEditorBackgroundOverrideOwner as jt, GRADIENT_PRESET_IDS as k, contrastRatio as kn, resolveThemeColorReferenceValue as kt, createDryRunReport as l, ACCORDION_TRIGGER_FONT_SCALE as ln, slugPart as lr, getTextColorForBackground as lt, getSiteCommandMetadata as m, getDefaultCardVariants as mn, generateMobileOverlayBackdropFilterFix as mt, sdkEventOccurrenceInputSchema as n, NAV_LINK_TEXT_SIZE_MAP as nn, createFooterRef as nr, themeButtonClassName as nt, venueManagementFields as o, LABEL_FONT_SIZE_MAP as on, createSiteCommandSiteRef as or, formatButtonColourContractError as ot, evaluateSiteCommandExposure as p, generateCardCoreCss as pn, generateHeaderScrollTreatmentStyles as pt, contextualButtonSurfaceResetStyles as q, buttonVariantIdSchema as qn, visibleHeadingDecorationFamilyValues as qt, sdkEventUpsertOptionalFieldSchemas as r, FONT_WEIGHT_MAP$1 as rn, createFormRef as rr, themeButtonClassNameWithTrailingIcon as rt, sortRecord as s, ACCORDION_OPEN_ACCENT_THICKNESS as sn, createVenueRef as sr, generateShadowValue as st, classifySdkEventTiming as t, NAV_LINK_PADDING_X_MAP as tn, createEventSeriesRef as tr, parseThemeButtonVariantId as tt, parseAndPlanCommandBatchForTypes as u, ACCORDION_TRIGGER_LETTER_SPACING as un, __toESM as ur, themeSchema as ut, pageIdentifierForBlockCommand as v, borderColorStyle as vn, BREAKPOINT_MD as vt, mergeHeaderNavTypographyPreference as w, isOnColorBackgroundCandidateToken as wn, MQ_SM_UP as wt, hasConfiguredBookingVertical as x, rgbColorValue as xn, MQ_LG_UP as xt, pageIdentifierForCommand as y, cssVarValue as yn, BREAKPOINT_SM as yt, pickShadeTowardBackground as z, cardVariantRoleValues as zn, eyebrowTreatmentValues as zt };