You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/eidos/lib/recipes/base.ts

4388 lines
211 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import type {
RecipeTokenSet,
RecipeTokenMap,
RecipeTokenMultiDeclaration,
RecipeTokenDeclaration
} from '../config-types';
import { defineRecipes } from './define';
// ── Per-instance palette cascade (THEMING §25.5) ──────────────────────────
//
// `palette-{slot}` is RESERVED recipe vocabulary: the GENERATOR appends the
// full per-scale `color:{scale}` cascade to any token with that name
// (`universalPaletteDecls`, render-css.ts) — recipes only author the
// role/intent declarations. Button below is the pilot consumer.
// Wrapped in `defineRecipes` — the TYPE WALL of the dimensional vocabulary:
// a physical-axis key (`px` / `py` / `padding-x`…) is a COMPILE error
// (recipe-contract §1; normalized 2026-07-06).
export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
accordion: {
'provider-border-width': 'var(--border-width)',
'provider-radius': 'var(--radius-lg)',
'outline-provider-bg': 'var(--color-surface-default)',
'outline-provider-border': 'var(--color-border-default)',
'outline-item-border': 'var(--color-border-subtle)',
'outline-content-bg': 'var(--color-surface-default)',
'surface-provider-bg': 'var(--color-surface-raised)',
'surface-provider-border': 'var(--color-border-subtle)',
'surface-provider-shadow': 'var(--shadow-subtle)',
'surface-item-border': 'var(--color-border-subtle)',
'surface-content-bg': 'var(--color-surface-default)',
'ghost-provider-bg': 'transparent',
'ghost-provider-border': 'transparent',
'ghost-item-border': 'var(--color-border-subtle)',
'ghost-content-bg': 'transparent',
'item-border-width': 'var(--border-width)',
'trigger-min-height-xs': 'var(--size-xs-control-height)',
'trigger-min-height-sm': 'var(--size-sm-control-height)',
'trigger-min-height-md': 'var(--size-md-control-height)',
'trigger-min-height-lg': 'var(--size-lg-control-height)',
'trigger-min-height-full': 'var(--size-xl-control-height)',
'trigger-padding-inline-xs': 'var(--space-2-5)',
'trigger-padding-inline-sm': 'var(--space-3)',
'trigger-padding-inline-md': 'var(--space-4)',
'trigger-padding-inline-lg': 'var(--space-5)',
'trigger-padding-inline-full': 'var(--space-6)',
'trigger-padding-block-xs': 'var(--space-2)',
'trigger-padding-block-sm': 'var(--space-2-5)',
'trigger-padding-block-md': 'var(--space-3)',
'trigger-padding-block-lg': 'var(--space-3-5)',
'trigger-padding-block-full': 'var(--space-4)',
'trigger-gap': 'var(--space-3)',
'trigger-font-family': 'var(--style-label-font-family)',
'trigger-font-size-xs': 'var(--size-xs-font-size)',
'trigger-font-size-sm': 'var(--size-sm-font-size)',
'trigger-font-size-md': 'var(--size-md-font-size)',
'trigger-font-size-lg': 'var(--size-lg-font-size)',
'trigger-font-size-full': 'var(--size-xl-font-size)',
'trigger-font-weight': 'var(--font-weight-medium)',
'trigger-line-height': 'var(--leading-ui)',
'trigger-color': 'var(--color-content-primary)',
// Open state: keep the trigger text in the primary content tone.
// The chevron rotation + revealed content already signal openness;
// recoloring the heading adds noise without information (matches
// shadcn/Radix/Ark).
'trigger-color-open': 'var(--color-content-primary)',
'trigger-color-disabled': 'var(--color-content-disabled)',
'trigger-hover-color': 'var(--color-content-primary)',
'trigger-transition-duration': 'var(--duration-fast)',
'trigger-transition-ease': 'var(--ease-default)',
'trigger-focus-ring-offset': 'var(--focus-ring-offset)',
'trigger-focus-ring-width': 'var(--focus-ring-width)',
'trigger-focus-ring-color': 'var(--focus-ring-color)',
'content-padding-inline-xs': 'var(--space-2-5)',
'content-padding-inline-sm': 'var(--space-3)',
'content-padding-inline-md': 'var(--space-4)',
'content-padding-inline-lg': 'var(--space-5)',
'content-padding-inline-full': 'var(--space-6)',
'content-padding-block-xs': 'var(--space-2)',
'content-padding-block-sm': 'var(--space-2-5)',
'content-padding-block-md': 'var(--space-3)',
'content-padding-block-lg': 'var(--space-4)',
'content-padding-block-full': 'var(--space-5)',
'content-font-family': 'var(--font-family-secondary)',
'content-font-size': 'var(--size-md-font-size)',
'content-line-height': 'var(--leading-prose)',
'content-color': 'var(--color-content-secondary)',
'content-open-duration': 'var(--duration-normal)',
'content-close-duration': 'var(--duration-fast)',
'content-open-ease': 'var(--ease-out)',
'content-close-ease': 'var(--ease-in)',
'indicator-size-xs': 'var(--size-xs-icon-size)',
'indicator-size-sm': 'var(--size-sm-icon-size)',
'indicator-size-md': 'var(--size-sm-icon-size)',
'indicator-size-lg': 'var(--size-md-icon-size)',
'indicator-color': 'var(--color-content-secondary)',
// Indicator turns to the primary content tone when open — a subtle
// emphasis without the loud accent recolor.
'indicator-color-open': 'var(--color-content-primary)',
'indicator-rotation': '180deg',
'indicator-transition-duration': 'var(--duration-fast)',
'indicator-transition-ease': 'var(--ease-default)'
},
avatar: ((): RecipeTokenMap => {
// TSC v2.2 — 2D matrix (variant × color) cascade.
//
// Avatar's per-instance palette varies across 8 colors × 3
// variants. Each variant flavor (solid/soft/outline) reads
// different slots from the same color block. Encoding 24
// composite scopes by hand for 6 tokens (3 on the root, 3 on the
// badge part) is 144 lines of boilerplate; this small helper
// generates them with the same intent.
//
// Helper consumed only here — explicit declarations would obscure
// the variant logic. Keep the helper local to the entry to avoid
// polluting the recipes module surface.
const COLORS = [
'primary',
'secondary',
'neutral',
'affirm',
'fulfill',
'risk',
'threat',
'loss'
] as const;
type AvatarColor = (typeof COLORS)[number];
type AvatarVariant = 'solid' | 'soft' | 'outline';
/** Build (variant × color) declarations for a slot getter. */
function matrix(
pick: (color: AvatarColor, variant: AvatarVariant) => string | undefined,
parts?: readonly string[]
): RecipeTokenMultiDeclaration {
const declarations: RecipeTokenDeclaration[] = [];
for (const variant of ['solid', 'soft', 'outline'] as const) {
for (const color of COLORS) {
const value = pick(color, variant);
if (value === undefined) continue;
declarations.push({
value,
scope: [`variant:${variant}`, `color:${color}`]
});
}
}
return parts ? { parts, declarations } : { declarations };
}
return {
'size-xs': '24px',
'size-sm': '32px',
'size-md': '40px',
'size-lg': '48px',
'size-xl': '64px',
'size-xxl': '96px',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xxs-font-size)',
'font-size-sm': 'var(--size-xs-font-size)',
'font-size-md': 'var(--size-sm-font-size)',
'font-size-lg': 'var(--size-md-font-size)',
'font-size-xl': 'var(--size-lg-font-size)',
'font-size-xxl': 'var(--size-xl-font-size)',
'font-weight': 'var(--font-weight-medium)',
'radius-full': 'var(--radius-full)',
'radius-md': 'var(--radius-md)',
'radius-sm': 'var(--radius-sm)',
'radius-none': '0',
'border-width': 'var(--border-width-medium)',
'primary-solid-bg': 'var(--color-primary-solid)',
'primary-solid-fg': 'var(--color-primary-contrast)',
'primary-soft-bg': 'var(--color-primary-surface)',
'primary-soft-fg': 'var(--color-primary-text)',
'primary-outline-border': 'var(--color-primary-border)',
'primary-outline-fg': 'var(--color-primary-text)',
'secondary-solid-bg': 'var(--color-secondary-solid)',
'secondary-solid-fg': 'var(--color-secondary-contrast)',
'secondary-soft-bg': 'var(--color-secondary-surface)',
'secondary-soft-fg': 'var(--color-secondary-text)',
'secondary-outline-border': 'var(--color-secondary-border)',
'secondary-outline-fg': 'var(--color-secondary-text)',
'neutral-solid-bg': 'var(--color-neutral-solid)',
'neutral-solid-fg': 'var(--color-neutral-contrast)',
'neutral-soft-bg': 'var(--color-neutral-surface)',
'neutral-soft-fg': 'var(--color-neutral-text)',
'neutral-outline-border': 'var(--color-neutral-border)',
'neutral-outline-fg': 'var(--color-neutral-text)',
'affirm-solid-bg': 'var(--color-affirm-solid)',
'affirm-solid-fg': 'var(--color-affirm-contrast)',
'affirm-soft-bg': 'var(--color-affirm-surface)',
'affirm-soft-fg': 'var(--color-affirm-text)',
'affirm-outline-border': 'var(--color-affirm-border)',
'affirm-outline-fg': 'var(--color-affirm-text)',
'fulfill-solid-bg': 'var(--color-fulfill-solid)',
'fulfill-solid-fg': 'var(--color-fulfill-contrast)',
'fulfill-soft-bg': 'var(--color-fulfill-surface)',
'fulfill-soft-fg': 'var(--color-fulfill-text)',
'fulfill-outline-border': 'var(--color-fulfill-border)',
'fulfill-outline-fg': 'var(--color-fulfill-text)',
'risk-solid-bg': 'var(--color-risk-solid)',
'risk-solid-fg': 'var(--color-risk-contrast)',
'risk-soft-bg': 'var(--color-risk-surface)',
'risk-soft-fg': 'var(--color-risk-text)',
'risk-outline-border': 'var(--color-risk-border)',
'risk-outline-fg': 'var(--color-risk-text)',
'threat-solid-bg': 'var(--color-threat-solid)',
'threat-solid-fg': 'var(--color-threat-contrast)',
'threat-soft-bg': 'var(--color-threat-surface)',
'threat-soft-fg': 'var(--color-threat-text)',
'threat-outline-border': 'var(--color-threat-border)',
'threat-outline-fg': 'var(--color-threat-text)',
'loss-solid-bg': 'var(--color-loss-solid)',
'loss-solid-fg': 'var(--color-loss-contrast)',
'loss-soft-bg': 'var(--color-loss-surface)',
'loss-soft-fg': 'var(--color-loss-text)',
'loss-outline-border': 'var(--color-loss-border)',
'loss-outline-fg': 'var(--color-loss-text)',
// ── Ring (outline halo around the avatar) ────────────────────────
'ring-width-sm': '1.5px',
'ring-width-md': '2px',
'ring-width-lg': '3px',
// ── Custom color (consumer-provided CSS color string) ────────────
// Set inline by the Svelte file via `style="--avatar-color-custom: …"`.
// Declared here so the recipe-css-contract test sees a declaration.
'color-custom': 'currentColor',
'color-custom-contrast': 'white',
'ring-color-custom': 'currentColor',
// ── Badge ─────────────────────────────────────────────────────────
'badge-border-width': 'var(--border-width-medium)',
'badge-color-custom': 'currentColor',
'badge-color-custom-contrast': 'white',
// ── AvatarGroup ──────────────────────────────────────────────────
'group-gap-xs': 'var(--space-1)',
'group-gap-sm': 'var(--space-2)',
'group-gap-md': 'var(--space-3)',
'group-gap-lg': 'var(--space-4)',
'group-overlap': '-0.6em',
'group-carve-width': 'var(--border-width-medium)',
'group-carve-color': 'var(--color-surface-default)',
'group-reverse-z-max': '20',
'group-max': '99',
'overflow-font-weight': 'var(--font-weight-medium)',
// ── TSC v2.2 — composite (variant × color) cascade ────────────────
// 8 colors × 3 variants = 24 composite scopes per slot, applied
// to the root for {_bg, _fg, _border} and to the badge part for
// {_badge-bg, _badge-fg, _badge-border} (matrix helper above
// generates the declarations[] from the slot pickers).
//
// Defaults at `host` match the legacy CSS base rule (avatar:
// neutral-soft; badge: affirm-solid). Outline is the only variant
// that touches `_border` / `_badge-border`; solid+soft keep the
// transparent / surface-default defaults.
// THM-2 (2026-07-12): the ROOT palette resolves via the shared
// per-instance layer. `_palette-{slot}` carries only a host fallback —
// the avatar always emits a canonical `data-color`, so the presence-
// guarded forward routes every role AND the 33 scales; the host is the
// out-of-vocab fallback. `_bg/_fg/_border` then pick the slot by
// VARIANT (was a variant×color matrix; the color dimension moved to the
// shared layer). The badge keeps its own matrix (independent color).
'_palette-solid': { value: 'var(--avatar-neutral-solid-bg)', scope: 'host' },
'_palette-surface': { value: 'var(--avatar-neutral-soft-bg)', scope: 'host' },
'_palette-contrast': { value: 'var(--avatar-neutral-solid-fg)', scope: 'host' },
'_palette-text': { value: 'var(--avatar-neutral-soft-fg)', scope: 'host' },
'_palette-border': { value: 'var(--avatar-neutral-outline-border)', scope: 'host' },
_bg: {
declarations: [
{ value: 'var(--_avatar-palette-surface)', scope: 'host' },
{ value: 'var(--_avatar-palette-solid)', scope: 'variant:solid' },
{ value: 'var(--_avatar-palette-surface)', scope: 'variant:soft' },
{ value: 'transparent', scope: 'variant:outline' }
]
},
_fg: {
declarations: [
{ value: 'var(--_avatar-palette-text)', scope: 'host' },
{ value: 'var(--_avatar-palette-contrast)', scope: 'variant:solid' },
{ value: 'var(--_avatar-palette-text)', scope: 'variant:soft' },
{ value: 'var(--_avatar-palette-text)', scope: 'variant:outline' }
]
},
_border: {
declarations: [
{ value: 'transparent', scope: 'host' },
{ value: 'var(--_avatar-palette-border)', scope: 'variant:outline' }
]
},
'_badge-bg': {
parts: ['badge'],
declarations: [
{ value: 'var(--avatar-affirm-solid-bg)', scope: 'host' },
...matrix((color, variant) =>
variant === 'outline'
? 'var(--color-surface-default)'
: `var(--avatar-${color}-${variant}-bg)`
).declarations
]
},
'_badge-fg': {
parts: ['badge'],
declarations: [
{ value: 'var(--avatar-affirm-solid-fg)', scope: 'host' },
...matrix((color, variant) => `var(--avatar-${color}-${variant}-fg)`).declarations
]
},
'_badge-border': {
parts: ['badge'],
declarations: [
{ value: 'var(--color-surface-default)', scope: 'host' },
...matrix((color, variant) =>
variant === 'outline' ? `var(--avatar-${color}-outline-border)` : undefined
).declarations
]
}
};
})(),
badge: {
// ── Padding scale ────────────────────────────────────────────────
'padding-inline-xs': 'var(--space-1-5)',
'padding-inline-sm': 'var(--space-2)',
'padding-inline-md': 'var(--space-2-5)',
'padding-inline-lg': 'var(--space-3)',
'padding-block-xs': 'var(--space-0-5)',
'padding-block-sm': 'var(--space-0-5)',
'padding-block-md': 'var(--space-1)',
'padding-block-lg': 'var(--space-1-5)',
// ── Gap between dot / icon / label / remove ───────────────────────
'gap-xs': 'var(--space-1)',
'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)',
'gap-lg': 'var(--space-2)',
// ── Typography ────────────────────────────────────────────────────
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'font-weight': 'var(--font-weight-medium)',
'line-height': 'var(--leading-ui)',
'letter-spacing': '0.01em',
// ── Borders + radii ───────────────────────────────────────────────
'border-width': 'var(--border-width)',
'radius-sm': 'var(--radius-sm)',
'radius-md': 'var(--radius-md)',
'radius-lg': 'var(--radius-lg)',
'radius-full': 'var(--radius-full)',
// ── Inner slots ───────────────────────────────────────────────────
// Use em so internal parts scale with the badge's resolved font-size.
'dot-size': '0.5em',
'icon-size': '1em',
'remove-size': '1.25em',
'remove-margin-start': '0.125em',
// ── Color palette (mirrors the canonical 8-color matrix) ──────────
'primary-solid': 'var(--color-primary-solid)',
'primary-text': 'var(--color-primary-text)',
'primary-contrast': 'var(--color-primary-contrast)',
'secondary-solid': 'var(--color-secondary-solid)',
'secondary-text': 'var(--color-secondary-text)',
'secondary-contrast': 'var(--color-secondary-contrast)',
'neutral-solid': 'var(--color-neutral-solid)',
'neutral-text': 'var(--color-neutral-text)',
'neutral-contrast': 'var(--color-neutral-contrast)',
'affirm-solid': 'var(--color-affirm-solid)',
'affirm-text': 'var(--color-affirm-text)',
'affirm-contrast': 'var(--color-affirm-contrast)',
'fulfill-track': 'var(--color-fulfill-track)',
'fulfill-border': 'var(--color-fulfill-border)',
'fulfill-solid': 'var(--color-fulfill-solid)',
'fulfill-text': 'var(--color-fulfill-text)',
'fulfill-contrast': 'var(--color-fulfill-contrast)',
'risk-solid': 'var(--color-risk-solid)',
'risk-text': 'var(--color-risk-text)',
'risk-contrast': 'var(--color-risk-contrast)',
'threat-solid': 'var(--color-threat-solid)',
'threat-text': 'var(--color-threat-text)',
'threat-contrast': 'var(--color-threat-contrast)',
'loss-track': 'var(--color-loss-track)',
'loss-border': 'var(--color-loss-border)',
'loss-solid': 'var(--color-loss-solid)',
'loss-text': 'var(--color-loss-text)',
'loss-contrast': 'var(--color-loss-contrast)',
// Restored after accidental sed-removal during switch migration.
'primary-track': 'var(--color-primary-track)',
'primary-border': 'var(--color-primary-border)',
'secondary-track': 'var(--color-secondary-track)',
'secondary-border': 'var(--color-secondary-border)',
'neutral-track': 'var(--color-neutral-track)',
'neutral-border': 'var(--color-neutral-border)',
'affirm-track': 'var(--color-affirm-track)',
'affirm-border': 'var(--color-affirm-border)',
'risk-track': 'var(--color-risk-track)',
'risk-border': 'var(--color-risk-border)',
'threat-track': 'var(--color-threat-track)',
'threat-border': 'var(--color-threat-border)',
// Private palette (TSC v2.1 · canonical `_palette-*` slots so the THM-2
// shared-layer forward routes color="teal" + the 33 scales; renamed from
// the bare `_{slot}` prefix 2026-07-13). 8 colors, 5 slots.
'_palette-track': {
declarations: [
{ value: 'var(--badge-primary-track)', scope: 'host' },
{ value: 'var(--badge-secondary-track)', scope: 'color:secondary' },
{ value: 'var(--badge-neutral-track)', scope: 'color:neutral' },
{ value: 'var(--badge-affirm-track)', scope: 'color:affirm' },
{ value: 'var(--badge-fulfill-track)', scope: 'color:fulfill' },
{ value: 'var(--badge-risk-track)', scope: 'color:risk' },
{ value: 'var(--badge-threat-track)', scope: 'color:threat' },
{ value: 'var(--badge-loss-track)', scope: 'color:loss' }
]
},
'_palette-border': {
declarations: [
{ value: 'var(--badge-primary-border)', scope: 'host' },
{ value: 'var(--badge-secondary-border)', scope: 'color:secondary' },
{ value: 'var(--badge-neutral-border)', scope: 'color:neutral' },
{ value: 'var(--badge-affirm-border)', scope: 'color:affirm' },
{ value: 'var(--badge-fulfill-border)', scope: 'color:fulfill' },
{ value: 'var(--badge-risk-border)', scope: 'color:risk' },
{ value: 'var(--badge-threat-border)', scope: 'color:threat' },
{ value: 'var(--badge-loss-border)', scope: 'color:loss' }
]
},
'_palette-solid': {
declarations: [
{ value: 'var(--badge-primary-solid)', scope: 'host' },
{ value: 'var(--badge-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--badge-neutral-solid)', scope: 'color:neutral' },
{ value: 'var(--badge-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--badge-fulfill-solid)', scope: 'color:fulfill' },
{ value: 'var(--badge-risk-solid)', scope: 'color:risk' },
{ value: 'var(--badge-threat-solid)', scope: 'color:threat' },
{ value: 'var(--badge-loss-solid)', scope: 'color:loss' }
]
},
'_palette-text': {
declarations: [
{ value: 'var(--badge-primary-text)', scope: 'host' },
{ value: 'var(--badge-secondary-text)', scope: 'color:secondary' },
{ value: 'var(--badge-neutral-text)', scope: 'color:neutral' },
{ value: 'var(--badge-affirm-text)', scope: 'color:affirm' },
{ value: 'var(--badge-fulfill-text)', scope: 'color:fulfill' },
{ value: 'var(--badge-risk-text)', scope: 'color:risk' },
{ value: 'var(--badge-threat-text)', scope: 'color:threat' },
{ value: 'var(--badge-loss-text)', scope: 'color:loss' }
]
},
'_palette-contrast': {
declarations: [
{ value: 'var(--badge-primary-contrast)', scope: 'host' },
{ value: 'var(--badge-secondary-contrast)', scope: 'color:secondary' },
{ value: 'var(--badge-neutral-contrast)', scope: 'color:neutral' },
{ value: 'var(--badge-affirm-contrast)', scope: 'color:affirm' },
{ value: 'var(--badge-fulfill-contrast)', scope: 'color:fulfill' },
{ value: 'var(--badge-risk-contrast)', scope: 'color:risk' },
{ value: 'var(--badge-threat-contrast)', scope: 'color:threat' },
{ value: 'var(--badge-loss-contrast)', scope: 'color:loss' }
]
}
},
breadcrumb: {
'gap-xs': 'var(--space-0-5)',
'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)',
'gap-lg': 'var(--space-2)',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'line-height': 'var(--leading-ui)',
color: 'var(--color-content-secondary)',
'link-radius': 'var(--radius-sm)',
'link-color': 'var(--color-content-secondary)',
'link-color-hover': 'var(--color-content-primary)',
'link-color-current': 'var(--color-content-primary)',
'link-font-weight': 'var(--font-weight-medium)',
'link-font-weight-current': 'var(--font-weight-semibold)',
'separator-color': 'var(--color-content-muted)',
'ellipsis-size': 'var(--size-xs-control-height)',
'ellipsis-radius': 'var(--radius-md)',
'ellipsis-border-width': 'var(--border-width)',
'ellipsis-border': 'transparent',
'ellipsis-border-hover': 'var(--color-border-subtle)',
'ellipsis-bg': 'transparent',
'ellipsis-color': 'var(--color-content-muted)',
'ellipsis-color-hover': 'var(--color-content-primary)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)'
},
card: {
// ── Padding scale ────────────────────────────────────────────────
'padding-inline-xs': 'var(--space-3)',
'padding-inline-sm': 'var(--space-4)',
'padding-inline-md': 'var(--space-5)',
'padding-inline-lg': 'var(--space-6)',
'padding-inline-xl': 'var(--space-8)',
'padding-block-xs': 'var(--space-3)',
'padding-block-sm': 'var(--space-4)',
'padding-block-md': 'var(--space-5)',
'padding-block-lg': 'var(--space-6)',
'padding-block-xl': 'var(--space-8)',
// ── Gap between header / body / footer slots ─────────────────────
'gap-xs': 'var(--space-2)',
'gap-sm': 'var(--space-3)',
'gap-md': 'var(--space-4)',
'gap-lg': 'var(--space-5)',
'gap-xl': 'var(--space-6)',
// ── Typography ────────────────────────────────────────────────────
'font-family': 'var(--style-label-font-family)',
'line-height': 'var(--leading-ui)',
'title-font-family': 'var(--font-family-display)',
'title-font-size-xs': 'var(--size-sm-font-size)',
'title-font-size-sm': 'var(--size-md-font-size)',
'title-font-size-md': 'var(--size-lg-font-size)',
'title-font-size-lg': 'var(--size-xl-font-size)',
'title-font-size-xl': 'var(--size-xxl-font-size)',
'title-font-weight': 'var(--font-weight-semibold)',
'title-line-height': 'var(--leading-heading)',
'title-letter-spacing': 'var(--tracking-tight)',
'description-font-family': 'var(--style-label-font-family)',
'description-font-size-xs': 'var(--size-xs-font-size)',
'description-font-size-sm': 'var(--size-sm-font-size)',
'description-font-size-md': 'var(--size-sm-font-size)',
'description-font-size-lg': 'var(--size-md-font-size)',
'description-font-size-xl': 'var(--size-md-font-size)',
'description-line-height': 'var(--leading-prose)',
'description-color': 'var(--color-content-secondary)',
'body-font-size-xs': 'var(--size-sm-font-size)',
'body-font-size-sm': 'var(--size-sm-font-size)',
'body-font-size-md': 'var(--size-md-font-size)',
'body-font-size-lg': 'var(--size-md-font-size)',
'body-font-size-xl': 'var(--size-lg-font-size)',
'body-line-height': 'var(--leading-prose)',
// ── Borders + radii ───────────────────────────────────────────────
'border-width': 'var(--border-width)',
'radius-sm': 'var(--radius-md)',
'radius-md': 'var(--radius-lg)',
'radius-lg': 'var(--radius-xl)',
// ── Inner slot spacing ────────────────────────────────────────────
'header-gap': 'var(--space-1-5)',
'footer-gap': 'var(--space-3)',
'footer-padding-start': 'var(--space-3)',
'footer-border-color': 'var(--color-border-subtle)',
// ── Animations ────────────────────────────────────────────────────
'emerge-duration': 'var(--duration-slow)',
'emerge-ease': 'var(--ease-out)',
'emerge-distance': '8px',
'hover-lift': '-2px',
'hover-shadow': 'var(--depth-overlay-shadow), var(--depth-overlay-halo)',
'press-scale': 'var(--press-scale)',
'press-duration': 'var(--press-duration)',
'selected-ring-width': 'var(--border-width-medium)',
'disabled-opacity': 'var(--opacity-disabled)',
// ── Custom color (set inline by consumer via style) ───────────────
// The Svelte file emits `style="--card-color-custom: <value>;"`
// when the consumer passes a non-canonical CSS color. Declared
// here so the recipe-css-contract test sees a declaration.
'color-custom': 'currentColor',
'color-custom-contrast': 'white',
// ── Color palette (canonical 8-color × 5 slots) ──────────────────
'primary-solid': 'var(--color-primary-solid)',
'primary-text': 'var(--color-primary-text)',
'primary-contrast': 'var(--color-primary-contrast)',
'secondary-solid': 'var(--color-secondary-solid)',
'secondary-text': 'var(--color-secondary-text)',
'secondary-contrast': 'var(--color-secondary-contrast)',
'neutral-solid': 'var(--color-neutral-solid)',
'neutral-text': 'var(--color-neutral-text)',
'neutral-contrast': 'var(--color-neutral-contrast)',
'affirm-solid': 'var(--color-affirm-solid)',
'affirm-text': 'var(--color-affirm-text)',
'affirm-contrast': 'var(--color-affirm-contrast)',
'fulfill-track': 'var(--color-fulfill-track)',
'fulfill-border': 'var(--color-fulfill-border)',
'fulfill-solid': 'var(--color-fulfill-solid)',
'fulfill-text': 'var(--color-fulfill-text)',
'fulfill-contrast': 'var(--color-fulfill-contrast)',
'risk-solid': 'var(--color-risk-solid)',
'risk-text': 'var(--color-risk-text)',
'risk-contrast': 'var(--color-risk-contrast)',
'threat-solid': 'var(--color-threat-solid)',
'threat-text': 'var(--color-threat-text)',
'threat-contrast': 'var(--color-threat-contrast)',
'loss-track': 'var(--color-loss-track)',
'loss-border': 'var(--color-loss-border)',
'loss-solid': 'var(--color-loss-solid)',
'loss-text': 'var(--color-loss-text)',
'loss-contrast': 'var(--color-loss-contrast)',
// Restored after accidental sed-removal during switch migration.
'primary-track': 'var(--color-primary-track)',
'primary-border': 'var(--color-primary-border)',
'secondary-track': 'var(--color-secondary-track)',
'secondary-border': 'var(--color-secondary-border)',
'neutral-track': 'var(--color-neutral-track)',
'neutral-border': 'var(--color-neutral-border)',
'affirm-track': 'var(--color-affirm-track)',
'affirm-border': 'var(--color-affirm-border)',
'risk-track': 'var(--color-risk-track)',
'risk-border': 'var(--color-risk-border)',
'threat-track': 'var(--color-threat-track)',
'threat-border': 'var(--color-threat-border)',
// Private palette (TSC v2.1 · canonical `_palette-*` slots so the THM-2
// shared-layer forward routes color="teal" + the 33 scales; renamed from
// the bare `_{slot}` prefix 2026-07-13). 8 colors, 5 slots. Default = neutral.
'_palette-track': {
declarations: [
{ value: 'var(--card-neutral-track)', scope: 'host' },
{ value: 'var(--card-primary-track)', scope: 'color:primary' },
{ value: 'var(--card-secondary-track)', scope: 'color:secondary' },
{ value: 'var(--card-affirm-track)', scope: 'color:affirm' },
{ value: 'var(--card-fulfill-track)', scope: 'color:fulfill' },
{ value: 'var(--card-risk-track)', scope: 'color:risk' },
{ value: 'var(--card-threat-track)', scope: 'color:threat' },
{ value: 'var(--card-loss-track)', scope: 'color:loss' }
]
},
'_palette-border': {
declarations: [
{ value: 'var(--card-neutral-border)', scope: 'host' },
{ value: 'var(--card-primary-border)', scope: 'color:primary' },
{ value: 'var(--card-secondary-border)', scope: 'color:secondary' },
{ value: 'var(--card-affirm-border)', scope: 'color:affirm' },
{ value: 'var(--card-fulfill-border)', scope: 'color:fulfill' },
{ value: 'var(--card-risk-border)', scope: 'color:risk' },
{ value: 'var(--card-threat-border)', scope: 'color:threat' },
{ value: 'var(--card-loss-border)', scope: 'color:loss' }
]
},
'_palette-solid': {
declarations: [
{ value: 'var(--card-neutral-solid)', scope: 'host' },
{ value: 'var(--card-primary-solid)', scope: 'color:primary' },
{ value: 'var(--card-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--card-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--card-fulfill-solid)', scope: 'color:fulfill' },
{ value: 'var(--card-risk-solid)', scope: 'color:risk' },
{ value: 'var(--card-threat-solid)', scope: 'color:threat' },
{ value: 'var(--card-loss-solid)', scope: 'color:loss' }
]
},
'_palette-text': {
declarations: [
{ value: 'var(--card-neutral-text)', scope: 'host' },
{ value: 'var(--card-primary-text)', scope: 'color:primary' },
{ value: 'var(--card-secondary-text)', scope: 'color:secondary' },
{ value: 'var(--card-affirm-text)', scope: 'color:affirm' },
{ value: 'var(--card-fulfill-text)', scope: 'color:fulfill' },
{ value: 'var(--card-risk-text)', scope: 'color:risk' },
{ value: 'var(--card-threat-text)', scope: 'color:threat' },
{ value: 'var(--card-loss-text)', scope: 'color:loss' }
]
},
'_palette-contrast': {
declarations: [
{ value: 'var(--card-neutral-contrast)', scope: 'host' },
{ value: 'var(--card-primary-contrast)', scope: 'color:primary' },
{ value: 'var(--card-secondary-contrast)', scope: 'color:secondary' },
{ value: 'var(--card-affirm-contrast)', scope: 'color:affirm' },
{ value: 'var(--card-fulfill-contrast)', scope: 'color:fulfill' },
{ value: 'var(--card-risk-contrast)', scope: 'color:risk' },
{ value: 'var(--card-threat-contrast)', scope: 'color:threat' },
{ value: 'var(--card-loss-contrast)', scope: 'color:loss' }
]
}
},
'chat-message': {
// ── Row layout ────────────────────────────────────────────────────
gap: 'var(--space-2)',
'row-gap-run': 'var(--space-0-5)',
'row-gap-group': 'var(--space-3)',
// Avatar column reserve — follows the composed Avatar's md diameter.
'gutter-inline-size': 'var(--avatar-size-md, var(--space-8))',
// ── Bubble ────────────────────────────────────────────────────────
'bubble-padding-inline-sm': 'var(--space-2-5)',
'bubble-padding-inline-md': 'var(--space-3)',
'bubble-padding-inline-lg': 'var(--space-4)',
'bubble-padding-block-sm': 'var(--space-1-5)',
'bubble-padding-block-md': 'var(--space-2)',
'bubble-padding-block-lg': 'var(--space-2-5)',
'bubble-radius': 'var(--radius-lg)',
// Flattened corners between messages of one run.
'bubble-radius-run': 'var(--radius-sm)',
'bubble-max-inline-size': 'min(var(--measure-narrow), 78%)',
'bubble-font-size-sm': 'var(--size-sm-font-size)',
'bubble-font-size-md': 'var(--size-md-font-size)',
'bubble-font-size-lg': 'var(--size-lg-font-size)',
'bubble-line-height': 'var(--leading-prose)',
'bubble-bg-in': 'var(--color-neutral-element)',
'bubble-fg-in': 'var(--color-content-primary)',
'bubble-bg-out': 'var(--color-primary-solid)',
'bubble-fg-out': 'var(--color-primary-contrast)',
// ── Header (author + time, first of a run) ───────────────────────
'header-gap': 'var(--space-2)',
'header-font-size': 'var(--size-sm-font-size)',
'header-font-weight': 'var(--font-weight-medium)',
'header-color': 'var(--color-content-secondary)',
// ── Meta (time + status) ─────────────────────────────────────────
'meta-gap': 'var(--space-1)',
'meta-font-size': 'var(--size-xs-font-size)',
'meta-color': 'var(--color-content-muted)',
// ── Delivery status indicator ────────────────────────────────────
'status-size': '1em',
'status-color': 'var(--color-content-muted)',
'status-color-read': 'var(--color-primary-solid)',
'status-color-failed': 'var(--color-threat-solid)',
// ── Reply (quoted preview) ───────────────────────────────────────
'reply-padding-inline': 'var(--space-2)',
'reply-padding-block': 'var(--space-1)',
'reply-accent-size': 'var(--space-0-5)',
'reply-radius': 'var(--radius-sm)',
'reply-bg': 'var(--color-neutral-track)',
'reply-accent': 'var(--color-primary-solid)',
'reply-font-size': 'var(--size-xs-font-size)',
// ── Reaction chips ───────────────────────────────────────────────
'reaction-gap': 'var(--space-1)',
'reaction-height': 'var(--size-xs-control-height)',
'reaction-padding-inline': 'var(--space-2)',
'reaction-radius': 'var(--radius-full)',
'reaction-font-size': 'var(--size-xs-font-size)',
'reaction-bg': 'var(--color-neutral-track)',
'reaction-border': 'var(--color-neutral-separator)',
'reaction-pressed-bg': 'var(--color-primary-surface)',
'reaction-pressed-border': 'var(--color-primary-border)',
'reaction-pressed-fg': 'var(--color-primary-text)',
// ── Mention highlight ────────────────────────────────────────────
'mentioned-bg': 'var(--color-primary-surface)',
'mentioned-accent': 'var(--color-primary-solid)',
// ── Actions cluster ──────────────────────────────────────────────
'actions-gap': 'var(--space-0-5)',
'actions-offset': 'var(--space-1)'
},
checkbox: {
'size-xs-box-size': '14px',
'size-xs-indicator-size': '8px',
'size-xs-indicator-stroke': '2px',
'size-sm-box-size': '16px',
'size-sm-indicator-size': '10px',
'size-sm-indicator-stroke': '2px',
'size-md-box-size': '18px',
'size-md-indicator-size': '12px',
'size-md-indicator-stroke': '2px',
'size-lg-box-size': '22px',
'size-lg-indicator-size': '14px',
'size-lg-indicator-stroke': '2px',
'size-xl-box-size': '26px',
'size-xl-indicator-size': '16px',
'size-xl-indicator-stroke': '2px',
radius: 'var(--radius-sm)',
'border-width': 'var(--border-width)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
// Checkmark stroke draw — synced with the box transition (--duration-fast).
// Was a hardcoded 220ms, which read as laggy next to the 120ms box fill.
'stroke-duration': 'var(--duration-fast)',
'stroke-ease': 'cubic-bezier(0.4, 0, 0.2, 1)',
'bg-off': 'var(--color-surface-default)',
'bg-off-hover':
'color-mix( in srgb, var(--color-surface-default) 82%, var(--color-surface-overlay) )',
'border-off': 'var(--color-border-default)',
'border-off-hover': 'var(--color-border-strong)',
'bg-disabled':
'color-mix( in srgb, var(--color-surface-overlay) 78%, var(--color-surface-default) )',
'border-disabled':
'color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) )',
'disabled-opacity': 'var(--opacity-disabled)',
'indicator-color': 'var(--color-content-on-solid)',
'indicator-color-disabled':
'color-mix( in srgb, var(--color-content-on-solid) 82%, var(--color-surface-default) )',
'primary-solid': 'var(--color-primary-solid)',
'primary-solid-hover': 'var(--color-primary-solid-hover)',
'neutral-solid': 'var(--color-neutral-solid)',
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
'secondary-solid': 'var(--color-secondary-solid)',
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
'affirm-solid': 'var(--color-affirm-solid)',
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
// Private palette (TSC v2.1 `_` prefix → emits `--_checkbox-palette-*`).
// Recipe-internal cascade: host default = primary, plus overrides
// for the 3 supported color values. Migrated from manual blocks
// in checkbox.css.
'_palette-solid': {
declarations: [
{ value: 'var(--checkbox-primary-solid)', scope: 'host' },
{ value: 'var(--checkbox-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--checkbox-neutral-solid)', scope: 'color:neutral' },
{ value: 'var(--checkbox-affirm-solid)', scope: 'color:affirm' }
]
},
'_palette-solid-hover': {
declarations: [
{ value: 'var(--checkbox-primary-solid-hover)', scope: 'host' },
{ value: 'var(--checkbox-secondary-solid-hover)', scope: 'color:secondary' },
{ value: 'var(--checkbox-neutral-solid-hover)', scope: 'color:neutral' },
{ value: 'var(--checkbox-affirm-solid-hover)', scope: 'color:affirm' }
]
},
'_palette-text': { value: 'var(--checkbox-indicator-color)', scope: 'host' },
'group-gap': 'var(--space-3)',
'group-label-font-family': 'var(--style-label-font-family)',
'group-label-font-size': 'var(--size-sm-font-size)',
'group-label-font-weight': 'var(--font-weight-medium)',
'group-label-line-height': 'var(--leading-ui)',
'group-label-color': 'var(--color-content-primary)',
'group-label-color-disabled': 'var(--color-content-secondary)'
},
collapsible: {
'trigger-font-family': 'var(--style-label-font-family)',
'trigger-font-size': 'var(--size-sm-font-size)',
'trigger-font-weight': 'var(--font-weight-medium)',
'trigger-color': 'var(--color-content-primary)',
'trigger-color-hover': 'var(--color-content-primary)',
'trigger-radius': 'var(--radius-sm)',
'trigger-padding': 'var(--space-2) var(--space-3)',
'content-padding': 'var(--space-3)',
'transition-duration': 'var(--duration-normal)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
dialog: {
'overlay-bg': 'var(--color-overlay)',
'overlay-opacity': '62%',
'overlay-blur': 'var(--blur-lg)',
'overlay-z': 'var(--z-index-overlay-backdrop)',
'content-z': 'var(--z-index-overlay-content)',
// Elevation (bg · border · shadow) now comes from the `modal` plane via
// `data-depth` (A1, Decisión 8). The recipe keeps only non-elevation:
// content color + the panel radius (separate axis — Radix factor, A2).
'content-color': 'var(--color-content-primary)',
'content-radius': 'var(--radius-xl)',
'content-max-height': 'calc(100dvh - var(--space-8))',
'content-max-height-sheet': '85dvh',
'content-width-sm': '420px',
'content-width-md': '560px',
'content-width-lg': '760px',
'content-width-xl': '1024px',
'content-width-inset': 'var(--space-6)',
'content-full-inset': 'var(--space-3)',
'content-full-radius': 'var(--radius-xxl)',
'content-position-inset': 'var(--space-4)',
'content-padding-sm': 'var(--space-4)',
'content-padding-md': 'var(--space-5)',
'content-padding-lg': 'var(--space-6)',
'content-padding-full': 'var(--space-6)',
'stack-gap': 'var(--space-4)',
'header-gap': 'var(--space-3)',
'footer-gap': 'var(--space-3)',
'footer-margin-top': 'var(--space-2)',
// Title = the canonical `h3` heading role (Arq. 5 / THEMING §3). These stay
// public knobs (a theme / instance overrides them to restyle dialog titles
// without retuning every h3); their DEFAULT is the role. Adopting `h3` moves
// the title off the old `--font-family-display` (Lora) — off the type scale at an
// h3 size — onto `primary` (sans), per h3–h6 = primary.
'title-font-family': 'var(--style-h3-font-family)',
'title-font-size': 'var(--style-h3-font-size)',
'title-font-weight': 'var(--style-h3-font-weight)',
'title-line-height': 'var(--style-h3-line-height)',
'title-letter-spacing': 'var(--style-h3-letter-spacing)',
'title-color': 'var(--style-h3-color)',
'description-font-family': 'var(--style-label-font-family)',
'description-font-size': 'var(--size-sm-font-size)',
'description-line-height': 'var(--leading-ui)',
'description-color': 'var(--color-content-secondary)',
// The close composes <Button>/<IconButton>, which own all visual chrome
// (bg / hover / focus-ring / disabled / radius). Only the dialog-specific
// floating geometry stays here: `close-size` reserves header room beside a
// floating close, `close-inset` is its corner offset.
'close-size': 'var(--size-sm-control-height)',
'close-inset': 'var(--space-3)'
},
field: {
'gap-xs': 'var(--space-1)',
'gap-sm': 'var(--space-1-5)',
'gap-md': 'var(--space-2)',
'gap-lg': 'var(--space-2-5)',
'gap-xl': 'var(--space-3)',
'font-family': 'var(--style-label-font-family)',
'label-gap': 'var(--space-1)',
// Label size is derived in field.css (control font − one step), not a
// per-size token — keeps it coherent with the segmented fields' calc.
'label-font-weight': 'var(--font-weight-regular)',
'label-line-height': 'var(--leading-ui)',
'label-color': 'var(--color-content-primary)',
'label-color-invalid': 'var(--color-risk-text)',
'label-color-disabled': 'var(--color-content-disabled)',
// Floated-label vertical anchor (floatingLabel mode). `0` = the label's
// centre lands ON the control's top border (outlined style, default). A
// theme can retarget it — negative to float fully above, or a positive
// offset to float top-inside (filled style) — without touching the recipe.
'floating-label-top': '0px',
'layout-column-gap': 'var(--space-4)',
'required-color': 'var(--color-risk-text)',
'control-height-xs': 'var(--size-xs-control-height)',
'control-height-sm': 'var(--size-sm-control-height)',
'control-height-md': 'var(--size-md-control-height)',
'control-height-lg': 'var(--size-lg-control-height)',
'control-height-xl': 'var(--size-xl-control-height)',
// Segment height — derived from the SIZE axis at the FIELD family level
// (B.1 mandate, audit S6): the segmented fields (date/time/color + ranges)
// read `--field-segment-height` instead of each re-declaring `height-{k}`.
// The `size:` TSC scope emits `[data-field][data-size='{k}']`; host default
// = md (the field's default size). Inert until a field wraps in `[data-field]`.
'segment-height': {
declarations: [
{ value: 'calc(var(--field-control-height-md) - var(--space-2))', scope: 'host' },
{ value: 'calc(var(--field-control-height-xs) - var(--space-2))', scope: 'size:xs' },
{ value: 'calc(var(--field-control-height-sm) - var(--space-2))', scope: 'size:sm' },
{ value: 'calc(var(--field-control-height-lg) - var(--space-2))', scope: 'size:lg' },
{ value: 'calc(var(--field-control-height-xl) - var(--space-2))', scope: 'size:xl' }
]
},
'control-padding-inline-xs': 'var(--space-2)',
'control-padding-inline-sm': 'var(--space-2-5)',
'control-padding-inline-md': 'var(--space-3)',
'control-padding-inline-lg': 'var(--space-3-5)',
'control-padding-inline-xl': 'var(--space-4)',
'control-gap-xs': 'var(--space-1)',
'control-gap-sm': 'var(--space-1-5)',
'control-gap-md': 'var(--space-2)',
'control-gap-lg': 'var(--space-2-5)',
'control-gap-xl': 'var(--space-3)',
'control-font-size-xs': 'var(--size-xs-font-size)',
'control-font-size-sm': 'var(--size-sm-font-size)',
'control-font-size-md': 'var(--size-md-font-size)',
'control-font-size-lg': 'var(--size-lg-font-size)',
'control-font-size-xl': 'var(--size-xl-font-size)',
'control-line-height': 'var(--leading-ui)',
// Value/control text sits one weight step ABOVE the label: the value is
// `medium`, the label stays `regular` (the "label one step below the
// component" doctrine — the size offset is derived in field.css).
'control-font-weight': 'var(--font-weight-medium)',
'control-radius': 'var(--radius-md)',
'control-border-width': 'var(--border-width)',
'control-border': 'var(--color-border-default)',
'control-border-hover': 'var(--color-border-strong)',
'control-border-focus': 'var(--focus-ring-color)',
'control-border-invalid': 'var(--color-risk-border)',
// Per-instance palette (THM-2 forward). The `color` prop's only live accent
// is the floating-label focus tint (`--_field-palette-text`). Host default =
// primary; the shared forward routes roles + 33 scales + custom automatically.
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' },
'control-bg': 'var(--color-surface-default)',
'control-ghost-border': 'transparent',
'control-ghost-bg': 'transparent',
'control-bg-readonly': 'var(--color-surface-muted)',
'control-bg-disabled': 'var(--color-surface-muted)',
'control-color': 'var(--color-content-primary)',
'control-placeholder-color': 'var(--color-content-muted)',
'affix-color': 'var(--color-content-secondary)',
// Shared field-family segment highlight (single source, theme-retunable
// from one place): the date/time/color ACTIVE segment being edited.
// `segment-active-bg` bakes its own transparency (color-mix), so a
// theme retunes colour + opacity + text together. Segment HOVER is the
// state-layer (`--state-hover`), not a field token
// (field-segment-state.css).
'segment-active-bg': 'color-mix(in srgb, var(--color-primary-border) 40%, transparent)',
'segment-active-text': 'var(--color-primary-text)',
'message-line-height': 'var(--leading-ui)',
'helper-color': 'var(--color-content-secondary)',
'error-color': 'var(--color-risk-text)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
form: {
'gap-xs': 'var(--space-3)',
'gap-sm': 'var(--space-4)',
'gap-md': 'var(--space-5)',
'gap-lg': 'var(--space-6)',
'gap-xl': 'var(--space-7)',
'font-family': 'var(--style-label-font-family)',
color: 'var(--color-content-primary)',
radius: 'var(--radius-lg)',
'border-width': 'var(--border-width)',
'panel-padding': 'var(--space-5)',
'panel-border': 'var(--color-border-default)',
'panel-bg': 'var(--color-surface-raised)',
'panel-shadow': 'var(--depth-raised-shadow), var(--depth-raised-halo)',
'grid-min': '18rem',
'grid-column-gap': 'var(--space-5)',
'action-height-xs': 'var(--size-xs-control-height)',
'action-height-sm': 'var(--size-sm-control-height)',
'action-height-md': 'var(--size-md-control-height)',
'action-height-lg': 'var(--size-lg-control-height)',
'action-height-xl': 'var(--size-xl-control-height)',
'action-padding-inline-xs': 'var(--space-2-5)',
'action-padding-inline-sm': 'var(--space-3)',
'action-padding-inline-md': 'var(--space-3-5)',
'action-padding-inline-lg': 'var(--space-4)',
'action-padding-inline-xl': 'var(--space-4-5)',
'action-font-size-xs': 'var(--size-xs-font-size)',
'action-font-size-sm': 'var(--size-xs-font-size)',
'action-font-size-md': 'var(--size-sm-font-size)',
'action-font-size-lg': 'var(--size-sm-font-size)',
'action-font-size-xl': 'var(--size-md-font-size)',
'action-font-weight': 'var(--font-weight-medium)',
'action-line-height': 'var(--leading-ui)',
'action-gap': 'var(--space-2)',
'action-radius': 'var(--radius-md)',
'action-border-width': 'var(--border-width)',
'error-summary-padding': 'var(--space-3)',
'error-summary-border-width': 'var(--border-width)',
'error-summary-radius': 'var(--radius-md)',
'error-summary-font-size': 'var(--size-sm-font-size)',
'error-summary-line-height': 'var(--leading-ui)',
'error-summary-list-indent': 'var(--space-4)',
'link-underline-offset': '0.18em',
'auto-fields-gap': 'var(--space-3)',
'auto-fields-array-padding': 'var(--space-3)',
'auto-fields-border-width': 'var(--border-width)',
'auto-fields-radius': 'var(--radius-md)',
'auto-fields-item-gap': 'var(--space-2)',
'auto-fields-item-padding': 'var(--space-3)',
'widget-padding-inline': 'var(--space-3)',
'widget-radius': 'var(--radius-md)',
'widget-border-width': 'var(--border-width)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
drawer: {
'overlay-bg': 'var(--color-overlay)',
'overlay-blur': 'var(--blur-lg)',
'overlay-opacity': '62%',
'overlay-z': 'var(--z-index-overlay-backdrop)',
'content-z': 'var(--z-index-overlay-content)',
'inline-z': 'var(--z-index-overlay-inline)',
'handle-bg': 'var(--color-neutral-track)',
'handle-bg-active': 'var(--color-content-secondary)',
'handle-radius': 'var(--radius-full)',
'handle-length': '36px',
'handle-thickness': '4px',
'handle-active-scale': '1.18',
'handle-transition-duration': 'var(--duration-fast)',
'handle-transition-ease': 'var(--ease-out)',
// Surface · border · base-shadow now come from the `modal` plane (data-depth,
// A1); the in-flight drag shadow keeps its own token.
'content-color': 'var(--color-content-primary)',
'content-radius': 'var(--radius-xl)',
'content-shadow-dragging':
'var(--shadow-overlay-strong, var(--depth-overlay-shadow)), var(--depth-overlay-halo)',
'content-ring-dragging-width': 'var(--border-width-medium)',
'content-ring-dragging-color':
'color-mix( in srgb, var(--color-content-secondary) 32%, transparent )',
'content-ring-dragging-offset': '2px',
'content-inset': 'var(--space-3)',
'content-width-sm': '360px',
'content-width-md': '440px',
'content-width-lg': '560px',
'content-height-sm': '260px',
'content-height-md': '360px',
'content-height-lg': '480px',
'content-padding-sm': 'var(--space-4)',
'content-padding-md': 'var(--space-5)',
'content-padding-lg': 'var(--space-6)',
'content-padding-full': 'var(--space-6)',
'stack-gap': 'var(--space-4)',
'header-gap': 'var(--space-3)',
'footer-gap': 'var(--space-3)',
'footer-margin-top': 'var(--space-2)',
'title-font-family': 'var(--font-family-display)',
'title-font-size': 'var(--size-lg-font-size)',
'title-font-weight': 'var(--font-weight-semibold)',
'title-line-height': 'var(--leading-heading)',
'title-color': 'var(--color-content-primary)',
'description-font-family': 'var(--style-label-font-family)',
'description-font-size': 'var(--size-sm-font-size)',
'description-line-height': 'var(--leading-ui)',
'description-color': 'var(--color-content-secondary)'
// `close-*` tokens removed: Drawer.Close composes the canonical
// <IconButton>/<Button>, so its chrome lives in button.css.
},
icon: {
size: 'var(--size-md-icon-size)',
'stroke-width': 'var(--icon-stroke-width-md)'
},
image: {
// ── Size presets (square boxes) ──────────────────────────────────
'size-xs': '64px',
'size-sm': '96px',
'size-md': '160px',
'size-lg': '240px',
'size-xl': '360px',
// ── Radius scale ──────────────────────────────────────────────────
'radius-none': '0',
'radius-sm': 'var(--radius-sm)',
'radius-md': 'var(--radius-md)',
'radius-lg': 'var(--radius-lg)',
'radius-full': 'var(--radius-full)',
// ── Fade-in transition for the inner <img> ───────────────────────
'fade-duration': 'var(--duration-normal)',
'fade-ease': 'var(--ease-out)',
// ── Skeleton placeholder shimmer ──────────────────────────────────
'placeholder-base': 'var(--color-surface-raised)',
'shimmer-duration': '1.8s',
// ── Custom placeholder color (set inline by Svelte file) ─────────
'placeholder-color-custom': 'var(--color-neutral-track)',
// ── Fallback typography (for text-only fallback content) ─────────
'font-family': 'var(--style-label-font-family)',
'fallback-color': 'var(--color-content-secondary)',
'fallback-font-size': 'var(--size-sm-font-size)',
'fallback-line-height': 'var(--leading-ui)',
// ── Error state ──────────────────────────────────────────────────
'error-bg': 'var(--color-surface-raised)',
'error-color': 'var(--color-content-muted)',
'error-font-size': 'var(--size-sm-font-size)',
'error-line-height': 'var(--leading-ui)'
},
meter: {
'height-xs': '2px',
'height-sm': '4px',
'height-md': '6px',
'height-lg': '8px',
'height-xl': '12px',
'radius-xs': 'var(--radius-full)',
'radius-sm': 'var(--radius-full)',
'radius-md': 'var(--radius-full)',
'radius-lg': 'var(--radius-full)',
'radius-xl': 'var(--radius-full)',
'ring-size-xs': '2rem',
'ring-size-sm': '2.5rem',
'ring-size-md': '3rem',
'ring-size-lg': '3.5rem',
'ring-size-xl': '4rem',
'ring-thickness-xs': '3px',
'ring-thickness-sm': '4px',
'ring-thickness-md': '6px',
'ring-thickness-lg': '8px',
'ring-thickness-xl': '10px',
'track-bg': 'var(--color-neutral-track)',
'ring-track-bg':
'color-mix(in srgb, var(--color-content-primary) 12%, var(--color-surface-default))',
'ring-center-bg': 'var(--color-surface-default)',
'indicator-bg-below': 'var(--color-risk-solid)',
'indicator-bg-optimum': 'var(--color-fulfill-solid)',
'indicator-bg-above': 'var(--color-threat-solid)',
'transition-duration': 'var(--duration-normal)',
'transition-ease': 'var(--ease-default)'
},
// spin-field — shared visual for the stepper-field family (number-field +
// css-field). Both reuse these tokens via structural identity (`data-spin-
// field*`) and the shared `spin-field.css`. A theme tints per component by
// scoping `[data-number-field] { --spin-field-… }`. See THEMING §34.
'spin-field': {
border: 'var(--color-border-default)',
// Steppers are square segments flush to the field edge (the field clips
// their outer corners), separated from the input by a divider on the
// inner side.
'control-radius': '0',
'control-border-width': 'var(--border-width)',
'control-border': 'transparent',
'control-divider': 'var(--color-border-default)',
'control-border-hover': 'var(--color-border-strong)',
'control-border-active': 'var(--color-primary-border)',
'control-bg': 'transparent',
// Higher-contrast hover/focus fill than the field surface.
'control-bg-focus': 'var(--color-neutral-hover)',
'control-bg-active': 'var(--color-primary-track)',
'control-color': 'var(--color-content-secondary)',
'control-color-hover': 'var(--color-content-primary)',
'control-color-active': 'var(--color-primary-text)',
// Default stepper glyphs — themeable, decorative (the buttons carry an
// aria-label). Rendered via `:empty::before`, so passing children to a
// trigger overrides them. Split uses +/−; the stacked column uses up/down
// triangles. A theme retints/reshapes by overriding any of the four.
'control-increment-glyph': "'+'",
'control-decrement-glyph': "'\\2212'",
'control-increment-glyph-stacked': "'\\25B2'",
'control-decrement-glyph-stacked': "'\\25BC'",
'control-disabled-opacity': '0.48',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)'
},
'float-panel': {
bg: 'var(--color-surface-overlay)',
border: 'var(--color-border-default)',
'border-width': 'var(--border-width)',
radius: 'var(--radius-lg)',
shadow: 'var(--shadow-4)',
'shadow-topmost': 'var(--shadow-5)',
// Elevation worn WHILE grabbed (drag/resize): the panel LIFTS to the
// theme's highest elevation. Applied on pointerdown so the lift transition
// finishes before the move; the shadow is then static during the drag, so
// the GPU layer just moves (no per-frame shadow repaint). See float-panel.css.
'shadow-active': 'var(--shadow-6)',
color: 'var(--color-content-primary)',
'font-family': 'var(--style-label-font-family)',
'font-size': 'var(--size-sm-font-size)',
'line-height': 'var(--leading-ui)',
'header-bg': 'var(--color-surface-raised)',
'header-padding-inline': 'var(--space-3)',
'header-padding-block': 'var(--space-2)',
'header-gap': 'var(--space-2)',
'controls-gap': 'var(--space-1)',
'title-font-size': 'var(--size-sm-font-size)',
'title-font-weight': 'var(--font-weight-semibold)',
'title-color': 'var(--color-content-primary)',
'body-padding-inline': 'var(--space-3)',
'body-padding-block': 'var(--space-3)',
'resize-size': 'var(--space-3-5)',
'resize-edge': 'var(--space-1-5)',
// Header icon-button (close / action / stage-trigger) hit target, and the
// se-corner resize grip — sized for comfort, not the tiny resize-handle.
'button-size': 'var(--space-7)',
'grip-size': 'var(--space-5)',
accent: 'var(--color-neutral-border)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)'
},
'image-adjustments': {
// Vertical rhythm: gap between adjustment rows, and within a row
// (label/value line → slider).
gap: 'var(--space-3)',
'row-gap': 'var(--space-1-5)',
'label-color': 'var(--color-content-secondary)',
'label-font-size': 'var(--size-sm-font-size)',
'label-font-weight': 'var(--font-weight-medium)',
// Read-out: tabular numerals so the value doesn't jitter while dragging.
// The TERTIARY ACCENT ROLE's text slot (audit B.3, 2026-07-07): the
// original `--color-content-tertiary` mixed the content namespace with
// the role name and never existed — the read-out inherited its color
// since birth (7da7285d).
'value-color': 'var(--color-tertiary-text)',
'value-font-size': 'var(--size-xs-font-size)',
'value-font-family': 'var(--font-family-mono)',
'reset-gap': 'var(--space-2)',
'reset-color': 'var(--color-content-secondary)',
'reset-font-size': 'var(--size-xs-font-size)',
// The `hue` row paints its slider track as a hue wheel so the control
// reads as a hue picker. A literal rainbow (the hues ARE the value) — kept
// as a token so it's themeable and stays out of the component CSS.
'hue-track':
'linear-gradient(to right, hsl(0 85% 60%), hsl(60 85% 60%), hsl(120 85% 60%), hsl(180 85% 60%), hsl(240 85% 60%), hsl(300 85% 60%), hsl(360 85% 60%))'
},
'image-picker': {
// Vertical stack: preview → adjustments.
gap: 'var(--space-3)',
// Preview box (ready state). The composed <Image> fills it.
'preview-radius': 'var(--radius-lg)',
'preview-bg': 'var(--color-surface-muted)',
'preview-border': 'var(--color-border-default)',
'preview-aspect': '4 / 3',
// Toolbar floats over the preview's top-right corner.
'toolbar-gap': 'var(--space-1-5)',
'toolbar-inset': 'var(--space-2)',
// Icon buttons (rotate / remove).
'button-size': 'var(--space-8)',
'button-bg': 'color-mix(in srgb, var(--color-surface-default) 88%, transparent)',
'button-fg': 'var(--color-content-primary)',
'button-border': 'var(--color-border-default)',
'button-radius': 'var(--radius-md)',
'button-font-size': 'var(--size-md-font-size)'
},
cropper: {
'viewport-bg': 'var(--color-surface-muted)',
'viewport-radius': 'var(--radius-lg)',
// The darken-outside mask + the thirds grid carry literal rgba/rgb (an
// overlay tint, not a theme color) — kept as tokens so they stay out of
// the component CSS (raw-color contract) and remain themeable.
'mask-bg': 'rgb(0 0 0 / 0.55)',
'grid-line': 'rgb(255 255 255 / 0.4)',
'selection-border': 'var(--color-surface-default)',
'selection-border-width': 'var(--border-width)',
'handle-size': 'var(--space-3-5)',
'handle-bg': 'var(--color-surface-default)',
'handle-border': 'var(--color-primary-solid)',
'handle-border-width': 'var(--border-width)',
'zoom-gap': 'var(--space-2)',
'zoom-btn-size': 'var(--space-7)',
'zoom-btn-bg': 'var(--color-surface-muted)',
'zoom-btn-fg': 'var(--color-content-primary)',
'zoom-btn-border': 'var(--color-border-default)'
},
'date-field': {
'gap-xs': 'var(--space-0-5)',
'gap-sm': 'var(--space-0-5)',
'gap-md': 'var(--space-1)',
'gap-lg': 'var(--space-1-5)',
'gap-xl': 'var(--space-2)',
'segment-padding-inline-xs': 'var(--space-0-5)',
'segment-padding-inline-sm': 'var(--space-0-5)',
'segment-padding-inline-md': 'var(--space-1)',
'segment-padding-inline-lg': 'var(--space-1-5)',
'segment-padding-inline-xl': 'var(--space-2)',
'placeholder-color': 'var(--color-content-muted)',
'literal-color': 'var(--color-content-muted)',
'segment-radius': 'var(--radius-sm)',
'segment-color': 'var(--color-content-primary)',
'segment-invalid-color': 'var(--color-risk-text)',
'segment-readonly-bg': 'var(--color-surface-overlay)',
'segment-readonly-color': 'var(--color-content-secondary)',
'segment-readonly-decoration': 'underline dotted var(--color-content-muted)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
'time-field': {
'gap-xs': 'var(--space-0-5)',
'gap-sm': 'var(--space-0-5)',
'gap-md': 'var(--space-1)',
'gap-lg': 'var(--space-1-5)',
'gap-xl': 'var(--space-2)',
'segment-padding-inline-xs': 'var(--space-0-5)',
'segment-padding-inline-sm': 'var(--space-0-5)',
'segment-padding-inline-md': 'var(--space-1)',
'segment-padding-inline-lg': 'var(--space-1-5)',
'segment-padding-inline-xl': 'var(--space-2)',
'placeholder-color': 'var(--color-content-muted)',
'literal-color': 'var(--color-content-muted)',
'segment-radius': 'var(--radius-sm)',
'segment-color': 'var(--color-content-primary)',
'segment-invalid-color': 'var(--color-risk-text)',
'segment-readonly-bg': 'var(--color-surface-overlay)',
'segment-readonly-color': 'var(--color-content-secondary)',
'segment-readonly-decoration': 'underline dotted var(--color-content-muted)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
'color-field': {
'segment-padding-inline-xs': 'var(--space-0-5)',
'segment-padding-inline-sm': 'var(--space-0-5)',
'segment-padding-inline-md': 'var(--space-1)',
'segment-padding-inline-lg': 'var(--space-1-5)',
'segment-padding-inline-xl': 'var(--space-2)',
'placeholder-color': 'var(--color-content-muted)',
'literal-color': 'var(--color-content-muted)',
'segment-radius': 'var(--radius-sm)',
'segment-color': 'var(--color-content-primary)',
'segment-invalid-color': 'var(--color-risk-text)',
'segment-readonly-bg': 'var(--color-surface-overlay)',
'segment-readonly-color': 'var(--color-content-secondary)',
'segment-readonly-decoration': 'underline dotted var(--color-content-muted)',
// Leading value swatch: exactly the control icon size for this field size
// (user spec: "del mismo tamaño que los iconos del control"). A concrete
// length per size — never `1em` — so it can't recompute on re-render.
'swatch-size-xs': 'var(--size-xs-icon-size)',
'swatch-size-sm': 'var(--size-sm-icon-size)',
'swatch-size-md': 'var(--size-md-icon-size)',
'swatch-size-lg': 'var(--size-lg-icon-size)',
'swatch-size-xl': 'var(--size-xl-icon-size)',
'format-select-color': 'var(--color-content-secondary)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
'color-picker': {
// Popover content shell. The panel padding is the popover's canonical
// size-aware `--popover-padding-*` (shared by every floating panel) — the
// picker declares NO padding of its own. Inter-row gap is owned by
// [data-picker-body]; both kept the picker from being a padding outlier.
'content-width-sm': '17rem',
'content-width-md': '19.5rem',
'content-width-lg': '22rem',
// Trigger button (compact field-shaped pill with a swatch dot)
'trigger-height-sm': 'var(--size-sm-control-height)',
'trigger-height-md': 'var(--size-md-control-height)',
'trigger-height-lg': 'var(--size-lg-control-height)',
'trigger-padding-inline-sm': 'var(--space-2)',
'trigger-padding-inline-md': 'var(--space-2-5)',
'trigger-padding-inline-lg': 'var(--space-3)',
'trigger-gap': 'var(--space-2)',
'trigger-radius': 'var(--radius-md)',
'trigger-border-width': 'var(--border-width)',
'trigger-border': 'var(--color-border-default)',
'trigger-bg': 'var(--color-surface-default)',
'trigger-color': 'var(--color-content-primary)',
'trigger-hover-border': 'var(--color-border-strong)',
'trigger-swatch-size-sm': 'calc(var(--size-sm-control-height) - var(--space-2))',
'trigger-swatch-size-md': 'calc(var(--size-md-control-height) - var(--space-2))',
'trigger-swatch-size-lg': 'calc(var(--size-lg-control-height) - var(--space-2))',
'trigger-swatch-radius': 'var(--color-picker-trigger-radius)',
// 2D area
'area-height': '11rem',
'area-radius': 'var(--radius-md)',
'area-border-width': 'var(--border-width)',
'area-border': 'var(--color-border-subtle)',
'area-thumb-size': '16px',
'area-thumb-radius': 'var(--radius-full)',
'area-thumb-border-width': 'var(--border-width-medium)',
'area-thumb-border': 'var(--color-surface-default)',
'area-thumb-shadow':
'0 0 0 1px color-mix(in srgb, var(--color-neutral-contrast) 45%, transparent), 0 1px 2px color-mix(in srgb, var(--color-neutral-contrast) 20%, transparent)',
// Channel sliders consume the generic `--slider-*` recipe tokens.
// Only the picker-specific decoration (gradient on track) lives
// in the picker CSS; sizes, shadows and radii come from the
// slider recipe so they stay in lockstep with the standalone
// Slider and the time-picker sliders.
// Transparency checker (alpha background)
'transparency-cell': 'var(--color-content-muted)',
'transparency-cell-alt': 'var(--color-surface-default)',
// Swatches
'swatch-size-sm': '1.25rem',
'swatch-size-md': '1.5rem',
'swatch-size-lg': '1.75rem',
'swatch-radius': 'var(--radius-sm)',
'swatch-border': 'var(--color-border-subtle)',
'swatch-checked-ring': 'var(--color-primary-solid)',
'swatch-gap': 'var(--space-1-5)',
// Eye-dropper button
'eye-dropper-size': 'var(--size-sm-control-height)',
'eye-dropper-radius': 'var(--radius-md)',
'eye-dropper-bg': 'transparent',
'eye-dropper-color': 'var(--color-content-secondary)',
'eye-dropper-hover-color': 'var(--color-content-primary)',
// Hue spectrum — drives the hue ring + saturation overlay. These
// sit in a sidecar file (color-picker-spectrum.css) so the raw
// hsl literals stay out of the main recipe; declaring them here
// keeps the recipe contract whole.
'hue-0': 'hsl(0, 100%, 50%)',
'hue-60': 'hsl(60, 100%, 50%)',
'hue-120': 'hsl(120, 100%, 50%)',
'hue-180': 'hsl(180, 100%, 50%)',
'hue-240': 'hsl(240, 100%, 50%)',
'hue-300': 'hsl(300, 100%, 50%)',
'hue-360': 'hsl(360, 100%, 50%)',
'saturation-floor': 'hsl(0, 0%, 50%)',
// Misc
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
button: {
// Size tokens
'height-xs': 'var(--size-xs-control-height)',
'height-sm': 'var(--size-sm-control-height)',
'height-md': 'var(--size-md-control-height)',
'height-lg': 'var(--size-lg-control-height)',
'height-xl': 'var(--size-xl-control-height)',
'padding-inline-xs': 'var(--space-2)',
'padding-inline-sm': 'var(--space-3)',
'padding-inline-md': 'var(--space-4)',
'padding-inline-lg': 'var(--space-5)',
'padding-inline-xl': 'var(--space-6)',
'gap-xs': 'var(--space-1)',
'gap-sm': 'var(--space-1-5)',
'gap-md': 'var(--space-2)',
'gap-lg': 'var(--space-2-5)',
'gap-xl': 'var(--space-3)',
'font-family': 'var(--style-label-font-family)',
'line-height': 'var(--leading-ui)',
'letter-spacing': 'var(--tracking-normal)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'font-size-xl': 'var(--size-xl-font-size)',
'font-weight-xs': 'var(--font-weight-medium)',
'font-weight-sm': 'var(--font-weight-medium)',
'font-weight-md': 'var(--font-weight-medium)',
'font-weight-lg': 'var(--font-weight-medium)',
'font-weight-xl': 'var(--font-weight-medium)',
// Icon follows the button's font-size 1:1 (canonical icon↔type correlation):
// the font now scales 1:1 with the size (no longer compact), so the icon
// matches at the same step. The parallel icon scale preserves icon ≈ font+2
// in the body sizes (md: font 16 → icon 18). Density-scaled; never hardcoded px.
'icon-size-xs': 'var(--size-xs-icon-size)',
'icon-size-sm': 'var(--size-sm-icon-size)',
'icon-size-md': 'var(--size-md-icon-size)',
'icon-size-lg': 'var(--size-lg-icon-size)',
'icon-size-xl': 'var(--size-xl-icon-size)',
'radius-sm': 'var(--radius-sm)',
'radius-md': 'var(--radius-md)',
'radius-lg': 'var(--radius-lg)',
'radius-xl': 'var(--radius-xl)',
'border-width': 'var(--border-width)',
shadow: 'none',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)',
// Per-color palettes — 7 slots per color, uniform across all 8: track /
// element / border / solid / solid-hover / text / contrast. soft + surface
// + outline variants read track/border, so every color must define them
// (was only fulfill/loss — the others rendered with no bg/border).
'primary-track': 'var(--color-primary-track)',
'primary-element': 'var(--color-primary-element)',
'primary-border': 'var(--color-primary-border)',
'primary-solid': 'var(--color-primary-solid)',
'primary-solid-hover': 'var(--color-primary-solid-hover)',
'primary-text': 'var(--color-primary-text)',
'primary-contrast': 'var(--color-primary-contrast)',
'secondary-track': 'var(--color-secondary-track)',
'secondary-element': 'var(--color-secondary-element)',
'secondary-border': 'var(--color-secondary-border)',
'secondary-solid': 'var(--color-secondary-solid)',
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
'secondary-text': 'var(--color-secondary-text)',
'secondary-contrast': 'var(--color-secondary-contrast)',
'neutral-track': 'var(--color-neutral-track)',
'neutral-element': 'var(--color-neutral-element)',
'neutral-border': 'var(--color-neutral-border)',
'neutral-solid': 'var(--color-neutral-solid)',
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
'neutral-text': 'var(--color-neutral-text)',
'neutral-contrast': 'var(--color-neutral-contrast)',
'affirm-track': 'var(--color-affirm-track)',
'affirm-element': 'var(--color-affirm-element)',
'affirm-border': 'var(--color-affirm-border)',
'affirm-solid': 'var(--color-affirm-solid)',
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
'affirm-text': 'var(--color-affirm-text)',
'affirm-contrast': 'var(--color-affirm-contrast)',
'fulfill-track': 'var(--color-fulfill-track)',
'fulfill-element': 'var(--color-fulfill-element)',
'fulfill-border': 'var(--color-fulfill-border)',
'fulfill-solid': 'var(--color-fulfill-solid)',
'fulfill-solid-hover': 'var(--color-fulfill-solid-hover)',
'fulfill-text': 'var(--color-fulfill-text)',
'fulfill-contrast': 'var(--color-fulfill-contrast)',
'risk-track': 'var(--color-risk-track)',
'risk-element': 'var(--color-risk-element)',
'risk-border': 'var(--color-risk-border)',
'risk-solid': 'var(--color-risk-solid)',
'risk-solid-hover': 'var(--color-risk-solid-hover)',
'risk-text': 'var(--color-risk-text)',
'risk-contrast': 'var(--color-risk-contrast)',
'threat-track': 'var(--color-threat-track)',
'threat-element': 'var(--color-threat-element)',
'threat-border': 'var(--color-threat-border)',
'threat-solid': 'var(--color-threat-solid)',
'threat-solid-hover': 'var(--color-threat-solid-hover)',
'threat-text': 'var(--color-threat-text)',
'threat-contrast': 'var(--color-threat-contrast)',
'loss-track': 'var(--color-loss-track)',
'loss-element': 'var(--color-loss-element)',
'loss-border': 'var(--color-loss-border)',
'loss-solid': 'var(--color-loss-solid)',
'loss-solid-hover': 'var(--color-loss-solid-hover)',
'loss-text': 'var(--color-loss-text)',
'loss-contrast': 'var(--color-loss-contrast)',
// Active palette — 8 custom properties, each declared with 8
// declarations (host default = primary, plus per-color overrides
// for the 7 other supported values). Single source of truth via
// TSC v2 `declarations[]`. Replaces the per-color blocks that
// used to live in `button.css`.
'palette-track': {
declarations: [
{ value: 'var(--button-primary-track)', scope: 'host' },
{ value: 'var(--button-secondary-track)', scope: 'color:secondary' },
{ value: 'var(--button-neutral-track)', scope: 'color:neutral' },
{ value: 'var(--button-affirm-track)', scope: 'color:affirm' },
{ value: 'var(--button-fulfill-track)', scope: 'color:fulfill' },
{ value: 'var(--button-risk-track)', scope: 'color:risk' },
{ value: 'var(--button-threat-track)', scope: 'color:threat' },
{ value: 'var(--button-loss-track)', scope: 'color:loss' }
]
},
'palette-element': {
declarations: [
{ value: 'var(--button-primary-element)', scope: 'host' },
{ value: 'var(--button-secondary-element)', scope: 'color:secondary' },
{ value: 'var(--button-neutral-element)', scope: 'color:neutral' },
{ value: 'var(--button-affirm-element)', scope: 'color:affirm' },
{ value: 'var(--button-fulfill-element)', scope: 'color:fulfill' },
{ value: 'var(--button-risk-element)', scope: 'color:risk' },
{ value: 'var(--button-threat-element)', scope: 'color:threat' },
{ value: 'var(--button-loss-element)', scope: 'color:loss' }
]
},
'palette-border': {
declarations: [
{ value: 'var(--button-primary-border)', scope: 'host' },
{ value: 'var(--button-secondary-border)', scope: 'color:secondary' },
{ value: 'var(--button-neutral-border)', scope: 'color:neutral' },
{ value: 'var(--button-affirm-border)', scope: 'color:affirm' },
{ value: 'var(--button-fulfill-border)', scope: 'color:fulfill' },
{ value: 'var(--button-risk-border)', scope: 'color:risk' },
{ value: 'var(--button-threat-border)', scope: 'color:threat' },
{ value: 'var(--button-loss-border)', scope: 'color:loss' }
]
},
'palette-solid': {
declarations: [
{ value: 'var(--button-primary-solid)', scope: 'host' },
{ value: 'var(--button-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--button-neutral-solid)', scope: 'color:neutral' },
{ value: 'var(--button-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--button-fulfill-solid)', scope: 'color:fulfill' },
{ value: 'var(--button-risk-solid)', scope: 'color:risk' },
{ value: 'var(--button-threat-solid)', scope: 'color:threat' },
{ value: 'var(--button-loss-solid)', scope: 'color:loss' }
]
},
'palette-solid-hover': {
declarations: [
{ value: 'var(--button-primary-solid-hover)', scope: 'host' },
{ value: 'var(--button-secondary-solid-hover)', scope: 'color:secondary' },
{ value: 'var(--button-neutral-solid-hover)', scope: 'color:neutral' },
{ value: 'var(--button-affirm-solid-hover)', scope: 'color:affirm' },
{ value: 'var(--button-fulfill-solid-hover)', scope: 'color:fulfill' },
{ value: 'var(--button-risk-solid-hover)', scope: 'color:risk' },
{ value: 'var(--button-threat-solid-hover)', scope: 'color:threat' },
{ value: 'var(--button-loss-solid-hover)', scope: 'color:loss' }
]
},
'palette-text': {
declarations: [
{ value: 'var(--button-primary-text)', scope: 'host' },
{ value: 'var(--button-secondary-text)', scope: 'color:secondary' },
{ value: 'var(--button-neutral-text)', scope: 'color:neutral' },
{ value: 'var(--button-affirm-text)', scope: 'color:affirm' },
{ value: 'var(--button-fulfill-text)', scope: 'color:fulfill' },
{ value: 'var(--button-risk-text)', scope: 'color:risk' },
{ value: 'var(--button-threat-text)', scope: 'color:threat' },
{ value: 'var(--button-loss-text)', scope: 'color:loss' }
]
},
'palette-contrast': {
declarations: [
{ value: 'var(--button-primary-contrast)', scope: 'host' },
{ value: 'var(--button-secondary-contrast)', scope: 'color:secondary' },
{ value: 'var(--button-neutral-contrast)', scope: 'color:neutral' },
{ value: 'var(--button-affirm-contrast)', scope: 'color:affirm' },
{ value: 'var(--button-fulfill-contrast)', scope: 'color:fulfill' },
{ value: 'var(--button-risk-contrast)', scope: 'color:risk' },
{ value: 'var(--button-threat-contrast)', scope: 'color:threat' },
{ value: 'var(--button-loss-contrast)', scope: 'color:loss' }
]
}
},
'month-grid': {
// Palette forward (THM-2): host default = primary; the shared `--palette-*`
// layer routes roles + 33 scales + custom into `--_month-grid-palette-*`
// (was a per-role cascade in month-grid.css). MonthGrid reuses calendar
// tokens for chrome but owns its selected-cell palette.
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-solid-hover': { value: 'var(--color-primary-solid-hover)', scope: 'host' },
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
'_palette-border': { value: 'var(--color-primary-border)', scope: 'host' },
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
'year-grid': {
// Palette forward (THM-2) — same as month-grid: host default = primary,
// shared layer routes roles + 33 scales + custom into `--_year-grid-palette-*`.
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-solid-hover': { value: 'var(--color-primary-solid-hover)', scope: 'host' },
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
'_palette-border': { value: 'var(--color-primary-border)', scope: 'host' },
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
'range-calendar': {
// Palette forward (THM-2): the END accent + `--calendar-range-end-*` bind to
// `--_range-calendar-palette-*`; host default = primary, shared layer routes
// roles + 33 scales + custom. The affirm/fulfill START-endpoint swap stays a
// per-role rule in range-calendar.css (it's a distinctness refinement, not
// palette). (Was a per-role cascade over `--_calendar-accent-*`.)
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-solid-hover': { value: 'var(--color-primary-solid-hover)', scope: 'host' },
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
'_palette-border': { value: 'var(--color-primary-border)', scope: 'host' },
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
// ── List / data-grid family — the shared 2-slot selection palette ──────────
// Uniform pattern: `--_{c}-palette-solid` (accent: sort/active indicator) +
// `--_{c}-palette-element` (soft: selected-row tint). Host default = primary;
// the shared layer routes roles + 33 scales + custom. Was a per-role cascade
// in each component's CSS (`--_{c}-accent` / `--_{c}-accent-soft`). The `-root`
// components stamp `data-color` on `[data-{c}-root]` (not the bare provider),
// so their forward targets that part via `parts: ['root']`. drag-drop / combobox
// / timeline already own a recipe → their palette slots live in those entries.
table: {
'_palette-solid': { parts: ['root'], declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }] },
'_palette-element': { parts: ['root'], declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }] }
},
// tree-grid / tree-view only tint the SELECTED row (element); their `accent`
// (solid) token had no consumer, so they declare only `_palette-element`.
'tree-grid': {
'_palette-element': { parts: ['root'], declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }] }
},
'tree-view': {
'_palette-element': { parts: ['root'], declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }] }
},
// virtual-list / virtual-grid: their accent cascade was declaration-only (no CSS
// consumer) — pure virtualized containers with no colour-driven visual. The type
// stays open (guard/uniformity) + the wrapper stamps a harmless `data-color`, but
// there is nothing to forward, so NO recipe here (avoids an orphan palette token).
feed: {
'_palette-solid': { parts: ['root'], declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }] },
'_palette-element': { parts: ['root'], declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }] }
},
'grid-list': {
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
},
clipboard: {
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
},
carousel: {
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
},
calendar: {
gap: 'var(--space-3)',
'padding-xs': 'var(--space-2)',
'padding-sm': 'var(--space-3)',
'padding-md': 'var(--space-4)',
'padding-lg': 'var(--space-5)',
radius: 'var(--radius-lg)',
'border-width': 'var(--border-width)',
border: 'var(--color-border-subtle)',
'invalid-border': 'var(--color-risk-border)',
bg: 'var(--color-surface-raised)',
'outline-bg': 'var(--color-surface-default)',
'outline-shadow': 'none',
color: 'var(--color-content-primary)',
shadow: 'var(--depth-raised-shadow), var(--depth-raised-halo)',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'line-height': 'var(--leading-ui)',
'header-gap': 'var(--space-2)',
'heading-color': 'var(--color-content-primary)',
'heading-font-size': 'var(--size-sm-font-size)',
'heading-font-weight': 'var(--font-weight-semibold)',
'control-size-xs': 'var(--size-xxs-control-height)',
'control-size-sm': 'var(--size-xs-control-height)',
'control-size-md': 'var(--size-sm-control-height)',
'control-size-lg': 'var(--size-md-control-height)',
'control-radius': 'var(--radius-md)',
'control-border-width': 'var(--border-width)',
'control-border': 'transparent',
'control-border-hover': 'var(--color-border-subtle)',
'control-bg': 'transparent',
'control-color': 'var(--color-content-secondary)',
'control-color-hover': 'var(--color-content-primary)',
'select-min-width': '8ch',
'select-padding-inline': 'var(--space-2)',
'grid-gap': 'var(--space-0-5)',
'weekday-size': 'var(--space-5)',
'weekday-color': 'var(--color-content-muted)',
'weekday-font-size': 'var(--size-xs-font-size)',
'weekday-font-weight': 'var(--font-weight-medium)',
'day-size-xs': 'var(--size-xxs-control-height)',
'day-size-sm': 'var(--size-xs-control-height)',
'day-size-md': 'var(--size-sm-control-height)',
'day-size-lg': 'var(--size-md-control-height)',
'day-radius': 'var(--radius-md)',
'day-border-width': 'var(--border-width)',
'day-border': 'transparent',
'day-border-hover': 'var(--color-border-subtle)',
'day-bg': 'transparent',
'day-color': 'var(--color-content-primary)',
'day-color-hover': 'var(--color-content-primary)',
'day-font-weight': 'var(--font-weight-regular)',
'day-selected-color': 'var(--color-content-on-solid)',
// Range endpoints — the calendar-surface PUBLIC range API (S8). RangeCalendar
// paints start / end / dual-endpoint day cells from these; a consumer
// (DateRangePicker, a theme) retunes the range palette by setting the public
// token, never RangeCalendar's private. `:root` defaults; RangeCalendar binds
// `range-end-*` to its live accent and swaps `range-start-*` per data-color
// (both depend on the `[data-range-calendar]` host cascade, so they stay in
// range-calendar.css). Start defaults to `affirm` (the range "opens"); end
// follows the accent (default primary).
'range-start-solid': 'var(--color-affirm-solid)',
'range-start-solid-hover': 'var(--color-affirm-solid-hover)',
'range-start-border': 'var(--color-affirm-border)',
'range-start-text': 'var(--color-affirm-contrast)',
'range-end-solid': 'var(--color-primary-solid)',
'range-end-solid-hover': 'var(--color-primary-solid-hover)',
'range-end-border': 'var(--color-primary-solid)',
'range-end-text': 'var(--color-content-on-solid)',
'day-today-border': 'var(--color-border-strong)',
'day-today-color': 'var(--color-content-primary)',
'day-today-font-weight': 'var(--font-weight-semibold)',
'day-outside-color': 'var(--color-content-muted)',
'day-outside-opacity': '0.62',
'day-weekend-color': 'var(--color-content-secondary)',
// Per-instance palette (THM-2 forward): the `color` accent slots (selected
// day, today ring, range track). Host default = primary; the shared
// `--palette-*` layer routes roles + 33 scales + custom into
// `--_calendar-palette-*` (was a per-role cascade in calendar.css).
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-solid-hover': { value: 'var(--color-primary-solid-hover)', scope: 'host' },
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
'_palette-border': { value: 'var(--color-primary-border)', scope: 'host' },
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' },
// Host-scoped ON PURPOSE (audit B.2, 2026-07-07): both values reference the
// private `--_calendar-palette-border`, which lives on `[data-calendar]`
// (the recipe host default + the THM-2 forward). Emitted at `:root` they
// froze guaranteed-invalid (the private doesn't exist there) and the
// holiday/event marks NEVER painted. On the host, the custom property
// computes where the private (and its colour retunes) actually cascade.
'day-holiday-shadow': {
value: 'inset 0 -2px 0 var(--_calendar-palette-border)',
scope: 'host'
},
'day-unavailable-decoration': 'var(--color-risk-border)',
'event-shadow': {
value:
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--_calendar-palette-border)',
scope: 'host'
},
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
chronos: {
// Component-semantic dimensions only. Spacing, typography and colors come
// straight from the eidos primitives in chronos.css (no per-component
// font/color tokens — those belong to the eidos foundation).
'cell-min-block-sm': 'calc(var(--size-md-control-height) * 2.6)',
'cell-min-block-md': 'calc(var(--size-md-control-height) * 3.2)',
'cell-min-block-lg': 'calc(var(--size-md-control-height) * 4)',
'num-row': 'var(--space-7)',
'lane-height-sm': 'var(--space-5)',
'lane-height-md': 'var(--space-6)',
'lane-height-lg': 'var(--space-7)',
// Week/Day time-grid viewport height (scrolls internally).
'timegrid-block-size': 'min(70vh, 40rem)',
'disabled-opacity': 'var(--opacity-disabled)'
},
pagination: {
'gap-xs': 'var(--space-0-5)',
'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)',
'gap-lg': 'var(--space-2)',
'control-size-xs': 'var(--size-xxs-control-height)',
'control-size-sm': 'var(--size-xs-control-height)',
'control-size-md': 'var(--size-sm-control-height)',
'control-size-lg': 'var(--size-md-control-height)',
'control-padding-inline-xs': 'var(--space-1-5)',
'control-padding-inline-sm': 'var(--space-2)',
'control-padding-inline-md': 'var(--space-2-5)',
'control-padding-inline-lg': 'var(--space-3)',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'line-height': 'var(--leading-ui)',
'control-radius': 'var(--radius-md)',
'control-border-width': 'var(--border-width)',
'control-border': 'transparent',
'control-border-hover': 'var(--color-border-subtle)',
'control-bg': 'transparent',
'control-color': 'var(--color-content-secondary)',
'control-color-hover': 'var(--color-content-primary)',
'selected-border': 'var(--color-primary-border)',
'selected-border-hover': 'var(--color-primary-border)',
'selected-bg': 'var(--color-primary-solid)',
'selected-bg-hover': 'var(--color-primary-solid-hover)',
'selected-color': 'var(--color-content-on-solid)',
'ellipsis-color': 'var(--color-content-muted)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
popover: {
'content-z': 'var(--z-index-overlay-floating)',
'overlay-z': 'var(--z-index-overlay-backdrop)',
'overlay-bg': 'var(--color-overlay)',
'trigger-font-size': 'var(--size-sm-font-size)',
gap: 'var(--space-3)',
'max-width': '32rem',
'min-width-sm': '14rem',
'min-width-md': '18rem',
'min-width-lg': '22rem',
'min-width-xl': '28rem',
'padding-block-sm': 'var(--space-2)',
'padding-inline-sm': 'var(--space-2-5)',
'padding-block-md': 'var(--space-3)',
'padding-inline-md': 'var(--space-3)',
'padding-block-lg': 'var(--space-3-5)',
'padding-inline-lg': 'var(--space-4)',
'padding-block-xl': 'var(--space-4)',
'padding-inline-xl': 'var(--space-5)',
'font-size': 'var(--size-sm-font-size)',
'font-family': 'var(--style-label-font-family)',
'font-weight': 'var(--font-weight-regular)',
'line-height': 'var(--leading-text)',
'title-font-family': 'var(--font-family-display)',
'title-font-size': 'var(--size-md-font-size)',
'title-font-weight': 'var(--font-weight-semibold)',
'title-line-height': 'var(--leading-heading)',
'title-color': 'var(--color-content-primary)',
'description-font-family': 'var(--style-label-font-family)',
'description-font-size': 'var(--size-sm-font-size)',
'description-line-height': 'var(--leading-ui)',
'description-color': 'var(--color-content-secondary)',
// Elevation (surface · border · shadow · z) now comes from the `overlay`
// plane via `data-depth` (A1, Decisión 8) — the panel + arrow consume the
// plane directly. The recipe keeps only what's NOT elevation: the panel
// radius (separate axis — Radix factor, A2), the content `color`, and `bg`
// (still referenced by the close button's hover mix).
radius: 'var(--radius-lg)',
bg: 'var(--color-surface-overlay)',
color: 'var(--color-content-primary)',
// Arrow size — canonical density-aware tokens (NOT soma's hardcoded 10×5).
// width ≈ 2.5× height for a balanced triangle; both scale with density/scaling.
'arrow-width': 'var(--space-5)',
'arrow-height': 'var(--space-2)'
// `close-*` + `transition-*` tokens removed: Popover.Close composes the
// canonical <IconButton>/<Button>, so its chrome lives in button.css.
},
progress: {
'height-xs': '2px',
'height-sm': '4px',
'height-md': '6px',
'height-lg': '8px',
'height-xl': '12px',
'vertical-height': '10rem',
'radius-xs': 'var(--radius-full)',
'radius-sm': 'var(--radius-full)',
'radius-md': 'var(--radius-full)',
'radius-lg': 'var(--radius-full)',
'radius-xl': 'var(--radius-full)',
'ring-size-xs': '2rem',
'ring-size-sm': '2.5rem',
'ring-size-md': '3rem',
'ring-size-lg': '3.5rem',
'ring-size-xl': '4rem',
'ring-thickness-xs': '3px',
'ring-thickness-sm': '4px',
'ring-thickness-md': '6px',
'ring-thickness-lg': '8px',
'ring-thickness-xl': '10px',
'label-font-family': 'var(--style-label-font-family)',
'label-font-size': 'var(--size-sm-font-size)',
'label-font-weight': 'var(--font-weight-medium)',
'label-line-height': 'var(--leading-ui)',
'label-color': 'var(--color-content-primary)',
'value-text-font-family': 'var(--style-label-font-family)',
'value-text-font-size': 'var(--size-sm-font-size)',
'value-text-font-weight': 'var(--font-weight-regular)',
'value-text-line-height': 'var(--leading-ui)',
'value-text-color': 'var(--color-content-secondary)',
'track-bg':
'color-mix(in srgb, var(--color-content-primary) 12%, var(--color-surface-default))',
'ring-track-bg':
'color-mix(in srgb, var(--color-content-primary) 12%, var(--color-surface-default))',
'ring-center-bg': 'var(--color-surface-default)',
'indicator-bg': 'var(--color-primary-solid)',
'indicator-bg-loaded': 'var(--color-fulfill-solid)',
'transition-duration': 'var(--duration-normal)',
'transition-ease': 'var(--ease-default)',
'indeterminate-width': '35%',
'indeterminate-duration': '1.1s',
'indeterminate-ease': 'var(--ease-default)'
},
'radio-group': {
'gap-vertical': 'var(--space-3)',
'gap-horizontal': 'var(--space-4)',
'item-size-xs': '14px',
'item-size-sm': '16px',
'item-size-md': '18px',
'item-size-lg': '22px',
'item-size-xl': '26px',
'dot-size-xs': '5px',
'dot-size-sm': '6px',
'dot-size-md': '7px',
'dot-size-lg': '9px',
'dot-size-xl': '11px',
'item-gap': 'var(--space-3)',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'font-size-xl': 'var(--size-xl-font-size)',
'font-weight': 'var(--font-weight-medium)',
'line-height': 'var(--leading-ui)',
'item-label-color': 'var(--color-content-primary)',
'item-label-color-disabled': 'var(--color-content-disabled)',
'indicator-radius': 'var(--radius-full)',
'indicator-border-width': 'var(--border-width)',
'indicator-bg': 'var(--color-surface-default)',
'indicator-border': 'var(--color-border-default)',
'indicator-border-hover': 'var(--color-border-strong)',
'indicator-bg-disabled':
'color-mix( in srgb, var(--color-surface-overlay) 78%, var(--color-surface-default) )',
'indicator-border-disabled':
'color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) )',
'indicator-bg-checked-disabled':
'color-mix( in srgb, var(--color-content-disabled) 40%, var(--color-surface-overlay) )',
'dot-color': 'var(--color-content-on-solid)',
'dot-color-disabled':
'color-mix( in srgb, var(--color-content-on-solid) 82%, var(--color-surface-default) )',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)',
'primary-solid': 'var(--color-primary-solid)',
'primary-solid-hover': 'var(--color-primary-solid-hover)',
'neutral-solid': 'var(--color-neutral-solid)',
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
'secondary-solid': 'var(--color-secondary-solid)',
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
'affirm-solid': 'var(--color-affirm-solid)',
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
'label-font-family': 'var(--style-label-font-family)',
'label-font-size': 'var(--size-sm-font-size)',
'label-font-weight': 'var(--font-weight-medium)',
'label-line-height': 'var(--leading-ui)',
'label-color': 'var(--color-content-primary)',
// Segmented variant — a recessed track holds flush text segments; a
// NEUTRAL pill (surface-coloured, not the accent) slides to the checked
// one (iOS UISegmentedControl / Radix SegmentedControl / Ark SegmentGroup).
// Neutral so it reads on the muted track in light AND dark; text stays
// content-primary. Pill radius is concentric (track radius − padding).
'segmented-track-bg': 'var(--color-surface-muted)',
// Recess the track with an INNER shadow, so depth reads from the shadow —
// not from the surface tone. No single neutral tone is lighter than the
// muted track in BOTH light and dark (the elevation ladder inverts:
// `overlay` is lighter than `muted` in dark but darker in light), so a
// tone-only "elevated pill" loses contrast in one mode. Shadows are
// mode-robust — the iOS / macOS segmented approach.
'segmented-track-shadow': 'var(--shadow-inset-subtle)',
'segmented-track-padding': 'var(--space-1)',
'segmented-track-radius': 'var(--radius-md)',
'segmented-segment-padding-inline': 'var(--space-3)',
'segmented-segment-color': 'var(--color-content-secondary)',
'segmented-segment-color-hover': 'var(--color-content-primary)',
'segmented-segment-font-weight': 'var(--font-weight-medium)',
// The pill is the coloured selection marker (it consumes the shared
// `--_radio-group-palette-solid` in the CSS, so it follows `color`); the
// active segment's text flips to the on-solid ink. Only the shadow +
// radius are recipe tokens here — the fill is the palette.
'segmented-pill-shadow': 'var(--shadow-subtle)',
'segmented-pill-radius':
'max(0px, calc(var(--radio-group-segmented-track-radius) - var(--radio-group-segmented-track-padding)))',
'segmented-transition-duration': 'var(--duration-normal)',
'segmented-transition-ease': 'cubic-bezier(0.32, 0.72, 0, 1)',
// Private palette (TSC v2.1). Declared on the [data-radio-group]
// host, inherited by [data-radio-group-item] descendants via
// CSS custom property inheritance. Migrated from per-color
// blocks in radio-group.css.
'_palette-solid': {
declarations: [
{ value: 'var(--radio-group-primary-solid)', scope: 'host' },
{ value: 'var(--radio-group-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--radio-group-neutral-solid)', scope: 'color:neutral' },
{ value: 'var(--radio-group-affirm-solid)', scope: 'color:affirm' }
]
},
'_palette-solid-hover': {
declarations: [
{ value: 'var(--radio-group-primary-solid-hover)', scope: 'host' },
{ value: 'var(--radio-group-secondary-solid-hover)', scope: 'color:secondary' },
{ value: 'var(--radio-group-neutral-solid-hover)', scope: 'color:neutral' },
{ value: 'var(--radio-group-affirm-solid-hover)', scope: 'color:affirm' }
]
},
'_palette-text': { value: 'var(--radio-group-dot-color)', scope: 'host' }
},
// QrCode. Default fg/bg are FIXED-TONE (dark modules on a light backing),
// not theme tokens: a QR must stay dark-on-light to scan — inverting it in
// dark mode would break scanners. Theming goes through the `color` /
// `background` props (consumer owns the contrast).
chart: {
// Mark geometry sourced from canonical primitives. The two off-scale
// values (area fill tint, point radius) are NAMED here — never inlined
// as magic numbers in the wrapper.
'line-width': 'var(--border-width-medium)',
'axis-width': 'var(--border-width-thin)',
'grid-width': 'var(--border-width-thin)',
'bar-radius': 'var(--radius-sm)',
'point-radius': '3.5px',
'area-opacity': '0.15',
'slice-stroke-width': 'var(--border-width-thin)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-out)',
// Hover tooltip surface — matches the overlay / popover chrome.
'tooltip-bg': 'var(--color-surface-overlay)',
'tooltip-border': 'var(--color-border-default)',
'tooltip-radius': 'var(--radius-md)',
'tooltip-shadow': 'var(--shadow-3)',
'tooltip-padding': 'var(--space-2)',
'tooltip-gap': 'var(--space-1-5)',
'crosshair-color': 'var(--color-border-strong)',
'crosshair-width': 'var(--border-width-thin)'
},
'qr-code': {
fg: '#18181b',
bg: '#ffffff',
'size-xs': '96px',
'size-sm': '128px',
'size-md': '160px',
'size-lg': '200px',
'size-xl': '256px',
radius: 'var(--radius-md)',
// White padding behind a center logo — FIXED-TONE for the same reason as
// fg/bg above (the quiet zone around the logo must stay light to scan).
'overlay-bg': '#ffffff',
'overlay-radius': 'var(--radius-sm)'
},
// RadioCards — card-style skin over RadioGroup. Reuses RadioGroup's
// palette (`--_radio-group-palette-solid`, declared on the shared
// [data-radio-group] host) and `--radio-group-focus-ring`; adds only the
// card chrome (padding, radius, surface, title/description type).
'radio-cards': {
gap: 'var(--space-3)',
// Min card width in a horizontal row before wrapping (rem → scales with
// root font-size / zoom).
'horizontal-basis': '12rem',
'padding-xs': 'var(--space-3)',
'padding-sm': 'var(--space-3)',
'padding-md': 'var(--space-4)',
'padding-lg': 'var(--space-4)',
'padding-xl': 'var(--space-5)',
radius: 'var(--radius-lg)',
'border-width': 'var(--border-width)',
border: 'var(--color-border-default)',
'border-hover': 'var(--color-border-strong)',
bg: 'var(--color-surface-default)',
'content-gap': 'var(--space-3)',
'text-gap': 'var(--space-0-5)',
'title-font-family': 'var(--style-label-font-family)',
'title-font-size-xs': 'var(--size-sm-font-size)',
'title-font-size-sm': 'var(--size-sm-font-size)',
'title-font-size-md': 'var(--size-md-font-size)',
'title-font-size-lg': 'var(--size-md-font-size)',
'title-font-size-xl': 'var(--size-lg-font-size)',
'title-font-weight': 'var(--font-weight-medium)',
'title-line-height': 'var(--leading-ui)',
'title-color': 'var(--color-content-primary)',
'description-font-size-xs': 'var(--size-xs-font-size)',
'description-font-size-sm': 'var(--size-xs-font-size)',
'description-font-size-md': 'var(--size-sm-font-size)',
'description-font-size-lg': 'var(--size-sm-font-size)',
'description-font-size-xl': 'var(--size-md-font-size)',
'description-line-height': 'var(--leading-normal)',
'description-color': 'var(--color-content-secondary)',
// Icon follows the card's title font-size (canonical icon↔type correlation),
// not an inflated standalone size. A bigger card icon comes from a bigger
// title (the icon follows), never from inflating the icon by hand.
'icon-size-xs': 'var(--size-sm-icon-size)',
'icon-size-sm': 'var(--size-sm-icon-size)',
'icon-size-md': 'var(--size-md-icon-size)',
'icon-size-lg': 'var(--size-md-icon-size)',
'icon-size-xl': 'var(--size-lg-icon-size)',
'icon-color': 'var(--color-content-secondary)',
// NOTE: the checked treatment (active border + faint tint + icon tint)
// is NOT a token here. It references `--_radio-group-palette-solid`,
// which is undefined at :root (declared only on [data-radio-group]
// hosts) — a :root token referencing it is invalid-at-computed-value
// and poisons down the tree. It's inlined in radio-cards.css instead,
// applied on the card item where the palette IS in scope.
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
'rating-group': {
'gap-xs': 'var(--space-0-5)',
'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)',
'gap-lg': 'var(--space-2)',
'gap-xl': 'var(--space-2-5)',
'item-size-xs': '14px',
'item-size-sm': '18px',
'item-size-md': '22px',
'item-size-lg': '28px',
'item-size-xl': '34px',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'font-size-xl': 'var(--size-xl-font-size)',
'line-height': 'var(--leading-ui)',
'item-color-inactive': 'var(--color-neutral-border)',
'item-color-active': 'var(--color-fulfill-solid)',
'item-color-partial': 'var(--color-fulfill-solid)',
'item-hover-offset': '-1px',
'focus-radius': 'var(--radius-md)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
'search-field': {
'clear-hidden-opacity': '0',
'icon-size-xs': 'var(--size-xs-icon-size)',
'icon-size-sm': 'var(--size-sm-icon-size)',
'icon-size-md': 'var(--size-md-icon-size)',
'icon-size-lg': 'var(--size-lg-icon-size)',
'icon-size-xl': 'var(--size-xl-icon-size)',
'icon-color': 'var(--color-content-muted)',
'icon-margin-inline-end': 'var(--space-1-5)',
'loading-indicator-size-xs': 'var(--size-xs-icon-size)',
'loading-indicator-size-sm': 'var(--size-sm-icon-size)',
'loading-indicator-size-md': 'var(--size-md-icon-size)',
'loading-indicator-size-lg': 'var(--size-md-icon-size)',
'loading-indicator-size-xl': 'var(--size-lg-icon-size)',
'loading-indicator-color': 'var(--color-content-muted)',
'loading-indicator-track': 'var(--color-border-subtle)',
'loading-indicator-thickness': '2px',
'loading-indicator-duration': '900ms'
},
'password-field': {
'placeholder-color': 'var(--color-content-muted)',
'trigger-radius': 'var(--radius-sm)',
'trigger-bg': 'transparent',
'trigger-color': 'var(--color-content-secondary)',
'icon-size-xs': 'var(--size-xs-control-height)',
'icon-size-sm': 'var(--size-xs-control-height)',
'icon-size-md': 'var(--size-sm-control-height)',
'icon-size-lg': 'var(--size-sm-control-height)',
'icon-size-xl': 'var(--size-md-control-height)',
'caps-color': 'var(--color-risk-text)',
'caps-bg': 'color-mix(in srgb, var(--color-risk-track) 50%, transparent)',
'caps-font-size-xs': 'var(--size-xs-font-size)',
'caps-font-size-sm': 'var(--size-xs-font-size)',
'caps-font-size-md': 'var(--size-xs-font-size)',
'caps-font-size-lg': 'var(--size-sm-font-size)',
'caps-font-size-xl': 'var(--size-sm-font-size)',
'strength-segment-bg': 'var(--color-border-default)',
'strength-segment-gap': '4px',
'strength-segment-height': '6px',
'strength-radius': 'var(--radius-sm)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
select: {
'stack-gap': 'var(--space-2)',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'font-size-xl': 'var(--size-xl-font-size)',
'line-height': 'var(--leading-ui)',
color: 'var(--color-content-primary)',
// Per-color forwarders consumed only by the TSC v2.2 `_accent-*`
// cascade below. The `solid` slot is intentionally omitted —
// select.css doesn't read `_accent-solid`, so its forwarders
// would be orphans (caught by the recipe-css-contract guard).
'primary-text': 'var(--color-primary-text)',
'secondary-text': 'var(--color-secondary-text)',
'neutral-text': 'var(--color-neutral-text)',
'affirm-text': 'var(--color-affirm-text)',
'fulfill-track': 'var(--color-fulfill-track)',
'fulfill-border': 'var(--color-fulfill-border)',
'fulfill-text': 'var(--color-fulfill-text)',
'risk-text': 'var(--color-risk-text)',
'threat-text': 'var(--color-threat-text)',
'loss-track': 'var(--color-loss-track)',
'loss-border': 'var(--color-loss-border)',
'loss-text': 'var(--color-loss-text)',
'trigger-height-xs': 'var(--size-xs-control-height)',
'trigger-height-sm': 'var(--size-sm-control-height)',
'trigger-height-md': 'var(--size-md-control-height)',
'trigger-height-lg': 'var(--size-lg-control-height)',
'trigger-height-xl': 'var(--size-xl-control-height)',
'trigger-padding-inline-xs': 'var(--space-2)',
'trigger-padding-inline-sm': 'var(--space-2-5)',
'trigger-padding-inline-md': 'var(--space-3)',
'trigger-padding-inline-lg': 'var(--space-3-5)',
'trigger-padding-inline-xl': 'var(--space-4)',
'trigger-gap-xs': 'var(--space-1)',
'trigger-gap-sm': 'var(--space-1-5)',
'trigger-gap-md': 'var(--space-2)',
'trigger-gap-lg': 'var(--space-2-5)',
'trigger-gap-xl': 'var(--space-3)',
'trigger-border-width': 'var(--border-width)',
'trigger-border': 'var(--color-border-default)',
'trigger-border-hover': 'var(--color-border-strong)',
'trigger-radius': 'var(--radius-md)',
'trigger-bg': 'var(--color-surface-default)',
'trigger-shadow': 'var(--shadow-subtle)',
'trigger-color': 'var(--color-content-primary)',
'trigger-indicator-color': 'var(--color-content-secondary)',
'trigger-indicator-size': 'var(--size-sm-icon-size)',
'placeholder-color': 'var(--color-content-muted)',
'disabled-border': 'var(--color-border-subtle)',
'disabled-bg': 'var(--color-surface-raised)',
'disabled-color': 'var(--color-content-disabled)',
'content-z': 'var(--z-index-overlay-floating)',
'content-padding-block-xs': 'var(--space-0-5)',
'content-padding-block-sm': 'var(--space-1)',
'content-padding-block-md': 'var(--space-1-5)',
'content-padding-block-lg': 'var(--space-2)',
'content-padding-block-xl': 'var(--space-2-5)',
'content-padding-inline-xs': 'var(--space-0-5)',
'content-padding-inline-sm': 'var(--space-1)',
'content-padding-inline-md': 'var(--space-1-5)',
'content-padding-inline-lg': 'var(--space-2)',
'content-padding-inline-xl': 'var(--space-2-5)',
'content-min-width': '12rem',
'content-max-width': '24rem',
'content-max-height': 'min(20rem, calc(100dvh - var(--space-6)))',
'content-radius': 'var(--radius-lg)',
'content-color': 'var(--color-content-primary)',
'open-duration': 'var(--duration-normal)',
'close-duration': 'var(--duration-fast)',
'viewport-gap': 'var(--space-0-5)',
'viewport-padding': 'var(--space-0-5)',
'group-padding-block': 'var(--space-1)',
'group-heading-padding-block': 'var(--space-1)',
'group-heading-padding-inline': 'var(--space-2)',
'group-heading-font-family': 'var(--style-label-font-family)',
'group-heading-font-size': 'var(--size-xs-font-size)',
'group-heading-font-weight': 'var(--font-weight-semibold)',
'group-heading-letter-spacing': '0.04em',
'group-heading-color': 'var(--color-content-muted)',
'separator-size': 'var(--border-width)',
'separator-margin-block': 'var(--space-1)',
'separator-bg': 'var(--color-neutral-separator)',
'item-gap': 'var(--space-2)',
'item-padding-inline': 'var(--space-2)',
'item-color': 'var(--color-content-primary)',
'item-indicator-size': 'var(--size-sm-icon-size)',
'item-description-mt': '0.125rem',
'item-description-color': 'var(--color-content-muted)',
'item-description-font-size': '0.85em',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
// Restored after accidental sed-removal during switch migration.
'primary-track': 'var(--color-primary-track)',
'primary-border': 'var(--color-primary-border)',
'secondary-track': 'var(--color-secondary-track)',
'secondary-border': 'var(--color-secondary-border)',
'neutral-track': 'var(--color-neutral-track)',
'neutral-border': 'var(--color-neutral-border)',
'affirm-track': 'var(--color-affirm-track)',
'affirm-border': 'var(--color-affirm-border)',
'risk-track': 'var(--color-risk-track)',
'risk-border': 'var(--color-risk-border)',
'threat-track': 'var(--color-threat-track)',
'threat-border': 'var(--color-threat-border)',
// TSC v2.2 — multi-part palette cascade. Select's `data-color`
// attribute lives on the trigger AND content parts (NOT on the
// root `[data-select]`), because content portals out of the
// document tree and must carry the color independently. The
// `parts: ['trigger', 'content']` field tells the generator to
// emit each declaration as a comma-separated rule targeting both
// `[data-select-trigger]` and `[data-select-content]`.
//
// 1 host default (primary) + 7 per-color cascades for each of the
// 3 perceptual slots that select.css consumes (`track`, `border`,
// `text`). The legacy CSS also declared `--_select-accent-solid`
// but nothing in select.css read it; that orphan stayed out of
// the TSC migration on purpose.
'_palette-track': {
parts: ['trigger', 'content'],
declarations: [
{ value: 'var(--select-primary-track)', scope: 'host' },
{ value: 'var(--select-secondary-track)', scope: 'color:secondary' },
{ value: 'var(--select-neutral-track)', scope: 'color:neutral' },
{ value: 'var(--select-affirm-track)', scope: 'color:affirm' },
{ value: 'var(--select-fulfill-track)', scope: 'color:fulfill' },
{ value: 'var(--select-risk-track)', scope: 'color:risk' },
{ value: 'var(--select-threat-track)', scope: 'color:threat' },
{ value: 'var(--select-loss-track)', scope: 'color:loss' }
]
},
'_palette-border': {
parts: ['trigger', 'content'],
declarations: [
{ value: 'var(--select-primary-border)', scope: 'host' },
{ value: 'var(--select-secondary-border)', scope: 'color:secondary' },
{ value: 'var(--select-neutral-border)', scope: 'color:neutral' },
{ value: 'var(--select-affirm-border)', scope: 'color:affirm' },
{ value: 'var(--select-fulfill-border)', scope: 'color:fulfill' },
{ value: 'var(--select-risk-border)', scope: 'color:risk' },
{ value: 'var(--select-threat-border)', scope: 'color:threat' },
{ value: 'var(--select-loss-border)', scope: 'color:loss' }
]
},
'_palette-text': {
parts: ['trigger', 'content'],
declarations: [
{ value: 'var(--select-primary-text)', scope: 'host' },
{ value: 'var(--select-secondary-text)', scope: 'color:secondary' },
{ value: 'var(--select-neutral-text)', scope: 'color:neutral' },
{ value: 'var(--select-affirm-text)', scope: 'color:affirm' },
{ value: 'var(--select-fulfill-text)', scope: 'color:fulfill' },
{ value: 'var(--select-risk-text)', scope: 'color:risk' },
{ value: 'var(--select-threat-text)', scope: 'color:threat' },
{ value: 'var(--select-loss-text)', scope: 'color:loss' }
]
}
},
toolbar: {
'gap-xs': 'var(--space-0-5)',
'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)',
'gap-lg': 'var(--space-2)',
'padding-inline-xs': 'var(--space-0-5)',
'padding-inline-sm': 'var(--space-1)',
'padding-inline-md': 'var(--space-1-5)',
'padding-inline-lg': 'var(--space-2)',
'padding-block-xs': 'var(--space-0-5)',
'padding-block-sm': 'var(--space-1)',
'padding-block-md': 'var(--space-1)',
'padding-block-lg': 'var(--space-1-5)',
'control-height-xs': 'var(--size-xs-control-height)',
'control-height-sm': 'var(--size-sm-control-height)',
'control-height-md': 'var(--size-md-control-height)',
'control-height-lg': 'var(--size-lg-control-height)',
'control-padding-inline-xs': 'var(--space-1-5)',
'control-padding-inline-sm': 'var(--space-2)',
'control-padding-inline-md': 'var(--space-2-5)',
'control-padding-inline-lg': 'var(--space-3)',
'control-gap-xs': 'var(--space-0-5)',
'control-gap-sm': 'var(--space-1)',
'control-gap-md': 'var(--space-1-5)',
'control-gap-lg': 'var(--space-2)',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'line-height': 'var(--leading-ui)',
radius: 'var(--radius-lg)',
bg: 'var(--color-surface-raised)',
border: 'var(--color-border-subtle)',
'border-width': 'var(--border-width)',
shadow: 'var(--shadow-subtle)',
color: 'var(--color-content-primary)',
'group-gap': 'var(--space-0-5)',
'control-radius': 'var(--radius-md)',
'control-border-width': 'var(--border-width)',
'control-border': 'transparent',
'control-border-hover': 'var(--color-border-subtle)',
'control-border-active': 'var(--color-primary-border)',
'control-bg': 'transparent',
'control-bg-active': 'var(--color-primary-track)',
'control-color': 'var(--color-content-secondary)',
'control-color-hover': 'var(--color-content-primary)',
'control-color-active': 'var(--color-primary-text)',
'separator-bg': 'var(--color-neutral-separator)',
'separator-thickness': 'var(--border-width)',
'separator-length': 'var(--size-md-control-height)',
'separator-radius': 'var(--radius-full)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
'file-upload': {
'gap-sm': 'var(--space-2)',
'gap-md': 'var(--space-3)',
'gap-lg': 'var(--space-4)',
'padding-sm': 'var(--space-4)',
'padding-md': 'var(--space-5)',
'padding-lg': 'var(--space-6)',
'control-height-sm': 'var(--size-sm-control-height)',
'control-height-md': 'var(--size-md-control-height)',
'control-height-lg': 'var(--size-lg-control-height)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'preview-size-sm': '32px',
'preview-size-md': '40px',
'preview-size-lg': '48px',
'font-family': 'var(--style-label-font-family)',
'line-height': 'var(--leading-ui)',
color: 'var(--color-content-primary)',
'label-color': 'var(--color-content-secondary)',
'label-font-weight': 'var(--font-weight-medium)',
'dropzone-gap': 'var(--space-2)',
'dropzone-min-height': '9rem',
'dropzone-border-width': 'var(--border-width)',
'dropzone-border': 'var(--color-border-default)',
'dropzone-radius': 'var(--radius-lg)',
'dropzone-bg': 'var(--color-surface-raised)',
'dropzone-color': 'var(--color-content-secondary)',
'button-gap': 'var(--space-1-5)',
'button-padding-inline': 'var(--space-3)',
'button-border-width': 'var(--border-width)',
'button-border': 'var(--color-border-subtle)',
'button-border-hover': 'var(--color-border-default)',
'button-radius': 'var(--radius-md)',
'button-bg': 'var(--color-surface-raised)',
'button-color': 'var(--color-content-secondary)',
'button-color-hover': 'var(--color-content-primary)',
'button-solid-color': 'var(--color-content-on-solid)',
'list-gap': 'var(--space-2)',
'item-gap': 'var(--space-3)',
'item-padding': 'var(--space-2)',
'item-border-width': 'var(--border-width)',
'item-border': 'var(--color-border-subtle)',
'item-radius': 'var(--radius-md)',
'item-bg': 'var(--color-surface-raised)',
'item-shadow': 'var(--shadow-subtle)',
'preview-radius': 'var(--radius-sm)',
'preview-bg': 'var(--color-neutral-track)',
'preview-color': 'var(--color-neutral-text)',
'item-name-color': 'var(--color-content-primary)',
'item-name-font-weight': 'var(--font-weight-medium)',
'item-size-color': 'var(--color-content-muted)',
'item-size-font-size': 'var(--size-xs-font-size)',
'progress-height': '4px',
'progress-radius': 'var(--radius-full)',
'progress-track-bg': 'var(--color-neutral-track)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)',
'primary-solid': 'var(--color-primary-solid)',
'primary-solid-hover': 'var(--color-primary-solid-hover)',
'primary-text': 'var(--color-primary-text)',
'secondary-solid': 'var(--color-secondary-solid)',
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
'secondary-text': 'var(--color-secondary-text)',
'neutral-solid': 'var(--color-neutral-solid)',
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
'neutral-text': 'var(--color-neutral-text)',
'affirm-solid': 'var(--color-affirm-solid)',
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
'affirm-text': 'var(--color-affirm-text)',
'fulfill-solid': 'var(--color-fulfill-solid)',
'fulfill-solid-hover': 'var(--color-fulfill-solid-hover)',
'fulfill-track': 'var(--color-fulfill-track)',
'fulfill-border': 'var(--color-fulfill-border)',
'fulfill-text': 'var(--color-fulfill-text)',
'risk-solid': 'var(--color-risk-solid)',
'risk-solid-hover': 'var(--color-risk-solid-hover)',
'risk-text': 'var(--color-risk-text)',
'threat-solid': 'var(--color-threat-solid)',
'threat-solid-hover': 'var(--color-threat-solid-hover)',
'threat-text': 'var(--color-threat-text)',
'loss-solid': 'var(--color-loss-solid)',
'loss-solid-hover': 'var(--color-loss-solid-hover)',
'loss-track': 'var(--color-loss-track)',
'loss-border': 'var(--color-loss-border)',
'loss-text': 'var(--color-loss-text)',
// Restored after accidental sed-removal during switch migration.
'primary-track': 'var(--color-primary-track)',
'primary-border': 'var(--color-primary-border)',
'secondary-track': 'var(--color-secondary-track)',
'secondary-border': 'var(--color-secondary-border)',
'neutral-track': 'var(--color-neutral-track)',
'neutral-border': 'var(--color-neutral-border)',
'affirm-track': 'var(--color-affirm-track)',
'affirm-border': 'var(--color-affirm-border)',
'risk-track': 'var(--color-risk-track)',
'risk-border': 'var(--color-risk-border)',
'threat-track': 'var(--color-threat-track)',
'threat-border': 'var(--color-threat-border)',
// Private palette (TSC v2.1 · canonical `_palette-*` slots for the THM-2
// shared-layer forward — color="teal" + the 33 scales; renamed from
// `_accent-*` 2026-07-13). 8 colors, 5 slots.
'_palette-solid': {
declarations: [
{ value: 'var(--file-upload-primary-solid)', scope: 'host' },
{ value: 'var(--file-upload-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--file-upload-neutral-solid)', scope: 'color:neutral' },
{ value: 'var(--file-upload-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--file-upload-fulfill-solid)', scope: 'color:fulfill' },
{ value: 'var(--file-upload-risk-solid)', scope: 'color:risk' },
{ value: 'var(--file-upload-threat-solid)', scope: 'color:threat' },
{ value: 'var(--file-upload-loss-solid)', scope: 'color:loss' }
]
},
'_palette-solid-hover': {
declarations: [
{ value: 'var(--file-upload-primary-solid-hover)', scope: 'host' },
{ value: 'var(--file-upload-secondary-solid-hover)', scope: 'color:secondary' },
{ value: 'var(--file-upload-neutral-solid-hover)', scope: 'color:neutral' },
{ value: 'var(--file-upload-affirm-solid-hover)', scope: 'color:affirm' },
{ value: 'var(--file-upload-fulfill-solid-hover)', scope: 'color:fulfill' },
{ value: 'var(--file-upload-risk-solid-hover)', scope: 'color:risk' },
{ value: 'var(--file-upload-threat-solid-hover)', scope: 'color:threat' },
{ value: 'var(--file-upload-loss-solid-hover)', scope: 'color:loss' }
]
},
'_palette-track': {
declarations: [
{ value: 'var(--file-upload-primary-track)', scope: 'host' },
{ value: 'var(--file-upload-secondary-track)', scope: 'color:secondary' },
{ value: 'var(--file-upload-neutral-track)', scope: 'color:neutral' },
{ value: 'var(--file-upload-affirm-track)', scope: 'color:affirm' },
{ value: 'var(--file-upload-fulfill-track)', scope: 'color:fulfill' },
{ value: 'var(--file-upload-risk-track)', scope: 'color:risk' },
{ value: 'var(--file-upload-threat-track)', scope: 'color:threat' },
{ value: 'var(--file-upload-loss-track)', scope: 'color:loss' }
]
},
'_palette-border': {
declarations: [
{ value: 'var(--file-upload-primary-border)', scope: 'host' },
{ value: 'var(--file-upload-secondary-border)', scope: 'color:secondary' },
{ value: 'var(--file-upload-neutral-border)', scope: 'color:neutral' },
{ value: 'var(--file-upload-affirm-border)', scope: 'color:affirm' },
{ value: 'var(--file-upload-fulfill-border)', scope: 'color:fulfill' },
{ value: 'var(--file-upload-risk-border)', scope: 'color:risk' },
{ value: 'var(--file-upload-threat-border)', scope: 'color:threat' },
{ value: 'var(--file-upload-loss-border)', scope: 'color:loss' }
]
},
'_palette-text': {
declarations: [
{ value: 'var(--file-upload-primary-text)', scope: 'host' },
{ value: 'var(--file-upload-secondary-text)', scope: 'color:secondary' },
{ value: 'var(--file-upload-neutral-text)', scope: 'color:neutral' },
{ value: 'var(--file-upload-affirm-text)', scope: 'color:affirm' },
{ value: 'var(--file-upload-fulfill-text)', scope: 'color:fulfill' },
{ value: 'var(--file-upload-risk-text)', scope: 'color:risk' },
{ value: 'var(--file-upload-threat-text)', scope: 'color:threat' },
{ value: 'var(--file-upload-loss-text)', scope: 'color:loss' }
]
}
},
'tag-group': {
'gap-xs': 'var(--space-0-5)',
'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)',
'gap-lg': 'var(--space-2)',
'item-height-xs': 'var(--size-xs-control-height)',
'item-height-sm': 'var(--size-sm-control-height)',
'item-height-md': 'var(--size-md-control-height)',
'item-height-lg': 'var(--size-lg-control-height)',
'item-padding-inline-xs': 'var(--space-1-5)',
'item-padding-inline-sm': 'var(--space-2)',
'item-padding-inline-md': 'var(--space-2-5)',
'item-padding-inline-lg': 'var(--space-3)',
'item-gap-xs': 'var(--space-0-5)',
'item-gap-sm': 'var(--space-1)',
'item-gap-md': 'var(--space-1-5)',
'item-gap-lg': 'var(--space-2)',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'line-height': 'var(--leading-ui)',
color: 'var(--color-content-primary)',
'label-color': 'var(--color-content-secondary)',
'label-font-weight': 'var(--font-weight-medium)',
'item-radius': 'var(--radius-full)',
'item-border-width': 'var(--border-width)',
'item-border': 'var(--color-border-subtle)',
'item-border-hover': 'var(--color-border-default)',
'item-bg': 'var(--color-surface-raised)',
'item-shadow': 'var(--shadow-subtle)',
'item-color': 'var(--color-content-secondary)',
'item-color-hover': 'var(--color-content-primary)',
'solid-fg': 'var(--color-content-on-solid)',
'remove-size': 'var(--size-xs-control-height)',
'remove-radius': 'var(--radius-full)',
'remove-border-width': 'var(--border-width)',
'remove-border': 'transparent',
'remove-border-hover': 'var(--color-border-subtle)',
'remove-bg': 'transparent',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)',
'primary-solid-bg': 'var(--color-primary-solid)',
'primary-solid-bg-hover': 'var(--color-primary-solid-hover)',
'primary-soft-bg': 'var(--color-primary-surface)',
'primary-soft-bg-hover': 'var(--color-primary-surface-hover)',
'primary-soft-fg': 'var(--color-primary-text)',
'primary-outline-border': 'var(--color-primary-border)', 'secondary-solid-bg': 'var(--color-secondary-solid)',
'secondary-solid-bg-hover': 'var(--color-secondary-solid-hover)',
'secondary-soft-bg': 'var(--color-secondary-surface)',
'secondary-soft-bg-hover': 'var(--color-secondary-surface-hover)',
'secondary-soft-fg': 'var(--color-secondary-text)',
'secondary-outline-border': 'var(--color-secondary-border)', 'neutral-solid-bg': 'var(--color-neutral-solid)',
'neutral-solid-bg-hover': 'var(--color-neutral-solid-hover)',
'neutral-soft-bg': 'var(--color-neutral-surface)',
'neutral-soft-bg-hover': 'var(--color-neutral-surface-hover)',
'neutral-soft-fg': 'var(--color-neutral-text)',
'neutral-outline-border': 'var(--color-neutral-border)', 'affirm-solid-bg': 'var(--color-affirm-solid)',
'affirm-solid-bg-hover': 'var(--color-affirm-solid-hover)',
'affirm-soft-bg': 'var(--color-affirm-surface)',
'affirm-soft-bg-hover': 'var(--color-affirm-surface-hover)',
'affirm-soft-fg': 'var(--color-affirm-text)',
'affirm-outline-border': 'var(--color-affirm-border)', 'fulfill-solid-bg': 'var(--color-fulfill-solid)',
'fulfill-solid-bg-hover': 'var(--color-fulfill-solid-hover)',
'fulfill-soft-bg': 'var(--color-fulfill-surface)',
'fulfill-soft-bg-hover': 'var(--color-fulfill-surface-hover)',
'fulfill-soft-fg': 'var(--color-fulfill-text)',
'fulfill-outline-border': 'var(--color-fulfill-border)', 'risk-solid-bg': 'var(--color-risk-solid)',
'risk-solid-bg-hover': 'var(--color-risk-solid-hover)',
'risk-soft-bg': 'var(--color-risk-surface)',
'risk-soft-bg-hover': 'var(--color-risk-surface-hover)',
'risk-soft-fg': 'var(--color-risk-text)',
'risk-outline-border': 'var(--color-risk-border)', 'threat-solid-bg': 'var(--color-threat-solid)',
'threat-solid-bg-hover': 'var(--color-threat-solid-hover)',
'threat-soft-bg': 'var(--color-threat-surface)',
'threat-soft-bg-hover': 'var(--color-threat-surface-hover)',
'threat-soft-fg': 'var(--color-threat-text)',
'threat-outline-border': 'var(--color-threat-border)', 'loss-solid-bg': 'var(--color-loss-solid)',
'loss-solid-bg-hover': 'var(--color-loss-solid-hover)',
'loss-soft-bg': 'var(--color-loss-surface)',
'loss-soft-bg-hover': 'var(--color-loss-surface-hover)',
'loss-soft-fg': 'var(--color-loss-text)',
'loss-outline-border': 'var(--color-loss-border)',
// Private palette (TSC v2.1 · canonical `_palette-*` slots so the THM-2
// shared-layer forward routes color="teal" + the 33 scales, incl. the
// alpha `surface`/`surface-hover` slots for the soft variant; renamed
// from `_selected-*` 2026-07-12). solid + solid-hover (solid variant) ·
// surface + surface-hover + text (soft variant) · border + text (outline
// variant — its fg merged into `text`, same value). Cascade keyed on
// `data-color`.
'_palette-solid': {
declarations: [
{ value: 'var(--tag-group-primary-solid-bg)', scope: 'host' },
{ value: 'var(--tag-group-secondary-solid-bg)', scope: 'color:secondary' },
{ value: 'var(--tag-group-neutral-solid-bg)', scope: 'color:neutral' },
{ value: 'var(--tag-group-affirm-solid-bg)', scope: 'color:affirm' },
{ value: 'var(--tag-group-fulfill-solid-bg)', scope: 'color:fulfill' },
{ value: 'var(--tag-group-risk-solid-bg)', scope: 'color:risk' },
{ value: 'var(--tag-group-threat-solid-bg)', scope: 'color:threat' },
{ value: 'var(--tag-group-loss-solid-bg)', scope: 'color:loss' }
]
},
'_palette-solid-hover': {
declarations: [
{ value: 'var(--tag-group-primary-solid-bg-hover)', scope: 'host' },
{ value: 'var(--tag-group-secondary-solid-bg-hover)', scope: 'color:secondary' },
{ value: 'var(--tag-group-neutral-solid-bg-hover)', scope: 'color:neutral' },
{ value: 'var(--tag-group-affirm-solid-bg-hover)', scope: 'color:affirm' },
{ value: 'var(--tag-group-fulfill-solid-bg-hover)', scope: 'color:fulfill' },
{ value: 'var(--tag-group-risk-solid-bg-hover)', scope: 'color:risk' },
{ value: 'var(--tag-group-threat-solid-bg-hover)', scope: 'color:threat' },
{ value: 'var(--tag-group-loss-solid-bg-hover)', scope: 'color:loss' }
]
},
'_palette-surface': {
declarations: [
{ value: 'var(--tag-group-primary-soft-bg)', scope: 'host' },
{ value: 'var(--tag-group-secondary-soft-bg)', scope: 'color:secondary' },
{ value: 'var(--tag-group-neutral-soft-bg)', scope: 'color:neutral' },
{ value: 'var(--tag-group-affirm-soft-bg)', scope: 'color:affirm' },
{ value: 'var(--tag-group-fulfill-soft-bg)', scope: 'color:fulfill' },
{ value: 'var(--tag-group-risk-soft-bg)', scope: 'color:risk' },
{ value: 'var(--tag-group-threat-soft-bg)', scope: 'color:threat' },
{ value: 'var(--tag-group-loss-soft-bg)', scope: 'color:loss' }
]
},
'_palette-surface-hover': {
declarations: [
{ value: 'var(--tag-group-primary-soft-bg-hover)', scope: 'host' },
{ value: 'var(--tag-group-secondary-soft-bg-hover)', scope: 'color:secondary' },
{ value: 'var(--tag-group-neutral-soft-bg-hover)', scope: 'color:neutral' },
{ value: 'var(--tag-group-affirm-soft-bg-hover)', scope: 'color:affirm' },
{ value: 'var(--tag-group-fulfill-soft-bg-hover)', scope: 'color:fulfill' },
{ value: 'var(--tag-group-risk-soft-bg-hover)', scope: 'color:risk' },
{ value: 'var(--tag-group-threat-soft-bg-hover)', scope: 'color:threat' },
{ value: 'var(--tag-group-loss-soft-bg-hover)', scope: 'color:loss' }
]
},
'_palette-text': {
declarations: [
{ value: 'var(--tag-group-primary-soft-fg)', scope: 'host' },
{ value: 'var(--tag-group-secondary-soft-fg)', scope: 'color:secondary' },
{ value: 'var(--tag-group-neutral-soft-fg)', scope: 'color:neutral' },
{ value: 'var(--tag-group-affirm-soft-fg)', scope: 'color:affirm' },
{ value: 'var(--tag-group-fulfill-soft-fg)', scope: 'color:fulfill' },
{ value: 'var(--tag-group-risk-soft-fg)', scope: 'color:risk' },
{ value: 'var(--tag-group-threat-soft-fg)', scope: 'color:threat' },
{ value: 'var(--tag-group-loss-soft-fg)', scope: 'color:loss' }
]
},
'_palette-border': {
declarations: [
{ value: 'var(--tag-group-primary-outline-border)', scope: 'host' },
{ value: 'var(--tag-group-secondary-outline-border)', scope: 'color:secondary' },
{ value: 'var(--tag-group-neutral-outline-border)', scope: 'color:neutral' },
{ value: 'var(--tag-group-affirm-outline-border)', scope: 'color:affirm' },
{ value: 'var(--tag-group-fulfill-outline-border)', scope: 'color:fulfill' },
{ value: 'var(--tag-group-risk-outline-border)', scope: 'color:risk' },
{ value: 'var(--tag-group-threat-outline-border)', scope: 'color:threat' },
{ value: 'var(--tag-group-loss-outline-border)', scope: 'color:loss' }
]
}
},
editable: {
'gap-xs': 'var(--space-0-5)',
'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)',
'gap-lg': 'var(--space-2)',
'gap-xl': 'var(--space-2-5)',
'control-height-xs': 'var(--size-xs-control-height)',
'control-height-sm': 'var(--size-sm-control-height)',
'control-height-md': 'var(--size-md-control-height)',
'control-height-lg': 'var(--size-lg-control-height)',
'control-height-xl': 'var(--size-xl-control-height)',
'control-padding-inline-xs': 'var(--space-1-5)',
'control-padding-inline-sm': 'var(--space-2)',
'control-padding-inline-md': 'var(--space-2-5)',
'control-padding-inline-lg': 'var(--space-3)',
'control-padding-inline-xl': 'var(--space-3-5)',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'font-size-xl': 'var(--size-xl-font-size)',
'line-height': 'var(--leading-ui)',
color: 'var(--color-content-primary)',
'area-min-inline-size': '12ch',
'control-radius': 'var(--radius-md)',
'control-border-width': 'var(--border-width)',
'control-border': 'var(--color-border-subtle)',
'control-border-hover': 'var(--color-border-default)',
'control-bg': 'var(--color-surface-default)',
'control-shadow': 'var(--shadow-subtle)',
'placeholder-color': 'var(--color-content-muted)',
'trigger-gap': 'var(--space-1)',
'trigger-padding-inline': 'var(--space-2-5)',
'trigger-radius': 'var(--radius-md)',
'trigger-border-width': 'var(--border-width)',
'trigger-border': 'var(--color-border-subtle)',
'trigger-border-hover': 'var(--color-border-default)',
'trigger-bg': 'var(--color-surface-raised)',
'trigger-color': 'var(--color-content-secondary)',
'trigger-color-hover': 'var(--color-content-primary)',
'submit-color': 'var(--color-content-on-solid)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)',
'primary-solid': 'var(--color-primary-solid)',
'secondary-solid': 'var(--color-secondary-solid)',
'neutral-solid': 'var(--color-neutral-solid)',
'affirm-solid': 'var(--color-affirm-solid)',
'risk-solid': 'var(--color-risk-solid)',
'threat-solid': 'var(--color-threat-solid)',
// Restored after accidental sed-removal during switch migration.
'primary-border': 'var(--color-primary-border)',
'secondary-border': 'var(--color-secondary-border)',
'neutral-border': 'var(--color-neutral-border)',
'affirm-border': 'var(--color-affirm-border)',
'risk-border': 'var(--color-risk-border)',
'threat-border': 'var(--color-threat-border)',
// Private palette (TSC v2.1 · canonical `_palette-*` slots for the THM-2
// shared-layer forward — color="teal" + the 33 scales; renamed from
// `_accent-*` 2026-07-13). solid + border + solid-hover consumed.
'_palette-solid': {
declarations: [
{ value: 'var(--editable-primary-solid)', scope: 'host' },
{ value: 'var(--editable-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--editable-neutral-solid)', scope: 'color:neutral' },
{ value: 'var(--editable-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--editable-risk-solid)', scope: 'color:risk' },
{ value: 'var(--editable-threat-solid)', scope: 'color:threat' }
]
},
'_palette-border': {
declarations: [
{ value: 'var(--editable-primary-border)', scope: 'host' },
{ value: 'var(--editable-secondary-border)', scope: 'color:secondary' },
{ value: 'var(--editable-neutral-border)', scope: 'color:neutral' },
{ value: 'var(--editable-affirm-border)', scope: 'color:affirm' },
{ value: 'var(--editable-risk-border)', scope: 'color:risk' },
{ value: 'var(--editable-threat-border)', scope: 'color:threat' }
]
},
'_palette-solid-hover': {
declarations: [
{ value: 'var(--color-primary-solid-hover)', scope: 'host' },
{ value: 'var(--color-secondary-solid-hover)', scope: 'color:secondary' },
{ value: 'var(--color-neutral-solid-hover)', scope: 'color:neutral' },
{ value: 'var(--color-affirm-solid-hover)', scope: 'color:affirm' },
{ value: 'var(--color-risk-solid-hover)', scope: 'color:risk' },
{ value: 'var(--color-threat-solid-hover)', scope: 'color:threat' }
]
}
},
'tags-input': {
'gap-xs': 'var(--space-1)',
'gap-sm': 'var(--space-1-5)',
'gap-md': 'var(--space-2)',
'gap-lg': 'var(--space-2-5)',
'gap-xl': 'var(--space-3)',
'control-min-height-xs': 'var(--size-sm-control-height)',
'control-min-height-sm': 'var(--size-md-control-height)',
'control-min-height-md': 'var(--size-lg-control-height)',
'control-min-height-lg': 'calc(var(--size-lg-control-height) + var(--space-2))',
'control-min-height-xl': 'calc(var(--size-xl-control-height) + var(--space-2))',
'control-padding-inline-xs': 'var(--space-1-5)',
'control-padding-inline-sm': 'var(--space-2)',
'control-padding-inline-md': 'var(--space-2-5)',
'control-padding-inline-lg': 'var(--space-3)',
'control-padding-inline-xl': 'var(--space-3-5)',
'control-padding-block-xs': 'var(--space-0-5)',
'control-padding-block-sm': 'var(--space-1)',
'control-padding-block-md': 'var(--space-1-5)',
'control-padding-block-lg': 'var(--space-2)',
'control-padding-block-xl': 'var(--space-2-5)',
'item-height-xs': 'var(--size-xs-control-height)',
'item-height-sm': 'var(--size-sm-control-height)',
'item-height-md': 'var(--size-md-control-height)',
'item-height-lg': 'var(--size-lg-control-height)',
'item-height-xl': 'var(--size-xl-control-height)',
'item-padding-inline-xs': 'var(--space-1-5)',
'item-padding-inline-sm': 'var(--space-2)',
'item-padding-inline-md': 'var(--space-2-5)',
'item-padding-inline-lg': 'var(--space-3)',
'item-padding-inline-xl': 'var(--space-3-5)',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'font-size-xl': 'var(--size-xl-font-size)',
'line-height': 'var(--leading-ui)',
color: 'var(--color-content-primary)',
'item-gap': 'var(--space-1-5)',
'control-radius': 'var(--radius-lg)',
'control-border-width': 'var(--border-width)',
'control-border': 'var(--color-border-default)',
'control-border-hover': 'var(--color-border-strong)',
'control-bg': 'var(--color-surface-default)',
'control-shadow': 'var(--shadow-subtle)',
'input-min-inline-size': '8ch',
'placeholder-color': 'var(--color-content-muted)',
'item-radius': 'var(--radius-full)',
'item-border-width': 'var(--border-width)',
'item-border': 'var(--color-border-subtle)',
'item-bg': 'var(--color-surface-overlay)',
'item-color': 'var(--color-content-primary)',
'action-gap': 'var(--space-1)',
'action-padding-inline': 'var(--space-2-5)',
'action-radius': 'var(--radius-md)',
'action-border-width': 'var(--border-width)',
'action-border': 'transparent',
'action-border-hover': 'var(--color-border-subtle)',
'action-bg': 'transparent',
'action-color': 'var(--color-content-secondary)',
'action-color-hover': 'var(--color-content-primary)',
'delete-size': 'var(--size-xs-control-height)',
'delete-radius': 'var(--radius-full)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)',
'primary-text': 'var(--color-primary-text)',
'secondary-text': 'var(--color-secondary-text)',
'neutral-text': 'var(--color-neutral-text)',
'affirm-text': 'var(--color-affirm-text)',
'fulfill-track': 'var(--color-fulfill-track)',
'fulfill-border': 'var(--color-fulfill-border)',
'fulfill-text': 'var(--color-fulfill-text)',
'risk-text': 'var(--color-risk-text)',
'threat-text': 'var(--color-threat-text)',
'loss-track': 'var(--color-loss-track)',
'loss-border': 'var(--color-loss-border)',
'loss-text': 'var(--color-loss-text)',
// Restored after accidental sed-removal during switch migration.
// Consumed by tags-input.css for tag chip color cues per intent.
'primary-track': 'var(--color-primary-track)',
'primary-border': 'var(--color-primary-border)',
'secondary-track': 'var(--color-secondary-track)',
'secondary-border': 'var(--color-secondary-border)',
'neutral-track': 'var(--color-neutral-track)',
'neutral-border': 'var(--color-neutral-border)',
'affirm-track': 'var(--color-affirm-track)',
'affirm-border': 'var(--color-affirm-border)',
'risk-track': 'var(--color-risk-track)',
'risk-border': 'var(--color-risk-border)',
'threat-track': 'var(--color-threat-track)',
'threat-border': 'var(--color-threat-border)',
// Private palette (TSC v2.1 · canonical `_palette-*` slots so the
// THM-2 shared-layer forward routes `color="teal"` + the 33 scales;
// renamed from `_accent-*` 2026-07-13). 8 colors, 3 slots (track,
// border, text). `solid` was declared pre-TSC but never consumed.
'_palette-track': {
declarations: [
{ value: 'var(--tags-input-primary-track)', scope: 'host' },
{ value: 'var(--tags-input-secondary-track)', scope: 'color:secondary' },
{ value: 'var(--tags-input-neutral-track)', scope: 'color:neutral' },
{ value: 'var(--tags-input-affirm-track)', scope: 'color:affirm' },
{ value: 'var(--tags-input-fulfill-track)', scope: 'color:fulfill' },
{ value: 'var(--tags-input-risk-track)', scope: 'color:risk' },
{ value: 'var(--tags-input-threat-track)', scope: 'color:threat' },
{ value: 'var(--tags-input-loss-track)', scope: 'color:loss' }
]
},
'_palette-border': {
declarations: [
{ value: 'var(--tags-input-primary-border)', scope: 'host' },
{ value: 'var(--tags-input-secondary-border)', scope: 'color:secondary' },
{ value: 'var(--tags-input-neutral-border)', scope: 'color:neutral' },
{ value: 'var(--tags-input-affirm-border)', scope: 'color:affirm' },
{ value: 'var(--tags-input-fulfill-border)', scope: 'color:fulfill' },
{ value: 'var(--tags-input-risk-border)', scope: 'color:risk' },
{ value: 'var(--tags-input-threat-border)', scope: 'color:threat' },
{ value: 'var(--tags-input-loss-border)', scope: 'color:loss' }
]
},
'_palette-text': {
declarations: [
{ value: 'var(--tags-input-primary-text)', scope: 'host' },
{ value: 'var(--tags-input-secondary-text)', scope: 'color:secondary' },
{ value: 'var(--tags-input-neutral-text)', scope: 'color:neutral' },
{ value: 'var(--tags-input-affirm-text)', scope: 'color:affirm' },
{ value: 'var(--tags-input-fulfill-text)', scope: 'color:fulfill' },
{ value: 'var(--tags-input-risk-text)', scope: 'color:risk' },
{ value: 'var(--tags-input-threat-text)', scope: 'color:threat' },
{ value: 'var(--tags-input-loss-text)', scope: 'color:loss' }
]
}
},
metrics: {
// Metrics is SURFACELESS — no per-colour palette here. The Delta's colour
// comes from the composed <Badge>; the featured icon and the live flash
// tint use the canonical `--color-*` tokens directly.
// ── Vertical rhythm between label / value / delta / description ──
'gap-xs': 'var(--space-1)',
'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)',
'gap-lg': 'var(--space-2)',
'gap-xl': 'var(--space-2)',
'gap-xxl': 'var(--space-3)',
// ── Featured icon (leading container; soft/solid/outline × colour from
// canonical tokens in CSS; padding + glyph size scale with the metric
// size; the shape drives the radius from canonical `--radius-*`). ──
'icon-gap-xs': 'var(--space-2)',
'icon-gap-sm': 'var(--space-3)',
'icon-gap-md': 'var(--space-4)',
'icon-gap-lg': 'var(--space-4)',
'icon-gap-xl': 'var(--space-4)',
'icon-gap-xxl': 'var(--space-5)',
// Explicit featured-icon BOX (off the type scale on purpose — the Untitled
// "featured icon" is a fixed 48px square at lg; the glyph centres inside via
// flex). Decoupled from font changes so it never drifts. Justified off-ladder.
'icon-box-xs': '1.75rem',
'icon-box-sm': '2rem',
'icon-box-md': '2.5rem',
'icon-box-lg': '3rem',
'icon-box-xl': '3.5rem',
'icon-box-xxl': '4.5rem',
'icon-border-width': 'var(--border-width)',
'icon-bg': 'var(--color-surface-muted)',
'icon-color': 'var(--color-content-secondary)',
// ── Label ──
'label-font-family': 'var(--style-label-font-family)',
'label-font-size-xs': 'var(--size-xxs-font-size)',
'label-font-size-sm': 'var(--size-xs-font-size)',
'label-font-size-md': 'var(--size-sm-font-size)',
'label-font-size-lg': 'var(--size-md-font-size)',
'label-font-size-xl': 'var(--size-lg-font-size)',
'label-font-size-xxl': 'var(--size-xl-font-size)',
'label-font-weight': 'var(--font-weight-medium)',
'label-color': 'var(--color-content-secondary)',
'label-line-height': 'var(--leading-ui)',
'label-letter-spacing': 'var(--tracking-normal)',
// ── Value (headline figure, tabular numerals) ──
// REBASED UP the type scale: the value is the only large element, ~2 steps
// above the label, so the small-label → big-number jump (the canonical KPI
// hierarchy) actually happens. NOT 1:1 with the size name — that collapsed
// the hierarchy (md value == label). Still token-driven + theme-overridable.
'value-font-family': 'var(--style-label-font-family)',
'value-font-size-xs': 'var(--size-md-font-size)',
'value-font-size-sm': 'var(--size-lg-font-size)',
'value-font-size-md': 'var(--size-xl-font-size)',
'value-font-size-lg': 'var(--size-xxl-font-size)',
'value-font-size-xl': 'var(--font-size-xxxl)',
'value-font-size-xxl': 'var(--font-size-xxxl)',
'value-font-weight': 'var(--font-weight-semibold)',
'value-color': 'var(--color-content-primary)',
'value-line-height': 'var(--leading-tight)',
'value-letter-spacing': 'var(--tracking-tight)',
'value-font-feature': 'var(--font-feature-tabular)',
// ── Description ──
'description-font-family': 'var(--style-label-font-family)',
'description-font-size-xs': 'var(--size-xxs-font-size)',
'description-font-size-sm': 'var(--size-xs-font-size)',
'description-font-size-md': 'var(--size-sm-font-size)',
'description-font-size-lg': 'var(--size-md-font-size)',
'description-font-size-xl': 'var(--size-lg-font-size)',
'description-font-size-xxl': 'var(--size-xl-font-size)',
'description-color': 'var(--color-content-secondary)',
'description-line-height': 'var(--leading-normal)',
// ── Trend chart (sparkline) slot ──
'chart-margin-block-start': 'var(--space-3)',
'chart-right-width': '7rem',
// ── Progress / gauge to goal (compose <Meter>) ──
'progress-margin-block-start': 'var(--space-3)',
'progress-gap': 'var(--space-1-5)',
'gauge-value-font-size': 'var(--size-sm-font-size)',
'gauge-value-font-weight': 'var(--font-weight-semibold)',
'gauge-value-color': 'var(--color-content-primary)',
// ── Actions row (View report link / dropdown) ──
'actions-gap': 'var(--space-3)',
'actions-margin-block-start': 'var(--space-3)',
'actions-padding-block-start': 'var(--space-3)',
'actions-border-color': 'var(--color-border-subtle)',
// ── Live-update flash on the value (data-event-family='signal') ──
'flash-duration': 'var(--duration-slow)',
'flash-ease': 'var(--ease-out)',
'flash-radius': 'var(--radius-sm)',
'flash-inset': 'var(--space-1)'
},
stepper: {
'gap-sm': 'var(--space-3)',
'gap-md': 'var(--space-4)',
'gap-lg': 'var(--space-5)',
'indicator-size-sm': 'var(--size-sm-control-height)',
'indicator-size-md': 'var(--size-md-control-height)',
'indicator-size-lg': 'var(--size-lg-control-height)',
'font-family': 'var(--style-label-font-family)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'line-height': 'var(--leading-ui)',
color: 'var(--color-content-primary)',
'list-gap': 'var(--space-1)',
'item-gap': 'var(--space-2)',
'trigger-gap': 'var(--space-2)',
'trigger-padding-inline': 'var(--space-2)',
'trigger-padding-block': 'var(--space-1)',
'trigger-radius': 'var(--radius-md)',
'trigger-border-width': 'var(--border-width)',
'trigger-color': 'var(--color-content-secondary)',
'trigger-color-hover': 'var(--color-content-primary)',
'indicator-radius': 'var(--radius-full)',
'indicator-border-width': 'var(--border-width)',
'indicator-border': 'var(--color-border-default)',
'indicator-bg': 'var(--color-surface-raised)',
'indicator-color': 'var(--color-content-secondary)',
'indicator-font-weight': 'var(--font-weight-semibold)',
'complete-color': 'var(--color-content-on-solid)',
'separator-min-size': '3rem',
'separator-thickness': 'var(--border-width-medium)',
'separator-radius': 'var(--radius-full)',
'separator-bg': 'var(--color-neutral-separator)',
'content-padding': 'var(--space-4)',
'content-radius': 'var(--radius-lg)',
'content-border-width': 'var(--border-width)',
'content-border': 'var(--color-border-subtle)',
'content-bg': 'var(--color-surface-raised)',
'content-shadow': 'var(--shadow-subtle)',
'nav-height': 'var(--size-md-control-height)',
'nav-padding-inline': 'var(--space-3)',
'nav-radius': 'var(--radius-md)',
'nav-border-width': 'var(--border-width)',
'nav-border': 'var(--color-border-subtle)',
'nav-bg': 'var(--color-surface-raised)',
'nav-color': 'var(--color-content-primary)',
'nav-hover-brightness': '1.04',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)',
'primary-solid': 'var(--color-primary-solid)',
'primary-text': 'var(--color-primary-text)',
'secondary-solid': 'var(--color-secondary-solid)',
'secondary-text': 'var(--color-secondary-text)',
'neutral-solid': 'var(--color-neutral-solid)',
'neutral-text': 'var(--color-neutral-text)',
'affirm-solid': 'var(--color-affirm-solid)',
'affirm-text': 'var(--color-affirm-text)',
'fulfill-solid': 'var(--color-fulfill-solid)',
'fulfill-track': 'var(--color-fulfill-track)',
'fulfill-border': 'var(--color-fulfill-border)',
'fulfill-text': 'var(--color-fulfill-text)',
'primary-track': 'var(--color-primary-track)',
'primary-border': 'var(--color-primary-border)',
'secondary-track': 'var(--color-secondary-track)',
'secondary-border': 'var(--color-secondary-border)',
'neutral-track': 'var(--color-neutral-track)',
'neutral-border': 'var(--color-neutral-border)',
'affirm-track': 'var(--color-affirm-track)',
'affirm-border': 'var(--color-affirm-border)',
// Private palette (TSC v2.1 · canonical `_palette-*` slots for the
// THM-2 shared-layer forward — color="teal" + the 33 scales; renamed
// from `_accent-*` 2026-07-13). Per-color cascade in config.
'_palette-solid': {
declarations: [
{ value: 'var(--stepper-primary-solid)', scope: 'host' },
{ value: 'var(--stepper-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--stepper-neutral-solid)', scope: 'color:neutral' },
{ value: 'var(--stepper-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--stepper-fulfill-solid)', scope: 'color:fulfill' }
]
},
'_palette-track': {
declarations: [
{ value: 'var(--stepper-primary-track)', scope: 'host' },
{ value: 'var(--stepper-secondary-track)', scope: 'color:secondary' },
{ value: 'var(--stepper-neutral-track)', scope: 'color:neutral' },
{ value: 'var(--stepper-affirm-track)', scope: 'color:affirm' },
{ value: 'var(--stepper-fulfill-track)', scope: 'color:fulfill' }
]
},
'_palette-border': {
declarations: [
{ value: 'var(--stepper-primary-border)', scope: 'host' },
{ value: 'var(--stepper-secondary-border)', scope: 'color:secondary' },
{ value: 'var(--stepper-neutral-border)', scope: 'color:neutral' },
{ value: 'var(--stepper-affirm-border)', scope: 'color:affirm' },
{ value: 'var(--stepper-fulfill-border)', scope: 'color:fulfill' }
]
},
'_palette-text': {
declarations: [
{ value: 'var(--stepper-primary-text)', scope: 'host' },
{ value: 'var(--stepper-secondary-text)', scope: 'color:secondary' },
{ value: 'var(--stepper-neutral-text)', scope: 'color:neutral' },
{ value: 'var(--stepper-affirm-text)', scope: 'color:affirm' },
{ value: 'var(--stepper-fulfill-text)', scope: 'color:fulfill' }
]
}
},
timeline: {
// ── Rhythm (size-scaled) ──
'gap-sm': 'var(--space-3)',
'gap-md': 'var(--space-4)',
'gap-lg': 'var(--space-5)',
'item-gap-sm': 'var(--space-5)',
'item-gap-md': 'var(--space-6)',
'item-gap-lg': 'var(--space-7)',
'marker-size-sm': 'var(--size-sm-control-height)',
'marker-size-md': 'var(--size-md-control-height)',
'marker-size-lg': 'var(--size-lg-control-height)',
'marker-icon-size-sm': 'var(--size-sm-icon-size)',
'marker-icon-size-md': 'var(--size-md-icon-size)',
'marker-icon-size-lg': 'var(--size-lg-icon-size)',
'font-family': 'var(--style-label-font-family)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
// Time meta sits one step below the item's font size (field-label rhythm).
'time-font-size-sm': 'var(--size-xs-font-size)',
'time-font-size-md': 'var(--size-sm-font-size)',
'time-font-size-lg': 'var(--size-md-font-size)',
'line-height': 'var(--leading-ui)',
color: 'var(--color-content-primary)',
// ── Marker (idle / default treatment) ──
'marker-radius': 'var(--radius-full)',
'marker-border-width': 'var(--border-width-medium)',
'marker-border': 'var(--color-border-default)',
'marker-bg': 'var(--color-surface-default)',
'marker-color': 'var(--color-content-secondary)',
'marker-font-weight': 'var(--font-weight-semibold)',
'marker-ring-width': 'var(--space-1)',
// ── Connector ──
'connector-thickness': 'var(--border-width-medium)',
'connector-color': 'var(--color-border-subtle)',
'connector-min-size': 'var(--space-6)',
// Comfortable width for a horizontal entry so its text doesn't collapse
// when the rail overflows (the timeline scrolls instead of shrinking).
'item-inline': '10rem',
// Fixed region heights for horizontal `align=alternate`: the content and
// time rows are pinned so the marker lands at a constant offset and the
// rail stays straight across entries. Keep horizontal-alternate content
// concise (title + one description line).
'alt-content-block': '3.25rem',
'alt-time-block': '1.5rem',
// ── Content / time / title / description ──
'content-gap': 'var(--space-1)',
'time-font-weight': 'var(--font-weight-medium)',
'time-color': 'var(--color-content-secondary)',
'title-font-weight': 'var(--font-weight-semibold)',
'title-color': 'var(--color-content-primary)',
'description-color': 'var(--color-content-secondary)',
// ── Live-feed reveal ──
'reveal-duration': 'var(--duration-moderate)',
'reveal-ease': 'var(--ease-out)',
'reveal-distance': 'var(--space-3)',
// ── Transitions ──
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)'
},
slider: {
'track-size-xs': '3px',
'track-size-sm': '4px',
'track-size-md': '6px',
'track-size-lg': '8px',
'track-size-xl': '10px',
'thumb-size-xs': '12px',
'thumb-size-sm': '16px',
'thumb-size-md': '20px',
'thumb-size-lg': '24px',
'thumb-size-xl': '28px',
'hit-size-xs': '24px',
'hit-size-sm': '28px',
'hit-size-md': '32px',
'hit-size-lg': '36px',
'hit-size-xl': '40px',
'tick-size-xs': '3px',
'tick-size-sm': '4px',
'tick-size-md': '5px',
'tick-size-lg': '6px',
'tick-size-xl': '7px',
'min-inline-size': '12rem',
'min-block-size': '10rem',
'track-radius': 'var(--radius-full)',
// The unfilled groove. `--color-neutral-track` equals the surface in the
// base theme (invisible against panels), so the track uses the same
// visible-track mix as Progress: a light tint of content-primary on the
// surface. Reads on white AND dark, and follows the theme.
'track-bg':
'color-mix(in srgb, var(--color-content-primary) 12%, var(--color-surface-default))',
'range-bg': 'var(--color-primary-solid)',
'thumb-radius': 'var(--radius-full)',
'thumb-border-width': 'var(--border-width)',
'thumb-border': 'var(--color-border-default)',
'thumb-bg': 'var(--color-surface-default)',
'thumb-shadow': 'var(--shadow-subtle)',
// Pickup lift WHILE grabbed (pointer :active): the thumb scales toward the
// viewer + rides a higher shadow — the draggable-surface lift doctrine
// (THEMING §14), scaled up from the surface `--motion-scale-lift` (1.02)
// because a ~20px thumb needs a larger multiplier to read as a lift.
'thumb-scale-active': '1.15',
'thumb-shadow-active': 'var(--shadow-3)',
'tick-radius': 'var(--radius-full)',
'tick-bg': 'var(--color-neutral-border)',
'tick-bg-active': 'var(--color-primary-solid)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
'media-player': {
// Projector accent — a component-owned colour (not a role), theme-stable
// so the scrubber reads over arbitrary video in light AND dark. Overridable
// per theme via `--media-player-accent`. The scrubber consumes it through
// the Slider's own `--slider-range-bg` / `--slider-thumb-bg` tokens, so it
// stays a system Slider — only its colour is re-tinted.
accent: 'var(--scale-amber-9)',
'accent-strong': 'var(--scale-amber-10)'
},
surface: {
// Surface — the themeable canvas (Box + treatment): the palette-tint
// mechanism of Card WITHOUT the chrome (no header slots, no border by
// default, no interactivity). Rides THM-2 (color axis) + the gradient
// finish (§39). Default = neutral, like Card.
radius: 'var(--radius-lg)',
// Layer-4 per-color slots — 3 slots × 8 colors (track/solid/contrast:
// exactly what soft/solid consume; border/text pruned by the orphan
// guard — Surface has no outline/text variant BY DECISION).
'neutral-track': 'var(--color-neutral-track)',
'neutral-solid': 'var(--color-neutral-solid)',
'neutral-contrast': 'var(--color-neutral-contrast)',
'primary-track': 'var(--color-primary-track)',
'primary-solid': 'var(--color-primary-solid)',
'primary-contrast': 'var(--color-primary-contrast)',
'secondary-track': 'var(--color-secondary-track)',
'secondary-solid': 'var(--color-secondary-solid)',
'secondary-contrast': 'var(--color-secondary-contrast)',
'affirm-track': 'var(--color-affirm-track)',
'affirm-solid': 'var(--color-affirm-solid)',
'affirm-contrast': 'var(--color-affirm-contrast)',
'fulfill-track': 'var(--color-fulfill-track)',
'fulfill-solid': 'var(--color-fulfill-solid)',
'fulfill-contrast': 'var(--color-fulfill-contrast)',
'risk-track': 'var(--color-risk-track)',
'risk-solid': 'var(--color-risk-solid)',
'risk-contrast': 'var(--color-risk-contrast)',
'threat-track': 'var(--color-threat-track)',
'threat-solid': 'var(--color-threat-solid)',
'threat-contrast': 'var(--color-threat-contrast)',
'loss-track': 'var(--color-loss-track)',
'loss-solid': 'var(--color-loss-solid)',
'loss-contrast': 'var(--color-loss-contrast)',
// Private palette (canonical `_palette-*` slots → the THM-2 forward
// routes color="teal" + the 33 scales). 8 colors, 3 slots.
'_palette-track': {
declarations: [
{ value: 'var(--surface-neutral-track)', scope: 'host' },
{ value: 'var(--surface-primary-track)', scope: 'color:primary' },
{ value: 'var(--surface-secondary-track)', scope: 'color:secondary' },
{ value: 'var(--surface-affirm-track)', scope: 'color:affirm' },
{ value: 'var(--surface-fulfill-track)', scope: 'color:fulfill' },
{ value: 'var(--surface-risk-track)', scope: 'color:risk' },
{ value: 'var(--surface-threat-track)', scope: 'color:threat' },
{ value: 'var(--surface-loss-track)', scope: 'color:loss' }
]
},
'_palette-solid': {
declarations: [
{ value: 'var(--surface-neutral-solid)', scope: 'host' },
{ value: 'var(--surface-primary-solid)', scope: 'color:primary' },
{ value: 'var(--surface-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--surface-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--surface-fulfill-solid)', scope: 'color:fulfill' },
{ value: 'var(--surface-risk-solid)', scope: 'color:risk' },
{ value: 'var(--surface-threat-solid)', scope: 'color:threat' },
{ value: 'var(--surface-loss-solid)', scope: 'color:loss' }
]
},
'_palette-contrast': {
declarations: [
{ value: 'var(--surface-neutral-contrast)', scope: 'host' },
{ value: 'var(--surface-primary-contrast)', scope: 'color:primary' },
{ value: 'var(--surface-secondary-contrast)', scope: 'color:secondary' },
{ value: 'var(--surface-affirm-contrast)', scope: 'color:affirm' },
{ value: 'var(--surface-fulfill-contrast)', scope: 'color:fulfill' },
{ value: 'var(--surface-risk-contrast)', scope: 'color:risk' },
{ value: 'var(--surface-threat-contrast)', scope: 'color:threat' },
{ value: 'var(--surface-loss-contrast)', scope: 'color:loss' }
]
}
},
switch: {
'size-xs-track-width': '26px',
'size-xs-track-height': '15px',
'size-xs-thumb-size': '11px',
'size-xs-thumb-translate': '12px',
'size-sm-track-width': '32px',
'size-sm-track-height': '18px',
'size-sm-thumb-size': '14px',
'size-sm-thumb-translate': '14px',
'size-md-track-width': '38px',
'size-md-track-height': '22px',
'size-md-thumb-size': '18px',
'size-md-thumb-translate': '16px',
'size-lg-track-width': '46px',
'size-lg-track-height': '28px',
'size-lg-thumb-size': '24px',
'size-lg-thumb-translate': '18px',
'size-xl-track-width': '54px',
'size-xl-track-height': '34px',
'size-xl-thumb-size': '30px',
'size-xl-thumb-translate': '20px',
'track-radius': 'var(--radius-full)',
'track-border-width': 'var(--border-width)',
'thumb-inset': '1px',
'thumb-radius': 'var(--radius-full)',
'thumb-border-width': 'var(--border-width)',
'thumb-shadow': '0 1px 2px rgb(17 14 10 / 0.12)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'track-bg-off': 'var(--color-surface-overlay)',
'track-bg-off-hover':
'color-mix( in srgb, var(--color-surface-overlay) 82%, var(--color-surface-raised) )',
'track-border-off': 'var(--color-border-default)',
'track-border-off-hover': 'var(--color-border-strong)',
'track-bg-disabled':
'color-mix( in srgb, var(--color-surface-overlay) 70%, var(--color-surface-default) )',
'track-border-disabled':
'color-mix( in srgb, var(--color-border-default) 70%, var(--color-border-subtle) )',
'thumb-bg-disabled':
'color-mix( in srgb, var(--color-surface-default) 88%, var(--color-surface-raised) )',
'thumb-border-disabled':
'color-mix( in srgb, var(--color-border-subtle) 88%, var(--color-surface-overlay) )',
'disabled-opacity': 'var(--opacity-disabled)',
'thumb-bg': 'var(--color-surface-default)',
'thumb-border':
'color-mix( in srgb, var(--color-border-default) 82%, var(--color-surface-overlay) )',
'thumb-icon-color': 'var(--color-content-muted)',
'primary-solid': 'var(--color-primary-solid)',
'primary-solid-hover': 'var(--color-primary-solid-hover)',
'neutral-solid': 'var(--color-neutral-solid)',
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
'risk-solid': 'var(--color-risk-solid)',
'risk-solid-hover': 'var(--color-risk-solid-hover)',
'threat-solid': 'var(--color-threat-solid)',
'threat-solid-hover': 'var(--color-threat-solid-hover)',
'secondary-solid': 'var(--color-secondary-solid)',
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
'affirm-solid': 'var(--color-affirm-solid)',
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
// Private palette (TSC v2.1). Host default = neutral, plus
// per-color overrides. Migrated from manual blocks in switch.css.
// Only solid/solid-hover are actively consumed; track/border
// existed in the pre-TSC defaults but were never referenced.
'_palette-solid': {
declarations: [
{ value: 'var(--switch-neutral-solid)', scope: 'host' },
{ value: 'var(--switch-primary-solid)', scope: 'color:primary' },
{ value: 'var(--switch-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--switch-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--switch-risk-solid)', scope: 'color:risk' },
{ value: 'var(--switch-threat-solid)', scope: 'color:threat' }
]
},
'_palette-solid-hover': {
declarations: [
{ value: 'var(--switch-neutral-solid-hover)', scope: 'host' },
{ value: 'var(--switch-primary-solid-hover)', scope: 'color:primary' },
{ value: 'var(--switch-secondary-solid-hover)', scope: 'color:secondary' },
{ value: 'var(--switch-affirm-solid-hover)', scope: 'color:affirm' },
{ value: 'var(--switch-risk-solid-hover)', scope: 'color:risk' },
{ value: 'var(--switch-threat-solid-hover)', scope: 'color:threat' }
]
}
},
'color-swatch': {
// Dedicated swatch scale — a colour chip is a SUBSTANTIAL, tappable sample,
// NOT an inline glyph, so it is bigger than the icon scale (which read
// ridiculously small). Values match the prior ColorPicker swatch (20/24/28
// for sm/md/lg) + ends; follows --scaling like the other metrics.
'size-xs': 'calc(16px * var(--scaling))',
'size-sm': 'calc(20px * var(--scaling))',
'size-md': 'calc(24px * var(--scaling))',
'size-lg': 'calc(28px * var(--scaling))',
'size-xl': 'calc(32px * var(--scaling))',
radius: 'var(--radius-sm)',
'border-width': 'var(--border-width)',
'border-color': 'var(--color-border-subtle)',
// The sampled colour — set per-instance inline by the wrapper; default
// transparent (an empty swatch shows the checker). Public knob.
fill: 'transparent',
// Transparency checker — mode-aware neutral base + a slightly darker square,
// so alpha colours read on a subtle grid (never a hard white).
'checker-cell': '6px',
'checker-bg': 'var(--color-surface-default)',
'checker-square': 'var(--color-content-muted)'
},
tabs: {
'stack-gap': 'var(--space-4)',
'line-stack-gap': 'var(--space-3)',
'provider-radius': 'var(--radius-xl)',
'provider-border-width': 'var(--border-width)',
'provider-border': 'var(--color-border-default)',
'provider-bg': 'var(--color-surface-raised)',
'surface-provider-padding': 'var(--space-3)',
'surface-provider-shadow': 'var(--shadow-subtle)',
'list-gap': 'var(--space-1)',
'list-padding': 'var(--space-1)',
'list-radius': 'var(--radius-lg)',
'list-width': '220px',
'list-bg': 'transparent',
'list-border-width': 'var(--border-width)',
'list-border': 'var(--color-border-default)',
'line-list-padding-inline-end': 'var(--space-2)',
'surface-list-bg': 'color-mix(in srgb, var(--color-surface-default) 82%, transparent)',
'pills-list-bg': 'color-mix(in srgb, var(--color-surface-default) 90%, transparent)',
'trigger-height-xs': 'var(--size-xs-control-height)',
'trigger-height-sm': 'var(--size-sm-control-height)',
'trigger-height-md': 'var(--size-md-control-height)',
'trigger-height-lg': 'var(--size-lg-control-height)',
'trigger-padding-inline-xs': 'var(--space-2-5)',
'trigger-padding-inline-sm': 'var(--space-3)',
'trigger-padding-inline-md': 'var(--space-3-5)',
'trigger-padding-inline-lg': 'var(--space-4)',
'trigger-gap-xs': 'var(--space-1)',
'trigger-gap-sm': 'var(--space-1-5)',
'trigger-gap-md': 'var(--space-2)',
'trigger-gap-lg': 'var(--space-2)',
'trigger-radius': 'var(--radius-md)',
'trigger-border-width': 'var(--border-width)',
'trigger-font-family': 'var(--style-label-font-family)',
'trigger-font-size-xs': 'var(--size-xs-font-size)',
'trigger-font-size-sm': 'var(--size-sm-font-size)',
'trigger-font-size-md': 'var(--size-md-font-size)',
'trigger-font-size-lg': 'var(--size-lg-font-size)',
'trigger-font-weight': 'var(--font-weight-medium)',
'trigger-line-height': 'var(--leading-ui)',
'trigger-letter-spacing': 'var(--tracking-normal)',
'trigger-color': 'var(--color-content-secondary)',
'trigger-color-hover': 'var(--color-content-primary)',
'trigger-color-active': 'var(--color-content-primary)',
'trigger-bg': 'transparent',
'trigger-bg-active': 'var(--color-surface-overlay)',
'trigger-border': 'transparent',
'trigger-border-active': 'var(--color-primary-border)',
'trigger-transition-duration': 'var(--duration-fast)',
'trigger-transition-ease': 'var(--ease-default)',
'trigger-focus-ring-offset': 'var(--focus-ring-offset)',
'trigger-focus-ring-width': 'var(--focus-ring-width)',
'trigger-focus-ring-color': 'var(--focus-ring-color)',
'surface-trigger-border-active':
'color-mix( in srgb, var(--tabs-trigger-border-active) 58%, transparent )',
'pills-trigger-radius': 'var(--radius-full)',
'pills-trigger-border-active':
'color-mix( in srgb, var(--tabs-trigger-border-active) 58%, transparent )',
'pills-trigger-shadow-active': 'var(--shadow-subtle)',
'segmented-list-bg': 'var(--color-surface-muted)',
'segmented-list-radius': 'var(--radius-md)',
'segmented-trigger-radius':
'max(0px, calc(var(--tabs-segmented-list-radius) - var(--tabs-list-padding)))',
'segmented-trigger-bg-active': 'var(--color-primary-solid)',
'segmented-trigger-color-active': 'var(--color-content-on-solid)',
'segmented-trigger-shadow-active': 'var(--shadow-subtle)',
'content-padding-xs': 'var(--space-3)',
'content-padding-sm': 'var(--space-4)',
'content-padding-md': 'var(--space-5)',
'content-padding-lg': 'var(--space-6)',
'content-radius': 'var(--radius-lg)',
'content-bg': 'transparent',
'content-border-width': 'var(--border-width)',
'content-border': 'transparent',
'content-color': 'var(--color-content-primary)',
'surface-content-border': 'color-mix( in srgb, var(--tabs-provider-border) 84%, transparent )',
'surface-content-bg': 'color-mix( in srgb, var(--color-surface-overlay) 92%, transparent )',
'pills-content-padding-block-start': 'var(--space-2)',
'disabled-opacity': 'var(--opacity-disabled)',
'indicator-transition-duration': 'var(--duration-normal)',
'indicator-transition-ease': 'cubic-bezier(0.32, 0.72, 0, 1)',
'indicator-line-thickness': '2px',
'scrollable-fade': '16px'
},
's-text': {
'hint-color': 'var(--color-content-secondary)',
'hint-opacity': '0.8',
'hint-margin-top': 'var(--space-1)'
},
's-text-virtual-list': {
'line-height': 'var(--leading-prose)',
color: 'var(--color-content-primary)',
bg: 'var(--color-surface-default)',
'border-width': 'var(--border-width)',
'border-color': 'var(--color-border-subtle)',
radius: 'var(--radius-md)',
'row-padding-inline': 'var(--space-3)',
'row-border-width': 'var(--border-width)',
'row-border-color': 'var(--color-border-subtle)'
},
toast: {
'toaster-z': 'var(--z-index-overlay-toast)',
'toaster-inset': 'var(--space-4)',
'toaster-gap': 'var(--space-3)',
'toaster-width': '420px',
'provider-min-width': '280px',
'provider-max-width': '420px',
'provider-gap': 'var(--space-3)',
'provider-stack-gap': 'var(--space-1)',
'provider-padding-inline': 'var(--space-4)',
'provider-padding-block': 'var(--space-3)',
'provider-border-width': 'var(--border-width)',
'provider-radius': 'var(--radius-lg)',
'provider-shadow': 'var(--depth-overlay-shadow), var(--depth-overlay-halo)',
'accent-width': 'var(--border-width-thick)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'closed-offset': '10px',
'closed-scale': '0.98',
'status-size': '32px',
'status-radius': 'var(--radius-full)',
'title-font-family': 'var(--style-label-font-family)',
'title-font-size': 'var(--size-sm-font-size)',
'title-font-weight': 'var(--font-weight-semibold)',
'title-line-height': 'var(--leading-ui)',
'title-color': 'var(--color-content-primary)',
'message-color': 'var(--color-content-primary)',
'description-font-family': 'var(--style-label-font-family)',
'description-font-size': 'var(--size-xs-font-size)',
'description-line-height': 'var(--leading-prose)',
'description-color': 'var(--color-content-secondary)',
'action-gap': 'var(--space-1)',
'action-padding-inline': 'var(--space-2)',
'action-padding-block': 'var(--space-1)',
'action-margin-top': 'var(--space-1)',
'action-border-width': 'var(--border-width)',
'action-radius': 'var(--radius-md)',
'action-font-family': 'var(--style-label-font-family)',
'action-font-size': 'var(--size-xs-font-size)',
'action-font-weight': 'var(--font-weight-medium)',
'action-line-height': 'var(--leading-ui)',
'action-focus-ring-offset': 'var(--focus-ring-offset)',
'action-focus-ring-width': 'var(--focus-ring-width)',
'action-focus-ring-color': 'var(--focus-ring-color)',
// `close-*` tokens removed: Toast.Close composes the canonical
// <IconButton rounded="full">, so its chrome lives in button.css.
'default-surface':
'color-mix( in srgb, var(--color-surface-overlay) 92%, var(--color-neutral-track) )',
'default-border':
'color-mix( in srgb, var(--color-neutral-border) 72%, var(--color-border-default) )',
'default-accent': 'var(--color-neutral-border)',
'default-status-bg': 'var(--color-neutral-track)',
'default-status-color': 'var(--color-neutral-text)',
'default-action-color': 'var(--color-neutral-text)',
'default-action-hover-bg': 'var(--color-neutral-track)',
'affirm-surface':
'color-mix( in srgb, var(--color-surface-overlay) 90%, var(--color-affirm-track) )',
'affirm-border':
'color-mix( in srgb, var(--color-affirm-border) 70%, var(--color-border-default) )',
'affirm-accent': 'var(--color-affirm-border)',
'affirm-status-bg': 'var(--color-affirm-track)',
'affirm-status-color': 'var(--color-affirm-text)',
'affirm-action-color': 'var(--color-affirm-text)',
'affirm-action-hover-bg': 'var(--color-affirm-track)',
'fulfill-surface':
'color-mix( in srgb, var(--color-surface-overlay) 90%, var(--color-fulfill-track) )',
'fulfill-border':
'color-mix( in srgb, var(--color-fulfill-border) 70%, var(--color-border-default) )',
'fulfill-accent': 'var(--color-fulfill-border)',
'fulfill-status-bg': 'var(--color-fulfill-track)',
'fulfill-status-color': 'var(--color-fulfill-text)',
'fulfill-action-color': 'var(--color-fulfill-text)',
'fulfill-action-hover-bg': 'var(--color-fulfill-track)',
'risk-surface':
'color-mix( in srgb, var(--color-surface-overlay) 90%, var(--color-risk-track) )',
'risk-border':
'color-mix( in srgb, var(--color-risk-border) 72%, var(--color-border-default) )',
'risk-accent': 'var(--color-risk-border)',
'risk-status-bg': 'var(--color-risk-track)',
'risk-status-color': 'var(--color-risk-text)',
'risk-action-color': 'var(--color-risk-text)',
'risk-action-hover-bg': 'var(--color-risk-track)',
'threat-surface':
'color-mix( in srgb, var(--color-surface-overlay) 90%, var(--color-threat-track) )',
'threat-border':
'color-mix( in srgb, var(--color-threat-border) 72%, var(--color-border-default) )',
'threat-accent': 'var(--color-threat-border)',
'threat-status-bg': 'var(--color-threat-track)',
'threat-status-color': 'var(--color-threat-text)',
'threat-action-color': 'var(--color-threat-text)',
'threat-action-hover-bg': 'var(--color-threat-track)',
'loss-surface':
'color-mix( in srgb, var(--color-surface-overlay) 90%, var(--color-loss-track) )',
'loss-border':
'color-mix( in srgb, var(--color-loss-border) 72%, var(--color-border-default) )',
'loss-accent': 'var(--color-loss-border)',
'loss-status-bg': 'var(--color-loss-track)',
'loss-status-color': 'var(--color-loss-text)',
'loss-action-color': 'var(--color-loss-text)',
'loss-action-hover-bg': 'var(--color-loss-track)'
},
toggle: {
// SIZE-BUNDLE PILOT (Fase D adoption pattern): where the recipe sits ON
// the canonical coordinate, it consumes `--size-{k}-*` (retuning the
// bundle at theme level now moves this recipe); where it DELIBERATELY
// deviates (px / gap tuned tighter than the bundle's padding, leading
// anchored to `--leading-ui`), it keeps its own value — the deviation is
// now visible instead of buried in a parallel re-declaration.
'height-xs': 'var(--size-xs-control-height)',
'height-sm': 'var(--size-sm-control-height)',
'height-md': 'var(--size-md-control-height)',
'height-lg': 'var(--size-lg-control-height)',
'height-xl': 'var(--size-xl-control-height)',
'padding-inline-xs': 'var(--space-2)',
'padding-inline-sm': 'var(--space-2-5)',
'padding-inline-md': 'var(--space-3-5)',
'padding-inline-lg': 'var(--space-4-5)',
'padding-inline-xl': 'var(--space-5)',
'gap-xs': 'var(--space-1)',
'gap-sm': 'var(--space-1-5)',
'gap-md': 'var(--space-1-5)',
'gap-lg': 'var(--space-2)',
'gap-xl': 'var(--space-2-5)',
'font-family': 'var(--style-label-font-family)',
'line-height': 'var(--leading-ui)',
'letter-spacing': 'var(--tracking-normal)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'font-size-xl': 'var(--size-xl-font-size)',
'font-weight-xs': 'var(--font-weight-medium)',
'font-weight-sm': 'var(--font-weight-medium)',
'font-weight-md': 'var(--font-weight-medium)',
'font-weight-lg': 'var(--font-weight-medium)',
'font-weight-xl': 'var(--font-weight-medium)',
// Radius is NOT a per-size token: it is DECOUPLED from `size` (Radix model).
// toggle.css resolves the resting radius from `--radius-default` (control
// archetype default) and the `rounded` override from the global `--radius-*`
// scale. No `--toggle-radius-*` aliases — see toggle.css "Rounded override".
'border-width': 'var(--border-width)',
shadow: 'none',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
// Per-color forwarders consumed by the palette-* cascades below.
// 8 slots per color (track / element / hover / border / solid /
// solid-hover / text / contrast) matching the 8 palette-* tokens
// the cascades target. Missing any forwarder makes the
// corresponding palette-{slot} resolve to empty for that color,
// which downstream collapses any derived token consuming it
// (e.g. ghost-on-bg uses palette-track).
'primary-track': 'var(--color-primary-track)',
'primary-element': 'var(--color-primary-element)',
'primary-hover': 'var(--color-primary-hover)',
'primary-border': 'var(--color-primary-border)',
'primary-solid': 'var(--color-primary-solid)',
'primary-solid-hover': 'var(--color-primary-solid-hover)',
'primary-text': 'var(--color-primary-text)',
'primary-contrast': 'var(--color-primary-contrast)',
'neutral-track': 'var(--color-neutral-track)',
'neutral-element': 'var(--color-neutral-element)',
'neutral-hover': 'var(--color-neutral-hover)',
'neutral-border': 'var(--color-neutral-border)',
'neutral-solid': 'var(--color-neutral-solid)',
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
'neutral-text': 'var(--color-neutral-text)',
'neutral-contrast': 'var(--color-neutral-contrast)',
'risk-track': 'var(--color-risk-track)',
'risk-element': 'var(--color-risk-element)',
'risk-hover': 'var(--color-risk-hover)',
'risk-border': 'var(--color-risk-border)',
'risk-solid': 'var(--color-risk-solid)',
'risk-solid-hover': 'var(--color-risk-solid-hover)',
'risk-text': 'var(--color-risk-text)',
'risk-contrast': 'var(--color-risk-contrast)',
'threat-track': 'var(--color-threat-track)',
'threat-element': 'var(--color-threat-element)',
'threat-hover': 'var(--color-threat-hover)',
'threat-border': 'var(--color-threat-border)',
'threat-solid': 'var(--color-threat-solid)',
'threat-solid-hover': 'var(--color-threat-solid-hover)',
'threat-text': 'var(--color-threat-text)',
'threat-contrast': 'var(--color-threat-contrast)',
'secondary-track': 'var(--color-secondary-track)',
'secondary-element': 'var(--color-secondary-element)',
'secondary-hover': 'var(--color-secondary-hover)',
'secondary-border': 'var(--color-secondary-border)',
'secondary-solid': 'var(--color-secondary-solid)',
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
'secondary-text': 'var(--color-secondary-text)',
'secondary-contrast': 'var(--color-secondary-contrast)',
'affirm-track': 'var(--color-affirm-track)',
'affirm-element': 'var(--color-affirm-element)',
'affirm-hover': 'var(--color-affirm-hover)',
'affirm-border': 'var(--color-affirm-border)',
'affirm-solid': 'var(--color-affirm-solid)',
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
'affirm-text': 'var(--color-affirm-text)',
'affirm-contrast': 'var(--color-affirm-contrast)',
// Palette tokens: 8 custom properties, each declared 7 times.
// One default declaration at scope 'host' (the neutral color), plus
// one override per supported color value (scope 'color:X'). The
// generator emits them as a `[data-toggle]` block (default) plus
// six `[data-toggle][data-color='X']` blocks (overrides). The CSS
// cascade picks the most specific match per element.
//
// This replaces the per-color blocks that used to live in
// `toggle.css` — the config now owns the entire palette cascade.
'palette-track': {
declarations: [
{ value: 'var(--toggle-neutral-track)', scope: 'host' },
{ value: 'var(--toggle-primary-track)', scope: 'color:primary' },
{ value: 'var(--toggle-secondary-track)', scope: 'color:secondary' },
{ value: 'var(--toggle-affirm-track)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-track)', scope: 'color:risk' },
{ value: 'var(--toggle-threat-track)', scope: 'color:threat' }
]
},
'palette-element': {
declarations: [
{ value: 'var(--toggle-neutral-element)', scope: 'host' },
{ value: 'var(--toggle-primary-element)', scope: 'color:primary' },
{ value: 'var(--toggle-secondary-element)', scope: 'color:secondary' },
{ value: 'var(--toggle-affirm-element)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-element)', scope: 'color:risk' },
{ value: 'var(--toggle-threat-element)', scope: 'color:threat' }
]
},
'palette-hover': {
declarations: [
{ value: 'var(--toggle-neutral-hover)', scope: 'host' },
{ value: 'var(--toggle-primary-hover)', scope: 'color:primary' },
{ value: 'var(--toggle-secondary-hover)', scope: 'color:secondary' },
{ value: 'var(--toggle-affirm-hover)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-hover)', scope: 'color:risk' },
{ value: 'var(--toggle-threat-hover)', scope: 'color:threat' }
]
},
'palette-border': {
declarations: [
{ value: 'var(--toggle-neutral-border)', scope: 'host' },
{ value: 'var(--toggle-primary-border)', scope: 'color:primary' },
{ value: 'var(--toggle-secondary-border)', scope: 'color:secondary' },
{ value: 'var(--toggle-affirm-border)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-border)', scope: 'color:risk' },
{ value: 'var(--toggle-threat-border)', scope: 'color:threat' }
]
},
'palette-solid': {
declarations: [
{ value: 'var(--toggle-neutral-solid)', scope: 'host' },
{ value: 'var(--toggle-primary-solid)', scope: 'color:primary' },
{ value: 'var(--toggle-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--toggle-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-solid)', scope: 'color:risk' },
{ value: 'var(--toggle-threat-solid)', scope: 'color:threat' }
]
},
'palette-solid-hover': {
declarations: [
{ value: 'var(--toggle-neutral-solid-hover)', scope: 'host' },
{ value: 'var(--toggle-primary-solid-hover)', scope: 'color:primary' },
{ value: 'var(--toggle-secondary-solid-hover)', scope: 'color:secondary' },
{ value: 'var(--toggle-affirm-solid-hover)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-solid-hover)', scope: 'color:risk' },
{ value: 'var(--toggle-threat-solid-hover)', scope: 'color:threat' }
]
},
'palette-text': {
declarations: [
{ value: 'var(--toggle-neutral-text)', scope: 'host' },
{ value: 'var(--toggle-primary-text)', scope: 'color:primary' },
{ value: 'var(--toggle-secondary-text)', scope: 'color:secondary' },
{ value: 'var(--toggle-affirm-text)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-text)', scope: 'color:risk' },
{ value: 'var(--toggle-threat-text)', scope: 'color:threat' }
]
},
'palette-contrast': {
declarations: [
{ value: 'var(--toggle-neutral-contrast)', scope: 'host' },
{ value: 'var(--toggle-primary-contrast)', scope: 'color:primary' },
{ value: 'var(--toggle-secondary-contrast)', scope: 'color:secondary' },
{ value: 'var(--toggle-affirm-contrast)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-contrast)', scope: 'color:risk' },
{ value: 'var(--toggle-threat-contrast)', scope: 'color:threat' }
]
},
// Variant tokens that depend on the runtime `--toggle-palette-*`
// values are scoped to `host` so the var() reference resolves
// against the toggle element (where the palette override lives),
// not against `:root` (which would freeze them at neutral).
// The generator infers the `depends` set automatically from the
// `var(--toggle-palette-*)` references — no manual `depends`
// arrays needed. See Token Scope Contract in `lib/config-types.ts`.
'solid-bg': 'var(--color-surface-raised)',
'solid-fg': 'var(--color-content-primary)',
'solid-border': 'var(--color-border-default)',
'solid-hover-bg': {
value: 'color-mix( in srgb, var(--color-surface-overlay) 68%, var(--toggle-palette-track) )',
scope: 'host'
},
'solid-hover-border': {
value: 'color-mix( in srgb, var(--toggle-palette-border) 45%, var(--color-border-strong) )',
scope: 'host'
},
'solid-on-bg': { value: 'var(--toggle-palette-solid)', scope: 'host' },
'solid-on-fg': { value: 'var(--toggle-palette-contrast)', scope: 'host' },
'solid-on-border': { value: 'var(--toggle-palette-solid)', scope: 'host' },
'solid-on-hover-bg': { value: 'var(--toggle-palette-solid-hover)', scope: 'host' },
'solid-on-hover-border': { value: 'var(--toggle-palette-solid-hover)', scope: 'host' },
'outline-bg': 'transparent',
'outline-fg': { value: 'var(--toggle-palette-text)', scope: 'host' },
'outline-border': {
value: 'color-mix( in srgb, var(--toggle-palette-border) 88%, var(--color-border-default) )',
scope: 'host'
},
'outline-hover-bg': { value: 'var(--toggle-palette-track)', scope: 'host' },
'outline-hover-border': {
value: 'color-mix( in srgb, var(--toggle-palette-border) 100%, var(--color-border-default) )',
scope: 'host'
},
'outline-on-bg': {
value:
'color-mix( in srgb, var(--toggle-palette-element) 100%, var(--color-surface-raised) )',
scope: 'host'
},
'outline-on-fg': { value: 'var(--toggle-palette-text)', scope: 'host' },
'outline-on-border': { value: 'var(--toggle-palette-border)', scope: 'host' },
'outline-on-hover-bg': { value: 'var(--toggle-palette-hover)', scope: 'host' },
'outline-on-hover-border': { value: 'var(--toggle-palette-border)', scope: 'host' },
'ghost-bg': 'transparent',
'ghost-fg': { value: 'var(--toggle-palette-text)', scope: 'host' },
'ghost-border': 'transparent',
'ghost-hover-bg': {
value: 'color-mix(in srgb, var(--toggle-palette-track) 72%, transparent)',
scope: 'host'
},
'ghost-hover-border': 'transparent',
'ghost-on-bg': { value: 'var(--toggle-palette-track)', scope: 'host' },
'ghost-on-fg': { value: 'var(--toggle-palette-text)', scope: 'host' },
'ghost-on-border': {
value: 'color-mix( in srgb, var(--toggle-palette-border) 72%, transparent )',
scope: 'host'
},
'ghost-on-hover-bg': { value: 'var(--toggle-palette-element)', scope: 'host' },
'ghost-on-hover-border': {
value: 'color-mix( in srgb, var(--toggle-palette-border) 88%, transparent )',
scope: 'host'
},
'disabled-opacity': 'var(--opacity-disabled)'
},
tooltip: {
'content-z': 'var(--z-index-overlay-tooltip)',
'max-width': '22rem',
'padding-block-sm': 'var(--space-1)',
'padding-inline-sm': 'var(--space-2)',
'padding-block-md': 'var(--space-1-5)',
'padding-inline-md': 'var(--space-2-5)',
'padding-block-lg': 'var(--space-2)',
'padding-inline-lg': 'var(--space-3)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'font-family': 'var(--style-label-font-family)',
'font-weight': 'var(--font-weight-regular)',
'line-height': 'var(--leading-text)',
radius: 'var(--radius-md)',
'border-width': 'var(--border-width)',
bg: 'var(--color-surface-overlay)',
color: 'var(--color-content-primary)',
border: 'color-mix( in srgb, var(--color-border-default) 84%, var(--tooltip-bg) )',
shadow: 'var(--depth-overlay-shadow), var(--depth-overlay-halo)'
},
// ─────────────────────────────────────────────────────────────────────
// Layout primitives. Box, Flex, Grid, Float and Group expose their CSS
// variables as a pass-through channel: callers push per-instance values
// inline via `pushStyleVar`, and the recipe simply mirrors the CSS
// fallbacks so the contract sees each var as declared. `revert-layer`
// means "fall back to the underlying tag's natural rule" — the right
// default for unset Box props.
// ─────────────────────────────────────────────────────────────────────
box: {
display: 'revert-layer',
position: 'revert-layer',
width: 'revert-layer',
'min-width': '0',
'max-width': 'revert-layer',
height: 'revert-layer',
'min-height': 'revert-layer',
'max-height': 'revert-layer',
padding: 'revert-layer',
'padding-block': 'revert-layer',
'padding-inline': 'revert-layer',
'padding-top': 'revert-layer',
'padding-right': 'revert-layer',
'padding-bottom': 'revert-layer',
'padding-left': 'revert-layer',
margin: 'revert-layer',
'margin-block': 'revert-layer',
'margin-inline': 'revert-layer',
'margin-top': 'revert-layer',
'margin-right': 'revert-layer',
'margin-bottom': 'revert-layer',
'margin-left': 'revert-layer',
gap: 'revert-layer',
top: 'revert-layer',
right: 'revert-layer',
bottom: 'revert-layer',
left: 'revert-layer',
inset: 'revert-layer',
'inset-block': 'revert-layer',
'inset-inline': 'revert-layer',
overflow: 'revert-layer',
'overflow-x': 'revert-layer',
'overflow-y': 'revert-layer',
flex: 'revert-layer',
grow: 'revert-layer',
shrink: 'revert-layer',
basis: 'revert-layer',
order: 'revert-layer',
'align-self': 'revert-layer',
'justify-self': 'revert-layer',
'place-self': 'revert-layer',
'grid-column': 'revert-layer',
'grid-row': 'revert-layer',
'grid-area': 'revert-layer'
},
flex: {
direction: 'row',
align: 'stretch',
justify: 'flex-start',
wrap: 'nowrap',
'align-content': 'normal',
'row-gap': 'var(--box-gap, revert-layer)',
'column-gap': 'var(--box-gap, revert-layer)'
},
grid: {
'template-columns': 'none',
'template-rows': 'none',
'auto-columns': 'auto',
'auto-rows': 'auto',
'auto-flow': 'row',
align: 'stretch',
justify: 'start',
'align-content': 'normal',
'place-items': 'revert-layer',
'place-content': 'revert-layer',
gap: 'revert-layer',
'row-gap': 'var(--grid-gap, revert-layer)',
'column-gap': 'var(--grid-gap, revert-layer)'
},
float: {
gap: 'var(--space-3)'
},
group: {
'attached-overlap': '1px'
},
// ─────────────────────────────────────────────────────────────────────
// Banner — announcement strip. Public surface area is intentionally
// small (gap + radius); the rest of the chrome resolves from
// data-intent + data-variant.
// ─────────────────────────────────────────────────────────────────────
banner: {
gap: 'var(--space-3)',
radius: '0',
// Per-intent palette forwarders → Banner is retintable via
// `recipes.banner.{intent}-{slot}` like every other color component
// (button/badge/toggle). banner.css reads these instead of the raw
// foundation `--color-{intent}-*` tokens, so a per-component recipe
// override actually reaches it. (audit P1-5)
'primary-solid': 'var(--color-primary-solid)',
'primary-contrast': 'var(--color-primary-contrast)',
'primary-track': 'var(--color-primary-track)',
'primary-border': 'var(--color-primary-border)',
'primary-text': 'var(--color-primary-text)',
'secondary-solid': 'var(--color-secondary-solid)',
'secondary-contrast': 'var(--color-secondary-contrast)',
'secondary-track': 'var(--color-secondary-track)',
'secondary-border': 'var(--color-secondary-border)',
'secondary-text': 'var(--color-secondary-text)',
'neutral-solid': 'var(--color-neutral-solid)',
'neutral-contrast': 'var(--color-neutral-contrast)',
'neutral-track': 'var(--color-neutral-track)',
'neutral-border': 'var(--color-neutral-border)',
'neutral-text': 'var(--color-neutral-text)',
'affirm-solid': 'var(--color-affirm-solid)',
'affirm-contrast': 'var(--color-affirm-contrast)',
'affirm-track': 'var(--color-affirm-track)',
'affirm-border': 'var(--color-affirm-border)',
'affirm-text': 'var(--color-affirm-text)',
'fulfill-solid': 'var(--color-fulfill-solid)',
'fulfill-contrast': 'var(--color-fulfill-contrast)',
'fulfill-track': 'var(--color-fulfill-track)',
'fulfill-border': 'var(--color-fulfill-border)',
'fulfill-text': 'var(--color-fulfill-text)',
'risk-solid': 'var(--color-risk-solid)',
'risk-contrast': 'var(--color-risk-contrast)',
'risk-track': 'var(--color-risk-track)',
'risk-border': 'var(--color-risk-border)',
'risk-text': 'var(--color-risk-text)',
'threat-solid': 'var(--color-threat-solid)',
'threat-contrast': 'var(--color-threat-contrast)',
'threat-track': 'var(--color-threat-track)',
'threat-border': 'var(--color-threat-border)',
'threat-text': 'var(--color-threat-text)',
'loss-solid': 'var(--color-loss-solid)',
'loss-contrast': 'var(--color-loss-contrast)',
'loss-track': 'var(--color-loss-track)',
'loss-border': 'var(--color-loss-border)',
'loss-text': 'var(--color-loss-text)'
},
// ─────────────────────────────────────────────────────────────────────
// Container — full-bleed wrapper that caps `max-width` at the canonical
// page width by default: the xl container token (= breakpoint xl, 1280 —
// user decision 2026-07-06). Callers usually push `maxWidth` via Box.
// The recipe token carries its OWN name (`--container-max-width`): a
// recipe key must never re-emit a foundation token name (the previous
// `width-xl` key shadowed the `--container-width-xl` primitive at :root
// through a phantom `--layout-container-width-xl` reference).
// ─────────────────────────────────────────────────────────────────────
container: {
'max-width': 'var(--container-width-xl)'
},
// ─────────────────────────────────────────────────────────────────────
// Section — vertical rhythm scale. Sizes map to the spacing primitives.
// ─────────────────────────────────────────────────────────────────────
section: {
'padding-block-sm': 'var(--space-8)',
'padding-block-md': 'var(--space-12)',
'padding-block-lg': 'var(--space-16)',
'padding-block-xl': 'calc(var(--space-16) * 1.5)'
},
// ─────────────────────────────────────────────────────────────────────
// Separator — purely geometric divider.
// ─────────────────────────────────────────────────────────────────────
separator: {
thickness: 'var(--border-width)',
// The dedicated divider slot (step 6 — same tone as border-default, but
// semantically a SEPARATOR, so a theme can retune dividers independently).
color: 'var(--color-neutral-separator)',
'min-length': 'var(--space-4)'
},
// ─────────────────────────────────────────────────────────────────────
// ToggleGroup — gap is component-owned; the color cascade modifies
// Toggle's `palette-*` tokens via TSC v2.2 cross-recipe composition
// (see `composition` block below).
// ─────────────────────────────────────────────────────────────────────
'toggle-group': {
gap: 'var(--space-1)',
// Per-color palette: the group's `data-color` routes to every item's Toggle
// palette via the shared per-instance layer (THM-2). The composition DSL
// can't presence-scope (`[data-color]` with no value), so the routing is
// hand-authored in `toggle-group.css` — ONE rule covering all roles + the
// 33 scales, replacing the old 4-role composition enumeration.
},
// ─────────────────────────────────────────────────────────────────────
// PinInput — one-time code cells.
// ─────────────────────────────────────────────────────────────────────
'pin-input': {
gap: 'var(--space-2)',
'disabled-opacity': 'var(--opacity-disabled)',
'cell-size-sm': '2rem',
'cell-size-md': '2.5rem',
'cell-size-lg': '3rem',
'cell-size-xl': '3.5rem',
'cell-font-size-sm': 'var(--size-md-font-size)',
'cell-font-size-md': 'var(--size-lg-font-size)',
'cell-font-size-lg': 'var(--size-xl-font-size)',
'cell-font-size-xl': 'var(--size-xxl-font-size)',
'cell-font-weight': '500',
'cell-radius': 'var(--radius-md)',
'cell-border-width': 'var(--border-width)',
'cell-border-color': 'var(--color-border-default)',
'cell-bg': 'var(--color-surface-default)',
'cell-filled-bg': 'var(--color-primary-element)',
'cell-filled-border': 'var(--color-primary-border)',
'cell-active-border': 'var(--color-primary-solid)',
'cell-invalid-border': 'var(--color-threat-solid)',
'cell-readonly-bg': 'var(--color-surface-muted)'
},
// ─────────────────────────────────────────────────────────────────────
// ScrollArea — overlay scrollbars (macOS-style).
// ─────────────────────────────────────────────────────────────────────
'scroll-area': {
// Auto-fit max height — the CONSUMER sets this (e.g. the command list caps
// to min(60vh, 24rem)); the recipe declares the `none` default so the token
// is part of the public contract instead of an undeclared consumed var.
'auto-max-block': 'none',
bg: 'transparent',
radius: 'var(--radius-md)',
'scrollbar-padding': 'var(--space-0-5)',
'scrollbar-size': 'var(--space-2-5)',
'track-bg': 'color-mix(in srgb, var(--color-neutral-track) 60%, transparent)',
'track-bg-hover': 'color-mix(in srgb, var(--color-neutral-track) 84%, transparent)',
'thumb-bg': 'var(--color-content-muted)',
'thumb-bg-hover': 'var(--color-content-secondary)',
'thumb-radius': 'var(--radius-full)',
'thumb-min-size': 'var(--space-3)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)'
},
// ─────────────────────────────────────────────────────────────────────
// Splitter — resizable panels with a draggable handle + grip pill.
// ─────────────────────────────────────────────────────────────────────
splitter: {
bg: 'var(--color-surface-default)',
radius: 'var(--radius-md)',
'border-width': 'var(--border-width)',
'border-color': 'var(--color-border-default)',
'disabled-opacity': 'var(--opacity-disabled)',
'panel-bg': 'transparent',
'handle-size': 'var(--space-2)',
'handle-bg': 'var(--color-border-subtle)',
'handle-bg-hover': 'var(--color-border-default)',
'handle-bg-active': 'var(--color-primary-solid)',
'grip-radius': 'var(--radius-full)',
'grip-bg': 'var(--color-content-muted)',
'grip-main': 'var(--space-4)',
'grip-cross': '2px',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)'
},
// ─────────────────────────────────────────────────────────────────────
// NavigationMenu — soma writes the active-trigger rect into these
// indicator vars; the recipe paints the underline.
// ─────────────────────────────────────────────────────────────────────
// Combobox — floating result list. Shares the overlay micro-band rung
// `--z-index-overlay-floating` with `select` / `dropdown-menu` (the soma
// floating layer mirrors this computed z onto the positioner wrapper). The
// band is deliberately NOT the global `--z-index-*` ladder — see
// STATIC_Z_INDEX_OVERLAY.
combobox: {
'content-z': 'var(--z-index-overlay-floating)'
},
// DragDrop — the drag preview/ghost that tracks the cursor. Floats above
// local content during an active drag.
'drag-drop': {
'preview-z': 'var(--z-index-overlay-detached)',
// Selection palette (THM-2) — `data-color` sits on `[data-drag-drop-root]`.
'_palette-solid': { parts: ['root'], declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }] },
'_palette-element': { parts: ['root'], declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }] }
},
'navigation-menu': {
// Inline (non-portaled) dropdown panel: stacks above normal page flow.
'content-z': 'var(--z-index-overlay-inline)',
'indicator-w': '0px',
'indicator-x': '0px'
},
// ─────────────────────────────────────────────────────────────────────
// DropdownMenu — panel chrome + item rows. Mirror tokens to context-menu.
// ─────────────────────────────────────────────────────────────────────
'dropdown-menu': {
// Floating menu panel stacks in the top overlay band (matches
// `select` — both are click-triggered list overlays). Without this
// the portaled panel inherits `z-index: auto` and any positioned
// page element with a positive z-index (e.g. a selected
// `[data-toggle-group-item]`, z-index 1) paints THROUGH it.
'content-z': 'var(--z-index-overlay-floating)',
'content-min-width': '12rem',
'content-max-height': 'min(60vh, 22rem)',
'content-padding': 'var(--space-1)',
'content-radius': 'var(--radius-md)',
'content-font-size': 'var(--size-sm-font-size)',
'item-gap': 'var(--space-2)',
'item-height': '2rem',
'item-padding-inline': 'var(--space-2)',
'item-padding-block': 'var(--space-1)',
'item-radius': 'var(--radius-sm)',
'item-bg-hover': 'var(--color-primary-element)',
'item-fg-hover': 'var(--color-content-primary)',
'item-disabled-opacity': '0.55',
'heading-padding-inline': 'var(--space-2)',
'heading-padding-block': 'var(--space-1)',
'heading-font-size': 'var(--size-xs-font-size)',
'separator-bg': 'var(--color-neutral-separator)',
'separator-margin-block': 'var(--space-1)'
},
// ─────────────────────────────────────────────────────────────────────
// Menubar — the bar itself styles via foundation privates; these are the
// PANEL surface's public knobs (the role=dialog form surface a bar entry
// can drop instead of a role=menu Content).
// ─────────────────────────────────────────────────────────────────────
menubar: {
// Same overlay band as the sibling menu panels — both drop from the bar.
'panel-z': 'var(--z-index-overlay-floating)',
'panel-min-width': '16rem',
'panel-max-height': 'min(70vh, 28rem)',
'panel-padding': 'var(--space-3)',
'panel-radius': 'var(--radius-md)'
},
// ─────────────────────────────────────────────────────────────────────
// ContextMenu — same surface as DropdownMenu, with a span Trigger.
// ─────────────────────────────────────────────────────────────────────
'context-menu': {
'content-min-width': '12rem',
'content-max-height': 'min(60vh, 22rem)',
'content-padding': 'var(--space-1)',
'content-radius': 'var(--radius-md)',
'content-font-size': 'var(--size-sm-font-size)',
'item-gap': 'var(--space-2)',
'item-height': '2rem',
'item-padding-inline': 'var(--space-2)',
'item-padding-block': 'var(--space-1)',
'item-radius': 'var(--radius-sm)',
'item-bg-hover': 'var(--color-primary-element)',
'item-fg-hover': 'var(--color-content-primary)',
'item-disabled-opacity': '0.55',
'heading-padding-inline': 'var(--space-2)',
'heading-padding-block': 'var(--space-1)',
'heading-font-size': 'var(--size-xs-font-size)',
'separator-bg': 'var(--color-neutral-separator)',
'separator-margin-block': 'var(--space-1)'
},
// ─────────────────────────────────────────────────────────────────────
// DateRangeField — layout only; chrome inherits from date-field.
// ─────────────────────────────────────────────────────────────────────
'date-range-field': {
'row-gap': 'var(--space-3)',
'separator-color': 'var(--color-content-muted)',
'separator-glyph': "'—'"
},
// ─────────────────────────────────────────────────────────────────────
// TimeRangeField — layout only; chrome inherits from time-field.
// ─────────────────────────────────────────────────────────────────────
'time-range-field': {
'row-gap': 'var(--space-3)',
'separator-color': 'var(--color-content-muted)',
'separator-glyph': "'—'"
},
// ─────────────────────────────────────────────────────────────────────
// Fab — the FAB-specific size / icon / elevation scale. Deliberately OFF
// the control-height ladder (Material 40 / 56 / 72); the VALUES live here
// (generated to `--fab-*`), not as literals in fab.css.
// ─────────────────────────────────────────────────────────────────────
fab: {
'size-xs': '1.875rem',
'size-sm': '2.5rem',
'size-md': '3.5rem',
'size-lg': '4.5rem',
'icon-xs': '1rem',
'icon-sm': '1.25rem',
'icon-md': '1.5rem',
'icon-lg': '1.75rem',
offset: 'var(--space-4)',
z: 'var(--z-index-sticky)',
shadow: 'var(--shadow-overlay)',
lift: '2px'
},
// ─────────────────────────────────────────────────────────────────────
// OnionMenu — the radial chrome. The geometry comes from the geometry
// engine + the fills from the colour engine; this owns the magic numbers
// that USED to be literals in onion-menu.css. The centre trigger IS a FAB,
// so its lift / shadow REFERENCE the fab tokens — they can't drift apart.
// ─────────────────────────────────────────────────────────────────────
'onion-menu': {
'trigger-lift': 'var(--fab-lift)',
'trigger-shadow': 'var(--fab-shadow)',
'sector-shadow': 'var(--shadow-raised)',
'sector-ring-width': '2.5',
'sector-hover-ring-width': '2',
'glyph-bar-thickness': '2px',
'muted-opacity': '0.85',
'hover-ring-opacity': '0.65'
},
// ─────────────────────────────────────────────────────────────────────
// ButtonGroup — composes <Group> + <Button>; owns only the segmented seam.
// The divider strength (currentColor mix) is the lone magic number.
// ─────────────────────────────────────────────────────────────────────
'button-group': {
'divider-strength': '18%'
},
// ─────────────────────────────────────────────────────────────────────
// SplitButton — composes <ButtonGroup> + <Button> + <DropdownMenu>. Owns
// only the slim ▾ tail's inline padding (a consumer-overridable knob).
// ─────────────────────────────────────────────────────────────────────
'split-button': {
'trigger-padding-inline': 'var(--space-1)'
}
} as const satisfies RecipeTokenSet);

Powered by TurnKey Linux.