{"version":3,"file":"create-theme.cjs","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — one pass that emits the whole token set — surfaces,\n * text, borders, states, the chart ramps — for both modes, with every value checked\n * against the surface it will sit on. The checks are what make the length: each\n * token is derived and then verified against the same background table.\n */\n/**\n * Theme factory: turns a handful of brand colors into the full set of\n * `--tempest-*` token overrides, for both light and dark.\n *\n * Rebranding used to mean hand-writing ~167 custom properties (and getting the\n * dark ramp inversion right by hand). `createTheme({ primary: \"#7c3aed\" })`\n * emits the same thing, derived in OKLCH so the ramp stays perceptually even.\n */\nimport {\n    contrastRatio,\n    createColorScale,\n    hexToRgbaString,\n    readableForeground,\n    type ColorScale,\n    type ScaleStep,\n} from \"./color\";\nimport { buildDivergingRamp, buildRamp, hueOf } from \"./data-viz-ramps\";\nimport { isDevBuild } from \"../utils/dev-mode\";\n\n/** Radius presets, applied to the whole `--tempest-radius-*` family at once. */\nexport type ThemeRadius = \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\n/** Status token families that {@link createTheme} can regenerate. */\nexport type ThemeStatus = \"success\" | \"warning\" | \"danger\" | \"info\";\n\n/** Input for {@link createTheme}. Every field is optional — omitted families keep the built-in tokens. */\nexport interface CreateThemeOptions {\n    /** Brand color, used as the `500` step of the primary scale. */\n    primary?: string;\n    /** Neutral color, used as the `500` step of the gray scale (surfaces, borders, text). */\n    gray?: string;\n    /** Success color (`--tempest-success*`). */\n    success?: string;\n    /** Warning color (`--tempest-warning*`). */\n    warning?: string;\n    /** Danger color (`--tempest-danger*`). */\n    danger?: string;\n    /** Info color (`--tempest-info*`). */\n    info?: string;\n    /**\n     * Categorical series colors for `tempest-react-sdk/charts`, in cycle order.\n     * Written to `--tempest-chart-1` … `--tempest-chart-N`, so charts follow the\n     * theme instead of a hardcoded palette.\n     */\n    chart?: string[];\n    /** Corner radius scale. A preset name, or explicit per-step values. */\n    radius?: ThemeRadius | Partial<Record<\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", string>>;\n    /**\n     * Opacity of `--tempest-focus-ring-color`. Omit it — the default ring is opaque.\n     *\n     * A translucent ring has no contrast of its own; it has the contrast of\n     * whatever it composites over. Passing a value below `1` reintroduces the\n     * failure 0.61.0 fixed in the built-in tokens, and logs a warning in a\n     * development build. Kept because a translucent ring over a background you\n     * control is a legitimate choice.\n     */\n    focusRingAlpha?: number;\n    /** Selector the light tokens are written under. Default `\":root\"`. */\n    selector?: string;\n    /** Selector the dark tokens are written under. Default `'[data-tempest-theme=\"dark\"]'`. */\n    darkSelector?: string;\n}\n\n/** A generated theme: token maps per color scheme, plus the CSS text that carries them. */\nexport interface GeneratedTheme {\n    /** Light-scheme custom properties, without the leading `--`-less names (keys include `--`). */\n    light: Record<string, string>;\n    /** Dark-scheme custom properties. */\n    dark: Record<string, string>;\n    /** Both blocks rendered as CSS, ready for {@link applyTheme} or a stylesheet. */\n    css: string;\n}\n\nconst RADIUS_PRESETS: Record<ThemeRadius, Record<string, string>> = {\n    none: { xs: \"0\", sm: \"0\", md: \"0\", lg: \"0\", xl: \"0\", \"2xl\": \"0\" },\n    sm: { xs: \"1px\", sm: \"2px\", md: \"4px\", lg: \"6px\", xl: \"8px\", \"2xl\": \"12px\" },\n    md: { xs: \"2px\", sm: \"4px\", md: \"8px\", lg: \"12px\", xl: \"16px\", \"2xl\": \"24px\" },\n    lg: { xs: \"4px\", sm: \"6px\", md: \"12px\", lg: \"16px\", xl: \"22px\", \"2xl\": \"32px\" },\n    xl: { xs: \"6px\", sm: \"10px\", md: \"16px\", lg: \"24px\", xl: \"32px\", \"2xl\": \"44px\" },\n    full: { xs: \"4px\", sm: \"8px\", md: \"9999px\", lg: \"9999px\", xl: \"9999px\", \"2xl\": \"9999px\" },\n};\n\nconst SCALE_STEPS: ScaleStep[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];\n\nfunction writeScale(tokens: Record<string, string>, name: string, scale: ColorScale): void {\n    for (const step of SCALE_STEPS) {\n        tokens[`--tempest-${name}-${step}`] = scale[step];\n    }\n}\n\n/** Minimum contrast for body text, WCAG 2.x AA. */\nconst AA_TEXT_CONTRAST = 4.5;\n\n/**\n * Choose the ramp step for text sitting on the soft tint.\n *\n * A fixed step does not survive an arbitrary brand color: the built-in blue only\n * reaches 4.37:1 at `500` over its own `50` tint, and a generated emerald lands\n * at 4.41:1 at `600` — both fail AA for body text by a hair. So the step is\n * picked by measuring, walking away from the tint until it clears AA, and\n * falling back to the most extreme candidate when even that cannot (a very light\n * or very desaturated brand).\n *\n * @param scale - The generated ramp.\n * @param softStep - Step used as `--tempest-primary-soft`.\n * @param candidates - Steps to try, in order of preference.\n * @returns The first candidate clearing {@link AA_TEXT_CONTRAST}, else the last.\n */\nfunction pickOnSoftStep(\n    scale: ColorScale,\n    softStep: ScaleStep,\n    candidates: ScaleStep[],\n): ScaleStep {\n    for (const step of candidates) {\n        if (contrastRatio(scale[softStep], scale[step]) >= AA_TEXT_CONTRAST) return step;\n    }\n    return candidates[candidates.length - 1];\n}\n\n/** Minimum contrast for a non-text indicator, WCAG 2.2 SC 1.4.11. */\nconst INDICATOR_CONTRAST = 3;\n\n/**\n * The four surfaces a focus ring can land on, as the SDK's own `colors.css`\n * paints them.\n *\n * Ported from `src/styles/colors.css` (`--tempest-bg`, `--tempest-surface`,\n * `--tempest-surface-2`, `--tempest-surface-3`, resolved through the gray ramp),\n * and pinned by a test that reads that file — a theme that only names `primary`\n * keeps these, so the ring has to be measured against them and not against a\n * guess. The light values resolve through `--tempest-gray-50/100/200`; the dark\n * ones are literals in the dark block.\n */\nconst SDK_SURFACES: Record<\"light\" | \"dark\", readonly string[]> = {\n    light: [\"#ffffff\", \"#f8f9fb\", \"#f1f3f6\", \"#e4e7ec\"],\n    dark: [\"#0b0d12\", \"#14171f\", \"#1d2230\", \"#262d3f\"],\n};\n\n/**\n * Ramp steps to try for the focus ring, in order.\n *\n * `500` first, because the ring should read as the brand when it can, then up\n * the ramp — which moves away from the surfaces in **both** schemes, because\n * `createColorScale` inverts the dark ramp: there `50` is the darkest step (it\n * is what `--tempest-bg` is made of) and `900` the lightest. Walking down in\n * dark mode looks like the right inversion and is the wrong direction; measured,\n * it left a near-black brand's ring at 1.04:1 over its own background.\n */\nconst FOCUS_RING_CANDIDATES: readonly ScaleStep[] = [500, 600, 700, 800, 900];\n\n/**\n * Choose the ramp step for the focus ring, by measuring it against the surfaces\n * it will actually be drawn on.\n *\n * A fixed step does not survive an arbitrary brand, and the measurement says so\n * loudly: of twelve brands checked against the four surfaces in both schemes —\n * 96 pairings — `500` opaque clears 3:1 in 58 of them, and `500` at the old\n * default alpha of 0.35 in 3. Picking the step by measurement clears all 96. The\n * brand from the report that opened this\n * (`#8100D7`) passes in light at 7.20:1 and fails in **dark** at 2.70:1 down to\n * 1.91:1 on `surface-3`; a yellow fails in light at 1.43:1. So the step is\n * picked the way {@link pickOnSoftStep} picks text on a tint: walk away from the\n * surfaces until the ring clears the floor against *all* of them.\n *\n * The fallback is the candidate with the best worst case rather than the last\n * one, because \"the most extreme step\" is not always the most contrasting one\n * once a brand is near-neutral.\n *\n * @param scale - The generated ramp.\n * @param surfaces - The four surfaces this theme will paint, opaque hex.\n * @returns The first candidate clearing {@link INDICATOR_CONTRAST} on every\n *   surface, else the candidate whose weakest pairing is strongest.\n */\nfunction pickFocusRingStep(scale: ColorScale, surfaces: readonly string[]): ScaleStep {\n    const worstCase = (step: ScaleStep): number =>\n        Math.min(...surfaces.map((surface) => contrastRatio(scale[step], surface)));\n    let best: ScaleStep = FOCUS_RING_CANDIDATES[0] as ScaleStep;\n    let bestRatio = -1;\n    for (const step of FOCUS_RING_CANDIDATES) {\n        const measured = worstCase(step);\n        if (measured >= INDICATOR_CONTRAST) return step;\n        if (measured > bestRatio) {\n            best = step;\n            bestRatio = measured;\n        }\n    }\n    return best;\n}\n\n/**\n * The surfaces this theme will actually paint, per scheme.\n *\n * A theme that names `gray` repaints all four, so measuring the ring against the\n * SDK's would be measuring the wrong backdrop — and a theme that does not keeps\n * the SDK's, which is the common case. The order and the ramp steps mirror\n * {@link writeNeutralAliases}, which is what emits them.\n *\n * @param scheme - Which scheme is being emitted.\n * @param grayScales - The generated neutral ramps, when the theme names one.\n * @returns Four opaque hex colors: bg, surface, surface-2, surface-3.\n */\nfunction surfacesOf(\n    scheme: \"light\" | \"dark\",\n    grayScales: { light: ColorScale; dark: ColorScale } | undefined,\n): readonly string[] {\n    if (!grayScales) return SDK_SURFACES[scheme];\n    const scale = grayScales[scheme];\n    return scheme === \"light\"\n        ? [\"#ffffff\", scale[50], scale[100], scale[200]]\n        : [scale[50], scale[100], scale[200], scale[300]];\n}\n\n/**\n * Warn, in a development build, that an alpha was asked for the focus ring.\n *\n * A translucent ring has no contrast of its own — it has the contrast of\n * whatever it composites over — and that is the failure this option used to\n * ship by default: measured at alpha `0.35`, twelve brands across four surfaces\n * and both schemes produced 96 pairings, of which 3 cleared the 3:1 of WCAG 2.2\n * SC 1.4.11. It stays available, because a translucent ring over a\n * known background is a legitimate choice, but it is a choice a theme now has to\n * make on purpose.\n *\n * The warning is the difficult part to notice otherwise: a failing focus ring is\n * visible, it just does not separate.\n *\n * @param focusRingAlpha - The opacity the caller asked for, if any.\n */\nfunction warnOnTranslucentFocusRing(focusRingAlpha: number | undefined): void {\n    if (focusRingAlpha === undefined || focusRingAlpha >= 1) return;\n    if (!isDevBuild()) return;\n    console.warn(\n        `[tempest-react-sdk] createTheme({ focusRingAlpha: ${focusRingAlpha} }) makes the focus ring ` +\n            \"translucent, so its contrast becomes whatever it composites over — measured below the 3:1 \" +\n            \"of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque \" +\n            \"ring, which is derived from your brand ramp and measured against those surfaces.\",\n    );\n}\n\n/**\n * The dark ink for content sitting on a saturated fill.\n *\n * Near-black tinted toward warm rather than pure black, matching the value\n * `colors.css` uses for the same job: it reads as part of the swatch instead of\n * a hole punched in it.\n */\nconst ON_SOLID_INK = \"#1f0606\";\n\n/**\n * Emit the primary aliases for one scheme.\n *\n * The dark scheme walks the ramp the other way (hover is *lighter*, the soft\n * tint is a dark shade, and readable text on that tint is a light shade) — the\n * same inversion the built-in `colors.css` dark block does by hand.\n *\n * The selected-state indicator is derived from the same measurement as the focus\n * ring and emitted as its own token. It never takes `focusRingAlpha`: a\n * translucent ring over a known background is a legitimate choice a theme can\n * make, while a translucent *state* indicator has the contrast of whatever it\n * composites over, which is the defect this token exists to end.\n *\n * @param tokens - The token map for this scheme, written in place.\n * @param scale - The generated primary ramp.\n * @param scheme - Which scheme is being emitted.\n * @param surfaces - The surfaces this theme paints, for the focus-ring measurement.\n * @param focusRingAlpha - Opacity asked for the ring, or `undefined` for opaque.\n */\nfunction writePrimaryAliases(\n    tokens: Record<string, string>,\n    scale: ColorScale,\n    scheme: \"light\" | \"dark\",\n    surfaces: readonly string[],\n    focusRingAlpha: number | undefined,\n): void {\n    tokens[\"--tempest-primary\"] = \"var(--tempest-primary-500)\";\n    if (scheme === \"light\") {\n        tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-600)\";\n        tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-700)\";\n        tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-50)\";\n        tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-100)\";\n        tokens[\"--tempest-primary-on-soft\"] =\n            `var(--tempest-primary-${pickOnSoftStep(scale, 50, [600, 700, 800, 900])})`;\n    } else {\n        tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-400)\";\n        tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-300)\";\n        tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-100)\";\n        tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-200)\";\n        tokens[\"--tempest-primary-on-soft\"] =\n            `var(--tempest-primary-${pickOnSoftStep(scale, 100, [700, 800, 900])})`;\n    }\n\n    const foreground = readableForeground(scale[500]);\n    tokens[\"--tempest-primary-foreground\"] = foreground;\n    tokens[\"--tempest-text-on-primary\"] = foreground;\n    const ring = scale[pickFocusRingStep(scale, surfaces)];\n    tokens[\"--tempest-focus-ring-color\"] =\n        focusRingAlpha === undefined || focusRingAlpha >= 1\n            ? ring\n            : hexToRgbaString(ring, focusRingAlpha);\n    tokens[\"--tempest-selected-indicator\"] = ring;\n}\n\n/**\n * Emit the neutral surface/border/text aliases from a generated gray scale.\n *\n * `--tempest-bg` is pushed past the ramp on purpose: pure white in light mode and\n * a shade darker than `gray-50` in dark mode, so a raised surface still reads as\n * raised against the page.\n */\nfunction writeNeutralAliases(\n    tokens: Record<string, string>,\n    scale: ColorScale,\n    scheme: \"light\" | \"dark\",\n): void {\n    if (scheme === \"light\") {\n        tokens[\"--tempest-bg\"] = \"#ffffff\";\n        tokens[\"--tempest-surface\"] = \"var(--tempest-gray-50)\";\n        tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-100)\";\n        tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-200)\";\n        tokens[\"--tempest-border\"] = \"var(--tempest-gray-200)\";\n        tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-300)\";\n        tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n        tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-600)\";\n        tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-500)\";\n    } else {\n        tokens[\"--tempest-bg\"] = scale[50];\n        tokens[\"--tempest-surface\"] = \"var(--tempest-gray-100)\";\n        tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-200)\";\n        tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-300)\";\n        tokens[\"--tempest-border\"] = \"var(--tempest-gray-300)\";\n        tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-400)\";\n        tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n        tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-700)\";\n        tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-600)\";\n    }\n    tokens[\"--tempest-neutral-on-solid\"] = readableForeground(scale[700], \"#ffffff\", ON_SOLID_INK);\n}\n\n/**\n * Emit one status family (`--tempest-danger`, `-fg`, `-bg`, `-border`, `-solid`).\n *\n * `-fg` is the text shade over `-bg`, so it has to cross the ramp in opposite\n * directions per scheme; `-solid` stays the saturated fill used by badges.\n *\n * `-on-solid` is derived from the `-solid` this call just emitted, rather than\n * left to fall through. Falling through was the bug: the SDK's own\n * `-on-solid` values are measured against the SDK's own fills, so a brand whose\n * `danger-600` lands light kept white ink over it and nothing said so. The\n * generated neutral hit exactly that — white over a generated `gray-700` of\n * `#a8b2c6` measures 2.13:1.\n */\nfunction writeStatus(\n    tokens: Record<string, string>,\n    name: ThemeStatus,\n    scale: ColorScale,\n    scheme: \"light\" | \"dark\",\n): void {\n    if (scheme === \"light\") {\n        tokens[`--tempest-${name}`] = scale[700];\n        tokens[`--tempest-${name}-fg`] = scale[800];\n        tokens[`--tempest-${name}-bg`] = scale[50];\n        tokens[`--tempest-${name}-border`] = scale[200];\n        tokens[`--tempest-${name}-solid`] = scale[600];\n        tokens[`--tempest-${name}-on-solid`] = readableForeground(\n            scale[600],\n            \"#ffffff\",\n            ON_SOLID_INK,\n        );\n    } else {\n        tokens[`--tempest-${name}`] = scale[700];\n        tokens[`--tempest-${name}-fg`] = scale[700];\n        tokens[`--tempest-${name}-bg`] = scale[50];\n        tokens[`--tempest-${name}-border`] = scale[200];\n        tokens[`--tempest-${name}-solid`] = scale[500];\n        tokens[`--tempest-${name}-on-solid`] = readableForeground(\n            scale[500],\n            \"#ffffff\",\n            ON_SOLID_INK,\n        );\n    }\n}\n\nfunction renderBlock(selector: string, tokens: Record<string, string>): string {\n    const entries = Object.entries(tokens);\n    if (entries.length === 0) return \"\";\n    const body = entries.map(([name, value]) => `    ${name}: ${value};`).join(\"\\n\");\n    return `${selector} {\\n${body}\\n}`;\n}\n\n/**\n * Generate `--tempest-*` overrides from a small brand description.\n *\n * Only the families you pass are generated; everything else falls through to the\n * SDK's own tokens, so a theme stays a patch and not a fork of `colors.css`.\n *\n * @example\n * ```ts\n * import { applyTheme, createTheme } from \"tempest-react-sdk\";\n *\n * const theme = createTheme({\n *   primary: \"#7c3aed\",\n *   radius: \"lg\",\n *   chart: [\"#7c3aed\", \"#0ea5e9\", \"#22c55e\", \"#f59e0b\"],\n * });\n *\n * applyTheme(theme);\n * ```\n *\n * @param options - Brand colors plus optional radius / chart / focus-ring tuning.\n * @returns The light and dark token maps and the CSS text that carries them.\n */\nexport function createTheme(options: CreateThemeOptions = {}): GeneratedTheme {\n    const {\n        primary,\n        gray,\n        chart,\n        radius,\n        focusRingAlpha,\n        selector = \":root\",\n        darkSelector = '[data-tempest-theme=\"dark\"]',\n    } = options;\n\n    warnOnTranslucentFocusRing(focusRingAlpha);\n\n    const light: Record<string, string> = {};\n    const dark: Record<string, string> = {};\n\n    // `anchor: false`: a neutral ramp has to keep its tuned lightness curve.\n    // Anchoring it at the input compressed both halves and dropped\n    // text-muted-on-surface-3 to ~4.2:1 — below AA — which the browser axe\n    // sweep caught on the generated themes.\n    const grayScales = gray\n        ? {\n              light: createColorScale(gray, \"light\", { anchor: false, neutral: true }),\n              dark: createColorScale(gray, \"dark\", { anchor: false, neutral: true }),\n          }\n        : undefined;\n\n    if (primary) {\n        const lightScale = createColorScale(primary, \"light\");\n        const darkScale = createColorScale(primary, \"dark\");\n        writeScale(light, \"primary\", lightScale);\n        writeScale(dark, \"primary\", darkScale);\n        writePrimaryAliases(\n            light,\n            lightScale,\n            \"light\",\n            surfacesOf(\"light\", grayScales),\n            focusRingAlpha,\n        );\n        writePrimaryAliases(\n            dark,\n            darkScale,\n            \"dark\",\n            surfacesOf(\"dark\", grayScales),\n            focusRingAlpha,\n        );\n    }\n\n    if (grayScales) {\n        writeScale(light, \"gray\", grayScales.light);\n        writeScale(dark, \"gray\", grayScales.dark);\n        writeNeutralAliases(light, grayScales.light, \"light\");\n        writeNeutralAliases(dark, grayScales.dark, \"dark\");\n    }\n\n    for (const status of [\"success\", \"warning\", \"danger\", \"info\"] as const) {\n        const value = options[status];\n        if (!value) continue;\n        writeStatus(light, status, createColorScale(value, \"light\"), \"light\");\n        writeStatus(dark, status, createColorScale(value, \"dark\"), \"dark\");\n    }\n\n    if (chart?.length) {\n        chart.forEach((color, index) => {\n            light[`--tempest-chart-${index + 1}`] = color;\n        });\n        // Declares how many series colors this theme owns. Without it the reader\n        // keeps walking into the SDK's built-in `--tempest-chart-7`/`-8`, so a\n        // 6-color brand palette silently mixes with two leftover defaults — which\n        // is exactly what a 7-series chart would show.\n        light[\"--tempest-chart-count\"] = String(chart.length);\n    }\n\n    /*\n     * Continuous scales follow the brand too.\n     *\n     * Without this a rebranded app gets its categorical slots recoloured and then a\n     * heatmap still painted in the SDK's blue — the one chart on the page that\n     * silently ignores the brand. Both scales derive from the first series hue (or\n     * the primary), stepped by perceptual lightness for each mode separately.\n     *\n     * Skipped entirely when the theme names no colour to derive from, so the\n     * built-in tokens survive instead of being overwritten with a guess.\n     */\n    const scaleSource = chart?.[0] ?? primary;\n    if (scaleSource) {\n        const coolHue = hueOf(scaleSource);\n        // The opposite pole: the brand's own danger colour when it has one, since a\n        // diverging scale's warm arm and \"bad\" should not disagree on screen.\n        const warmHue = options.danger ? hueOf(options.danger) : (coolHue + 180) % 360;\n        for (const [target, mode] of [\n            [light, \"light\"],\n            [dark, \"dark\"],\n        ] as const) {\n            buildRamp(coolHue, mode).forEach((stepColor, index) => {\n                target[`--tempest-chart-sequential-${index + 1}`] = stepColor;\n            });\n            const diverging = buildDivergingRamp({\n                coolHue,\n                warmHue,\n                mid: mode === \"light\" ? \"#e4e7ec\" : \"#262d3f\",\n                mode,\n            });\n            [...diverging.cool, diverging.mid, ...diverging.warm].forEach((stepColor, index) => {\n                target[`--tempest-chart-diverging-${index + 1}`] = stepColor;\n            });\n        }\n    }\n\n    if (radius) {\n        const steps = typeof radius === \"string\" ? RADIUS_PRESETS[radius] : radius;\n        for (const [step, value] of Object.entries(steps)) {\n            light[`--tempest-radius-${step}`] = value;\n        }\n    }\n\n    const css = [renderBlock(selector, light), renderBlock(darkSelector, dark)]\n        .filter(Boolean)\n        .join(\"\\n\\n\");\n\n    return { light, dark, css };\n}\n\n/**\n * Contrast ratio of `--tempest-primary-foreground` over the brand color.\n *\n * Exposed so an app (or a test) can assert its own brand clears WCAG AA (4.5) for\n * body text or AA-large (3.0) for button labels, instead of trusting the pick.\n *\n * @param options - The same input given to {@link createTheme}.\n * @returns The ratio, or `null` when no `primary` was provided.\n */\nexport function themeContrast(options: CreateThemeOptions): number | null {\n    if (!options.primary) return null;\n    const scale = createColorScale(options.primary, \"light\");\n    return contrastRatio(scale[500], readableForeground(scale[500]));\n}\n"],"mappings":"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"}