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

8255 lines
400 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) ────────────────────────────
//
// `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-fg': '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).
'open-trigger-fg': 'var(--color-content-primary)',
'disabled-trigger-fg': 'var(--color-content-disabled)',
'trigger-hover-fg': '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-fg': '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-fg': 'var(--color-content-secondary)',
// Indicator turns to the primary content tone when open — a subtle
// emphasis without the loud accent recolor.
'open-indicator-fg': '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 colour ─────────────────────────────────────────────────
// The main fill routes through the shared `--color-custom` seed
// (`resolveComponentColor` + the palette forward), so only the ring
// keeps its own custom channel, set inline by the Svelte file.
'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
]
}
};
})(),
aura: {
// ── La costura (2026-08-22) ──────────────────────────────────────────
// El texto de estado y su detalle bajan UN paso del bundle por talla, y las
// tres coordenadas viajan juntas (tamaño, interlineado, tracking): doce
// declaraciones, doce valores distintos, doce claves. El valor sigue siendo
// del bundle; el knob pasa a ser del componente.
'status-font-size-md': 'var(--size-sm-font-size)',
'status-line-height-md': 'var(--size-sm-font-line-height)',
'status-letter-spacing-md': 'var(--size-sm-font-letter-spacing)',
'status-font-size-lg': 'var(--size-md-font-size)',
'status-line-height-lg': 'var(--size-md-font-line-height)',
'status-letter-spacing-lg': 'var(--size-md-font-letter-spacing)',
'status-detail-font-size-md': 'var(--size-xs-font-size)',
'status-detail-line-height-md': 'var(--size-xs-font-line-height)',
'status-detail-letter-spacing-md': 'var(--size-xs-font-letter-spacing)',
'status-detail-font-size-lg': 'var(--size-sm-font-size)',
'status-detail-line-height-lg': 'var(--size-sm-font-line-height)',
'status-detail-letter-spacing-lg': 'var(--size-sm-font-letter-spacing)',
// El reposo no desaparece: se retira a media opacidad.
'idle-opacity': 'var(--opacity-50)',
// ── Orb geometry per size ────────────────────────────────────────
// Derived from the AVATAR primitive (the canonical round
// presence/identity disc — Aura is the agent's presence): one
// source, themes that retune avatars retune the orb with them.
'orb-size-sm': 'var(--avatar-size-sm)',
'orb-size-md': 'var(--avatar-size-md)',
'orb-size-lg': 'var(--avatar-size-lg)',
'orb-radius': 'var(--radius-full)',
gap: 'var(--space-2)',
// ── State tint (role slots — themes re-tint here). The mapping is
// canon-aligned: reviewing=risk («revisa esto»), escalated=threat
// («atiende ahora»), acting/offered/planned=primary, idle=neutral.
'orb-color-idle': 'var(--color-neutral-solid)',
'orb-color-offered': 'var(--color-primary-solid)',
'orb-color-planned': 'var(--color-primary-solid)',
'orb-color-reviewing': 'var(--color-risk-solid)',
'orb-color-acting': 'var(--color-primary-solid)',
'orb-color-escalated': 'var(--color-threat-solid)',
'orb-halo-alpha': 'var(--primitive-neutral-a5)',
// ── Custody ring ─────────────────────────────────────────────────
// The ring is the orb's FRAME (it wraps the Indicator), so its box is
// a multiple of the orb size — one axis scales both. The stroke is
// where the state becomes SHAPE; the tint comes from the orb's role
// slot above (one source, never a second colour to keep in sync).
'ring-scale': '1.7',
'ring-track-fg': 'var(--color-border-default)',
'ring-track-width': 'var(--border-width-thin)',
'ring-arc-width': 'var(--border-width-medium)',
'ring-halo-width': 'var(--border-width-thin)',
'ring-halo-opacity': 'var(--opacity-50)',
// The Status is two lines: the state carries the weight (it is the
// answer to «¿quién actúa?»), the detail recedes.
'status-fg': 'var(--color-content-primary)',
'status-weight': 'var(--font-weight-medium)',
'status-gap': 'var(--space-0-5)',
'status-detail-fg': 'var(--color-content-secondary)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)'
},
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)',
// NO block padding: the chip's height is a CONTROL height
// (`--control-height-{size}` via `min-block-size`), not a padded line of
// text. Keeping these as tokens nobody reads is what the orphan guard
// exists to catch — and having two ways to set the same height is how `md`
// ended up meaning 30px in a chip and 36px in every control.
// ── 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.
// The chip's HEIGHT rides the control scale — the badge sizes from the
// control token so `md` means one thing whether or not the chip composes a
// real control (the removable ✕ is an IconButton; measured, the same `md`
// badge came out 30px without it and 46px with it). The scale was a
// private reading `--control-height-*` straight, so no theme could move
// the chip without moving every control in the app. Through the contract
// the value goes to the canonical BUNDLE coordinate, not the raw
// primitive: `recipe-css-contract` caught the verbatim copy the moment it
// entered (theming/reference §5) — the same trap that bit `code-block`.
//
// The NAME is `min-height`, not `min-block-size`: the property is logical
// but the vocabulary's slot for it is `height` (recipe-contract §1, and
// the census maps `min-block-size → height` itself). Twenty-four
// components size by `…-height-{k}`; the root-level twin with this exact
// shape — per size, over `--size-{k}-control-height` — is
// `proof-of-human.min-height-{k}`. Shipped as `min-block-size-*` for a
// few hours on 2026-08-23 and renamed the same day.
'min-height-xs': 'var(--size-xs-control-height)',
'min-height-sm': 'var(--size-sm-control-height)',
'min-height-md': 'var(--size-md-control-height)',
'min-height-lg': 'var(--size-lg-control-height)',
'min-height': {
declarations: [
{ value: 'var(--badge-min-height-md)', scope: 'host' },
{ value: 'var(--badge-min-height-xs)', scope: 'size:xs' },
{ value: 'var(--badge-min-height-sm)', scope: 'size:sm' },
{ value: 'var(--badge-min-height-lg)', scope: 'size:lg' }
]
},
// The dot's shape, the pair `dot-size` was missing. Round is the default,
// not the definition: `knob.indicator-radius` is `--radius-sm`, so the
// catalogue already treats a marker's radius as a knob.
'dot-radius': 'var(--radius-full)',
'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' }
]
}
},
barcode: {
// FIXED-TONE by construction: a barcode cannot invert in dark mode and
// stay scannable, so the default ink/quiet-zone are theme-independent.
// Theming is opt-in through the `color` / `background` props.
fg: '#18181b' /* literal: barcode ink must stay dark for any theme */,
bg: '#ffffff' /* literal: the quiet zone must stay true white */,
radius: 'var(--radius-md)',
// Human-readable interpretation. The standard's HRI face is OCR-B, a
// fixed-width design: the digits sit digit-under-digit over their module
// cells, so a proportional face misaligns them. The system's mono anchor
// is the closest honest equivalent. The size is geometry (an SVG
// attribute proportional to the bar height); family and ink live here.
'text-font-family': 'var(--style-code-font-family)',
'text-letter-spacing': '0',
// Scoped to the host so a `color` override on the instance reaches it —
// at `:root` the reference would freeze at the default (TSC).
'text-fg': {
value: 'var(--barcode-fg)',
scope: 'host'
},
'invalid-border': 'var(--color-border-default)'
},
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)',
fg: 'var(--color-content-secondary)',
'link-radius': 'var(--radius-sm)',
'link-fg': 'var(--color-content-secondary)',
'hover-link-fg': 'var(--color-content-primary)',
'current-link-fg': 'var(--color-content-primary)',
'link-font-weight': 'var(--font-weight-medium)',
'current-link-font-weight': 'var(--font-weight-semibold)',
'separator-fg': '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',
'hover-ellipsis-border': 'var(--color-border-subtle)',
'ellipsis-bg': 'transparent',
'ellipsis-fg': 'var(--color-content-muted)',
'hover-ellipsis-fg': '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-fg': '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-fg': '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 colour routes through the shared `--color-custom` seed
// (`resolveComponentColor` + the palette forward) — no bespoke token.
// ── 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-composer': {
// ── Shell — ONE field-like capsule: attach · input · actions · send
// (WhatsApp/Telegram input pill; stays capsule-sided when autosize
// grows it into a rounded rectangle) ──────────────────────────────
gap: 'var(--space-1-5)',
bg: 'var(--color-surface-default)',
border: 'var(--color-border-default)',
'border-width': 'var(--border-width)',
'focus-border': 'var(--focus-ring-color)',
radius: 'var(--radius-xl)',
'padding-inline': 'var(--space-2)',
'padding-block': 'var(--space-1)',
// ── Input (borderless textarea inside the shell) ──────────────────
// md (16px): matches the message body baseline AND stays at/above the
// 16px floor that stops iOS Safari from auto-zooming the page on focus.
'input-font-size': 'var(--size-md-font-size)',
'input-line-height': 'var(--leading-normal)',
'input-fg': 'var(--color-content-primary)',
'input-placeholder-fg': 'var(--color-content-muted)',
'input-padding-inline': 'var(--space-1)',
'input-padding-block': 'var(--space-1-5)',
// ── Context bar (reply/edit, above the input row) ─────────────────
'context-bg': 'var(--color-neutral-track)',
'context-accent': 'var(--color-primary-solid)',
'context-accent-size': 'var(--border-width-thick)',
'context-font-size': 'var(--size-sm-font-size)',
'context-fg': 'var(--color-content-secondary)',
'context-radius': 'var(--radius-sm)',
'context-padding-inline': 'var(--space-2)',
'context-padding-block': 'var(--space-1)',
'context-gap': 'var(--space-2)',
// ── Attachment tray ───────────────────────────────────────────────
'attachments-gap': 'var(--space-2)',
// ── Send — circular primary action ────────────────────────────────
'send-size': 'var(--size-md-control-height)',
'send-bg': 'var(--color-primary-solid)',
'send-fg': 'var(--color-primary-contrast)',
'send-radius': 'var(--radius-full)',
// ── Motion ────────────────────────────────────────────────────────
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
'chat-list': {
// ── Shell (a vertical list of conversation rows) ──────────────────
bg: 'var(--color-surface-default)',
'padding-inline': 'var(--space-1)',
'padding-block': 'var(--space-1)',
// ── Item (a conversation row; hover/selected come from archetype
// 'item' via the composed Listbox — not re-declared here) ──────────
'item-gap': 'var(--space-2-5)',
'item-padding-inline': 'var(--space-2)',
'item-padding-block': 'var(--space-2)',
'item-radius': 'var(--radius-md)',
'item-row-gap': 'var(--space-0-5)',
// ── Title (conversation name) ─────────────────────────────────────
'title-font-size': 'var(--size-sm-font-size)',
'title-font-weight': 'var(--font-weight-medium)',
'title-fg': 'var(--color-content-primary)',
'title-unread-weight': 'var(--font-weight-bold)',
// ── Preview (last message, truncated) ─────────────────────────────
'preview-font-size': 'var(--size-sm-font-size)',
'preview-fg': 'var(--color-content-muted)',
'preview-unread-fg': 'var(--color-content-secondary)',
// ── Meta (time + unread badge column) ─────────────────────────────
'meta-gap': 'var(--space-1)',
'meta-font-size': 'var(--size-xs-font-size)',
'meta-fg': 'var(--color-content-muted)'
},
'chat-log': {
// ── Shell — a muted canvas so surface bubbles float (reference:
// WhatsApp's tinted wallpaper / Telegram's soft backdrop) ─────────
bg: 'var(--color-surface-muted)',
'padding-inline': 'var(--space-3)',
'padding-block': 'var(--space-2)',
// ── Day separator (sticky pill) ──────────────────────────────────
'separator-gap': 'var(--space-3)',
'separator-font-size': 'var(--size-xs-font-size)',
'separator-fg': 'var(--color-content-muted)',
'separator-bg': 'var(--color-neutral-track)',
'separator-padding-inline': 'var(--space-2-5)',
'separator-padding-block': 'var(--space-0-5)',
'separator-radius': 'var(--radius-full)',
// ── Unread divider ───────────────────────────────────────────────
'divider-fg': 'var(--color-primary-solid)',
'divider-line': 'var(--color-primary-border)',
'divider-font-size': 'var(--size-xs-font-size)',
'divider-gap': 'var(--space-2)',
// ── System event row ─────────────────────────────────────────────
'event-font-size': 'var(--size-sm-font-size)',
'event-fg': 'var(--color-content-muted)',
'event-padding-block': 'var(--space-1)',
// ── Floating pill ("N new messages") ─────────────────────────────
'pill-bg': 'var(--color-primary-solid)',
'pill-fg': 'var(--color-primary-contrast)',
'pill-font-size': 'var(--size-sm-font-size)',
'pill-padding-inline': 'var(--space-3)',
'pill-height': 'var(--size-sm-control-height)',
'pill-radius': 'var(--radius-full)',
'pill-shadow': 'var(--shadow-3)',
'pill-offset': 'var(--space-3)',
// ── To-latest button ─────────────────────────────────────────────
'to-latest-size': 'var(--size-md-control-height)',
'to-latest-bg': 'var(--color-neutral-element)',
'to-latest-fg': 'var(--color-content-primary)',
'to-latest-border': 'var(--color-neutral-border)',
'to-latest-shadow': 'var(--shadow-2)',
'to-latest-offset': 'var(--space-3)'
},
'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)',
// The "tail" corner — the bottom corner facing the margin on the
// LAST bubble of a run (the WhatsApp/Telegram anchor cue).
'bubble-tail-radius': 'var(--radius-sm)',
'bubble-max-inline-size': 'min(var(--measure-narrow), 72%)',
// Canonical size contract: `md` IS the system md (16px). Density in
// chat comes from line-height + padding, NOT from down-shifting the
// scale (that silently makes size="md" render sm, a contract break).
'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-normal)',
// Reference palette (WhatsApp/Telegram): OWN messages are a SOFT
// role tint with normal text — never the saturated solid — and PEER
// messages are plain surface cards with a hairline shadow, floating
// over the log's muted canvas.
'bubble-bg-in': 'var(--color-surface-default)',
'bubble-fg-in': 'var(--color-content-primary)',
'bubble-bg-out': 'var(--color-primary-element)',
'bubble-fg-out': 'var(--color-content-primary)',
'bubble-shadow': 'var(--shadow-1)',
// ── Header (author name, first of a run — accent-colored) ────────
'header-gap': 'var(--space-2)',
'header-font-size': 'var(--size-sm-font-size)',
'header-font-weight': 'var(--font-weight-medium)',
'header-fg': 'var(--color-primary-text)',
// ── Meta (time + status) ─────────────────────────────────────────
'meta-gap': 'var(--space-1)',
'meta-font-size': 'var(--size-xs-font-size)',
'meta-fg': 'var(--color-content-muted)',
// ── Delivery status indicator ────────────────────────────────────
'status-size': '1em',
'status-fg': 'var(--color-content-muted)',
'read-status-fg': 'var(--color-primary-solid)',
'failed-status-fg': 'var(--color-threat-solid)',
// ── Read receipts (avatar row — decorative) ──────────────────────
'read-by-gap': 'var(--space-1)',
'read-by-font-size': 'var(--size-xs-font-size)',
'read-by-fg': 'var(--color-content-muted)',
'read-by-margin': 'var(--space-0-5)',
// ── Jump-to-message highlight (row flash, fades via transition) ───
// A clearly perceptible soft tint (a2 surface is near-invisible over
// the muted log canvas) — same color-mix idiom as field segment-active.
'emphasize-bg': 'color-mix(in srgb, var(--color-primary-solid) 14%, transparent)',
'emphasize-radius': 'var(--radius-md)',
'emphasize-duration': 'var(--duration-moderate)',
// ── 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 — floating pills lapping the bubble edge ──────
'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-surface-default)',
'reaction-border': 'var(--color-neutral-separator)',
'reaction-shadow': 'var(--shadow-1)',
'reaction-overlap': 'var(--space-1)',
'reaction-pressed-bg': 'var(--color-primary-surface)',
'reaction-pressed-border': 'var(--color-primary-border)',
'reaction-pressed-fg': 'var(--color-primary-text)',
// ── Quick-reaction panel (the tapback bar: big emoji, round cells,
// scale-on-hover — WhatsApp/Telegram/Slack) ──────────────────────
'quick-react-gap': 'var(--space-0-5)',
'quick-react-pad': 'var(--space-1)',
'quick-react-size': 'var(--size-lg-control-height)',
'quick-react-emoji-size': 'var(--size-xl-font-size)',
'quick-react-hover-bg': 'var(--color-neutral-hover)',
'quick-react-hover-scale': '1.18',
// ── Hover actions — one floating pill cluster ────────────────────
'actions-bg': 'var(--color-surface-default)',
'actions-border': 'var(--color-neutral-separator)',
'actions-shadow': 'var(--shadow-2)',
'actions-radius': 'var(--radius-full)',
'actions-padding': 'var(--space-0-5)',
// ── 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)'
},
'chat-typing': {
// ── Row (dots + aggregated label) ─────────────────────────────────
gap: 'var(--space-2)',
'font-size': 'var(--size-sm-font-size)',
fg: 'var(--color-content-muted)',
// Reserved line height — the row keeps its slot while idle so the
// conversation above never shifts when typing starts/stops.
'min-block-size': 'var(--size-sm-control-height)',
// ── Dots ──────────────────────────────────────────────────────────
'dot-size': 'var(--space-1)',
'dot-gap': 'var(--space-0-5)',
'dot-fg': 'var(--color-content-muted)'
},
'emoji-picker': {
// ── Trigger (icon button that opens the picker) ───────────────────
'trigger-size': 'var(--size-md-control-height)',
'trigger-radius': 'var(--radius-md)',
'trigger-font-size': 'var(--size-md-font-size)',
// ── Panel (portaled; Popover provides surface + depth). The picker owns
// its width as a component token (color-picker precedent: a concrete rem
// sized to ~9 emoji columns), passed to the Popover's `width`. ─────────
'panel-width': '23rem',
'panel-gap': 'var(--space-2)',
'header-gap': 'var(--space-2)',
// ── Grid (a scrollable CSS grid of emoji cells) ───────────────────
'grid-gap': 'var(--space-0-5)',
'grid-max-block-size': 'min(50vh, 17rem)',
// ── Option (one emoji cell — hover/selected from archetype 'item') ─
'option-radius': 'var(--radius-md)',
'option-font-size': 'var(--size-lg-font-size)',
// ── Footer (preview + skin-tone selector) ─────────────────────────
'footer-gap': 'var(--space-2)',
'footer-border-fg': 'var(--color-neutral-separator)',
'preview-gap': 'var(--space-2)',
'preview-glyph-size': 'var(--size-xl-font-size)',
'preview-name-font-size': 'var(--size-sm-font-size)',
'preview-name-fg': 'var(--color-content-secondary)',
'preview-min-block-size': 'var(--size-lg-control-height)'
},
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',
// Invalid wins over the active colour across every variant: the border and
// the double focus ring (gap + ring) that marks it.
'invalid-border': 'var(--color-threat-element)',
'invalid-ring': 'var(--color-threat-element)',
'invalid-ring-gap': 'var(--color-surface-default)',
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)',
'border-off': 'var(--color-border-default)',
'hover-border-off': 'var(--color-border-strong)',
'disabled-bg':
'color-mix( in srgb, var(--color-surface-overlay) 78%, var(--color-surface-default) )',
'disabled-border':
'color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) )',
'disabled-opacity': 'var(--opacity-disabled)',
'indicator-fg': 'var(--color-content-on-solid)',
'disabled-indicator-fg':
'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-fg)', 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-fg': 'var(--color-content-primary)',
'disabled-group-label-fg': '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-fg': 'var(--color-content-primary)',
'hover-trigger-fg': '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-fg': '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-fg': '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-fg': '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-fg': 'var(--color-content-primary)',
'invalid-label-fg': 'var(--color-risk-text)',
'disabled-label-fg': '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',
// Side breathing room of the floated label: it sits ON the control's
// border, so the padding is what CUTS the border under the text.
'floating-label-padding-inline': 'var(--space-1)',
'layout-column-gap': 'var(--space-4)',
'required-fg': 'var(--color-risk-text)',
// The optional marker is the required marker's twin: muted, and a weight
// step below the label it trails.
'optional-fg': 'var(--color-content-muted)',
'optional-font-weight': 'var(--font-weight-regular)',
'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)',
'hover-control-border': 'var(--color-border-strong)',
'focus-control-border': 'var(--focus-ring-color)',
'invalid-control-border': '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)',
'disabled-control-bg': 'var(--color-surface-muted)',
'control-fg': 'var(--color-content-primary)',
'control-placeholder-fg': 'var(--color-content-muted)',
'affix-fg': 'var(--color-content-secondary)',
// The icon affordance inside the control (date/time/colour pickers,
// password reveal). ONE flush treatment for every field: no border, no
// shadow, the system's neutral hover tint — never the field's accent.
// Its box is sized from the field font, so the em ratio travels with the
// size axis instead of being re-declared per step.
'control-trigger-size': '1.75em',
'control-trigger-radius': 'var(--radius-sm)',
'control-trigger-fg': 'var(--color-content-secondary)',
'hover-control-trigger-fg': 'var(--color-content-primary)',
'focus-control-trigger-fg': 'var(--color-content-primary)',
// 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-fg': 'var(--color-content-secondary)',
'error-fg': '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)',
fg: '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)',
// Action palette forward (THM-2) — submit/reset tint through the shared
// layer (roles + 33 scales + custom). Host = primary (submit's stamped
// default); `'neutral'` stamps nothing and keeps the base action look.
'_palette-solid': {
parts: ['submit', 'reset'],
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
},
'_palette-solid-hover': {
parts: ['submit', 'reset'],
declarations: [{ value: 'var(--color-primary-solid-hover)', scope: 'host' }]
},
'_palette-border': {
parts: ['submit', 'reset'],
declarations: [{ value: 'var(--color-primary-border)', scope: 'host' }]
},
'_palette-contrast': {
parts: ['submit', 'reset'],
declarations: [{ value: 'var(--color-primary-contrast)', scope: 'host' }]
},
'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: {
// El disparador por defecto — el mismo cromo que el de popover, y como allí
// `min-block-size` lee el BUNDLE de talla y no el primitivo crudo que el CSS
// traía. Su hover comparte valor con el estado abierto de popover; aquí no
// hay estado abierto que teñir, así que una sola clave.
'trigger-gap': 'var(--space-2)',
'trigger-min-block-size': 'var(--size-md-control-height)',
'trigger-padding-inline': 'var(--space-3)',
'trigger-border-width': 'var(--border-width)',
'trigger-border': 'var(--color-border-default)',
'trigger-radius': 'var(--radius-md)',
'trigger-bg': 'var(--color-surface-raised)',
'trigger-fg': 'var(--color-content-primary)',
'trigger-font-weight': 'var(--font-weight-medium)',
'hover-trigger-bg': 'var(--color-surface-overlay)',
'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)',
'active-handle-bg': '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-fg': '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-fg':
'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-fg': '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-fg': '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)',
// The shimmering bar itself: the system gradient, named here so a theme can
// retune the skeleton without touching every recipe that shimmers.
'skeleton-bg': 'var(--gradient-shimmer)',
'shimmer-duration': '1.8s',
// Placeholder palette forward (THM-2) — `placeholderColor` tints the
// colour block via the shared layer (roles + 33 scales + custom).
// Colored-only: `'neutral'` (the default) stamps no `data-color`, so
// the host value here is the forward fallback only.
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
// ── Fallback typography (for text-only fallback content) ─────────
'font-family': 'var(--style-label-font-family)',
'fallback-fg': '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-fg': '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)',
'hover-control-border': 'var(--color-border-strong)',
'active-control-border': 'var(--color-primary-border)',
'control-bg': 'transparent',
// Higher-contrast hover/focus fill than the field surface.
'focus-control-bg': 'var(--color-neutral-hover)',
'active-control-bg': 'var(--color-primary-track)',
'control-fg': 'var(--color-content-secondary)',
'hover-control-fg': 'var(--color-content-primary)',
'active-control-fg': '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': {
// Las dos variantes que ajustan desde el default elevado, y los botones de
// la barra. El asa de redimensionado leía
// `var(--color-content-tertiary, …)`: ese primitivo NO EXISTE —pintaba el
// respaldo—, así que el contrato guarda lo que de verdad pintaba.
'outline-bg': 'var(--color-surface-default)',
'outline-border': 'var(--color-border-strong)',
'ghost-bg': 'var(--color-surface-default)',
'ghost-border': 'var(--color-border-subtle)',
'ghost-shadow': 'var(--shadow-2)',
'action-radius': 'var(--radius-sm)',
'action-fg': 'var(--color-content-secondary)',
'action-font-size': 'var(--size-md-font-size)',
'hover-action-bg': 'var(--color-surface-muted)',
'hover-action-fg': 'var(--color-content-primary)',
'resize-grip-fg': 'var(--color-content-secondary)',
'resize-grip-radius': 'var(--radius-sm)',
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.
'active-shadow': 'var(--shadow-6)',
fg: '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-fg': '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)',
// Accent palette forward (THM-2) — `color` tints the header rule, focus
// ring and resize grip via the shared layer (roles + 33 scales + custom)
// on the `content` part (the panel portals; it stamps its own
// `data-color`). Host = neutral-border, the stamped default's value.
'_palette-border': {
parts: ['content'],
declarations: [{ value: 'var(--color-neutral-border)', scope: 'host' }]
},
'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-fg': '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-fg': '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-fg': '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)',
// The zoom buttons' own chrome. `font-size` is the SEAM: the value is the
// system's type scale, the knob is this component's, so a theme can size
// the ± glyphs without moving `md` text everywhere.
'zoom-btn-radius': 'var(--radius-md)',
'zoom-btn-font-size': 'var(--size-md-font-size)',
'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-fg': 'var(--color-content-muted)',
'literal-fg': 'var(--color-content-muted)',
'segment-radius': 'var(--radius-sm)',
'segment-fg': 'var(--color-content-primary)',
'segment-invalid-fg': 'var(--color-risk-text)',
'segment-readonly-bg': 'var(--color-surface-overlay)',
'segment-readonly-fg': '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-fg': 'var(--color-content-muted)',
'literal-fg': 'var(--color-content-muted)',
'segment-radius': 'var(--radius-sm)',
'segment-fg': 'var(--color-content-primary)',
'segment-invalid-fg': 'var(--color-risk-text)',
'segment-readonly-bg': 'var(--color-surface-overlay)',
'segment-readonly-fg': '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-fg': 'var(--color-content-muted)',
'literal-fg': 'var(--color-content-muted)',
'segment-radius': 'var(--radius-sm)',
'segment-fg': 'var(--color-content-primary)',
'segment-invalid-fg': 'var(--color-risk-text)',
'segment-readonly-bg': 'var(--color-surface-overlay)',
'segment-readonly-fg': '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-fg': 'var(--color-content-secondary)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)',
// The channel SEPARATORS (",", " / ") render as thin vertical divider
// lines between the editable segments — a rule of the recipe's own, so
// its ink and its thickness are knobs. `1px` IS `--border-width`: the
// divider is a hairline, and `dropdown-menu.separator-thickness` reads
// the same family. (The format's prefix / suffix GLYPHS, "#" and "%",
// are the same part rendered as real text and take `literal-fg`.)
'separator-bg': 'var(--color-border-default)',
'separator-thickness': 'var(--border-width)',
// A segment holds a FIXED number of characters so the field never
// reflows while typing: 3 for a channel (RGB 0-255 · HSL 0-360/0-100 ·
// alpha) and 6 for hex (RRGGBB). Font-relative on purpose — the width
// has to follow the face the theme picks. The twins (`date-field` /
// `time-field`) carry the same fact inside a `calc(2ch + padding)`, so
// theirs already rode a token; this one was a bare literal.
'segment-min-width': '3ch',
'hex-segment-min-width': '6ch'
},
'color-picker': {
// ── La costura (2026-08-22) ──────────────────────────────────────────
// El cromo que montaba sobre primitivos crudos. Los valores son los del
// canon; el knob pasa a ser del componente (PLAN §2-A).
gap: 'var(--space-1-5)',
'control-gap': 'var(--space-2)',
'label-fg': 'var(--color-content-primary)',
'trigger-font-family': 'var(--style-label-font-family)',
'trigger-line-height': 'var(--leading-ui)',
// La lectura del valor es metadato: mono, regular, y su propia tinta.
'value-font-family': 'var(--font-family-mono)',
'value-font-weight': 'var(--font-weight-regular)',
'value-fg': 'var(--color-content-primary)',
'value-line-height': 'var(--leading-ui)',
'swatch-border-width': 'var(--border-width)',
// El raíl de canal y su pulgar: el pulgar lleva DOS sombras que hacen una
// sola cosa (contorno + elevación), así que es un token, no dos.
'channel-track-border-width': 'var(--border-width)',
'channel-track-border': 'var(--color-border-subtle)',
'channel-thumb-border-width': 'var(--border-width-medium)',
'channel-thumb-border': 'var(--color-surface-default)',
'channel-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)',
// El hueco entre la muestra y su anillo de selección: es superficie, y por
// eso se llama por lo que hace y no por el color que lleva hoy.
'swatch-ring-gap': 'var(--color-surface-default)',
'eye-dropper-border-width': 'var(--border-width)',
// 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-fg': '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-fg': 'var(--color-content-secondary)',
'eye-dropper-hover-fg': '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)',
// Accent palette forward (THM-2) — `color` tints the trigger focus
// border and the portaled content's focus rings. The content portals
// OUTSIDE the root, so both parts stamp `data-color` themselves and the
// forward targets them; roles + 33 scales + custom route through the
// shared layer. Host = primary (the previous hard-coded accent).
'_palette-border': {
parts: ['trigger', 'content'],
declarations: [{ value: 'var(--color-primary-border)', scope: 'host' }]
}
},
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)',
// The pill step of the SAME scale: `data-rounded='full'` read the global
// primitive straight, so the one shape a theme is most likely to retune was
// the only step outside the contract.
'radius-full': 'var(--radius-full)',
'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: {
// Row height per size - 1:1 with the bundle, measured. NOTE the name: the
// value is the ROW's height (the centred content of a cell), not the
// root's; the old private was called `root-*` and named the wrong thing.
'row-height-xs': 'var(--size-xs-control-height)',
'row-height-sm': 'var(--size-sm-control-height)',
'row-height-md': 'var(--size-md-control-height)',
'row-height-lg': 'var(--size-lg-control-height)',
'row-height-xl': 'var(--size-xl-control-height)',
// Cell padding - the table keeps its OWN rhythm, denser than the control
// bundle at xs and looser from md up. Verbatim on purpose: a data grid
// packs rows tighter than a control stack, and forcing the bundle here
// would change the default. (Spacing is outside the bundle guard, which
// only governs control-height / font-size / icon-size.)
'cell-padding-inline-xs': 'var(--space-2)',
'cell-padding-inline-sm': 'var(--space-2)',
'cell-padding-inline-md': 'var(--space-3)',
'cell-padding-inline-lg': 'var(--space-4)',
'cell-padding-inline-xl': 'var(--space-5)',
'cell-padding-block-xs': 'var(--space-1)',
'cell-padding-block-sm': 'var(--space-1)',
'cell-padding-block-md': 'var(--space-2)',
'cell-padding-block-lg': 'var(--space-3)',
'cell-padding-block-xl': 'var(--space-3)',
// Type. The table reads ONE STEP BELOW the 1:1 from md up - a grid is read
// in bulk, not glanced at. The coordinate names that step explicitly, so
// the deviation is legible instead of hidden in a raw primitive.
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-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)',
// The RESOLVED names. `data-size` is stamped on `[data-table-root]`, so the
// per-size cascade targets that part, not the host.
'row-height': {
parts: ['root'],
declarations: [
{ value: 'var(--table-row-height-md)', scope: 'host' },
{ value: 'var(--table-row-height-xs)', scope: 'size:xs' },
{ value: 'var(--table-row-height-sm)', scope: 'size:sm' },
{ value: 'var(--table-row-height-lg)', scope: 'size:lg' },
{ value: 'var(--table-row-height-xl)', scope: 'size:xl' }
]
},
'cell-padding-inline': {
parts: ['root'],
declarations: [
{ value: 'var(--table-cell-padding-inline-md)', scope: 'host' },
{ value: 'var(--table-cell-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--table-cell-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--table-cell-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--table-cell-padding-inline-xl)', scope: 'size:xl' }
]
},
'cell-padding-block': {
parts: ['root'],
declarations: [
{ value: 'var(--table-cell-padding-block-md)', scope: 'host' },
{ value: 'var(--table-cell-padding-block-xs)', scope: 'size:xs' },
{ value: 'var(--table-cell-padding-block-sm)', scope: 'size:sm' },
{ value: 'var(--table-cell-padding-block-lg)', scope: 'size:lg' },
{ value: 'var(--table-cell-padding-block-xl)', scope: 'size:xl' }
]
},
'font-size': {
parts: ['root'],
declarations: [
{ value: 'var(--table-font-size-md)', scope: 'host' },
{ value: 'var(--table-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--table-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--table-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--table-font-size-xl)', scope: 'size:xl' }
]
},
// Chassis. No `root-` prefix: the wrapper IS the component, so the token is
// named by its axis - recipe-contract §1 does not repeat the part when the
// part is the component itself.
radius: 'var(--radius-md)',
'font-family': 'var(--style-label-font-family)',
fg: 'var(--color-content-primary)',
'line-height': 'var(--leading-ui)',
'border-width': 'var(--border-width)',
divider: 'var(--color-border-default)',
// Variants. `surface` carries the card chrome; `outline` keeps the border
// over nothing; `ghost` has no chrome at all.
'surface-bg': 'var(--color-surface-default)',
'row-bg': 'var(--color-surface-default)',
// Header / footer share one surface.
'header-bg': 'var(--color-surface-muted)',
'header-fg': 'var(--color-content-primary)',
'header-font-weight': 'var(--font-weight-semibold)',
'header-z': '2',
// Row states. The striped and hover tints are the table's own: the row's
// `archetype: 'item'` veil is currently cancelled by the row background
// (see README - the `hoverable` prop depends on it).
'striped-row-bg': 'var(--color-surface-muted)',
'hover-row-bg': 'var(--color-surface-overlay)',
// Row detail (disclosure).
'row-detail-bg': 'var(--color-surface-muted)',
'row-detail-fg': 'var(--color-content-secondary)',
'row-detail-padding-block': 'var(--space-3)',
// Sort trigger + its indicator.
'sort-trigger-gap': 'var(--space-1)',
'sort-trigger-radius': 'var(--radius-sm)',
'sort-indicator-size': '1em',
'sort-indicator-opacity': 'var(--opacity-disabled)',
'_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': {
// ── Per-size coordinates (xs..xl) ──
// Row height is 1:1 with the bundle. Padding and indent are the grid's OWN
// rhythm: denser than the control bundle at xs, looser from md up, because
// a tree is read in bulk. Spacing sits outside the bundle guard, which
// governs control-height / font-size / icon-size only.
'row-height-xs': 'var(--size-xs-control-height)',
'row-height-sm': 'var(--size-sm-control-height)',
'row-height-md': 'var(--size-md-control-height)',
'row-height-lg': 'var(--size-lg-control-height)',
'row-height-xl': 'var(--size-xl-control-height)',
'row-padding-block-xs': 'var(--space-1)',
'row-padding-block-sm': 'var(--space-1)',
'row-padding-block-md': 'var(--space-2)',
'row-padding-block-lg': 'var(--space-3)',
'row-padding-block-xl': 'var(--space-3)',
'row-padding-inline-xs': 'var(--space-2)',
'row-padding-inline-sm': 'var(--space-2)',
'row-padding-inline-md': 'var(--space-3)',
'row-padding-inline-lg': 'var(--space-4)',
'row-padding-inline-xl': 'var(--space-5)',
// The depth step. It is the grid's own axis - no bundle coordinate means
// "one level of nesting".
'indent-xs': 'var(--space-3)',
'indent-sm': 'var(--space-3)',
'indent-md': 'var(--space-4)',
'indent-lg': 'var(--space-5)',
'indent-xl': 'var(--space-6)',
// Type: like `table`, the grid reads ONE STEP BELOW the 1:1 from md up.
// The coordinate names that step instead of hiding it in a primitive.
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-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)',
// The RESOLVED names. `data-size` is stamped on `[data-tree-grid-root]`,
// so the per-size cascade targets that part (precedent: proof-of-human).
'row-height': {
parts: ['root'],
declarations: [
{ value: 'var(--tree-grid-row-height-md)', scope: 'host' },
{ value: 'var(--tree-grid-row-height-xs)', scope: 'size:xs' },
{ value: 'var(--tree-grid-row-height-sm)', scope: 'size:sm' },
{ value: 'var(--tree-grid-row-height-lg)', scope: 'size:lg' },
{ value: 'var(--tree-grid-row-height-xl)', scope: 'size:xl' }
]
},
'row-padding-block': {
parts: ['root'],
declarations: [
{ value: 'var(--tree-grid-row-padding-block-md)', scope: 'host' },
{ value: 'var(--tree-grid-row-padding-block-xs)', scope: 'size:xs' },
{ value: 'var(--tree-grid-row-padding-block-sm)', scope: 'size:sm' },
{ value: 'var(--tree-grid-row-padding-block-lg)', scope: 'size:lg' },
{ value: 'var(--tree-grid-row-padding-block-xl)', scope: 'size:xl' }
]
},
'row-padding-inline': {
parts: ['root'],
declarations: [
{ value: 'var(--tree-grid-row-padding-inline-md)', scope: 'host' },
{ value: 'var(--tree-grid-row-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--tree-grid-row-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--tree-grid-row-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--tree-grid-row-padding-inline-xl)', scope: 'size:xl' }
]
},
indent: {
parts: ['root'],
declarations: [
{ value: 'var(--tree-grid-indent-md)', scope: 'host' },
{ value: 'var(--tree-grid-indent-xs)', scope: 'size:xs' },
{ value: 'var(--tree-grid-indent-sm)', scope: 'size:sm' },
{ value: 'var(--tree-grid-indent-lg)', scope: 'size:lg' },
{ value: 'var(--tree-grid-indent-xl)', scope: 'size:xl' }
]
},
'font-size': {
parts: ['root'],
declarations: [
{ value: 'var(--tree-grid-font-size-md)', scope: 'host' },
{ value: 'var(--tree-grid-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--tree-grid-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--tree-grid-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--tree-grid-font-size-xl)', scope: 'size:xl' }
]
},
// ── Chassis ── no `root-` prefix: the wrapper IS the component.
radius: 'var(--radius-md)',
'font-family': 'var(--style-label-font-family)',
'line-height': 'var(--leading-ui)',
fg: 'var(--color-content-primary)',
'border-width': 'var(--border-width)',
divider: 'var(--color-border-default)',
'surface-bg': 'var(--color-surface-default)',
'row-bg': 'var(--color-surface-default)',
// Header.
'header-bg': 'var(--color-surface-muted)',
'header-font-weight': 'var(--font-weight-semibold)',
'header-z': '2',
// Row states. See the README: the striped tint outweighs the hover, so on
// a banded row the hover is inert - a pre-existing defect, measured.
'striped-row-bg': 'var(--color-surface-muted)',
'hover-row-bg': 'var(--color-surface-overlay)',
'disabled-row-fg': 'var(--color-content-muted)',
// ── Indent guides ──
// ONE colour. The five stacked gradients that draw one line per ancestor
// level are CONSTRUCTION and stay in the recipe - the same split as
// gradient-builder's checkerboard: the colour is the knob, the pattern is
// the recipe's.
'guide-fg': 'var(--color-border-default)',
'guide-width': '1px',
// Branchless rows keep a placeholder so labels align with their siblings.
'branch-placeholder-size': '1em',
'_palette-element': {
parts: ['root'],
declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }]
}
},
'tree-view': {
// ── Per-size coordinates (xs..xl) ──
// Row height is 1:1 with the bundle. Padding and indent are the tree's OWN
// rhythm (spacing sits outside the bundle guard, which governs
// control-height / font-size / icon-size only) — same split as `tree-grid`.
'row-height-xs': 'var(--size-xs-control-height)',
'row-height-sm': 'var(--size-sm-control-height)',
'row-height-md': 'var(--size-md-control-height)',
'row-height-lg': 'var(--size-lg-control-height)',
'row-height-xl': 'var(--size-xl-control-height)',
// xs collapses the block padding to zero: at that size the row height
// alone carries the rhythm.
'row-padding-block-xs': '0',
'row-padding-block-sm': 'var(--space-1)',
'row-padding-block-md': 'var(--space-1)',
'row-padding-block-lg': 'var(--space-1)',
'row-padding-block-xl': 'var(--space-1)',
'row-padding-inline-xs': 'var(--space-1)',
'row-padding-inline-sm': 'var(--space-2)',
'row-padding-inline-md': 'var(--space-2)',
'row-padding-inline-lg': 'var(--space-3)',
'row-padding-inline-xl': 'var(--space-4)',
// The depth step — the tree's own axis, multiplied by soma's
// `--tree-depth`. It is NOT the row's inline padding: the two add up in
// one calc, which is why the generated proposal fused them into a single
// name and why they must stay apart.
'indent-xs': 'var(--space-3)',
'indent-sm': 'var(--space-3)',
'indent-md': 'var(--space-4)',
'indent-lg': 'var(--space-5)',
'indent-xl': 'var(--space-6)',
// Type: like `table` and `tree-grid`, one step below the 1:1 from lg up.
// The coordinate names that step instead of hiding it in a primitive.
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-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)',
// The RESOLVED names. `data-size` is stamped on `[data-tree-view-root]` —
// an eidos wrapper, not a morfo part — so the per-size cascade targets
// that part (precedent: tree-grid, proof-of-human).
'row-height': {
parts: ['root'],
declarations: [
{ value: 'var(--tree-view-row-height-md)', scope: 'host' },
{ value: 'var(--tree-view-row-height-xs)', scope: 'size:xs' },
{ value: 'var(--tree-view-row-height-sm)', scope: 'size:sm' },
{ value: 'var(--tree-view-row-height-lg)', scope: 'size:lg' },
{ value: 'var(--tree-view-row-height-xl)', scope: 'size:xl' }
]
},
'row-padding-block': {
parts: ['root'],
declarations: [
{ value: 'var(--tree-view-row-padding-block-md)', scope: 'host' },
{ value: 'var(--tree-view-row-padding-block-xs)', scope: 'size:xs' },
{ value: 'var(--tree-view-row-padding-block-sm)', scope: 'size:sm' },
{ value: 'var(--tree-view-row-padding-block-lg)', scope: 'size:lg' },
{ value: 'var(--tree-view-row-padding-block-xl)', scope: 'size:xl' }
]
},
'row-padding-inline': {
parts: ['root'],
declarations: [
{ value: 'var(--tree-view-row-padding-inline-md)', scope: 'host' },
{ value: 'var(--tree-view-row-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--tree-view-row-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--tree-view-row-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--tree-view-row-padding-inline-xl)', scope: 'size:xl' }
]
},
indent: {
parts: ['root'],
declarations: [
{ value: 'var(--tree-view-indent-md)', scope: 'host' },
{ value: 'var(--tree-view-indent-xs)', scope: 'size:xs' },
{ value: 'var(--tree-view-indent-sm)', scope: 'size:sm' },
{ value: 'var(--tree-view-indent-lg)', scope: 'size:lg' },
{ value: 'var(--tree-view-indent-xl)', scope: 'size:xl' }
]
},
'font-size': {
parts: ['root'],
declarations: [
{ value: 'var(--tree-view-font-size-md)', scope: 'host' },
{ value: 'var(--tree-view-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--tree-view-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--tree-view-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--tree-view-font-size-xl)', scope: 'size:xl' }
]
},
// ── Chassis ── no `root-` prefix: the wrapper IS the component.
radius: 'var(--radius-md)',
'font-family': 'var(--style-label-font-family)',
'line-height': 'var(--leading-ui)',
fg: 'var(--color-content-primary)',
'border-width': 'var(--border-width)',
// Shared by the surface and outline variants. Ghost's `transparent` is
// variant IDENTITY, not a knob, and stays literal.
border: 'var(--color-border-default)',
'surface-bg': 'var(--color-surface-default)',
// Surface and outline pad alike; ghost's `0` is identity.
padding: 'var(--space-2)',
// ── Rows ── one rule paints BOTH `branch-control` and `item`.
'row-gap': 'var(--space-2)',
'row-radius': 'var(--radius-sm)',
// Coined on purpose, unlike grid-list's: `branch-control` carries NO
// archetype, so on that node the recipe's plane is the ONLY paint (veil
// measured absent) and this token is the hover's whole theming surface.
// On `[data-tree-view-item]` the system veil stacks on top; see the
// README and next-features §12 for the branch-veil spill.
'hover-row-bg': 'var(--color-surface-overlay)',
'disabled-row-fg': 'var(--color-content-muted)',
// ── BranchIndicator (chevron) ──
'branch-indicator-size': '1em',
'branch-indicator-fg': 'var(--color-content-secondary)',
// ── Indent guides ──
// The generated proposal called these `root-width` / `root-bg`: they are
// neither. The painted node is the branch's `::before`, a vertical line.
// Same names as tree-grid's, for the same thing.
'guide-width': '1px',
'guide-fg': 'var(--color-border-default)',
'_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: {
// ── Per-size coordinates (xs..xl) ──
// The feed types BELOW the 1:1 from md up (`font-size-md` reads the sm
// coordinate): a stream is read in bulk, like `table` and `tree-grid`.
// The coordinate names that step instead of hiding it in a primitive.
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-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)',
'article-padding-block-xs': 'var(--space-2)',
'article-padding-block-sm': 'var(--space-2)',
'article-padding-block-md': 'var(--space-3)',
'article-padding-block-lg': 'var(--space-4)',
'article-padding-block-xl': 'var(--space-5)',
'article-padding-inline-xs': 'var(--space-3)',
'article-padding-inline-sm': 'var(--space-3)',
'article-padding-inline-md': 'var(--space-4)',
'article-padding-inline-lg': 'var(--space-5)',
'article-padding-inline-xl': 'var(--space-6)',
// How far a nested thread steps in from its parent.
'thread-indent-xs': 'var(--space-4)',
'thread-indent-sm': 'var(--space-4)',
'thread-indent-md': 'var(--space-5)',
'thread-indent-lg': 'var(--space-6)',
'thread-indent-xl': 'var(--space-7)',
// The RESOLVED names. `data-size` is stamped on `[data-feed-root]`.
'font-size': {
parts: ['root'],
declarations: [
{ value: 'var(--feed-font-size-md)', scope: 'host' },
{ value: 'var(--feed-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--feed-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--feed-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--feed-font-size-xl)', scope: 'size:xl' }
]
},
'article-padding-block': {
parts: ['root'],
declarations: [
{ value: 'var(--feed-article-padding-block-md)', scope: 'host' },
{ value: 'var(--feed-article-padding-block-xs)', scope: 'size:xs' },
{ value: 'var(--feed-article-padding-block-sm)', scope: 'size:sm' },
{ value: 'var(--feed-article-padding-block-lg)', scope: 'size:lg' },
{ value: 'var(--feed-article-padding-block-xl)', scope: 'size:xl' }
]
},
'article-padding-inline': {
parts: ['root'],
declarations: [
{ value: 'var(--feed-article-padding-inline-md)', scope: 'host' },
{ value: 'var(--feed-article-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--feed-article-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--feed-article-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--feed-article-padding-inline-xl)', scope: 'size:xl' }
]
},
'thread-indent': {
parts: ['root'],
declarations: [
{ value: 'var(--feed-thread-indent-md)', scope: 'host' },
{ value: 'var(--feed-thread-indent-xs)', scope: 'size:xs' },
{ value: 'var(--feed-thread-indent-sm)', scope: 'size:sm' },
{ value: 'var(--feed-thread-indent-lg)', scope: 'size:lg' },
{ value: 'var(--feed-thread-indent-xl)', scope: 'size:xl' }
]
},
// ── Chassis ──
radius: 'var(--radius-md)',
'font-family': 'var(--style-label-font-family)',
'line-height': 'var(--leading-ui)',
fg: 'var(--color-content-primary)',
'border-width': 'var(--border-width)',
divider: 'var(--color-border-default)',
gap: 'var(--space-2)',
'surface-bg': 'var(--color-surface-default)',
// ── Article ──
'article-gap': 'var(--space-1)',
'article-title-fg': 'var(--color-content-primary)',
'article-title-font-weight': 'var(--font-weight-semibold)',
// The title steps by HEADING LEVEL, not by the component's size - the
// audit read these four as a size collision, and they are `aria-level`
// 3..6. A deeper heading is smaller because it is deeper.
'article-title-font-size-3': 'var(--size-md-font-size)',
'article-title-font-size-4': 'var(--size-sm-font-size)',
'article-title-font-size-5': 'var(--size-sm-font-size)',
'article-title-font-size-6': 'var(--size-xs-font-size)',
'article-description-fg': 'var(--color-content-secondary)',
// ── Thread ──
'thread-margin-block-start': 'var(--space-2)',
'thread-rail-width': 'var(--border-width-medium)',
// ── Sentinel ──
// TWO different measures, which the audit flagged as one ⚠: the probe row
// keeps a minimum height so the observer has something to intersect, and
// the spinner inside it has its own diameter.
'sentinel-min-block-size': 'var(--space-6)',
'sentinel-fg': 'var(--color-content-secondary)',
'sentinel-font-size': 'var(--size-xs-font-size)',
'sentinel-spinner-size': '0.75rem',
'sentinel-spinner-width': 'var(--border-width-medium)',
'sentinel-spinner-gap': 'var(--space-2)',
'sentinel-spinner-duration': '0.7s',
'sentinel-spinner-reduced-duration': '4s',
'_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': {
// ── Per-size coordinates (xs..xl) ──
// Row height is 1:1 with the bundle (`--size-{k}-control-height` IS
// `--control-height-{k}`, verified in generated/base.css). Padding keeps
// the list's OWN rhythm — spacing sits outside the bundle guard, which
// governs control-height / font-size / icon-size only.
'row-height-xs': 'var(--size-xs-control-height)',
'row-height-sm': 'var(--size-sm-control-height)',
'row-height-md': 'var(--size-md-control-height)',
'row-height-lg': 'var(--size-lg-control-height)',
'row-height-xl': 'var(--size-xl-control-height)',
'row-padding-inline-xs': 'var(--space-2)',
'row-padding-inline-sm': 'var(--space-2)',
'row-padding-inline-md': 'var(--space-3)',
'row-padding-inline-lg': 'var(--space-4)',
'row-padding-inline-xl': 'var(--space-5)',
// Type: like `table` and `tree-grid`, the list reads ONE STEP BELOW the
// 1:1 from md up — a list of rows is read in bulk, not glanced at. The
// coordinate names that step instead of hiding it in a raw primitive.
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-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)',
// The RESOLVED names — the only ones the recipe reads. `data-size` is
// stamped on the PROVIDER (the eidos wrapper's only root), so the
// per-size cascade hangs from the host and needs no `parts` (precedent:
// command, not table/tree-grid — those stamp on a `root` part).
'row-height': {
declarations: [
{ value: 'var(--grid-list-row-height-md)', scope: 'host' },
{ value: 'var(--grid-list-row-height-xs)', scope: 'size:xs' },
{ value: 'var(--grid-list-row-height-sm)', scope: 'size:sm' },
{ value: 'var(--grid-list-row-height-lg)', scope: 'size:lg' },
{ value: 'var(--grid-list-row-height-xl)', scope: 'size:xl' }
]
},
'row-padding-inline': {
declarations: [
{ value: 'var(--grid-list-row-padding-inline-md)', scope: 'host' },
{ value: 'var(--grid-list-row-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--grid-list-row-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--grid-list-row-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--grid-list-row-padding-inline-xl)', scope: 'size:xl' }
]
},
'font-size': {
declarations: [
{ value: 'var(--grid-list-font-size-md)', scope: 'host' },
{ value: 'var(--grid-list-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--grid-list-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--grid-list-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--grid-list-font-size-xl)', scope: 'size:xl' }
]
},
// ── Chassis ── no `root-` prefix: the wrapper IS the component.
gap: '1px',
padding: 'var(--space-1)',
'border-width': 'var(--border-width)',
border: 'var(--color-border-default)',
radius: 'var(--radius-md)',
bg: 'var(--color-surface-default)',
fg: 'var(--color-content-primary)',
'font-family': 'var(--style-label-font-family)',
'line-height': 'var(--leading-ui)',
'max-block': 'min(60vh, 22rem)',
'invalid-border': 'var(--color-threat-element)',
// ── Row ──
// NO `hover-row-bg`: the row carries `archetype: 'item'`, so the system
// state layer paints it (full specificity, by design) and this recipe's
// own plane loses. Measured 2026-08-21 — the hover paints TWICE on the
// same node. Coining a token here would bless a condemned duplicate
// (same call as listbox.highlighted). Registered in next-features §12.
'row-gap': 'var(--space-2)',
'row-padding-block': 'var(--space-1)',
'row-radius': 'var(--radius-sm)',
'row-fg': 'var(--color-content-primary)',
'disabled-row-fg': 'var(--color-content-muted)',
// ── SelectionCheckbox ──
// It MIRRORS the menu-indicator visual (empty outlined square → filled
// accent + glyph); it does not consume that layer — no selector of
// `lib/menu-indicator.css` targets grid-list. Its border width is the
// chassis' `border-width`: one border weight per component.
'selection-checkbox-size': '0.875rem',
'selection-checkbox-border': 'var(--color-border-default)',
'selection-checkbox-radius': 'var(--radius-sm)',
'checked-selection-checkbox-fg': 'var(--color-content-on-solid)',
// The accent the CHECKED checkbox fills with. The row's own
// `_palette-element` forward was retired 2026-08-21: its only consumer
// selected `[data-grid-list-row][data-selected]`, an attribute neither
// the morfo declares nor soma stamps (it stamps `data-state='selected'`
// + `aria-selected`), so the rule never matched and the forward fed
// nothing. A selected row reads through the archetype's neutral veil;
// repairing that moves pixels and awaits signature (next-features §13).
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' }
},
// ─────────────────────────────────────────────────────────────────────
// DateRangePicker — only the DECADE / MONTH view is its own invention; the
// calendar half borrows `--calendar-*` (that is the calendar-family decision
// in theming-audit §B, not this axis) and the field half borrows `--field-*`
// (the field-composition mandate). Theme-reach 2026-08-20.
'date-range-picker': {
'accent-border': 'var(--color-primary-border)',
'accent-track': 'var(--color-primary-track)',
'accent-text': 'var(--color-primary-text)',
'overview-gap': 'var(--space-3)',
'overview-width': 'min(20rem, 100%)',
'overview-header-gap': 'var(--space-2)',
'overview-heading-fg': 'var(--color-content-primary)',
'overview-heading-font-weight': 'var(--font-weight-semibold)',
'overview-nav-size': 'var(--size-sm-control-height)',
'overview-nav-radius': 'var(--radius-md)',
'overview-nav-fg': 'var(--color-content-primary)',
'overview-nav-font-size': 'var(--size-md-font-size)',
'overview-grid-gap': 'var(--space-2)',
'overview-cell-height': 'var(--size-md-control-height)',
'overview-cell-radius': 'var(--radius-md)',
'overview-cell-fg': 'var(--color-content-primary)',
'overview-cell-font-size': 'var(--size-sm-font-size)',
'in-range-overview-cell-bg': 'var(--color-primary-track)',
'in-range-overview-cell-fg': 'var(--color-primary-text)',
'endpoint-overview-cell-bg': 'var(--color-primary-solid)',
'endpoint-overview-cell-fg': 'var(--color-content-on-solid)',
'endpoint-overview-cell-border': 'var(--color-primary-border)'
},
// ─────────────────────────────────────────────────────────────────────
// TimeRangePicker — same split as `time-picker`: the two-column clock and
// the AM/PM strip are its own, the field-shaped half stays on `--field-*`
// (it composes Field). Theme-reach 2026-08-20.
'time-range-picker': {
'accent-border': 'var(--color-primary-border)',
'accent-track': 'var(--color-primary-track)',
'accent-text': 'var(--color-primary-text)',
'clock-column-gap': 'var(--space-10)',
'clock-width': '22rem',
'clock-fg': 'var(--color-content-primary)',
'clock-font-family': 'var(--style-label-font-family)',
'clock-font-size': 'var(--size-sm-font-size)',
'clock-line-height': 'var(--leading-ui)',
'clock-hand-hour': 'var(--color-primary-border)',
'clock-hand-minute': 'var(--color-secondary-border)',
'clock-hand-second': 'var(--color-neutral-border)',
'day-period-toggle-gap': 'var(--space-1)',
'day-period-toggle-radius': 'var(--radius-md)',
'day-period-toggle-bg': 'var(--color-surface-default)',
'day-period-toggle-border': 'var(--color-border-default)',
'day-period-item-min-width': '2.25rem',
'day-period-item-padding-inline': 'var(--space-2)',
'day-period-item-padding-block': 'var(--space-1)',
'day-period-item-font-size': 'var(--size-xs-font-size)',
'day-period-item-font-weight': 'var(--font-weight-semibold)',
'day-period-item-fg': 'var(--color-content-muted)'
},
// ─────────────────────────────────────────────────────────────────────
// TimePicker — the CLOCK face and the day-period strip, which are its own
// invention; the field-shaped half keeps consuming `--field-*` because it
// composes Field (the field-composition mandate, theming-audit §5.3-3), and
// the accent that used to hide behind a `--_time-field-*` prefix is public
// now. Theme-reach 2026-08-20.
'time-picker': {
'accent-border': 'var(--color-primary-border)',
'accent-track': 'var(--color-primary-track)',
'accent-text': 'var(--color-primary-text)',
gap: 'var(--space-1-5)',
'clock-gap': 'var(--space-2)',
'clock-width': '14rem',
'clock-fg': 'var(--color-content-primary)',
'clock-font-family': 'var(--style-label-font-family)',
'clock-font-size': 'var(--size-sm-font-size)',
'clock-line-height': 'var(--leading-ui)',
'clock-hand-hour': 'var(--color-primary-border)',
'clock-hand-minute': 'var(--color-secondary-border)',
'clock-hand-second': 'var(--color-neutral-border)',
'clock-thumb-shadow': 'var(--slider-thumb-shadow)',
'clock-thumb-ring': 'var(--color-surface-default)',
'trigger-radius': 'var(--radius-sm)'
},
// ─────────────────────────────────────────────────────────────────────
// NaturalTimePicker — the daylight band and its readout. The PANEL keeps
// borrowing `--popover-*`: its surface IS the canonical float (documented in
// the recipe header), so aliasing it under a private prefix would be the
// parallel vocabulary the contract forbids. The sky palette stays a set of
// annotated literals — the fixed-tone exception the recipe guard already
// carries (`FIXED_TONE_COMPONENTS`): a sky does not change with the theme.
// What IS the component's own chrome is here (theme-reach 2026-08-20).
'natural-time-picker': {
// Band geometry, derived from the control height like the recipe did.
'band-height-sm': 'calc(var(--size-sm-control-height) * 1.7 + var(--space-2))',
'band-height-md': 'calc(var(--size-md-control-height) * 1.7 + var(--space-2))',
'band-height-lg': 'calc(var(--size-lg-control-height) * 1.7 + var(--space-2))',
'knob-size-sm': 'calc(var(--size-sm-control-height) * 0.62)',
'knob-size-md': 'calc(var(--size-md-control-height) * 0.62)',
'knob-size-lg': 'calc(var(--size-lg-control-height) * 0.62)',
'band-height': {
parts: ['panel'],
declarations: [
{ value: 'var(--natural-time-picker-band-height-md)', scope: 'host' },
{ value: 'var(--natural-time-picker-band-height-sm)', scope: 'size:sm' },
{ value: 'var(--natural-time-picker-band-height-lg)', scope: 'size:lg' }
]
},
'knob-size': {
parts: ['panel'],
declarations: [
{ value: 'var(--natural-time-picker-knob-size-md)', scope: 'host' },
{ value: 'var(--natural-time-picker-knob-size-sm)', scope: 'size:sm' },
{ value: 'var(--natural-time-picker-knob-size-lg)', scope: 'size:lg' }
]
},
'panel-gap-sm': 'var(--space-2-5)',
'panel-gap-md': 'var(--space-3)',
'panel-gap-lg': 'var(--space-4)',
'panel-gap': {
parts: ['panel'],
declarations: [
{ value: 'var(--natural-time-picker-panel-gap-md)', scope: 'host' },
{ value: 'var(--natural-time-picker-panel-gap-sm)', scope: 'size:sm' },
{ value: 'var(--natural-time-picker-panel-gap-lg)', scope: 'size:lg' }
]
},
'band-radius': 'var(--radius-md)',
'band-border': 'var(--color-border-subtle)',
'band-border-width': 'var(--border-width)',
'line-width': 'calc(var(--border-width) * 1.5)',
'line-radius': 'var(--radius-sm)',
'knob-radius': 'var(--radius-full, 50%)',
'knob-shadow': 'var(--shadow-2)',
'knob-focus-ring': 'var(--color-primary-border)',
'knob-focus-ring-offset': 'var(--space-1)',
'ticks-inset-inline': 'var(--space-2)',
'ticks-inset-block-end': 'var(--space-1)',
'tick-font-size': 'var(--size-xxs-font-size)',
'tick-font-family': 'var(--font-family-mono, ui-monospace, monospace)',
// Readout
'display-gap': 'var(--space-3)',
'time-font-family': 'var(--font-family-mono, ui-monospace, monospace)',
'time-font-size': 'var(--size-xl-font-size)',
'time-font-weight': 'var(--font-weight-bold)',
'time-letter-spacing': 'var(--tracking-wide)',
'meridiem-margin-inline-start': 'var(--space-1)',
'meridiem-font-size': 'var(--size-md-font-size)',
'meridiem-font-weight': 'var(--font-weight-bold)',
'meridiem-fg': 'var(--color-primary-text)',
'period-gap': 'var(--space-1)',
'period-font-size': 'var(--size-xs-font-size)',
'period-font-weight': 'var(--font-weight-semibold)',
'period-fg': 'var(--color-primary-text)',
// Steppers + chips
'steppers-gap': 'var(--space-2)',
'stepper-group-gap': 'var(--space-1-5)',
'stepper-label-font-size': 'var(--size-xs-font-size)',
'stepper-label-font-weight': 'var(--font-weight-medium)',
'stepper-label-fg': 'var(--color-content-muted)',
'stepper-label-letter-spacing': 'var(--tracking-caps)',
'periods-gap': 'var(--space-2)',
'periods-margin-block': 'var(--space-3)',
'chip-font-size': 'var(--size-xs-font-size)',
// Trigger
'trigger-gap': 'var(--space-2)',
'trigger-min-width': '11rem',
'trigger-padding-block': 'var(--space-2-5)',
'trigger-padding-inline': 'var(--space-3)',
'trigger-bg': 'var(--color-surface-default)',
'trigger-fg': 'var(--color-content-primary)',
'trigger-border': 'var(--color-border-default)',
'trigger-border-width': 'var(--border-width)',
'trigger-radius': 'var(--radius-md)',
'trigger-font-size': 'var(--size-md-font-size)',
'hover-trigger-border': 'var(--color-border-strong)',
'open-trigger-border': 'var(--color-primary-border)',
'open-trigger-ring': 'var(--color-primary-border)',
'trigger-value-empty-fg': 'var(--color-content-muted)',
'trigger-icon-fg': 'var(--color-content-muted)'
},
// ─────────────────────────────────────────────────────────────────────
// GradientBuilder — the gradient editor's own chrome. Sized on its OWN
// geometry (px × `--scaling`), not on the `--size-{k}-*` bundle: an editor
// rail has no control-height coordinate. The two-tier shape is the canon
// (theme-reach F2): per-size coordinates at a stable scope + ONE resolved
// name per axis (`host` = md, `size:` overrides) which is the only thing the
// recipe CSS consumes.
'gradient-builder': {
'preview-height-sm': 'calc(56px * var(--scaling, 1))',
'preview-height-md': 'calc(128px * var(--scaling, 1))',
'preview-height-lg': 'calc(88px * var(--scaling, 1))',
'track-height-sm': 'calc(20px * var(--scaling, 1))',
'track-height-md': 'calc(24px * var(--scaling, 1))',
'track-height-lg': 'calc(28px * var(--scaling, 1))',
'stop-size-sm': 'calc(18px * var(--scaling, 1))',
'stop-size-md': 'calc(20px * var(--scaling, 1))',
'stop-size-lg': 'calc(22px * var(--scaling, 1))',
'gap-sm': 'var(--space-3)',
'gap-md': 'var(--space-4)',
'gap-lg': 'var(--space-5)',
'padding-block-sm': 'var(--space-3)',
'padding-block-md': 'var(--space-4)',
'padding-block-lg': 'var(--space-5)',
'padding-inline-sm': 'var(--space-3)',
'padding-inline-md': 'var(--space-4)',
'padding-inline-lg': 'var(--space-5)',
'preview-height': {
declarations: [
{ value: 'var(--gradient-builder-preview-height-md)', scope: 'host' },
{ value: 'var(--gradient-builder-preview-height-sm)', scope: 'size:sm' },
{ value: 'var(--gradient-builder-preview-height-lg)', scope: 'size:lg' }
]
},
'track-height': {
declarations: [
{ value: 'var(--gradient-builder-track-height-md)', scope: 'host' },
{ value: 'var(--gradient-builder-track-height-sm)', scope: 'size:sm' },
{ value: 'var(--gradient-builder-track-height-lg)', scope: 'size:lg' }
]
},
'stop-size': {
declarations: [
{ value: 'var(--gradient-builder-stop-size-md)', scope: 'host' },
{ value: 'var(--gradient-builder-stop-size-sm)', scope: 'size:sm' },
{ value: 'var(--gradient-builder-stop-size-lg)', scope: 'size:lg' }
]
},
gap: {
declarations: [
{ value: 'var(--gradient-builder-gap-md)', scope: 'host' },
{ value: 'var(--gradient-builder-gap-sm)', scope: 'size:sm' },
{ value: 'var(--gradient-builder-gap-lg)', scope: 'size:lg' }
]
},
'padding-block': {
declarations: [
{ value: 'var(--gradient-builder-padding-block-md)', scope: 'host' },
{ value: 'var(--gradient-builder-padding-block-sm)', scope: 'size:sm' },
{ value: 'var(--gradient-builder-padding-block-lg)', scope: 'size:lg' }
]
},
'padding-inline': {
declarations: [
{ value: 'var(--gradient-builder-padding-inline-md)', scope: 'host' },
{ value: 'var(--gradient-builder-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--gradient-builder-padding-inline-lg)', scope: 'size:lg' }
]
},
// The card
radius: 'var(--radius-lg)',
bg: 'var(--color-surface-raised, var(--color-surface-default))',
border: 'var(--color-border-default)',
'border-width': 'var(--border-width)',
// Preview + track
'preview-radius': 'var(--radius-md)',
'preview-bg': 'var(--color-surface-default)',
'preview-border': 'var(--color-border-subtle)',
'track-radius': 'var(--radius-md)',
'track-border': 'var(--color-border-subtle)',
// The transparency checker: the CELL COLOUR is the knob; the four-gradient
// pattern that composes it stays private to the recipe.
'checker-fg': 'var(--color-border-subtle)',
'checker-cell': 'calc(7px * var(--scaling, 1))',
// Stop handle
'stop-radius': 'var(--radius-full)',
'stop-bg': 'var(--color-surface-default)',
'stop-ring': 'var(--color-surface-default)',
'stop-ring-width': 'calc(2px * var(--scaling, 1))',
'stop-shadow':
'0 0 0 1px color-mix(in srgb, var(--color-neutral-contrast) 45%, transparent), 0 1px 3px color-mix(in srgb, var(--color-neutral-contrast) 30%, transparent)',
'selected-stop-shadow':
'0 0 0 calc(2px * var(--scaling, 1)) var(--color-primary-solid), 0 1px 3px color-mix(in srgb, var(--color-neutral-contrast) 35%, transparent)',
// Rows + angle dial
'stop-actions-gap': 'var(--space-2)',
'angle-dial-gap': 'var(--space-2)',
'angle-dial-head-gap': 'var(--space-2)',
'label-font-size': 'var(--size-sm-font-size)',
'label-fg': 'var(--color-content-secondary)',
'value-font-size': 'var(--size-sm-font-size)',
'value-fg': 'var(--color-content-primary)',
// Stop color editor
'stop-color-title-font-size': 'var(--size-sm-font-size)',
'stop-color-title-font-weight': 'var(--font-weight-medium, 500)',
'stop-color-title-fg': 'var(--color-content-primary)',
'stop-color-sliders-gap': 'var(--space-2)',
// Stop list
'stop-list-gap': 'var(--space-1)',
'stop-list-row-gap': 'var(--space-2)',
'stop-list-value-gap': 'var(--space-2)',
'stop-list-value-padding-block': 'var(--space-1)',
'stop-list-value-padding-inline': 'var(--space-2)',
'stop-list-value-radius': 'var(--radius-md)',
'stop-list-value-bg': 'var(--color-surface-default)',
'stop-list-value-fg': 'var(--color-content-primary)',
'stop-list-value-border': 'var(--color-border-subtle)',
'stop-list-value-font-size': 'var(--size-sm-font-size)',
'selected-stop-list-value-border': 'var(--color-primary-border)',
'stop-list-pos-fg': 'var(--color-content-secondary)',
// Presets
'presets-gap': 'var(--space-2)',
'preset-grid-gap': 'var(--space-2)',
'preset-height': 'calc(32px * var(--scaling, 1))',
'preset-radius': 'var(--radius-md)',
'preset-border': 'var(--color-border-subtle)',
'hover-preset-border': 'var(--color-border-strong)',
'selected-preset-border': 'var(--color-primary-border)',
'selected-preset-outline': 'var(--color-primary-border)'
},
// GradientPicker. Composes picker-shell + Popover + GradientBuilder, so it
// mints nothing those layers own — only its own trigger pill, the gradient
// chip, the panel widths and the presets grid.
'gradient-picker': {
// Per-size coordinates (sm | md | lg). Height and type are 1:1 with the
// bundle; the trigger's inline padding is its OWN rhythm (md and lg sit
// tighter than the bundle, so forcing it would change the default).
// The chip is the trigger's height minus its vertical breathing room, so
// it tracks the control instead of carrying a size of its own.
'chip-size-sm': 'calc(var(--size-sm-control-height) - var(--space-2))',
'chip-size-md': 'calc(var(--size-md-control-height) - var(--space-2))',
'chip-size-lg': 'calc(var(--size-lg-control-height) - var(--space-2))',
// The RESOLVED names. `data-size` is stamped on the root and the trigger
// hangs from it, so `host` scope is enough.
'chip-size': {
declarations: [
{ value: 'var(--gradient-picker-chip-size-md)', scope: 'host' },
{ value: 'var(--gradient-picker-chip-size-sm)', scope: 'size:sm' },
{ value: 'var(--gradient-picker-chip-size-lg)', scope: 'size:lg' }
]
},
// Root rhythm.
gap: 'var(--space-1-5)',
// Trigger — a field-shaped pill.
'trigger-gap': 'var(--space-2)',
'border-width': 'var(--border-width)',
// The gradient chip. Its FILL is not a knob: soma stamps the user's own
// gradient inline (`--gp-current-gradient`, a value channel), and a theme
// has no business overriding what the user picked.
'chip-radius': 'var(--radius-sm)',
'chip-border': 'var(--color-border-subtle)',
// Panel widths. The content is PORTALLED and sizes off `data-picker-size`
// (picker-shell's attr), not `data-size`, so these stay flat tokens read
// by three rules instead of a TSC size cascade - the scope vocabulary has
// no word for another component's attribute.
'content-width-sm': '18rem',
'content-width-md': '20rem',
'content-width-lg': '22rem',
// Stop list — content-sized up to a max, then scrolls.
'stoplist-max-block': 'calc(132px * var(--scaling, 1))',
'stoplist-scrollbar-size': 'var(--space-1-5)',
'stoplist-scrollbar-inset': 'var(--space-3)',
// Presets panel + its swatch grid.
'presets-panel-width': '15rem',
'presets-gap': 'var(--space-2)',
'presets-min-column': '48px',
'preset-height': 'calc(32px * var(--scaling, 1))',
'preset-radius': 'var(--radius-md)',
'preset-border': 'var(--color-border-subtle)',
'hover-preset-border': 'var(--color-border-strong)',
'selected-preset-border': 'var(--color-primary-border)',
'selected-preset-outline-offset': '2px',
// Actions row under the stops.
'actions-gap': 'var(--space-2)'
},
clipboard: {
// Host is `display: contents` and the trigger reuses Button's recipe, so
// everything here is the INDICATOR — the brief "Copied!" pill.
// `font-family` is absent: it reads the `--style-label-*` layer.
'indicator-gap': 'var(--space-1)',
'indicator-padding-block': '0.125rem',
'indicator-padding-inline': '0.5rem',
'indicator-radius': 'var(--radius-full)',
'indicator-font-size': 'var(--size-xs-font-size)',
'indicator-font-weight': 'var(--font-weight-medium, 500)',
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
},
// Carousel. The prev/next triggers compose `<IconButton>`, so their chrome is
// NOT minted here — a theme dresses them through the Button contract.
carousel: {
// The paginator dots. These rem values are the carousel's OWN geometry:
// the size bundle has no coordinate that means "a dot", so they are
// verbatim per size rather than a deviation to flag.
'indicator-size-xs': '0.375rem',
'indicator-size-sm': '0.4375rem',
'indicator-size-md': '0.5rem',
'indicator-size-lg': '0.625rem',
'indicator-size-xl': '0.75rem',
'indicator-gap-xs': 'var(--space-1)',
'indicator-gap-sm': 'var(--space-1)',
'indicator-gap-md': 'var(--space-2)',
'indicator-gap-lg': 'var(--space-3)',
'indicator-gap-xl': 'var(--space-3)',
// There is NO token for the gap BETWEEN SLIDES, on purpose: soma stamps
// `gap` INLINE on the item group from the consumer's `gap` prop (it needs
// the number for the flex-basis math), so a recipe declaration can never
// win and a token here would be a token that lies. Measured 2026-08-21
// (adversarial review of F2-A): the previous `item-gap` trio moved
// nothing; retiring it gave 0 computed diffs. The knob is soma's value
// channel, like `--gp-current-gradient`.
'indicator-size': {
declarations: [
{ value: 'var(--carousel-indicator-size-md)', scope: 'host' },
{ value: 'var(--carousel-indicator-size-xs)', scope: 'size:xs' },
{ value: 'var(--carousel-indicator-size-sm)', scope: 'size:sm' },
{ value: 'var(--carousel-indicator-size-lg)', scope: 'size:lg' },
{ value: 'var(--carousel-indicator-size-xl)', scope: 'size:xl' }
]
},
'indicator-gap': {
declarations: [
{ value: 'var(--carousel-indicator-gap-md)', scope: 'host' },
{ value: 'var(--carousel-indicator-gap-xs)', scope: 'size:xs' },
{ value: 'var(--carousel-indicator-gap-sm)', scope: 'size:sm' },
{ value: 'var(--carousel-indicator-gap-lg)', scope: 'size:lg' },
{ value: 'var(--carousel-indicator-gap-xl)', scope: 'size:xl' }
]
},
// Chassis.
gap: 'var(--space-2)',
radius: 'var(--radius-md)',
'font-family': 'var(--style-label-font-family)',
fg: 'var(--color-content-primary)',
// The viewport and the slides round INSIDE the frame, so their corner is
// the frame's minus the border - concentric, not a second radius.
'inner-radius-inset': '2px',
// How far the prev/next controls sit from the frame edge.
'trigger-offset': 'var(--space-3)',
// Variants. `surface` and `outline` inset the frame so the slides do not
// touch its edge; `ghost` keeps the border box (no layout jump when the
// consumer switches) but paints nothing.
'frame-padding': 'var(--space-2)',
'border-width': 'var(--border-width)',
border: 'var(--color-border-default)',
'surface-bg': 'var(--color-surface-default)',
// Vertical orientation needs a bounded block axis or the viewport
// collapses and the slides spill; this is that floor.
'vertical-block-size': '16rem',
// The dot strip.
'indicator-group-padding-block': 'var(--space-1)',
'indicator-bg': 'var(--color-surface-overlay)',
'indicator-radius': 'var(--radius-full)',
// The active dot stretches into a pill; the factor is what makes it read
// as "you are here" rather than as a bigger dot.
'active-indicator-scale': '2.25',
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
},
// Listbox. Consumes TWO shared layers, and the rule of those layers decides
// most of this entry: a consumer does NOT mint `--{c}-{axis}` for an axis the
// layer already owns. So the row rhythm (height, inline/block padding, gap
// and font-size) is NOT here — `lib/list-surface.css` owns it through
// `--list-item-*` / `--list-font-size`, per size, and the recipe adopts it
// through its private slots. Minting `--listbox-item-height` would be the
// parallel vocabulary that doctrine forbids, and it would silently outrank
// the layer for every surface that shares the rhythm (select, combobox,
// command and the menus).
listbox: {
// The surface the rows sit on - this IS the listbox's own.
gap: 'var(--space-1)',
padding: 'var(--space-1)',
radius: 'var(--radius-md)',
'border-width': 'var(--border-width)',
border: 'var(--color-border-default)',
bg: 'var(--color-surface-default)',
fg: 'var(--color-content-primary)',
'font-family': 'var(--style-label-font-family)',
'line-height': 'var(--leading-ui)',
// Generous but bounded; the consumer overrides it inline for a taller list.
'max-block-size': 'min(60vh, 22rem)',
'invalid-border': 'var(--color-threat-element)',
// Rows. The rhythm comes from the layer; what is left is the row's own
// corner and ink.
'item-radius': 'var(--radius-sm)',
'item-fg': 'var(--color-content-primary)',
'disabled-item-fg': 'var(--color-content-muted)',
// The check glyph. Its COLOUR is the palette forward, not a knob.
'item-indicator-size': '0.875rem',
// Groups.
'group-gap': 'var(--space-1)',
'group-label-padding-block': 'var(--space-1)',
'group-label-fg': 'var(--color-content-muted)',
'group-label-font-size': 'var(--size-xs-font-size)',
'group-label-font-weight': 'var(--font-weight-medium)',
'group-label-tracking': 'var(--tracking-caps)',
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
},
// Content-ink links — `color` tints the link text. Host = primary-text; the
// shared layer routes roles + 33 scales + custom. (Was a per-role `color:`
// cascade in link.css.) The hover ink rides the palette too since 2026-08-05:
// it was pinned to `--color-primary-solid-hover`, so a coloured link went
// purple under the pointer.
link: {
radius: 'var(--radius-sm)',
// Gap to the external-link glyph, and the glyph itself — em-relative so
// both track the link's inherited type.
gap: '0.25em',
'external-icon-size': '0.85em',
// The underline. Both rules that draw it (`always` and `hover`) share these
// two values, so it is one knob each.
'underline-width': '1px',
'underline-offset': '2px',
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' },
'_palette-solid-hover': { value: 'var(--color-primary-solid-hover)', scope: 'host' }
},
// Mark (inline highlight) — `element` tints the background (default = fulfill,
// the classic highlight), `text` tints the ink but ONLY when data-color is set
// (the base stays `color: inherit`), so text host is the forward fallback only.
mark: {
// No type knobs on purpose: a highlight inherits the ambient typography of
// the paragraph it lives in (`font: inherit`). What it owns is decoration.
// The padding is em-relative and deliberately tiny: the tint has to read
// WITHOUT breaking the line-height around it.
'padding-inline': '0.16em',
'padding-block': '0.04em',
radius: 'var(--radius-sm)',
'_palette-element': { value: 'var(--color-fulfill-element)', scope: 'host' },
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
// Typographic primitives (Fase 4, tinta de contenido) — `color` beyond the
// content-ink axis routes through the shared palette forward (roles + 33
// scales + custom). Colored-only: the ink axis (primary…on-solid) never
// stamps `data-color`, so these hosts are the forward fallback only.
text: {
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
heading: {
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
display: {
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
code: {
// The six TYPE axes are absent on purpose: they consume `--style-code-*`,
// the typographic layer, through the per-instance hatch. A primitive does
// not re-coin its layer's vocabulary (D-TH.2-b). What lives here is the
// chrome the `soft` / `outline` variants add on top.
radius: 'var(--radius-sm)',
'soft-bg': 'var(--color-neutral-element)',
'border-width': 'var(--border-width)',
border: 'var(--color-neutral-border)',
// em-relative on purpose: the pill grows with the code it wraps. Declared
// identically by both chromed variants, so one knob per axis.
'padding-inline': '0.32em',
'padding-block': '0.1em',
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
label: {
// The six TYPE axes are absent on purpose: `label` is a typographic
// primitive and they consume `--style-label-*` through the per-instance
// hatch (D-TH.2-b). These two are what the primitive adds on its own.
gap: 'var(--space-1)',
'disabled-fg': 'var(--color-content-disabled)',
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
// ProofOfHuman — `color` tints the challenge accent (path / needle / hand +
// soft corridor) via the shared layer (roles + 33 scales + custom) on the
// `root` part; the root renders `display: contents` and cascades to every
// challenge. Host = primary (the stamped default). The captcha's DEPICTION
// literals (pigeon / letter) stay fixed-tone — see recipe-css-contract.
'proof-of-human': {
'min-height-sm': 'calc(var(--size-sm-control-height) * 3)',
'min-height-md': 'calc(var(--size-md-control-height) * 3)',
'min-height-lg': 'calc(var(--size-lg-control-height) * 3)',
'min-height': {
parts: ['root'],
declarations: [
{ value: 'var(--proof-of-human-min-height-md)', scope: 'host' },
{ value: 'var(--proof-of-human-min-height-sm)', scope: 'size:sm' },
{ value: 'var(--proof-of-human-min-height-lg)', scope: 'size:lg' }
]
},
radius: 'var(--radius-md)',
bg: 'var(--color-surface-raised)',
border: 'var(--color-border-default)',
'border-width': 'var(--border-width)',
'verified-border': 'var(--color-fulfill-solid)',
'failed-border': 'var(--color-risk-solid)',
// The guide path + its token: the accent is the only colour axis a theme
// should reach here (the scene is fixed-tone).
'locked-opacity': '0.55',
'tolerance-opacity': '0.5',
'guide-opacity': '0.65',
'guide-width': 'var(--border-width-thick, 2px)',
'trail-width': '4px',
'goal-width': '3px',
'goal-opacity': '0.8',
'token-size': '1.25rem',
'token-radius': 'var(--radius-full)',
'token-focus-offset': '3px',
'_palette-solid': {
parts: ['root'],
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
},
'_palette-element': {
parts: ['root'],
declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }]
}
},
// Skeleton — `color` tints the placeholder wash (track slot). Colored-only:
// the `'neutral'` default stamps no `data-color` and keeps the base grey mix,
// so the host value here is the forward fallback only.
skeleton: {
// The one size coordinate: block height, circle diameter (via aspect-ratio)
// and the gap between text lines, which is `calc(size * 0.5)` — a
// PROPORTION, so construction rather than a knob.
'size-xs': '0.5rem',
'size-sm': '0.75rem',
'size-md': '1rem',
'size-lg': '1.5rem',
'size-xl': '2rem',
size: {
declarations: [
{ value: 'var(--skeleton-size-md)', scope: 'host' },
{ value: 'var(--skeleton-size-xs)', scope: 'size:xs' },
{ value: 'var(--skeleton-size-sm)', scope: 'size:sm' },
{ value: 'var(--skeleton-size-lg)', scope: 'size:lg' },
{ value: 'var(--skeleton-size-xl)', scope: 'size:xl' }
]
},
// Radius is per SHAPE, not per size: the rectangle's is a knob, the
// circle's `50%` is identity.
radius: 'var(--radius-md)',
'line-radius': 'var(--radius-sm)',
// The shimmer period. Bare `1.4s` until now — same call as spinner's.
duration: '1.4s',
// `--_skeleton-bg` / `-highlight` stay PRIVATE: each is one variable with
// two sources (the cross-mode neutral mix, or the palette forward when
// `data-color` lands), and flattening them would duplicate every rule per
// colour.
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' }
},
// Spinner — `color` tints the ink (text slot) + the ring track. The base
// tokens consume the palette directly (host default = primary); `'inherit'`
// keeps its own literal rule (currentColor takeover) outside the forward.
spinner: {
// ONE size coordinate drives the whole component: the ring's box, the dot
// track's height, the bar's height. Every other dimension is a PROPORTION
// of it (`calc(size * 0.3)` = the dot, `* 0.18` = the bar, `* 0.2` / `* 0.12`
// = the gaps) and those stay in the recipe: they are the SHAPE of each
// variant, not knobs. Same split as tree-grid's indent guides.
//
// The scale is the spinner's OWN, not the icon bundle's: measured
// 12/16/20/28/40 px here against 14/16/18/20/32 in `--icon-size-*`, which
// only agree at sm. Verbatim on purpose — forcing the bundle would move the
// default in four sizes out of five.
'size-xs': '0.75rem',
'size-sm': '1rem',
'size-md': '1.25rem',
'size-lg': '1.75rem',
'size-xl': '2.5rem',
'thickness-xs': '1.5px',
'thickness-sm': '2px',
'thickness-md': '2px',
'thickness-lg': '3px',
'thickness-xl': '4px',
size: {
declarations: [
{ value: 'var(--spinner-size-md)', scope: 'host' },
{ value: 'var(--spinner-size-xs)', scope: 'size:xs' },
{ value: 'var(--spinner-size-sm)', scope: 'size:sm' },
{ value: 'var(--spinner-size-lg)', scope: 'size:lg' },
{ value: 'var(--spinner-size-xl)', scope: 'size:xl' }
]
},
thickness: {
declarations: [
{ value: 'var(--spinner-thickness-md)', scope: 'host' },
{ value: 'var(--spinner-thickness-xs)', scope: 'size:xs' },
{ value: 'var(--spinner-thickness-sm)', scope: 'size:sm' },
{ value: 'var(--spinner-thickness-lg)', scope: 'size:lg' },
{ value: 'var(--spinner-thickness-xl)', scope: 'size:xl' }
]
},
gap: 'var(--space-2)',
// The spin period. It used to be `0.9s` bare; feed's spinner already earned
// duration tokens and media-player's did not (next-features §13) — done
// right here from the start.
duration: '0.9s',
// Only ever consumed under `prefers-reduced-motion`, where the ring stops
// and dims instead of spinning.
'track-opacity': 'var(--opacity-60)',
'bar-radius': 'var(--radius-sm)',
'label-font-size': 'var(--size-sm-font-size)',
'label-fg': 'var(--color-content-muted)',
// The ink and the track stay PRIVATE: each is one variable with two
// sources, switched by `data-color='inherit'` (palette forward →
// currentColor), and the track wraps its source in a different color-mix per
// branch (60 % normal, 25 % inherit). Flattening them would mean duplicating
// every rule per colour — same call as textarea's focus border.
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' },
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' }
},
// TextArea — `color` tints the focus border (solid slot). Colored-only: the
// `'primary'` default stamps no `data-color` and keeps `--focus-ring-color`,
// so the host value here is the forward fallback only.
textarea: {
// ── Per-size coordinates (xs..xl) ──
// Type is 1:1 with the bundle in all five (measured: `--font-size-{k}` IS
// `--size-{k}-font-size`), so it reads the bundle coordinate.
'input-font-size-xs': 'var(--size-xs-font-size)',
'input-font-size-sm': 'var(--size-sm-font-size)',
'input-font-size-md': 'var(--size-md-font-size)',
'input-font-size-lg': 'var(--size-lg-font-size)',
'input-font-size-xl': 'var(--size-xl-font-size)',
// Spacing keeps the field's OWN rhythm, verbatim. Measured against the
// bundle 2026-08-21: padding-inline matches at sm/md/lg/xl but NOT at xs
// (8px here, 6px there), and padding-block matches at xs only — from sm up
// this field is looser (6/8/12/16 vs 4/6/8/…). A multi-line field breathes
// more than a one-line control, and that is the intended default. Spacing
// sits outside the bundle guard, which governs control-height / font-size /
// icon-size only (same call as table's cell padding).
'input-padding-inline-xs': 'var(--space-2)',
'input-padding-inline-sm': 'var(--space-2)',
'input-padding-inline-md': 'var(--space-3)',
'input-padding-inline-lg': 'var(--space-4)',
'input-padding-inline-xl': 'var(--space-5)',
'input-padding-block-xs': 'var(--space-1)',
'input-padding-block-sm': 'var(--space-1-5)',
'input-padding-block-md': 'var(--space-2)',
'input-padding-block-lg': 'var(--space-3)',
'input-padding-block-xl': 'var(--space-4)',
// The RESOLVED names. `data-size` is stamped on `[data-textarea]` and the
// input inherits, so the cascade hangs from the host — no `parts` needed
// (precedent: command).
'input-font-size': {
declarations: [
{ value: 'var(--textarea-input-font-size-md)', scope: 'host' },
{ value: 'var(--textarea-input-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--textarea-input-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--textarea-input-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--textarea-input-font-size-xl)', scope: 'size:xl' }
]
},
'input-padding-inline': {
declarations: [
{ value: 'var(--textarea-input-padding-inline-md)', scope: 'host' },
{ value: 'var(--textarea-input-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--textarea-input-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--textarea-input-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--textarea-input-padding-inline-xl)', scope: 'size:xl' }
]
},
'input-padding-block': {
declarations: [
{ value: 'var(--textarea-input-padding-block-md)', scope: 'host' },
{ value: 'var(--textarea-input-padding-block-xs)', scope: 'size:xs' },
{ value: 'var(--textarea-input-padding-block-sm)', scope: 'size:sm' },
{ value: 'var(--textarea-input-padding-block-lg)', scope: 'size:lg' },
{ value: 'var(--textarea-input-padding-block-xl)', scope: 'size:xl' }
]
},
// ── Chassis ──
gap: 'var(--space-2)',
// ── Input ──
// Radius is DECOUPLED from size on purpose (Radix model): the control
// archetype default, constant across the five.
'input-radius': 'var(--radius-default)',
'input-border-width': 'var(--border-width)',
'input-border': 'var(--color-neutral-border)',
'hover-input-border': 'var(--color-border-strong)',
// The FOCUS border, not the resting one. Its per-color swap
// (`--_textarea-palette-solid`) is the THM-2 palette forward and stays
// private — it is one variable with two sources, not two knobs. The ghost
// variant reads this same token: a `ghost-input-border` would be one value
// under two names.
'focus-input-border': 'var(--focus-ring-color)',
'invalid-input-border': 'var(--color-threat-solid)',
'input-bg': 'var(--color-surface-default)',
'input-fg': 'var(--color-content-primary)',
'input-placeholder-fg': 'var(--color-content-muted)',
'input-font-family': 'var(--font-family-primary)',
'input-font-weight': 'var(--font-weight-medium)',
'input-line-height': 'var(--leading-normal)',
// ── Count chip ──
// ONE token, not five per size: the caption is PROPORTIONAL to the field
// (0.85×) and the input's own font-size already resolves per size, so the
// scale is written once. Five per-size tokens would have dropped the 0.85
// and grown the counter 18 % — the default does not move (D-TH.5).
// `scope: 'host'` is REQUIRED, not decoration: it reads the resolved
// `input-font-size`, which only exists on `[data-textarea]`. Emitted at
// :root the var() would resolve where its dependency is missing — the
// recipe scope contract catches it.
'count-font-size': {
value: 'calc(var(--textarea-input-font-size) * 0.85)',
scope: 'host'
},
'count-gap': '0.25em',
'count-fg': 'var(--color-content-muted)',
'count-font-family': 'var(--font-family-mono, ui-monospace, monospace)',
'overflow-count-fg': 'var(--color-threat-text)',
'overflow-count-font-weight': 'var(--font-weight-semibold)',
'_palette-solid': { value: 'var(--color-primary-solid)', 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',
fg: '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-fg': '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',
'hover-control-border': 'var(--color-border-subtle)',
'control-bg': 'transparent',
'control-fg': 'var(--color-content-secondary)',
'hover-control-fg': 'var(--color-content-primary)',
'select-min-width': '8ch',
'select-padding-inline': 'var(--space-2)',
// Between month COLUMNS in the paginated view — a full step, unlike the
// hairline `grid-gap` that separates day cells inside one month.
'months-gap': 'var(--space-4)',
'grid-gap': 'var(--space-0-5)',
'weekday-size': 'var(--space-5)',
'weekday-fg': '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',
'hover-day-border': 'var(--color-border-subtle)',
'day-bg': 'transparent',
'day-fg': 'var(--color-content-primary)',
'hover-day-fg': 'var(--color-content-primary)',
'day-font-weight': 'var(--font-weight-regular)',
'day-selected-fg': '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-fg': 'var(--color-content-primary)',
'day-today-font-weight': 'var(--font-weight-semibold)',
'day-outside-fg': 'var(--color-content-muted)',
'day-outside-opacity': '0.62',
'day-weekend-fg': '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' },
// `day-holiday-shadow` and `event-shadow` are NOT here any more: they live
// in `lib/calendar-surface.css` (2026-08-21). Audit B.2 had host-scoped
// them because their value reads `--_calendar-palette-border`, a private
// that only exists on `[data-calendar]` — correct for this component and
// silently broken for the other three surfaces of the family, which read
// the same two tokens from hosts that never carry that attribute (the
// variable resolved EMPTY, the box-shadow was invalid at computed-value
// time and the ring never painted; measured). The layer declares the
// SHAPE on the shared hook and takes the ACCENT through the
// `--_calendar-surface-accent` slot each surface feeds from its own
// palette forward.
'day-unavailable-decoration': 'var(--color-risk-border)',
'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',
'hover-control-border': 'var(--color-border-subtle)',
'control-bg': 'transparent',
'control-fg': 'var(--color-content-secondary)',
'hover-control-fg': 'var(--color-content-primary)',
'selected-border': 'var(--color-primary-border)',
'hover-selected-border': 'var(--color-primary-border)',
'selected-bg': 'var(--color-primary-solid)',
'hover-selected-bg': 'var(--color-primary-solid-hover)',
'selected-fg': 'var(--color-content-on-solid)',
'ellipsis-fg': 'var(--color-content-muted)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
popover: {
// El disparador GENÉRICO (el que no lleva el arquetipo `field-trigger`, que
// tiene su propio tratamiento): su cromo es de este componente. `height`
// lee el BUNDLE de talla, no el primitivo crudo que el CSS traía.
'trigger-height': 'var(--size-md-control-height)',
'trigger-padding-inline': 'var(--space-3)',
'trigger-border-width': 'var(--border-width)',
'trigger-border': 'var(--color-border-default)',
'trigger-radius': 'var(--radius-md)',
'trigger-bg': 'var(--color-surface-raised)',
'trigger-fg': 'var(--color-content-primary)',
// Hover y abierto comparten valor HOY, y son dos estados distintos: un
// tema puede querer el abierto más marcado que el hover.
'hover-trigger-bg': 'var(--color-surface-overlay)',
'hover-trigger-border': 'var(--color-border-strong)',
'open-trigger-bg': 'var(--color-surface-overlay)',
'open-trigger-border': 'var(--color-border-strong)',
'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-fg': '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-fg': '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` RETIRADO 2026-08-22: ningún CSS lo consumía. El único sitio que lo
// nombraba era un COMENTARIO de `popover-arrow.svelte` («fill:
// var(--popover-bg)»), y por eso el test de huérfanos lo daba por vivo —
// busca el nombre en el corpus del componente, comentarios incluidos. La
// superficie del panel la pinta el PLANO de profundidad
// (`--depth-overlay-surface`), que es lo que un plano debe poseer.
fg: '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-fg': '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-fg': '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-fg': 'var(--color-content-primary)',
'disabled-item-label-fg': '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)',
'hover-indicator-border': 'var(--color-border-strong)',
'disabled-indicator-bg':
'color-mix( in srgb, var(--color-surface-overlay) 78%, var(--color-surface-default) )',
'disabled-indicator-border':
'color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) )',
'checked-disabled-indicator-bg':
'color-mix( in srgb, var(--color-content-disabled) 40%, var(--color-surface-overlay) )',
'dot-fg': 'var(--color-content-on-solid)',
'disabled-dot-fg':
'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-fg': '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-fg': 'var(--color-content-secondary)',
'hover-segmented-segment-fg': '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-fg)', 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-fg': 'var(--color-border-strong)',
'crosshair-width': 'var(--border-width-thin)',
// ── The two TEXT ROLES, as a seam (2026-08-22, firmado) ──────────────
// Chart's text plays two roles of the canon and the value comes from
// them, but the KNOB has to be the component's own: PLAN §2-A asks every
// appearance knob to read `--{c}-{slot}`, and reading `var(--style-…)`
// straight from the CSS leaves a theme unable to retune THIS component
// without moving the role across the whole app. Same shape as the
// vertebration's own example, `--accordion-trigger-font-family:
// var(--style-label-font-family)`. Three axes, two roles, 19 text nodes:
// naming per part would be ~30 keys carrying two values.
//
// `caption` = the annotations (axis labels, legend, tooltip rows, smith
// labels); `label` = the chrome text (title, bar-list rows, funnel) and
// the emphasis ink + weight wherever a node stands out. The SIZE and the
// INK are orthogonal on purpose: `empty` and `bar-list-value` are label
// SIZE with caption INK, and the CSS says so.
'caption-font-size': 'var(--style-caption-font-size)',
'caption-fg': 'var(--style-caption-color)',
'label-font-size': 'var(--style-label-font-size)',
'label-fg': 'var(--style-label-color)',
'label-font-weight': 'var(--style-label-font-weight)',
// The gauge's big number is a data READOUT and no named style plays that
// role (an `h2` is a heading, not a datum), so this one takes the size
// BUNDLE. It is the escape hatch the previous commit left open.
'gauge-label-font-size': 'var(--size-xl-font-size)',
// ── The ink of everything that is NOT text (2026-08-22) ──────────────
// Text takes its size role from the named styles (`caption` / `label`)
// and its ink from the content slots — the recipe mints no typography.
// These four are lines and fills nothing else can name, and each is ONE
// concept the CSS repeats: the axis ink also draws the smith grid and the
// funnel connector; the grid ink also draws the radar rings; and
// `separator-fg` is the hairline of page surface that keeps adjacent
// marks apart — pie slice, active dot, smith point, polar petal, funnel
// segment: five parts, one idea. Naming them per part would be five keys
// carrying one value.
'axis-fg': 'var(--color-border-default)',
'grid-fg': 'var(--color-border-subtle)',
'separator-fg': 'var(--color-surface-default)',
// The RF disk outline is deliberately stronger than the grid it encloses.
'smith-boundary-fg': 'var(--color-content-secondary)',
// ── Rails (bar list, stacked bar, gauge) ─────────────────────────────
// `track-height` reads the size BUNDLE, not `--control-height-sm`: the raw
// coordinate is what `recipe-css-contract` forbids, and the CSS carried it
// (behind a dead `--space-7` fallback — the primitive does exist).
'track-bg': 'var(--color-surface-muted)',
'track-height': 'var(--size-sm-control-height)',
'track-radius': 'var(--radius-sm)',
// ── Chrome geometry ──────────────────────────────────────────────────
gap: 'var(--space-2)',
'legend-row-gap': 'var(--space-1-5)',
'legend-column-gap': 'var(--space-4)',
'legend-item-gap': 'var(--space-1-5)',
'swatch-size': '0.75em',
'swatch-radius': 'var(--radius-sm)',
'tooltip-swatch-size': '0.6em',
'bar-list-row-gap': 'var(--space-2)',
'bar-list-column-gap': 'var(--space-3)',
'bar-segment-gap': 'var(--space-3)',
'bar-segment-track-gap': 'var(--border-width-medium)',
'bar-segment-track-radius': 'var(--radius-md)',
'bar-segment-fill-min-inline-size': 'var(--border-width-thick)',
'bar-segment-legend-row-gap': 'var(--space-2)',
'bar-segment-legend-column-gap': 'var(--space-4)'
},
'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)',
'hover-border': 'var(--color-border-strong)',
// El borde de la tarjeta en validación fallida: propio de este skin (la
// tarjeta ES suya), a diferencia del indicador, que es un radio.
'invalid-border': 'var(--color-threat-element)',
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-fg': '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-fg': '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-fg': '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-fg': 'var(--color-neutral-border)',
'active-item-fg': 'var(--color-fulfill-solid)',
'partial-item-fg': '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-fg': '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-fg': 'var(--color-content-muted)',
'loading-indicator-track': 'var(--color-border-subtle)',
'loading-indicator-thickness': '2px',
'loading-indicator-duration': '900ms'
},
'password-field': {
// El tracking del input enmascarado (los puntos leen mejor algo sueltos) y el
// chip de bloq-mayús. Su peso llevaba un `500` muerto detrás del token.
'input-letter-spacing': 'var(--tracking-wide)',
'caps-radius': 'var(--radius-sm)',
'caps-font-weight': 'var(--font-weight-medium)',
// Los CUATRO peldaños del medidor de fuerza. Son una escala de intención,
// no cuatro colores sueltos: cada uno lee su rol —pérdida, riesgo,
// cumplimiento, afirmación— y un tema puede recorrer la escala entera.
'strength-1-bg': 'var(--color-loss-solid)',
'strength-2-bg': 'var(--color-risk-solid)',
'strength-3-bg': 'var(--color-fulfill-solid)',
'strength-4-bg': 'var(--color-affirm-solid)',
'placeholder-fg': 'var(--color-content-muted)',
'trigger-radius': 'var(--radius-sm)',
'trigger-bg': 'transparent',
'trigger-fg': '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-fg': '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)',
fg: '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)',
'hover-trigger-border': 'var(--color-border-strong)',
'trigger-radius': 'var(--radius-md)',
'trigger-bg': 'var(--color-surface-default)',
'trigger-shadow': 'var(--shadow-subtle)',
'trigger-fg': 'var(--color-content-primary)',
'trigger-indicator-fg': 'var(--color-content-secondary)',
'trigger-indicator-size': 'var(--size-sm-icon-size)',
'placeholder-fg': 'var(--color-content-muted)',
'disabled-border': 'var(--color-border-subtle)',
'disabled-bg': 'var(--color-surface-raised)',
'disabled-fg': '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-fg': '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-fg': '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-fg': 'var(--color-content-primary)',
'item-indicator-size': 'var(--size-sm-icon-size)',
'item-description-mt': '0.125rem',
'item-description-fg': '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)',
fg: 'var(--color-content-primary)',
'group-gap': 'var(--space-0-5)',
'control-radius': 'var(--radius-md)',
'control-border-width': 'var(--border-width)',
'control-border': 'transparent',
'hover-control-border': 'var(--color-border-subtle)',
'active-control-border': 'var(--color-primary-border)',
'control-bg': 'transparent',
'active-control-bg': 'var(--color-primary-track)',
'control-fg': 'var(--color-content-secondary)',
'hover-control-fg': 'var(--color-content-primary)',
'active-control-fg': '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)',
fg: 'var(--color-content-primary)',
'label-fg': '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-fg': '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)',
'hover-button-border': 'var(--color-border-default)',
'button-radius': 'var(--radius-md)',
'button-bg': 'var(--color-surface-raised)',
'button-fg': 'var(--color-content-secondary)',
'hover-button-fg': 'var(--color-content-primary)',
'button-solid-fg': '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-fg': 'var(--color-neutral-text)',
'item-name-fg': 'var(--color-content-primary)',
'item-name-font-weight': 'var(--font-weight-medium)',
'item-size-fg': '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)',
fg: 'var(--color-content-primary)',
'label-fg': '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)',
'hover-item-border': 'var(--color-border-default)',
'item-bg': 'var(--color-surface-raised)',
'item-shadow': 'var(--shadow-subtle)',
'item-fg': 'var(--color-content-secondary)',
'hover-item-fg': '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',
'hover-remove-border': '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)',
'hover-primary-solid-bg': 'var(--color-primary-solid-hover)',
'primary-soft-bg': 'var(--color-primary-surface)',
'hover-primary-soft-bg': '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)',
'hover-secondary-solid-bg': 'var(--color-secondary-solid-hover)',
'secondary-soft-bg': 'var(--color-secondary-surface)',
'hover-secondary-soft-bg': '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)',
'hover-neutral-solid-bg': 'var(--color-neutral-solid-hover)',
'neutral-soft-bg': 'var(--color-neutral-surface)',
'hover-neutral-soft-bg': '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)',
'hover-affirm-solid-bg': 'var(--color-affirm-solid-hover)',
'affirm-soft-bg': 'var(--color-affirm-surface)',
'hover-affirm-soft-bg': '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)',
'hover-fulfill-solid-bg': 'var(--color-fulfill-solid-hover)',
'fulfill-soft-bg': 'var(--color-fulfill-surface)',
'hover-fulfill-soft-bg': '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)',
'hover-risk-solid-bg': 'var(--color-risk-solid-hover)',
'risk-soft-bg': 'var(--color-risk-surface)',
'hover-risk-soft-bg': '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)',
'hover-threat-solid-bg': 'var(--color-threat-solid-hover)',
'threat-soft-bg': 'var(--color-threat-surface)',
'hover-threat-soft-bg': '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)',
'hover-loss-solid-bg': 'var(--color-loss-solid-hover)',
'loss-soft-bg': 'var(--color-loss-surface)',
'hover-loss-soft-bg': '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-hover-primary-solid-bg)', scope: 'host' },
{ value: 'var(--tag-group-hover-secondary-solid-bg)', scope: 'color:secondary' },
{ value: 'var(--tag-group-hover-neutral-solid-bg)', scope: 'color:neutral' },
{ value: 'var(--tag-group-hover-affirm-solid-bg)', scope: 'color:affirm' },
{ value: 'var(--tag-group-hover-fulfill-solid-bg)', scope: 'color:fulfill' },
{ value: 'var(--tag-group-hover-risk-solid-bg)', scope: 'color:risk' },
{ value: 'var(--tag-group-hover-threat-solid-bg)', scope: 'color:threat' },
{ value: 'var(--tag-group-hover-loss-solid-bg)', 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-hover-primary-soft-bg)', scope: 'host' },
{ value: 'var(--tag-group-hover-secondary-soft-bg)', scope: 'color:secondary' },
{ value: 'var(--tag-group-hover-neutral-soft-bg)', scope: 'color:neutral' },
{ value: 'var(--tag-group-hover-affirm-soft-bg)', scope: 'color:affirm' },
{ value: 'var(--tag-group-hover-fulfill-soft-bg)', scope: 'color:fulfill' },
{ value: 'var(--tag-group-hover-risk-soft-bg)', scope: 'color:risk' },
{ value: 'var(--tag-group-hover-threat-soft-bg)', scope: 'color:threat' },
{ value: 'var(--tag-group-hover-loss-soft-bg)', 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)',
fg: '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)',
'hover-control-border': 'var(--color-border-default)',
'control-bg': 'var(--color-surface-default)',
'control-shadow': 'var(--shadow-subtle)',
'placeholder-fg': '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)',
'hover-trigger-border': 'var(--color-border-default)',
'trigger-bg': 'var(--color-surface-raised)',
'trigger-fg': 'var(--color-content-secondary)',
'hover-trigger-fg': 'var(--color-content-primary)',
'submit-fg': '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)',
fg: '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)',
'hover-control-border': 'var(--color-border-strong)',
'control-bg': 'var(--color-surface-default)',
'control-shadow': 'var(--shadow-subtle)',
'input-min-inline-size': '8ch',
'placeholder-fg': '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-fg': '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',
'hover-action-border': 'var(--color-border-subtle)',
'action-bg': 'transparent',
'action-fg': 'var(--color-content-secondary)',
'hover-action-fg': '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: {
// El ritmo del trend y las tres FORMAS del icono, una por valor del eje
// `data-shape`: el radio es lo único que las distingue, así que cada forma
// lleva el suyo.
'trend-gap': 'var(--space-2)',
'icon-circle-radius': 'var(--radius-full)',
'icon-rounded-radius': 'var(--radius-lg)',
'icon-square-radius': 'var(--radius-md)',
// Metrics is SURFACELESS — the Delta's colour comes from the composed
// <Badge>; the live flash tint uses the canonical `--color-*` tokens
// directly (it grades by EVENT intent, not by the `color` prop). The
// featured icon routes through the shared palette forward below.
// ── 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-fg': 'var(--color-content-secondary)',
// Featured-icon palette forward (THM-2) — the five slots the variant rules
// read, on the `icon` part. Host = primary (the stamped default); shared
// layer routes roles + 33 scales + custom. `plain` stamps nothing and
// keeps the base `icon-bg` / `icon-color` defaults.
'_palette-track': {
parts: ['icon'],
declarations: [{ value: 'var(--color-primary-track)', scope: 'host' }]
},
'_palette-solid': {
parts: ['icon'],
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
},
'_palette-text': {
parts: ['icon'],
declarations: [{ value: 'var(--color-primary-text)', scope: 'host' }]
},
'_palette-contrast': {
parts: ['icon'],
declarations: [{ value: 'var(--color-primary-contrast)', scope: 'host' }]
},
'_palette-border': {
parts: ['icon'],
declarations: [{ value: 'var(--color-primary-border)', scope: 'host' }]
},
// ── 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-fg': '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-fg': '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-fg': '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-fg': '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-fg': '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)',
fg: '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-fg': 'var(--color-content-secondary)',
'hover-trigger-fg': '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-fg': 'var(--color-content-secondary)',
'indicator-font-weight': 'var(--font-weight-semibold)',
'complete-fg': '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-fg': '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: {
// Status-accent palette forward (THM-2): host = primary; the shared layer
// routes roles + 33 scales + custom into `--_timeline-palette-*`, which the
// item's `--_timeline-hue-*` reads (data-intent still overrides per item).
// Was a per-role cascade over `--_timeline-accent-*` in timeline.css.
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
'_palette-border': { value: 'var(--color-primary-border)', scope: 'host' },
'_palette-contrast': { value: 'var(--color-primary-contrast)', scope: 'host' },
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' },
// ── 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)',
fg: '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-fg': '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-fg': '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-fg': 'var(--color-content-secondary)',
'title-font-weight': 'var(--font-weight-semibold)',
'title-fg': 'var(--color-content-primary)',
'description-fg': '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)',
// Secondary track (buffer / preloaded region): sits BETWEEN the track and
// the range in weight — solid enough to read on the 12% track mix, muted
// enough that the value range stays the dominant fill on top of it.
'secondary-bg': 'color-mix(in srgb, var(--color-primary-solid) 28%, transparent)',
'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.
'active-thumb-scale': '1.15',
'active-thumb-shadow': 'var(--shadow-3)',
'tick-radius': 'var(--radius-full)',
'tick-bg': 'var(--color-neutral-border)',
'active-tick-bg': '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)',
// ── Chrome ──
// The `--_mp-*` private layer this recipe used to carry was an ABBREVIATED
// prefix (theming §6 r5 forbids it) AND a pure alias of its own source, so
// it is not renamed: it is gone. Every knob below is public.
bg: 'var(--color-surface-default)',
radius: 'var(--radius-lg)',
fg: 'var(--color-content-primary)',
'fg-muted': 'var(--color-content-secondary)',
// The control bar's glass and its hairline. Both ride over arbitrary
// video, which is why they are mixes of a surface rather than a flat role.
glass: 'color-mix(in srgb, var(--color-surface-overlay) 82%, transparent)',
border: 'color-mix(in srgb, var(--color-content-primary) 10%, transparent)',
// ONE scrim colour; the recipe paints TWO gradients from it (bottom for
// the control strip, top for the title). Two tokens would let a theme
// break the pair without meaning to.
scrim: 'color-mix(in srgb, var(--color-surface-default) 75%, transparent)',
'scrim-height': '42%',
// The unfilled rail behind both scrubbers, and the buffered sub-track.
track: 'color-mix(in srgb, var(--color-content-primary) 22%, transparent)',
'buffered-bg': 'color-mix(in srgb, var(--color-content-primary) 42%, transparent)',
// ── Captions ──
// Denser than the bar's glass on purpose: this one carries prose over
// moving frames rather than icons over a scrim.
'captions-bg': 'color-mix(in srgb, var(--color-surface-overlay) 94%, transparent)',
'captions-fg': 'var(--color-content-primary)',
'captions-radius': 'var(--radius-sm)',
'captions-gap': 'var(--space-1)',
'captions-padding-block': 'var(--space-1)',
'captions-padding-inline': 'var(--space-2)',
// ── Title overlay ──
'title-font-size': 'var(--size-md-font-size)',
'title-font-weight': 'var(--font-weight-semibold)',
'title-padding-block': 'var(--space-3)',
'title-padding-inline': 'var(--space-4)',
// ── Buffering spinner ──
'buffering-size': 'var(--space-8)',
'buffering-border-width': '3px',
// ── Per-size coordinates (sm | md | lg | xl) - all 1:1 with the bundle ──
'gap-sm': 'var(--size-sm-gap)',
'gap-md': 'var(--size-md-gap)',
'gap-lg': 'var(--size-lg-gap)',
'gap-xl': 'var(--size-xl-gap)',
'padding-block-sm': 'var(--size-sm-padding-block)',
'padding-block-md': 'var(--size-md-padding-block)',
'padding-block-lg': 'var(--size-lg-padding-block)',
'padding-block-xl': 'var(--size-xl-padding-block)',
'padding-inline-sm': 'var(--size-sm-padding-inline)',
'padding-inline-md': 'var(--size-md-padding-inline)',
'padding-inline-lg': 'var(--size-lg-padding-inline)',
'padding-inline-xl': 'var(--size-xl-padding-inline)',
'time-font-size-sm': 'var(--size-sm-font-size)',
'time-font-size-md': 'var(--size-md-font-size)',
'time-font-size-lg': 'var(--size-lg-font-size)',
'time-font-size-xl': 'var(--size-xl-font-size)',
'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)',
// The RESOLVED names - the only ones the recipe reads. `data-size` is on
// the provider, so `host` scope is enough.
gap: {
declarations: [
{ value: 'var(--media-player-gap-md)', scope: 'host' },
{ value: 'var(--media-player-gap-sm)', scope: 'size:sm' },
{ value: 'var(--media-player-gap-lg)', scope: 'size:lg' },
{ value: 'var(--media-player-gap-xl)', scope: 'size:xl' }
]
},
'padding-block': {
declarations: [
{ value: 'var(--media-player-padding-block-md)', scope: 'host' },
{ value: 'var(--media-player-padding-block-sm)', scope: 'size:sm' },
{ value: 'var(--media-player-padding-block-lg)', scope: 'size:lg' },
{ value: 'var(--media-player-padding-block-xl)', scope: 'size:xl' }
]
},
'padding-inline': {
declarations: [
{ value: 'var(--media-player-padding-inline-md)', scope: 'host' },
{ value: 'var(--media-player-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--media-player-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--media-player-padding-inline-xl)', scope: 'size:xl' }
]
},
'time-font-size': {
declarations: [
{ value: 'var(--media-player-time-font-size-md)', scope: 'host' },
{ value: 'var(--media-player-time-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--media-player-time-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--media-player-time-font-size-xl)', scope: 'size:xl' }
]
},
'control-height': {
declarations: [
{ value: 'var(--media-player-control-height-md)', scope: 'host' },
{ value: 'var(--media-player-control-height-sm)', scope: 'size:sm' },
{ value: 'var(--media-player-control-height-lg)', scope: 'size:lg' },
{ value: 'var(--media-player-control-height-xl)', scope: 'size:xl' }
]
},
// ── Scrubbers ──
// Named because the TIME scrubber floors itself against the volume one:
// the main control of a bar must never render narrower than the volume
// beside it, and two literals would drift apart the first time one moves.
'volume-width': 'calc(var(--space-8) * 2)',
'volume-vertical-length': 'calc(var(--space-8) * 3)',
'time-slider-min-width': 'var(--space-8)',
'rate-button-min-width': '4.6em',
'buffering-radius': 'var(--radius-full)',
'captions-line-height': 'var(--font-line-height-md)',
'time-font-family': 'var(--font-family-mono)',
'time-padding-inline': 'var(--space-2)',
// The audio shape drops the overlay: the title is in the flow, so it wears
// its own tighter padding instead of the video overlay's.
'audio-title-padding-block': 'var(--space-2)',
'audio-title-padding-inline': 'var(--space-3)',
// The floating volume panel: block padding clears the THUMB's overhang, so
// the two axes differ on purpose.
'volume-float-padding-block': 'var(--space-6)',
'volume-float-padding-inline': 'var(--space-2)'
},
'audio-player': {
// The audio variants' public knobs (F5, PLAN-audio-player-v2.md). Artwork
// sizes are design values of each identity: `card` is the hero cap, `row`
// matches a list entry (SoundCloud-track scale), `bar` a persistent app bar
// (Spotify scale) — public tokens, so a theme retunes them like the
// slider's `track-size-*`.
gap: 'var(--space-3)',
// La pieza tipográfica que ESTE reproductor añade sobre el chasis: el
// título en peso fuerte y la insignia LIVE en versalitas espaciadas.
'title-font-weight': 'var(--font-weight-semibold)',
'live-font-weight': 'var(--font-weight-semibold)',
'live-letter-spacing': '0.08em',
'artwork-radius': 'var(--radius-md)',
// `card` is a CAP (the artwork is `inline-size: 100%` up to this), and it
// is deliberately expressed on the space scale rather than as a flat rem:
// `--space-8` already carries density × scaling composed, so the hero
// artwork breathes with the foundation while the two compact sizes below
// stay fixed — a list row and an app bar must NOT grow with density, or
// they stop matching the rows and bars around them. 256px at 1×/1×.
'artwork-size-card': 'calc(var(--space-8) * 8)',
'artwork-size-row': '3.5rem',
'artwork-size-bar': '2.5rem'
},
waveform: {
// Waveform (D-WF, PLAN-waveform.md) — the audio-signal painting. The
// interaction chrome is the embedded canonical Slider, re-tinted by the
// recipe (track/range transparent, thumb = thin playhead); these tokens
// own only what the Slider does not have: the painting itself.
'height-xs': '1.5rem',
'height-sm': '2rem',
'height-md': '2.5rem',
'height-lg': '3.5rem',
'height-xl': '4.5rem',
// Bar thickness in viewBox units (unitless stroke-width): the svg
// stretches with `preserveAspectRatio="none"`, so bars keep a constant
// bar/gap duty cycle at any rendered width — 0.6 of a bucket slot at
// the wave's 100-unit width reads as the SoundCloud density.
'bar-width': '0.6',
// Unplayed signal: quiet tint of content over any surface; played takes
// the same role colour the Slider's range uses, so both stay theme-led.
fg: 'color-mix(in srgb, var(--color-content-primary) 28%, transparent)',
'played-fg': 'var(--color-primary-solid)',
// Buffered / preloaded: the middle rung. It is the SAME wave path clipped
// to the buffered edge, so it only ever shows ahead of the playhead (the
// played copy paints after and wins).
//
// It carries the PLAYED hue, washed — "loaded, on its way to heard". A
// lighter NEUTRAL was tried first and rejected ON SIGHT: separated from the
// unplayed wave by lightness alone, it read as more of the same grey. The
// HUE is what makes it a third state.
//
// Composited against the base theme: rgb(148,112,178) light /
// rgb(130,95,161) dark. Note the raw rgb distance to the unplayed wave is
// slightly SMALLER than the neutral's (89/98 vs 102/104) — that metric is
// lightness-dominated and blind to chroma, which is exactly why it endorsed
// a colour the eye rejected. The number to watch instead is the distance to
// the PLAYED accent (40/42): wash further if the two ever blur.
//
// The 55/20 pair sums to 75, so color-mix normalises the ratio to 73:27 AND
// takes the result to alpha .75 — the wash is that alpha. Retune freely:
// this is a public token, the designer's knob.
'buffered-fg':
'color-mix(in srgb, var(--color-primary-solid) 55%, var(--color-content-primary) 20%)',
// The embedded Slider's thumb restyled as a thin full-height playhead —
// a shape its square `--slider-thumb-size-*` tokens cannot express.
'playhead-width': '2px',
// …and its colour, the PAIR `playhead-width` was missing. Every other
// line of the re-tint already reads a token of this component's own
// (`--slider-secondary-bg: var(--waveform-buffered-fg)`) or an identity
// (`transparent` / `none`); the thumb was the one passing a RAW primitive
// (`--color-content-primary`) into a composed component, so the only way
// to repaint the playhead was to move every Slider in the app. Same shape
// as media-player's re-tint, which forwards `--media-player-*` throughout.
'playhead-bg': 'var(--color-content-primary)'
},
background: {
// ── Patterns — the knobs `Backdrop` kept as inline fallbacks, promoted to
// theme data so a product can retune the decoration without a recipe fork.
'pattern-rule': 'var(--color-border-subtle)',
'pattern-cell': '2.5rem',
'pattern-dot-size': '1.5px',
'pattern-glow-size': '60% 55%',
'pattern-glow-at': '50% 0%',
'pattern-glow-strength': '28%',
// A mesh is a veil at scrim weight — the same 0.45 Backdrop shipped, now
// tied to the semantic scale instead of a literal (R-4.2).
// The mesh pattern IS a system gradient — the seam: the VALUE belongs to
// the gradient catalogue, the KNOB belongs to this component, so a theme
// can give the mesh another image without moving `--gradient-aurora`
// everywhere it is used. (Measured 2026-08-23: it paints as a longhand
// `background-image` here — this token's serialisation starts with a
// radial-gradient, so the mesh-through-`background-image` trap does not
// apply to it.)
'pattern-mesh-image': 'var(--gradient-aurora)',
'pattern-mesh-opacity': 'var(--opacity-scrim)',
'pattern-noise-opacity': 'var(--opacity-ghost)',
'pattern-vignette-strength': '38%',
'pattern-lines-width': '1px',
'pattern-lines-gap': '0.5rem',
'pattern-lines-angle': '45deg',
'pattern-rings-width': '1px',
'pattern-rings-gap': '4rem',
// ── Fade (the edge mask) ────────────────────────────────────────────
'fade-size': '75% 75%',
'fade-at': '50% 35%',
// ── Scrim — the FLOOR of the ink in force, and the veil's own weights ──
//
// The ink: a scrim exists so the copy above it stays legible, so its
// colour is the FLOOR of whatever ink `on` put in force (D12, §39). It
// used to be `--color-overlay`, which is `surface.backdrop` — the modal
// dim (MD3/Radix/Vaul), tuned PER MODE because a light page needs less
// attenuation than a dark one. Borrowed as a legibility tool that brought
// two defects: the same photograph read 1.48:1 in light and 2.10:1 in
// dark (a photograph does not change with the mode), and its own alpha
// (0.42 / 0.66) capped the whole scale at 2.66:1 — no weight could reach
// AA. Now the ink is opaque and picked by context, so weight IS alpha.
'scrim-fg': 'var(--color-surface-default)',
'scrim-fg-over-dark': 'var(--color-content-on-solid-contrast)',
'scrim-fg-over-light': 'var(--color-content-on-solid)',
// The weights: five steps by the WORK each one does, measured with the
// framework's own maths (`$color` apcaLc + wcagContrastRatio) over the
// worst artwork of each context. `md` is the default and holds the 0.189
// the hero shipped — in light mode, identical to the digit, because the
// old backdrop's hue WAS this floor. `xl` is the only step that promises
// legibility over ANY photograph: it clears BOTH framework floors in BOTH
// contexts (on-solid's APCA ≥ 60 ∧ WCAG ≥ 3, and §40's AA 4.5 for text) —
// measured `on='dark'` 6.45:1 / Lc 85, `on='light'` 8.29:1 / Lc 61.
// The frost behind the scrim, one step per `blur` value. Same seam as the
// mesh above: the scale is the system's (`--blur-*`), the knob is the
// component's — a theme can soften the scrim's frost without touching
// every blur in the app. `blur` is a METRIC family of the `scaling` axis
// (theming §23), so these must stay tokens, never literals.
'scrim-blur-sm': 'var(--blur-sm)',
'scrim-blur-md': 'var(--blur-md)',
'scrim-blur-lg': 'var(--blur-lg)',
'scrim-blur-xl': 'var(--blur-xl)',
'scrim-blur-xxl': 'var(--blur-xxl)',
'scrim-strength-xs': '0.08',
'scrim-strength-sm': '0.13',
'scrim-strength-md': '0.19',
'scrim-strength-lg': '0.4',
'scrim-strength-xl': '0.7',
// ── Gradient drift (the `animate` layer) ────────────────────────────
'gradient-drift-duration': '24s',
// ── The pause control (WCAG 2.2.2) ──────────────────────────────────
// Composed from primitives that are already themeable: the space scale
// for the inset, and the `--z-index-*` ladder for the plane. The control
// only has to clear the host's own content, not the app's overlays — a
// dialog or a popover opening over this surface must still cover it,
// which is why this sits far below `--z-index-affix` (150).
'pause-offset': 'var(--space-3)',
'pause-z': 'var(--z-index-raised)',
// ── Parallax + pointer ──────────────────────────────────────────────
// The travel a layer covers at `speed: 1` across the whole scrollport.
// Config data, not a magic number in the recipe: a theme that wants a
// calmer page turns this down once and every background follows.
'parallax-travel': '4rem',
'spotlight-size': '40% 40%',
'spotlight-strength': '18%',
// The light's own ink. A separate token because a spotlight over LIGHT
// artwork needs a dark light — the scrim has the same escape (`scrim-color`)
// for the same reason.
'spotlight-fg': 'var(--color-content-on-solid)'
},
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-border-off': 'var(--color-border-default)',
'hover-track-border-off': 'var(--color-border-strong)',
'disabled-track-bg':
'color-mix( in srgb, var(--color-surface-overlay) 70%, var(--color-surface-default) )',
'disabled-track-border':
'color-mix( in srgb, var(--color-border-default) 70%, var(--color-border-subtle) )',
'disabled-thumb-bg':
'color-mix( in srgb, var(--color-surface-default) 88%, var(--color-surface-raised) )',
'disabled-thumb-border':
'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-fg': '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-fg': '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-fg': 'var(--color-content-secondary)',
'hover-trigger-fg': 'var(--color-content-primary)',
'active-trigger-fg': 'var(--color-content-primary)',
'trigger-bg': 'transparent',
'active-trigger-bg': 'var(--color-surface-overlay)',
'trigger-border': 'transparent',
'active-trigger-border': '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)',
'active-surface-trigger-border':
'color-mix( in srgb, var(--tabs-active-trigger-border) 58%, transparent )',
'pills-trigger-radius': 'var(--radius-full)',
'active-pills-trigger-border':
'color-mix( in srgb, var(--tabs-active-trigger-border) 58%, transparent )',
'active-pills-trigger-shadow': '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)))',
'active-segmented-trigger-bg': 'var(--color-primary-solid)',
'active-segmented-trigger-fg': 'var(--color-content-on-solid)',
'active-segmented-trigger-shadow': '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-fg': '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-fg': 'var(--color-content-secondary)',
'hint-opacity': '0.8',
'hint-margin-top': 'var(--space-1)',
// Content-ink tint (Fase 4, mirrors <Text>) — `color` beyond the ink axis
// routes through the shared palette forward. Colored-only: the ink axis
// (primary…on-solid) never stamps `data-color`, so this host is the
// forward fallback only.
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
's-text-virtual-list': {
'line-height': 'var(--leading-prose)',
fg: 'var(--color-content-primary)',
bg: 'var(--color-surface-default)',
'border-width': 'var(--border-width)',
'border-fg': 'var(--color-border-subtle)',
radius: 'var(--radius-md)',
'row-padding-inline': 'var(--space-3)',
'row-border-width': 'var(--border-width)',
'row-border-fg': '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-fg': 'var(--color-content-primary)',
'message-fg': '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-fg': '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-fg': 'var(--color-neutral-text)',
'default-action-fg': '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-fg': 'var(--color-affirm-text)',
'affirm-action-fg': '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-fg': 'var(--color-fulfill-text)',
'fulfill-action-fg': '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-fg': 'var(--color-risk-text)',
'risk-action-fg': '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-fg': 'var(--color-threat-text)',
'threat-action-fg': '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-fg': 'var(--color-loss-text)',
'loss-action-fg': '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)',
fg: '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)',
// La variante `ghost`: un velo translúcido sobre lo que haya detrás. Su
// superficie y su desenfoque son suyos — la sólida usa `bg`.
// NO `ghost-bg`: la superficie del panel la pinta el PLANO `overlay`
// (misma especificidad, más tarde en la cascada), así que un token de fondo
// aquí mentiría — medido 2026-08-23, igual que `bg`/`border`/`shadow`.
'ghost-blur': 'var(--blur-md)',
// La flecha de la variante `outline`: se rellena con la superficie y se
// perfila, porque el panel es transparente y el caret no puede serlo.
'outline-arrow-bg': 'var(--color-surface-default)',
'outline-arrow-border': 'var(--color-border-strong)'
},
// ─────────────────────────────────────────────────────────────────────
// 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',
// No `flex` token: the shorthand is expanded by the wrapper into the three
// longhands below (a shorthand in the recipe is erased by them — see
// `box.css`), so `--box-flex` would be a name nobody reads.
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',
// Densidad por talla: la franja es un anuncio a todo el ancho, y su respiro
// es lo primero que un tema toca.
'padding-block-sm': 'var(--space-2)',
'padding-block-md': 'var(--space-3)',
'padding-block-lg': 'var(--space-4)',
'padding-inline-sm': 'var(--space-3)',
'padding-inline-md': 'var(--space-4)',
'padding-inline-lg': 'var(--space-5)',
'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(--size-sm-font-line-height)',
// El trazo del anillo de la variante `outline` — la única que dibuja borde.
'border-width': 'var(--border-width)',
// 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)'
},
// ─────────────────────────────────────────────────────────────────────
// Sticky — position:sticky affix. Only a stacking z-index is public; the
// offset arrives as the soma-injected `--_sticky-offset` data property.
// ─────────────────────────────────────────────────────────────────────
sticky: {
'z-index': 'var(--z-index-sticky)'
},
// ─────────────────────────────────────────────────────────────────────
// SkipLink — the bypass affordance. Invisible until focused, then a
// pill against the start corner of the viewport. Every token is a
// role/scale reference: a skip link is the app's own chrome and has to
// follow its theme, not a hard-coded contrast pair.
// ─────────────────────────────────────────────────────────────────────
'skip-link': {
'z-index': 'var(--z-index-skip-link)',
offset: 'var(--space-2)',
'padding-block': 'var(--space-2)',
'padding-inline': 'var(--space-4)',
radius: 'var(--radius-md)',
bg: 'var(--color-primary-solid)',
fg: 'var(--color-primary-contrast)',
shadow: 'var(--shadow-overlay)'
},
// ─────────────────────────────────────────────────────────────────────
// Prose — long-form typography. The reading column + the reading
// line-height are public; the em-relative element scale is recipe-internal.
// ─────────────────────────────────────────────────────────────────────
prose: {
// ── La costura (2026-08-22) ──────────────────────────────────────────
// Prose estiliza HTML CRUDO (h1, code, table…) por `:where([data-prose] el)`,
// así que sus "partes" son elementos. Los valores siguen siendo del canon
// —bundle de talla, ranuras de contenido, escalas de peso y radio— y aquí
// están sus knobs, que es lo que PLAN §2-A pide.
//
// Su ESCALA editorial (2.25em, 1.5em, 0.9em…) NO entra: es `em`-relativa a
// propósito —un `font-size` raíz re-deriva encabezados y márgenes, y por
// eso no hay una escala duplicada por talla como en tailwind-typography— y
// va anotada `/* literal: */` en el CSS, la válvula de recipe-contract §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)',
fg: 'var(--color-content-primary)',
// La tinta apagada: h6, los marcadores de lista y el pie de figura.
'fg-muted': 'var(--color-content-muted)',
'heading-fg': 'var(--color-content-primary)',
'heading-font-weight': 'var(--font-weight-semibold)',
// h1 pesa más que sus hermanos, y es lo único que lo distingue aquí.
'h1-font-weight': 'var(--font-weight-bold)',
'link-fg': 'var(--color-primary-text)',
'link-font-weight': 'var(--font-weight-medium)',
'strong-font-weight': 'var(--font-weight-semibold)',
'dt-font-weight': 'var(--font-weight-semibold)',
'th-font-weight': 'var(--font-weight-semibold)',
'mark-bg': 'var(--color-risk-track)',
'mark-radius': 'var(--radius-sm)',
// El borde-acento del blockquote: 3px ES el paso `thick` de la escala.
'blockquote-border': 'var(--color-border-strong)',
'blockquote-border-width': 'var(--border-width-thick)',
'blockquote-fg': 'var(--color-content-secondary)',
'code-font-family': 'var(--font-family-mono)',
'code-bg': 'var(--color-surface-muted)',
'code-radius': 'var(--radius-sm)',
'pre-bg': 'var(--color-surface-muted)',
'pre-radius': 'var(--radius-md)',
'kbd-font-family': 'var(--font-family-mono)',
'kbd-bg': 'var(--color-surface-default)',
'kbd-border': 'var(--color-border-default)',
'kbd-border-width': 'var(--border-width)',
'kbd-radius': 'var(--radius-sm)',
'kbd-shadow': '0 1px 0 1px var(--color-border-subtle)',
'media-radius': 'var(--radius-md)',
'hr-border': 'var(--color-border-default)',
'hr-border-width': 'var(--border-width)',
// La tabla: la cabecera y el pie marcan más fuerte que la fila.
'table-header-border': 'var(--color-border-strong)',
'table-cell-border': 'var(--color-border-subtle)',
'table-border-width': 'var(--border-width)',
measure: '65ch',
'line-height': '1.75'
},
// ─────────────────────────────────────────────────────────────────────
// AnchorNav — scrollspy TOC rail. The per-depth indent step + the rail
// width are public; the active/depth logic keys off morfo data-attrs.
// ─────────────────────────────────────────────────────────────────────
'anchor-nav': {
indent: 'var(--space-3)',
'rail-width': '2px',
// El raíl: su trazo ya era público, su TINTA no — y la del enlace activo
// vivía en un `border-inline-start-color` que el censo ni veía.
'rail-fg': 'var(--color-border-subtle)',
'active-rail-fg': 'var(--color-primary-solid)',
// La escala del índice: lee el bundle, no el primitivo suelto.
'font-size': 'var(--size-sm-font-size)',
'line-height': 'var(--size-sm-font-line-height)',
'link-padding-block': 'var(--space-1)',
'link-radius': 'var(--radius-sm)',
'link-fg': 'var(--color-content-muted)',
'hover-link-fg': 'var(--color-content-primary)',
'active-link-fg': 'var(--color-primary-text)',
'active-link-font-weight': 'var(--font-weight-medium)'
},
// ─────────────────────────────────────────────────────────────────────
// NavTree — data-driven docs navigation tree. `indent` is the per-depth
// step (multiplied by `--_nav-tree-depth`). Active / expanded / trail
// states key off morfo data-attrs; the chevron rotates on `data-state`.
// ─────────────────────────────────────────────────────────────────────
'nav-tree': {
indent: 'var(--space-4)',
// La FILA es link + trigger a la vez: las reglas los tratan juntos y por eso
// el knob es de la fila, no de cada parte (pseudo-parte, como las regiones
// de waveform). El disparador sí tiene lo suyo: el suelo de 24 px del blanco
// táctil (WCAG 2.5.8) y su padding.
'font-size': 'var(--size-sm-font-size)',
'line-height': 'var(--size-sm-font-line-height)',
'row-gap': 'var(--space-1)',
'row-padding-block': 'var(--space-1)',
'row-padding-inline-end': 'var(--space-2)',
'row-radius': 'var(--radius-sm)',
'row-fg': 'var(--color-content-muted)',
'hover-row-fg': 'var(--color-content-primary)',
'active-row-fg': 'var(--color-primary-text)',
'active-row-font-weight': 'var(--font-weight-medium)',
'disabled-row-fg': 'var(--color-content-disabled)',
// La estela: los ancestros de la página actual leen como texto primario.
'trail-row-fg': 'var(--color-content-primary)',
'trigger-min-inline-size': 'var(--space-6)',
'trigger-padding-inline': 'var(--space-1)',
'badge-padding-inline-start': 'var(--space-2)',
// El galón es una escuadra dibujada con dos bordes: su lado es un knob y el
// grosor del trazo otro (mismo molde que la esquina de text-focus).
'chevron-size': '0.42em',
'chevron-stroke': '1.5px'
},
// ─────────────────────────────────────────────────────────────────────
// Sidebar — the app's vertical chrome. Widths are TOKENS, never TS
// constants (the dossier's note on shadcn shipping them as consts): the
// three of them are the whole layout contract — expanded, icon rail and
// the mobile Drawer. The panel animates `transform` when it goes
// off-canvas and `width` only in icon mode (animating width every frame
// is layout work; icon mode is the one case where the inset must follow).
//
// The ROW is a control, so it takes the canonical size bundle
// (`--size-{k}-*`, theming §5) instead of the hand-picked `--space-8` +
// `--font-size-sm` it carried until 2026-08-19: a spacing token as a
// control height rides the density-SPACE scale while every other control
// rides density-CONTROL, and a `font-size:` written in the recipe CSS is
// invisible to the bundle guard. Nav controls run `xs..lg` and default to
// `md` (the canon's default; `sm` reproduces the old density). The label
// steps ONE below its row — the label↔control hierarchy of the field
// doctrine, the only deliberate step-down in the system.
// ─────────────────────────────────────────────────────────────────────
sidebar: {
width: '16rem',
// The icon rail is the ROW plus the panel's padding on both sides —
// derived, not a constant: at `md` the row is 36px and a fixed 3rem
// would clip it. `host` scope because it reads a host-scoped row height.
'width-icon': {
value: 'calc(var(--sidebar-row-height) + 2 * var(--sidebar-padding))',
scope: 'host'
},
'width-mobile': '18rem',
'rail-width': 'var(--space-4)',
gap: 'var(--space-1)',
padding: 'var(--space-2)',
'row-height-xs': 'var(--size-xs-control-height)',
'row-height-sm': 'var(--size-sm-control-height)',
'row-height-md': 'var(--size-md-control-height)',
'row-height-lg': 'var(--size-lg-control-height)',
'row-font-size-xs': 'var(--size-xs-font-size)',
'row-font-size-sm': 'var(--size-sm-font-size)',
'row-font-size-md': 'var(--size-md-font-size)',
'row-font-size-lg': 'var(--size-lg-font-size)',
'row-line-height-xs': 'var(--size-xs-font-line-height)',
'row-line-height-sm': 'var(--size-sm-font-line-height)',
'row-line-height-md': 'var(--size-md-font-line-height)',
'row-line-height-lg': 'var(--size-lg-font-line-height)',
'row-icon-size-xs': 'var(--size-xs-icon-size)',
'row-icon-size-sm': 'var(--size-sm-icon-size)',
'row-icon-size-md': 'var(--size-md-icon-size)',
'row-icon-size-lg': 'var(--size-lg-icon-size)',
'row-padding-inline-xs': 'var(--size-xs-padding-inline)',
'row-padding-inline-sm': 'var(--size-sm-padding-inline)',
'row-padding-inline-md': 'var(--size-md-padding-inline)',
'row-padding-inline-lg': 'var(--size-lg-padding-inline)',
'row-gap-xs': 'var(--size-xs-gap)',
'row-gap-sm': 'var(--size-sm-gap)',
'row-gap-md': 'var(--size-md-gap)',
'row-gap-lg': 'var(--size-lg-gap)',
'row-radius-xs': 'var(--size-xs-radius)',
'row-radius-sm': 'var(--size-sm-radius)',
'row-radius-md': 'var(--size-md-radius)',
'row-radius-lg': 'var(--size-lg-radius)',
// One typographic step below the row (xs keeps xxs).
'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)',
// The resolved coordinates for the size in force. `host` default = `md`
// (the component's default); each `size:` scope is the emitted cascade
// entry, so the recipe CSS consumes ONE name per axis and the guard sees
// the bundle reference in the token, not a literal buried in a rule.
'row-height': {
declarations: [
{ value: 'var(--sidebar-row-height-md)', scope: 'host' },
{ value: 'var(--sidebar-row-height-xs)', scope: 'size:xs' },
{ value: 'var(--sidebar-row-height-sm)', scope: 'size:sm' },
{ value: 'var(--sidebar-row-height-lg)', scope: 'size:lg' }
]
},
'row-font-size': {
declarations: [
{ value: 'var(--sidebar-row-font-size-md)', scope: 'host' },
{ value: 'var(--sidebar-row-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--sidebar-row-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--sidebar-row-font-size-lg)', scope: 'size:lg' }
]
},
'row-line-height': {
declarations: [
{ value: 'var(--sidebar-row-line-height-md)', scope: 'host' },
{ value: 'var(--sidebar-row-line-height-xs)', scope: 'size:xs' },
{ value: 'var(--sidebar-row-line-height-sm)', scope: 'size:sm' },
{ value: 'var(--sidebar-row-line-height-lg)', scope: 'size:lg' }
]
},
'row-icon-size': {
declarations: [
{ value: 'var(--sidebar-row-icon-size-md)', scope: 'host' },
{ value: 'var(--sidebar-row-icon-size-xs)', scope: 'size:xs' },
{ value: 'var(--sidebar-row-icon-size-sm)', scope: 'size:sm' },
{ value: 'var(--sidebar-row-icon-size-lg)', scope: 'size:lg' }
]
},
'row-padding-inline': {
declarations: [
{ value: 'var(--sidebar-row-padding-inline-md)', scope: 'host' },
{ value: 'var(--sidebar-row-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--sidebar-row-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--sidebar-row-padding-inline-lg)', scope: 'size:lg' }
]
},
'row-gap': {
declarations: [
{ value: 'var(--sidebar-row-gap-md)', scope: 'host' },
{ value: 'var(--sidebar-row-gap-xs)', scope: 'size:xs' },
{ value: 'var(--sidebar-row-gap-sm)', scope: 'size:sm' },
{ value: 'var(--sidebar-row-gap-lg)', scope: 'size:lg' }
]
},
'row-radius': {
declarations: [
{ value: 'var(--sidebar-row-radius-md)', scope: 'host' },
{ value: 'var(--sidebar-row-radius-xs)', scope: 'size:xs' },
{ value: 'var(--sidebar-row-radius-sm)', scope: 'size:sm' },
{ value: 'var(--sidebar-row-radius-lg)', scope: 'size:lg' }
]
},
'label-font-size': {
declarations: [
{ value: 'var(--sidebar-label-font-size-md)', scope: 'host' },
{ value: 'var(--sidebar-label-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--sidebar-label-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--sidebar-label-font-size-lg)', scope: 'size:lg' }
]
},
'sub-indent': 'var(--space-4)',
// Band of the icon-mode flyout. The floating layer mirrors the content's
// computed z-index onto its positioner wrapper (the dropdown-menu note).
'sub-z': 'var(--z-index-overlay-floating)',
bg: 'var(--color-surface-muted)',
'border-fg': 'var(--color-border-subtle)'
},
// ─────────────────────────────────────────────────────────────────────
// Callout — inline admonition. Per-color forwarders (track/text/solid)
// + the canonical `_palette-*` slots so the THM-2 shared layer routes
// `color="teal"` and the 33 scales with zero extra CSS (badge pattern).
// ─────────────────────────────────────────────────────────────────────
callout: {
gap: 'var(--space-3)',
padding: 'var(--space-4)',
'accent-width': '3px',
radius: 'var(--radius-md)',
'row-gap': 'var(--space-1)',
// Typography of the admonition: it reads like body copy, so the value is
// the size bundle's and the knob is the callout's.
'font-size': 'var(--size-md-font-size)',
'line-height': 'var(--size-md-font-line-height)',
fg: 'var(--color-content-primary)',
'icon-size': 'var(--size-md-icon-size)',
'title-font-weight': 'var(--font-weight-semibold)',
'primary-track': 'var(--color-primary-track)',
'primary-text': 'var(--color-primary-text)',
'primary-solid': 'var(--color-primary-solid)',
'secondary-track': 'var(--color-secondary-track)',
'secondary-text': 'var(--color-secondary-text)',
'secondary-solid': 'var(--color-secondary-solid)',
'neutral-track': 'var(--color-neutral-track)',
'neutral-text': 'var(--color-neutral-text)',
'neutral-solid': 'var(--color-neutral-solid)',
'affirm-track': 'var(--color-affirm-track)',
'affirm-text': 'var(--color-affirm-text)',
'affirm-solid': 'var(--color-affirm-solid)',
'fulfill-track': 'var(--color-fulfill-track)',
'fulfill-text': 'var(--color-fulfill-text)',
'fulfill-solid': 'var(--color-fulfill-solid)',
'risk-track': 'var(--color-risk-track)',
'risk-text': 'var(--color-risk-text)',
'risk-solid': 'var(--color-risk-solid)',
'threat-track': 'var(--color-threat-track)',
'threat-text': 'var(--color-threat-text)',
'threat-solid': 'var(--color-threat-solid)',
'loss-track': 'var(--color-loss-track)',
'loss-text': 'var(--color-loss-text)',
'loss-solid': 'var(--color-loss-solid)',
'_palette-track': {
declarations: [
{ value: 'var(--callout-primary-track)', scope: 'host' },
{ value: 'var(--callout-secondary-track)', scope: 'color:secondary' },
{ value: 'var(--callout-neutral-track)', scope: 'color:neutral' },
{ value: 'var(--callout-affirm-track)', scope: 'color:affirm' },
{ value: 'var(--callout-fulfill-track)', scope: 'color:fulfill' },
{ value: 'var(--callout-risk-track)', scope: 'color:risk' },
{ value: 'var(--callout-threat-track)', scope: 'color:threat' },
{ value: 'var(--callout-loss-track)', scope: 'color:loss' }
]
},
'_palette-text': {
declarations: [
{ value: 'var(--callout-primary-text)', scope: 'host' },
{ value: 'var(--callout-secondary-text)', scope: 'color:secondary' },
{ value: 'var(--callout-neutral-text)', scope: 'color:neutral' },
{ value: 'var(--callout-affirm-text)', scope: 'color:affirm' },
{ value: 'var(--callout-fulfill-text)', scope: 'color:fulfill' },
{ value: 'var(--callout-risk-text)', scope: 'color:risk' },
{ value: 'var(--callout-threat-text)', scope: 'color:threat' },
{ value: 'var(--callout-loss-text)', scope: 'color:loss' }
]
},
'_palette-solid': {
declarations: [
{ value: 'var(--callout-primary-solid)', scope: 'host' },
{ value: 'var(--callout-secondary-solid)', scope: 'color:secondary' },
{ value: 'var(--callout-neutral-solid)', scope: 'color:neutral' },
{ value: 'var(--callout-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--callout-fulfill-solid)', scope: 'color:fulfill' },
{ value: 'var(--callout-risk-solid)', scope: 'color:risk' },
{ value: 'var(--callout-threat-solid)', scope: 'color:threat' },
{ value: 'var(--callout-loss-solid)', scope: 'color:loss' }
]
}
},
// ─────────────────────────────────────────────────────────────────────
// Result — terminal flow/page state. Status→role color forwarders for
// the glyph only (HTTP situations stay neutral in the recipe); spacing
// + reading measures. Shares EmptyState's layout language.
// ─────────────────────────────────────────────────────────────────────
result: {
// The BOX is what this recipe owns: the page inset and the rhythm between
// its blocks. Type and ink read the canon in the CSS and mint nothing —
// the size bundle, the content slots, the weight scale.
gap: 'var(--space-3)',
'padding-block': 'var(--space-12)',
'padding-inline': 'var(--space-5)',
'actions-gap': 'var(--space-2)',
'actions-margin-block-start': 'var(--space-1)',
'extra-margin-block-start': 'var(--space-4)',
// ── The seam for type + ink (2026-08-22, firmado) ────────────────────
// The values are the canon's — size bundle, content slots, weight and
// leading scales — but the KNOB is the component's: PLAN §2-A. Five parts
// with distinct typography, so it is per PART (the catalogue's dominant
// shape) and not per role as in `chart`, whose two roles repeat over 19
// nodes. No named style plays "page terminal state", so nothing is bound
// wholesale to one.
'title-font-size': 'var(--size-xl-font-size)',
'title-font-weight': 'var(--font-weight-semibold)',
'title-line-height': 'var(--font-line-height-sm)',
'title-fg': 'var(--color-content-primary)',
'description-font-size': 'var(--size-md-font-size)',
'description-line-height': 'var(--font-line-height-md)',
'description-fg': 'var(--color-content-muted)',
'extra-font-size': 'var(--size-sm-font-size)',
'extra-fg': 'var(--color-content-secondary)',
// The status glyph: THREE times the xl step is its identity, and the
// multiplier stays in the value so the glyph keeps following the scale.
'media-font-size': 'calc(var(--size-xl-font-size) * 3)',
'media-fg': 'var(--color-content-muted)',
// The big HTTP code — neutral by design (AntD never paints 404 red).
'code-font-family': 'var(--font-family-mono)',
'code-font-weight': 'var(--font-weight-semibold)',
'code-letter-spacing': '0.04em',
'code-fg': 'var(--color-content-muted)',
'description-measure': '45ch',
'extra-measure': '60ch',
'success-fg': 'var(--color-fulfill-solid)',
'error-fg': 'var(--color-threat-solid)',
'warning-fg': 'var(--color-risk-solid)',
'info-fg': 'var(--color-primary-solid)'
},
// ─────────────────────────────────────────────────────────────────────
// EmptyState — passive "nothing here / no results" surface. Public
// surface is intentionally small: spacing + the media plate + the
// description reading measure; density rides data-size internally.
// ─────────────────────────────────────────────────────────────────────
'empty-state': {
gap: 'var(--space-3)',
'actions-gap': 'var(--space-2)',
'media-bg': 'var(--color-neutral-track)',
'media-fg': 'var(--color-content-muted)',
'media-radius': 'var(--radius-lg)',
'description-measure': '45ch',
// Density by size — sm is the in-collection empty (table / select /
// command), md the panel default, lg the full-page hero.
'padding-block-sm': 'var(--space-4)',
'padding-block-md': 'var(--space-8)',
'padding-block-lg': 'var(--space-12)',
'padding-inline-sm': 'var(--space-3)',
'padding-inline-md': 'var(--space-4)',
'padding-inline-lg': 'var(--space-5)',
// The title steps ONE discrete size ABOVE the component size: hierarchy
// without coupling to a heading level (level is semantic, size is visual).
'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)',
'description-font-size-sm': 'var(--size-sm-font-size)',
'description-font-size-md': 'var(--size-md-font-size)',
'description-font-size-lg': 'var(--size-md-font-size)',
// The glyph; its tinted plate is derived from it (2x) in the recipe.
'media-glyph-size-sm': 'var(--size-sm-icon-size)',
'media-glyph-size-md': 'var(--size-lg-icon-size)',
'media-glyph-size-lg': 'var(--size-xl-icon-size)',
'title-font-weight': 'var(--font-weight-semibold)',
'title-line-height': 'var(--size-sm-font-line-height)',
'title-fg': 'var(--color-content-primary)',
'description-line-height': 'var(--size-md-font-line-height)',
'description-fg': 'var(--color-content-muted)'
},
// ─────────────────────────────────────────────────────────────────────
// 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).
fg: '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-fg': '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)',
'hover-track-bg': '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-fg': 'var(--color-border-default)',
'disabled-opacity': 'var(--opacity-disabled)',
'panel-bg': 'transparent',
'handle-size': 'var(--space-2)',
'handle-bg': 'var(--color-border-subtle)',
'active-handle-bg': '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.
// Kbd. `font-family` is NOT here: it reads `--style-code-font-family`, the
// typographic layer, and a consumer does not re-coin its layer's vocabulary
// (same call as code-block). The em-relative padding and min-width ARE knobs:
// a key cap grows with its letter, on purpose.
// Virtual-list paints chrome only — soma owns the windowing math. Size moves the
// chrome TYPE and nothing else: item dimensions come from props, and the radius
// is decoupled from size on purpose (Radix model, constant across the five).
'virtual-list': {
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-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': {
parts: ['root'],
declarations: [
{ value: 'var(--virtual-list-font-size-md)', scope: 'host' },
{ value: 'var(--virtual-list-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--virtual-list-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--virtual-list-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--virtual-list-font-size-xl)', scope: 'size:xl' }
]
},
radius: 'var(--radius-default)',
'font-family': 'var(--style-label-font-family)',
fg: 'var(--color-content-primary)',
// Shared by surface and outline; ghost's `transparent` is variant identity.
'border-width': 'var(--border-width)',
border: 'var(--color-border-default)',
'surface-bg': 'var(--color-surface-default)'
},
// Virtual-grid paints chrome only — soma owns the windowing math. Size moves the
// chrome TYPE and nothing else: cell dimensions come from props, and the radius
// is decoupled from size on purpose (Radix model, constant across the five).
'virtual-grid': {
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-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': {
parts: ['root'],
declarations: [
{ value: 'var(--virtual-grid-font-size-md)', scope: 'host' },
{ value: 'var(--virtual-grid-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--virtual-grid-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--virtual-grid-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--virtual-grid-font-size-xl)', scope: 'size:xl' }
]
},
radius: 'var(--radius-default)',
'font-family': 'var(--style-label-font-family)',
fg: 'var(--color-content-primary)',
// Shared by surface and outline; ghost's `transparent` is variant identity.
'border-width': 'var(--border-width)',
border: 'var(--color-border-default)',
'surface-bg': 'var(--color-surface-default)'
},
// Announce is an ARIA live-region SERVICE: its host is `display: contents`
// and the region is visually hidden by default. Everything here is the card
// chrome for the minority case where the consumer opts the region IN to
// visible UI (`visuallyHidden={false}`). `font-family` is absent on purpose:
// it reads the `--style-label-*` layer, which a consumer does not re-coin.
announce: {
fg: 'var(--color-content-primary)',
'padding-block': 'var(--space-2)',
'padding-inline': 'var(--space-3)',
radius: 'var(--radius-md)',
'border-width': 'var(--border-width)',
border: 'var(--color-border-default)',
bg: 'var(--color-surface-default)',
'font-size': 'var(--size-sm-font-size)',
'line-height': 'var(--leading-ui)'
},
// ScrollFrames is LAYOUT, not chrome: a tall track whose sticky viewport pins
// the media full-bleed while the track scrolls. The only thing it paints is
// the loading caption, so that is the whole contract.
// `--_scroll-frames-viewport-block-size` stays private on purpose: it is the
// hatch a consumer overrides to fit a container (100%) instead of the window
// (100vh) — layout geometry the SITE sets, not a theme value.
'scroll-frames': {
'loader-inset-block-end': '1.5rem',
'loader-font-size': 'var(--size-xs-font-size)',
'loader-letter-spacing': 'var(--tracking-widest)',
'loader-fg': 'var(--color-neutral-text)'
},
'menu-dial': {
// The dial owns ONLY its own chrome: the fan gap, the entrance stagger step
// and the backdrop tint. Actions are composed Buttons, and the floating
// placement belongs to the `viewport-placement` layer — whose override
// slots (`--_viewport-placement-offset` / `-z`) the recipe writes into.
// Those are NOT re-coined here: the axis is the layer's, and coining
// `placement-offset` would be the parallel vocabulary rule 2 forbids.
gap: 'var(--space-3)',
// The interval BETWEEN siblings, not an animation duration: it is what
// gives the fan its character.
'stagger-step': '28ms',
'backdrop-bg': 'color-mix(in oklab, var(--color-neutral-text-strong) 40%, transparent)'
},
// Knob. FOUR `--knob-*` names are deliberately absent: `progress`, `angle`,
// `start-angle` and `sweep` are written INLINE by the provider from control
// state (`knob-provider.svelte.ts:358`) — a value channel, not a theme
// surface, and no contract can outrank an inline style.
//
// The size axis is sm|md|lg|xl (no xs). It moves here rather than staying in
// a private on purpose: the recipe used to read `var(--knob-gap,
// var(--_knob-gap))`, where the public goes FIRST — so declaring the public
// in the contract would have killed the per-size scale outright.
// SkinMediaPlayer is a FIXED-TONE component: its hues are the depiction of
// physical objects (mahogany, brushed aluminium, chrome, ivory), not theme
// roles, and they live as literals in the SVG paint under the exception
// `recipe-css-contract.test.ts` already carves. What CAN be themed is its
// size — the recipe itself calls these "design values: retune here, in one
// place" — plus the unlit lamp and the key's hit area.
//
// The size axis is the PLAYER's: it stamps `data-size` on its own root and
// the skin reads it from there, so the resolved name targets the skin part
// while the scope comes from the ancestor.
'skin-media-player': {
// The base width is PER SKIN, not one number: each depicted object has its
// own natural width (a cassette is not a turntable). The root default is
// the deck's, which `controls.ts` supplies per device.
'base-width': '480px',
'record-base-width': '340px',
'cassette-base-width': '300px',
'deck-base-width': '480px',
'scale-sm': '0.7',
'scale-md': '1',
'scale-lg': '1.35',
'scale-xl': '1.7',
// An unlit bulb still shows its glass.
'lamp-opacity': '0.18',
'key-radius': 'var(--radius-sm)'
},
// TextCircular already consumed five publics through
// `var(--text-circular-x, <default>)` without declaring any of them, so a
// theme could not see them. Declaring is safe here (unlike `knob`): no
// per-size private hides behind the fallback, so the public cannot kill a
// scale. `color: currentColor` stays correct as a token — a custom property
// does not resolve `currentColor` until the point of use.
// TextFocus, same shape as text-circular: five publics were consumed through a
// fallback and none of them declared. The corner is a BRACKET drawn with
// borders — two of its four sides are zeroed per position — so its size is one
// knob, its stroke another. The `-0.625rem` offsets are NOT knobs: they place
// the bracket just outside the frame and follow from size + stroke.
'text-focus': {
gap: 'var(--space-4)',
'word-weight': '900',
'word-size': 'var(--font-size-4xl, 3rem)',
'corner-size': '1rem',
'corner-width': '3px',
// 3px verbatim, NOT `var(--radius-xs)`: that primitive DOES NOT EXIST.
// The recipe wrote `var(--radius-xs, 3px)` and the fallback was what
// painted — tokenizing the phantom name turned the declaration invalid and
// the corners square (48 diffs, measured 2026-08-22).
'corner-radius': '3px',
// The stroke and its halo: same accent, two layers, declared apart so a
// theme can tint the glow differently from the border.
'border-fg': 'var(--color-primary-solid)',
'glow-fg': 'var(--color-primary-solid)'
},
'text-circular': {
size: '12.5rem',
radius: 'var(--radius-full)',
weight: '900',
fg: 'currentColor',
'font-size': '1.5rem',
// The character's turn, kept verbatim: it is a gesture, not four axes.
'char-transition': 'all 0.5s cubic-bezier(0, 0, 0, 1)'
},
// TextGradient, third of the same shape: four publics consumed through a
// fallback and none declared, plus three literals. No size axis and no
// private behind the fallbacks, so declaring cannot kill a scale.
//
// `border-width` is the frame's stroke: the hole of the border is
// `calc(100% - 2px)`, which is TWO strokes of the linear-gradient showing
// around it, so the knob is the stroke and the hole derives from it.
// `blur` goes to the blur scale rather than a literal 4px because blur is a
// METRIC family under the scaling axis (theming §23) — a literal is blind to
// the global zoom the rest of the shell obeys.
//
// NOT declared: `duration` and `play-direction`. The wrapper writes both
// INLINE from props (`animationSpeed`, `yoyo`), and no contract outranks an
// inline style — same value channel as knob's four, carousel's item-gap and
// drag-drop's preview-z.
'text-gradient': {
radius: 'var(--radius-lg)',
'font-weight': 'var(--font-weight-medium)',
blur: 'var(--blur-sm)',
'border-padding-block': 'var(--space-1)',
'border-padding-inline': 'var(--space-2)',
'border-width': 'var(--border-width)',
// The seed hardcoded #000 here; the hole follows the theme's surface.
'border-inner-bg': 'var(--color-surface-default)'
},
knob: {
'gap-sm': 'var(--size-sm-gap)',
'gap-md': 'var(--size-md-gap)',
'gap-lg': 'var(--size-lg-gap)',
'gap-xl': 'var(--size-xl-gap)',
// The dial is TWICE the control height at every size — a proportion of the
// bundle, kept verbatim per coordinate.
'diameter-sm': 'calc(var(--size-sm-control-height) * 2)',
'diameter-md': 'calc(var(--size-md-control-height) * 2)',
'diameter-lg': 'calc(var(--size-lg-control-height) * 2)',
'diameter-xl': 'calc(var(--size-xl-control-height) * 2)',
'label-font-size-sm': 'var(--size-sm-font-size)',
'label-font-size-md': 'var(--size-md-font-size)',
'label-font-size-lg': 'var(--size-lg-font-size)',
'label-font-size-xl': 'var(--size-xl-font-size)',
'value-font-size-sm': 'var(--size-sm-font-size)',
'value-font-size-md': 'var(--size-md-font-size)',
'value-font-size-lg': 'var(--size-lg-font-size)',
'value-font-size-xl': 'var(--size-xl-font-size)',
gap: {
declarations: [
{ value: 'var(--knob-gap-md)', scope: 'host' },
{ value: 'var(--knob-gap-sm)', scope: 'size:sm' },
{ value: 'var(--knob-gap-lg)', scope: 'size:lg' },
{ value: 'var(--knob-gap-xl)', scope: 'size:xl' }
]
},
diameter: {
declarations: [
{ value: 'var(--knob-diameter-md)', scope: 'host' },
{ value: 'var(--knob-diameter-sm)', scope: 'size:sm' },
{ value: 'var(--knob-diameter-lg)', scope: 'size:lg' },
{ value: 'var(--knob-diameter-xl)', scope: 'size:xl' }
]
},
'label-font-size': {
declarations: [
{ value: 'var(--knob-label-font-size-md)', scope: 'host' },
{ value: 'var(--knob-label-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--knob-label-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--knob-label-font-size-xl)', scope: 'size:xl' }
]
},
'value-font-size': {
declarations: [
{ value: 'var(--knob-value-font-size-md)', scope: 'host' },
{ value: 'var(--knob-value-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--knob-value-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--knob-value-font-size-xl)', scope: 'size:xl' }
]
},
// The dial face and its ring.
radius: 'var(--radius-full)',
shadow: 'var(--shadow-2)',
'dragging-shadow': 'var(--shadow-3)',
'track-bg': 'var(--color-neutral-track)',
'face-bg': 'var(--color-surface-raised)',
// SPLIT from a single `--knob-arc-width` that carried two meanings with two
// different defaults: the face inset (--space-3) and the pointer's top
// offset (--space-2). One name that does not distinguish what it should.
'arc-width': 'var(--space-3)',
'pointer-inset': 'var(--space-2)',
'pointer-width': '3px',
'pointer-bg': 'var(--color-neutral-text-strong)',
'indicator-radius': 'var(--radius-sm)',
'value-fg': 'var(--color-neutral-text)',
'label-fg': 'var(--color-neutral-text)',
'label-line-height': 'var(--leading-ui)'
},
kbd: {
'font-size': 'var(--size-xs-font-size)',
'font-weight': 'var(--font-weight-medium)',
'letter-spacing': 'var(--tracking-wide)',
fg: 'var(--color-content-primary)',
radius: 'var(--radius-sm)',
'padding-inline': '0.4em',
'padding-block': '0.18em',
'min-width': '1.5em',
// Shared by the surface and outline variants.
'border-width': 'var(--border-width)',
border: 'var(--color-neutral-border)',
'surface-bg': 'var(--color-neutral-element)',
// The physical key-cap relief: a heavier bottom edge plus an inner
// highlight. Surface only — outline and ghost are flat by identity.
'surface-border-block-end-width': 'var(--border-width-medium)',
'surface-shadow': '0 1px 0 var(--color-neutral-border) inset'
},
// LinkPreview's panel is PORTALED and its `data-size` sits on the CONTENT,
// not on the trigger — hence `parts: ['content']` on every resolved name; a
// cascade on the host would never reach it. Size moves PHYSICAL dimensions
// only: family, weight and line-height are constant across sizes on purpose
// (the recipe says so), so those are flat.
'link-preview': {
'padding-block-xs': 'var(--space-2)',
'padding-block-sm': 'var(--space-2)',
'padding-block-md': 'var(--space-3)',
'padding-block-lg': 'var(--space-4)',
'padding-block-xl': 'var(--space-5)',
'padding-inline-xs': 'var(--space-3)',
'padding-inline-sm': 'var(--space-3)',
'padding-inline-md': 'var(--space-4)',
'padding-inline-lg': 'var(--space-5)',
'padding-inline-xl': 'var(--space-6)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-sm-font-size)',
'font-size-lg': 'var(--size-md-font-size)',
'font-size-xl': 'var(--size-md-font-size)',
'min-width-xs': '11rem',
'min-width-sm': '12rem',
'min-width-md': '14rem',
'min-width-lg': '18rem',
'min-width-xl': '22rem',
'padding-block': {
parts: ['content'],
declarations: [
{ value: 'var(--link-preview-padding-block-md)', scope: 'host' },
{ value: 'var(--link-preview-padding-block-xs)', scope: 'size:xs' },
{ value: 'var(--link-preview-padding-block-sm)', scope: 'size:sm' },
{ value: 'var(--link-preview-padding-block-lg)', scope: 'size:lg' },
{ value: 'var(--link-preview-padding-block-xl)', scope: 'size:xl' }
]
},
'padding-inline': {
parts: ['content'],
declarations: [
{ value: 'var(--link-preview-padding-inline-md)', scope: 'host' },
{ value: 'var(--link-preview-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--link-preview-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--link-preview-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--link-preview-padding-inline-xl)', scope: 'size:xl' }
]
},
'font-size': {
parts: ['content'],
declarations: [
{ value: 'var(--link-preview-font-size-md)', scope: 'host' },
{ value: 'var(--link-preview-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--link-preview-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--link-preview-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--link-preview-font-size-xl)', scope: 'size:xl' }
]
},
'min-width': {
parts: ['content'],
declarations: [
{ value: 'var(--link-preview-min-width-md)', scope: 'host' },
{ value: 'var(--link-preview-min-width-xs)', scope: 'size:xs' },
{ value: 'var(--link-preview-min-width-sm)', scope: 'size:sm' },
{ value: 'var(--link-preview-min-width-lg)', scope: 'size:lg' },
{ value: 'var(--link-preview-min-width-xl)', scope: 'size:xl' }
]
},
// Flat: one value across the five sizes.
'max-width': 'min(28rem, calc(100vw - var(--space-4)))',
gap: 'var(--space-2)',
radius: 'var(--radius-md)',
fg: 'var(--color-content-primary)',
'font-weight': 'var(--font-weight-regular)',
// `font-family` / `line-height` are declared even though the `overlay` depth
// plane currently outranks them by LOAD ORDER at an equal (0,1,0) — see the
// recipe. Keeping them is the conservative call: retiring them would fix
// that tie in the plane's favour, and who owns overlay typography is a
// signature still open (next-features §12.9, 20 components affected).
'font-family': 'var(--style-label-font-family)',
'line-height': 'var(--leading-normal)',
// Trigger. The chained fallback is PART of the value — the comma is not a
// second knob — so it travels verbatim.
'trigger-radius': 'var(--radius-sm)',
'trigger-fg': 'var(--color-primary-text, var(--color-content-primary))',
'hover-trigger-fg': 'var(--color-primary-solid, var(--color-content-primary))',
// The resting underline: a faint mix of the ink itself. The mix IS the
// look, so the knob is the whole declaration, not the 35 %.
'trigger-underline': 'color-mix(in srgb, currentColor 35%, transparent)'
// NO arrow tokens: its fill/stroke consume the depth system's `overlay`
// plane so the caret always matches the panel (A1, like popover). Coining
// `arrow-fg` would break exactly the guarantee that rule provides.
},
// PickerShell is a COMPONENT the composite pickers import (date, color,
// chronos), not a shared CSS layer — there is no layer hook and no borrowed
// vocabulary.
//
// Its size axis is `data-picker-size` on `[data-popover-content]`, a PORTALED
// ancestor — not `data-size` on itself. The TSC's `size:` scope would emit
// `[data-picker-shell][data-size='x']`, which never matches, so these are
// FLAT public coordinates and the recipe reads them from its own
// `[data-popover-content][data-picker-size='X']` selectors. The base rule is
// the `sm` default; `md` only re-declares what actually changes.
'picker-shell': {
'section-gap-xs': 'var(--space-2)',
'section-gap-sm': 'var(--space-3)',
'section-gap-md': 'var(--space-3-5)',
'section-gap-lg': 'var(--space-4)',
// Header's bottom inset and footer's top inset, one number so the two rows
// stay symmetric around the body.
'row-spacing-xs': 'var(--space-2)',
'row-spacing-sm': 'var(--space-3)',
'row-spacing-lg': 'var(--space-4)',
'footer-gap-xs': 'var(--space-1-5)',
'footer-gap-sm': 'var(--space-2)',
'footer-gap-lg': 'var(--space-2-5)',
'header-font-size-xs': 'var(--size-xs-font-size)',
'header-font-size-sm': 'var(--size-sm-font-size)',
'header-font-size-md': 'var(--size-md-font-size)',
'header-font-size-lg': 'var(--size-md-font-size)',
// Header and footer share the rule that separates them from the body: one
// width, one colour.
'border-width': 'var(--border-width)',
border: 'var(--color-border-subtle)',
'header-gap': 'var(--space-2)',
'header-fg': 'var(--color-content-primary)',
'header-font-weight': 'var(--font-weight-medium)',
'header-line-height': 'var(--leading-ui)',
'body-gap': 'var(--space-3)',
// The time row of the composite date+time pickers. It hangs off CLASS
// hooks (`.picker-time-row`) and carries a THIRD axis — `data-size` on the
// class itself, default md — so its coordinates are flat too. The class
// hooks are pre-existing debt (next-features §13, same as gradient-picker).
'time-row-gap': 'var(--space-3)',
'time-row-padding-block-start': 'var(--space-2)',
'time-row-padding-inline': 'var(--space-2)',
'time-row-border-width': 'var(--border-width)',
'time-row-border': 'var(--color-neutral-border)',
'time-label-fg': 'var(--color-content-secondary)',
'time-label-font-size-xs': 'var(--size-xxs-font-size)',
'time-label-font-size-sm': 'var(--size-xs-font-size)',
'time-label-font-size-md': 'var(--size-sm-font-size)',
'time-label-font-size-lg': 'var(--size-md-font-size)'
},
// CardGroup is a COMPOSITION SHELL (ToggleGroup + Collapsible + Card +
// Button): the cards' chrome belongs to Card's recipe and the title's to
// Button's. Only group-level visual lives here. The size axis is sm|md|lg —
// `CardGroupSize = Extract<Size, 'sm' | 'md' | 'lg'>` — so there are no
// xs/xl coordinates to write.
'card-group': {
'padding-sm': 'var(--space-3)',
'padding-md': 'var(--space-4)',
'padding-lg': 'var(--space-5)',
'grid-gap-sm': 'var(--space-2)',
'grid-gap-md': 'var(--space-3)',
'grid-gap-lg': 'var(--space-4)',
padding: {
declarations: [
{ value: 'var(--card-group-padding-md)', scope: 'host' },
{ value: 'var(--card-group-padding-sm)', scope: 'size:sm' },
{ value: 'var(--card-group-padding-lg)', scope: 'size:lg' }
]
},
'grid-gap': {
declarations: [
{ value: 'var(--card-group-grid-gap-md)', scope: 'host' },
{ value: 'var(--card-group-grid-gap-sm)', scope: 'size:sm' },
{ value: 'var(--card-group-grid-gap-lg)', scope: 'size:lg' }
]
},
// The card corner the group nests concentrically against. The recipe
// COMPUTES `--shape-outer-radius` from this plus `padding` (§30), so a
// theme moves the two coordinates and the outer radius recomposes itself —
// that system token is not re-coined here.
'card-radius': 'var(--radius-md)',
// Title — both forms (the disclosure Button and the static heading) share
// the weight. Everything else about the button is Button's recipe.
'title-font-weight': 'var(--font-weight-semibold)',
'title-fg': 'var(--color-content-primary)',
'title-font-family': 'var(--style-label-font-family)',
'title-font-size': 'var(--size-md-font-size)',
'title-line-height': 'var(--leading-ui)',
// The gap under the header, the one part of its padding shorthand that is
// not the group's own inset.
'title-padding-block-end': 'var(--space-2)',
// Chevron — own geometry (two borders rotated 45°), not a composed icon.
// Its colour is `currentColor` on purpose: it inherits from the title.
'chevron-size': '0.5em',
'chevron-width': 'var(--border-width-medium)',
'description-fg': 'var(--color-content-muted)',
'description-font-size': 'var(--size-sm-font-size)',
'description-padding-block-end': 'var(--space-2)'
},
// CodeBlock. The `[data-code-block-code]` node is deliberately ABSENT from
// this contract: it consumes the `--style-code-*` typographic layer (with the
// wrapper's per-instance escape hatch `--_code-block-font-size` in front),
// and a layer's vocabulary is not re-coined by its consumer (shared-layer
// rule 2, the D-TH.2-b reasoning). Everything here is the SHELL around it.
'code-block': {
radius: 'var(--radius-md)',
fg: 'var(--color-content-primary)',
// Shared by the surface/outline borders and the header's bottom rule.
// Ghost's `transparent` / `border: 0` are variant IDENTITY, not knobs.
'border-width': 'var(--border-width)',
border: 'var(--color-neutral-border)',
'surface-bg': 'var(--color-neutral-element)',
// Header strip.
'header-gap': 'var(--space-2)',
'header-padding-inline': 'var(--space-3)',
'header-padding-block': 'var(--space-1)',
'header-bg': 'var(--color-neutral-track)',
'title-font-family': 'var(--font-family-mono)',
'title-font-size': 'var(--size-xs-font-size)',
'title-fg': 'var(--color-content-secondary)',
'lang-font-family': 'var(--font-family-mono)',
'lang-font-size': 'var(--size-xxs-font-size)',
'lang-fg': 'var(--color-content-muted)',
'lang-letter-spacing': 'var(--tracking-caps)',
// Copy slot. `copy-fg` tints the transient "Copied" INDICATOR, not the
// trigger: the trigger's chrome comes from button.css.
'copy-gap': 'var(--space-1)',
'copy-font-size': 'var(--size-xs-font-size)',
'copy-fg': 'var(--color-affirm-text)',
// The <pre> gutter around the code.
'pre-padding': 'var(--space-3)'
},
combobox: {
'content-z': 'var(--z-index-overlay-floating)',
// ── Size axis (theme-reach 2026-08-20) ───────────────────────────────
// Two tiers: per-size coordinates at a stable scope + ONE resolved name
// the recipe consumes. The control's height/font/gap ride the `--size-{k}-*`
// bundle (identical computed, and the raw primitive is forbidden there);
// its inline padding DEVIATES from the bundle on xs/sm/lg/xl and keeps
// today's value verbatim — the deviation stays visible instead of being
// silently rounded to the bundle (that would be a visual change).
'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-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-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-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)',
'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-font-size-xs': 'var(--size-xs-font-size)',
'content-font-size-sm': 'var(--size-sm-font-size)',
'content-font-size-md': 'var(--size-md-font-size)',
'content-font-size-lg': 'var(--size-lg-font-size)',
'content-font-size-xl': 'var(--size-xl-font-size)',
// The resolved coordinates. The CONTENT is portaled, so its per-size
// cascade targets that part explicitly (`parts`) — the same reason
// `select` declares its accent on trigger + content (tsc.md, multi-part).
'control-height': {
parts: ['control', 'input'],
declarations: [
{ value: 'var(--combobox-control-height-md)', scope: 'host' },
{ value: 'var(--combobox-control-height-xs)', scope: 'size:xs' },
{ value: 'var(--combobox-control-height-sm)', scope: 'size:sm' },
{ value: 'var(--combobox-control-height-lg)', scope: 'size:lg' },
{ value: 'var(--combobox-control-height-xl)', scope: 'size:xl' }
]
},
'control-font-size': {
parts: ['control', 'input'],
declarations: [
{ value: 'var(--combobox-control-font-size-md)', scope: 'host' },
{ value: 'var(--combobox-control-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--combobox-control-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--combobox-control-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--combobox-control-font-size-xl)', scope: 'size:xl' }
]
},
'control-gap': {
parts: ['control', 'input'],
declarations: [
{ value: 'var(--combobox-control-gap-md)', scope: 'host' },
{ value: 'var(--combobox-control-gap-xs)', scope: 'size:xs' },
{ value: 'var(--combobox-control-gap-sm)', scope: 'size:sm' },
{ value: 'var(--combobox-control-gap-lg)', scope: 'size:lg' },
{ value: 'var(--combobox-control-gap-xl)', scope: 'size:xl' }
]
},
'control-padding-inline': {
parts: ['control', 'input'],
declarations: [
{ value: 'var(--combobox-control-padding-inline-md)', scope: 'host' },
{ value: 'var(--combobox-control-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--combobox-control-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--combobox-control-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--combobox-control-padding-inline-xl)', scope: 'size:xl' }
]
},
'content-padding-block': {
parts: ['content'],
declarations: [
{ value: 'var(--combobox-content-padding-block-md)', scope: 'host' },
{ value: 'var(--combobox-content-padding-block-xs)', scope: 'size:xs' },
{ value: 'var(--combobox-content-padding-block-sm)', scope: 'size:sm' },
{ value: 'var(--combobox-content-padding-block-lg)', scope: 'size:lg' },
{ value: 'var(--combobox-content-padding-block-xl)', scope: 'size:xl' }
]
},
'content-padding-inline': {
parts: ['content'],
declarations: [
{ value: 'var(--combobox-content-padding-inline-md)', scope: 'host' },
{ value: 'var(--combobox-content-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--combobox-content-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--combobox-content-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--combobox-content-padding-inline-xl)', scope: 'size:xl' }
]
},
'content-font-size': {
parts: ['content'],
declarations: [
{ value: 'var(--combobox-content-font-size-md)', scope: 'host' },
{ value: 'var(--combobox-content-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--combobox-content-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--combobox-content-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--combobox-content-font-size-xl)', scope: 'size:xl' }
]
},
// ── Chrome ───────────────────────────────────────────────────────────
gap: 'var(--space-2)',
'font-family': 'var(--style-label-font-family)',
fg: 'var(--color-content-primary)',
'border-width': 'var(--border-width)',
'control-radius': 'var(--radius-md)',
'control-bg': 'var(--color-surface-default)',
'control-border': 'var(--color-border-default)',
'control-shadow': 'var(--shadow-subtle)',
'control-fg': 'var(--color-content-primary)',
'control-line-height': 'var(--leading-ui)',
'disabled-control-bg': 'var(--color-surface-raised)',
'disabled-control-border': 'var(--color-border-subtle)',
'disabled-control-fg': 'var(--color-content-disabled)',
'placeholder-fg': 'var(--color-content-muted)',
'disabled-input-fg': 'var(--color-content-disabled)',
'trigger-size': 'var(--size-xs-control-height)',
'trigger-radius': 'var(--radius-full)',
'trigger-fg': 'var(--color-content-secondary)',
'indicator-size': 'var(--size-sm-icon-size)',
// ── Panel ────────────────────────────────────────────────────────────
'content-min-width': '14rem',
'content-max-width': '28rem',
'content-max-height': '22rem',
'content-radius': 'var(--radius-lg)',
'content-fg': 'var(--color-content-primary)',
'content-font-family': 'var(--style-label-font-family)',
'content-line-height': 'var(--leading-ui)',
'viewport-gap': 'var(--space-0-5)',
'viewport-padding-block-end': 'var(--space-0-5)',
'scrollbar-inset': 'var(--space-3-5)',
'group-gap': 'var(--space-1)',
'group-padding-block': 'var(--space-1)',
'group-heading-padding-block': 'var(--space-1)',
'group-heading-padding-inline': 'var(--space-2)',
'group-heading-bg': 'var(--color-surface-overlay)',
'group-heading-fg': 'var(--color-content-muted)',
'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-line-height': 'var(--leading-ui)',
'group-heading-letter-spacing': 'var(--tracking-caps)',
'separator-thickness': '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-fg': 'var(--color-content-primary)',
'disabled-item-fg': 'var(--color-content-disabled)',
'item-indicator-size': 'var(--size-sm-icon-size)',
// ── Selected tags ────────────────────────────────────────────────────
'selected-tags-gap': 'var(--space-1-5)',
'selected-tags-margin-block-end': 'var(--space-2)',
'selected-tag-gap': 'var(--space-1)',
'selected-tag-padding-inline-start': 'var(--space-2)',
'selected-tag-padding-inline-end': 'var(--space-0-5)',
// The tag is the control MINUS a step: the expression is the knob, so a
// theme moving `control-height` still gets a proportional tag.
'selected-tag-inset': 'var(--space-2)',
'selected-tag-radius': 'var(--radius-sm)',
'selected-tag-bg': 'var(--color-surface-raised)',
'selected-tag-border': 'var(--color-border-default)',
'selected-tag-fg': 'var(--color-content-primary)',
'selected-tag-line-height': 'var(--leading-ui)',
// Optical −1px on the token-derived size (kept verbatim from the recipe).
'selected-tag-font-size-offset': '1px',
'selected-tag-remove-size': 'var(--space-4)',
'selected-tag-remove-margin-inline-start': 'var(--space-0-5)',
'selected-tag-remove-radius': 'var(--radius-sm)',
// Accent palette forward (THM-2) — the cascade lived on FOUR parts
// (control / input / trigger / content), so the forward targets all four
// via `parts`. Host = primary; shared layer routes roles + 33 scales + custom.
'_palette-track': {
parts: ['control', 'input', 'trigger', 'content'],
declarations: [{ value: 'var(--color-primary-track)', scope: 'host' }]
},
'_palette-border': {
parts: ['control', 'input', 'trigger', 'content'],
declarations: [{ value: 'var(--color-primary-border)', scope: 'host' }]
},
'_palette-text': {
parts: ['control', 'input', 'trigger', 'content'],
declarations: [{ value: 'var(--color-primary-text)', scope: 'host' }]
}
},
// Command palette. Size axis sm | md | lg on the root; the panel is the same
// surface inline and inside <Command.Dialog>, so every knob lives here and
// the dialog only swaps the radius reference.
command: {
// Per-size coordinates - all four map 1:1 to the size bundle (measured
// 2026-08-20: space-2/3/4 and font-size sm/md/lg ARE the bundle's).
'input-padding-inline-sm': 'var(--size-sm-padding-inline)',
'input-padding-inline-md': 'var(--size-md-padding-inline)',
'input-padding-inline-lg': 'var(--size-lg-padding-inline)',
'input-font-size-sm': 'var(--size-sm-font-size)',
'input-font-size-md': 'var(--size-md-font-size)',
'input-font-size-lg': 'var(--size-lg-font-size)',
'item-padding-inline-sm': 'var(--size-sm-padding-inline)',
'item-padding-inline-md': 'var(--size-md-padding-inline)',
'item-padding-inline-lg': 'var(--size-lg-padding-inline)',
'item-font-size-sm': 'var(--size-sm-font-size)',
'item-font-size-md': 'var(--size-md-font-size)',
'item-font-size-lg': 'var(--size-lg-font-size)',
// The RESOLVED names - the only ones the recipe reads. Default `host`
// (= md) plus the two overrides; the root stamps `data-size` and every
// consumer hangs from it, so no `parts` are needed.
'input-padding-inline': {
declarations: [
{ value: 'var(--command-input-padding-inline-md)', scope: 'host' },
{ value: 'var(--command-input-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--command-input-padding-inline-lg)', scope: 'size:lg' }
]
},
'input-font-size': {
declarations: [
{ value: 'var(--command-input-font-size-md)', scope: 'host' },
{ value: 'var(--command-input-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--command-input-font-size-lg)', scope: 'size:lg' }
]
},
'item-padding-inline': {
declarations: [
{ value: 'var(--command-item-padding-inline-md)', scope: 'host' },
{ value: 'var(--command-item-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--command-item-padding-inline-lg)', scope: 'size:lg' }
]
},
'item-font-size': {
declarations: [
{ value: 'var(--command-item-font-size-md)', scope: 'host' },
{ value: 'var(--command-item-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--command-item-font-size-lg)', scope: 'size:lg' }
]
},
// Panel. `radius` is the INLINE palette's own surface radius; inside
// <Command.Dialog> the dialog panel is the surface and the recipe zeroes
// the command's radius, so the token has no consumer there (the input
// keeps its own `input-radius`). A re-point at the dialog's radius used
// to be claimed here - measured dead 2026-08-21 and retired.
radius: 'var(--radius-lg)',
'font-family': 'var(--style-label-font-family)',
'line-height': 'var(--leading-ui)',
fg: 'var(--color-content-primary)',
// Input - the boxed search field (same treatment as the Select trigger).
'input-padding-block': 'var(--space-1-5)',
'input-margin': 'var(--space-2)',
'input-border-width': 'var(--border-width)',
'input-border': 'var(--color-border-default)',
'input-radius': 'var(--radius-md)',
'input-bg': 'var(--color-surface-default)',
'input-fg': 'var(--color-content-primary)',
'input-placeholder-fg': 'var(--color-content-muted)',
'focus-input-border': 'var(--color-primary-border)',
// Loading bar under the input.
'loading-height': '2px',
'loading-fg': 'var(--color-primary-solid)',
// List viewport. `scrollbar-inset` is the extra inline room the design
// scrollbar takes when it shows - the same name combobox uses, so the
// two list surfaces read alike.
'viewport-max-block': 'min(60vh, 24rem)',
'viewport-gap': 'var(--space-1)',
'viewport-padding-inline': 'var(--space-2)',
'viewport-padding-block-end': 'var(--space-1)',
'scrollbar-inset': 'var(--space-3-5)',
'viewport-padding': 'var(--space-1)',
// Groups.
'group-gap': 'var(--space-1)',
'group-heading-padding-block': 'var(--space-1)',
'group-heading-fg': 'var(--color-content-muted)',
'group-heading-font-size': 'var(--size-xs-font-size)',
'group-heading-font-weight': 'var(--font-weight-medium)',
'group-heading-tracking': 'var(--tracking-caps)',
'group-items-gap': '1px',
// Rows.
'item-gap': 'var(--space-2)',
'item-fg': 'var(--color-content-primary)',
'item-icon-size': '1.125em',
'item-icon-fg': 'var(--color-content-secondary)',
'item-shortcut-gap': 'var(--space-1)',
'item-shortcut-fg': 'var(--color-content-muted)',
'item-shortcut-font-size': '0.875em',
'selected-item-sub-fg': 'var(--color-content-primary)',
// Empty state.
'empty-padding-block': 'var(--space-4)',
'empty-fg': 'var(--color-content-muted)',
// Separator.
'separator-height': 'var(--border-width)',
'separator-margin-block': 'var(--space-1)',
'separator-bg': 'var(--color-neutral-separator)'
},
// DragDrop — the drag preview/ghost that tracks the cursor. Floats above
// local content during an active drag.
'drag-drop': {
// The PREVIEW — the ghost that follows the pointer. It exists only during a
// drag, so neither the probe nor the guard sees it at rest.
'preview-padding-block': 'var(--space-2)',
'preview-padding-inline': 'var(--space-3)',
'preview-radius': 'var(--radius-md)',
'preview-border-width': 'var(--border-width)',
'preview-bg': 'var(--color-surface-default)',
'preview-fg': 'var(--color-content-primary)',
'preview-font-family': 'var(--style-label-font-family)',
'preview-font-size': 'var(--size-sm-font-size)',
// The lift under the ghost. The accent ring stacked on top of it comes from
// the palette forward and stays private.
'preview-shadow': 'var(--shadow-raised)',
// NO `preview-z`: soma writes `z-index: 9999` INLINE on the preview
// (`drag-drop-provider.svelte.ts:663,671`), so no cascade can win. Measured
// on the REAL node during an actual drag — the adversarial review of F2-B,
// 2026-08-22. Same class as carousel's retired `item-gap*`: a value channel
// of soma, not a theme surface (next-features §13).
// 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)',
// `-w` is soma's VALUE CHANNEL, not theming surface: the provider writes
// the active trigger's rect into it on every move and the recipe reads it
// with a `0px` fallback. `-x` sat here too and NOBODY read it — soma
// stamps the horizontal tracking as an inline `left` (its own README says
// so, and the recipe's comment records that adding `translateX(var(--…-x))`
// DOUBLED the offset), so the declaration moved nothing anywhere:
// retired 2026-08-23, 0 diffs, which is the proof it was dead. `-y` / `-h`
// were never declared, which is the same reading from the other side.
// The underline's own FORM — thickness, fill, radius — is what belongs to
// a theme, and those are the three below.
'indicator-w': '0px',
'indicator-thickness': 'var(--border-width-medium)',
'indicator-bg': 'var(--color-primary-solid)',
'indicator-radius': 'var(--radius-full)',
// The in-bar TRIGGER (and the Link, which shares its chrome) is a
// control, so it takes the canonical size bundle (`--size-{k}-*`) — the
// sidebar row's precedent, 2026-08-19. Until then its whole chrome lived
// in recipe privates: `--_navigation-menu-radius` pinned to
// `--radius-default` («decoupled from size», a line that read Radix as
// a rule when Radix paints nothing), `padding-block: var(--space-2)` as
// the height (a SPACE token riding the density-space scale: measured
// 33.5 / 36 / 41 px across sm/md/lg, only `md` on the control scale by
// arithmetic luck), and the hover / open fills hand-picked. None of it
// was reachable by a theme short of moving `--radius-default` for the
// whole system. Nav runs `sm..lg`, default `md` — and at `md` the
// default is exactly what shipped (36px · 6px radius).
'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-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-line-height-sm': 'var(--size-sm-font-line-height)',
'trigger-line-height-md': 'var(--size-md-font-line-height)',
'trigger-line-height-lg': 'var(--size-lg-font-line-height)',
'trigger-padding-inline-sm': 'var(--size-sm-padding-inline)',
'trigger-padding-inline-md': 'var(--size-md-padding-inline)',
'trigger-padding-inline-lg': 'var(--size-lg-padding-inline)',
'trigger-radius-sm': 'var(--size-sm-radius)',
'trigger-radius-md': 'var(--size-md-radius)',
'trigger-radius-lg': 'var(--size-lg-radius)',
// The resolved coordinates for the size in force (`host` = `md`, the
// component's default); each `size:` scope is the emitted cascade entry,
// so the recipe consumes ONE name per axis and the bundle guard sees
// the reference in the token, not a literal buried in a rule.
'trigger-height': {
declarations: [
{ value: 'var(--navigation-menu-trigger-height-md)', scope: 'host' },
{ value: 'var(--navigation-menu-trigger-height-sm)', scope: 'size:sm' },
{ value: 'var(--navigation-menu-trigger-height-lg)', scope: 'size:lg' }
]
},
'trigger-font-size': {
declarations: [
{ value: 'var(--navigation-menu-trigger-font-size-md)', scope: 'host' },
{ value: 'var(--navigation-menu-trigger-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--navigation-menu-trigger-font-size-lg)', scope: 'size:lg' }
]
},
'trigger-line-height': {
declarations: [
{ value: 'var(--navigation-menu-trigger-line-height-md)', scope: 'host' },
{ value: 'var(--navigation-menu-trigger-line-height-sm)', scope: 'size:sm' },
{ value: 'var(--navigation-menu-trigger-line-height-lg)', scope: 'size:lg' }
]
},
'trigger-padding-inline': {
declarations: [
{ value: 'var(--navigation-menu-trigger-padding-inline-md)', scope: 'host' },
{ value: 'var(--navigation-menu-trigger-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--navigation-menu-trigger-padding-inline-lg)', scope: 'size:lg' }
]
},
'trigger-radius': {
declarations: [
{ value: 'var(--navigation-menu-trigger-radius-md)', scope: 'host' },
{ value: 'var(--navigation-menu-trigger-radius-sm)', scope: 'size:sm' },
{ value: 'var(--navigation-menu-trigger-radius-lg)', scope: 'size:lg' }
]
},
// Text and the ACTIVE accent. Hover is NOT a token and NOT the
// recipe's: it is the system's state layer (`--state-hover`,
// `archetypes.css`, config `primitives.state`), painted on the trigger's
// OWN box and radius — the same hover every interactive element has. The
// grey square the author saw was that veil painted on the `<li>` (no
// radius, no shape) through a stray `archetype: 'item'`; the answer was
// to take the archetype off the wrapper, not to invent a nav-only hover.
'trigger-fg': 'var(--color-content-primary)',
'active-trigger-fg': 'var(--color-content-primary)',
'active-trigger-bg': 'var(--color-primary-element)',
'disabled-trigger-fg': 'var(--color-content-muted)',
// The bar's TYPE. The family completes the pair the size bundle already
// covered (`trigger-font-size` / `-line-height`, both consumed on the
// root): the value is the system's — the named typographic layer — and
// the KNOB is the component's, so a theme can set the nav's face without
// moving `label` across the app. Accordion's `trigger-font-family` is the
// mould. NOT declared for the panel: its `[data-depth='overlay']` plane
// writes `font-family` / `line-height` at the same specificity and later
// in the cascade, so a token there would read dead (§12.9, pending
// signature).
'trigger-font-family': 'var(--style-label-font-family)',
'trigger-gap': 'var(--space-1)',
// Chevron — the glyph is drawn with two borders, so ONE stroke token
// feeds both edges. Size stays a literal: every chevron in the catalogue
// is one (`nav-tree` 0.42em, `dropdown-menu` 0.625rem, `card-group`
// 0.5em) — a glyph is not a step of the spacing scale.
'chevron-size': '0.5rem',
'chevron-stroke': 'var(--border-width-medium)',
// The bar's rhythm rides the size axis: it was a private with THREE
// sources (0 / space-1 / space-2 by `data-size`), which is a SCALE, not a
// switch — so it goes up to the TSC like the trigger bundle above.
'list-gap-sm': '0',
'list-gap-md': 'var(--space-1)',
'list-gap-lg': 'var(--space-2)',
'list-gap': {
declarations: [
{ value: 'var(--navigation-menu-list-gap-md)', scope: 'host' },
{ value: 'var(--navigation-menu-list-gap-sm)', scope: 'size:sm' },
{ value: 'var(--navigation-menu-list-gap-lg)', scope: 'size:lg' }
]
},
// The mega-menu panel. Same names as the sibling menus (dropdown-menu /
// context-menu / select), different values — this panel is wider and
// rounder because it holds a grid of links, not a column of items.
'content-min-width': '14rem',
'content-padding': 'var(--space-2)',
'content-radius': 'var(--radius-lg)',
'content-fg': 'var(--color-content-primary)',
// The rows INSIDE the panel are the consumer's bare `<a>`, not the `Item`
// part (that is the bar's `<li>`) and not the bar `Link` (which shares the
// trigger's chrome): naming them `item-*` or `link-*` would promise a
// theme control over a node the token does not reach. Their tint keeps
// reading `trigger-fg`, as it shipped.
'content-link-padding-block': 'var(--space-2)',
'content-link-padding-inline': 'var(--space-3)',
'content-link-radius': 'var(--radius-md)'
},
// ─────────────────────────────────────────────────────────────────────
// 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)',
// El cromo del panel y de sus filas, gemelo del de context-menu: mismos
// nombres, misma doctrina. Con dos limpiezas de paso, las mismas que allí:
// el `font-family` era respaldo de SÍ MISMO y el peso del encabezado
// llevaba un `500` muerto detrás del token que ya vale 500.
// NO `content-font-family`: the panel carries `data-depth=overlay` and the
// PLANE declares the family at the same specificity, later in the cascade —
// it wins, so a token here would lie (measured 2026-08-23; PLAN §12.9).
'content-fg': 'var(--color-content-primary)',
'item-fg': 'var(--color-content-primary)',
'item-line-height': 'var(--leading-ui)',
// El galón del submenú: dos bordes girados 45°, un trazo y una caja.
'chevron-stroke': 'var(--border-width-medium)',
'chevron-size': '0.625rem',
'heading-fg': 'var(--color-content-muted)',
'heading-font-weight': 'var(--style-label-font-weight)',
'heading-letter-spacing': 'var(--tracking-caps)',
// El filo del separador es el trazo fino del sistema, no un 1px suelto.
'separator-thickness': 'var(--border-width)',
'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)',
'hover-item-bg': 'var(--color-primary-element)',
'hover-item-fg': '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 surface, the in-bar trigger and the role=dialog Panel
// (the form surface a bar entry can drop instead of a role=menu Content;
// the Content's chrome belongs to dropdown-menu, which paints it).
//
// The size axis is the bar's: the trigger resolves its step from the bar's
// `data-size`, the Panel from its own (it travels by portal). Nothing here
// is coined for the hover of the trigger — that veil is the SYSTEM's, on
// the archetype (recipe-contract §2), and the shorthand that overrides it
// today is a defect of the navigation-menu axis, not a knob to declare.
// ─────────────────────────────────────────────────────────────────────
menubar: {
// The bar.
bg: 'var(--color-surface-raised)',
'border-fg': 'var(--color-border-default)',
'border-width': 'var(--border-width)',
// Radius DECOUPLED from the size axis (Radix model): one value, and the
// shape engine derives the trigger's from it.
radius: 'var(--radius-default)',
'gap-sm': 'var(--space-0-5)',
'gap-md': 'var(--space-1)',
'gap-lg': 'var(--space-2)',
'padding-sm': 'var(--space-0-5)',
'padding-md': 'var(--space-1)',
'padding-lg': 'var(--space-2)',
'height-sm': 'var(--size-sm-control-height)',
'height-md': 'var(--size-md-control-height)',
'height-lg': 'var(--size-lg-control-height)',
// The in-bar trigger.
'trigger-fg': 'var(--color-content-primary)',
'trigger-font-family': 'var(--style-label-font-family)',
'trigger-font-weight': 'var(--font-weight-medium)',
'trigger-line-height': 'var(--leading-ui)',
'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-padding-inline-sm': 'var(--space-2)',
'trigger-padding-inline-md': 'var(--space-3)',
'trigger-padding-inline-lg': 'var(--space-4)',
// Open entry — its own chrome, not a hover: the pair travels together
// even where both read the same value today.
'open-trigger-bg': 'var(--color-primary-element)',
'open-trigger-fg': 'var(--color-content-primary)',
'disabled-trigger-fg': 'var(--color-content-muted)',
// The Panel. 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)',
'panel-fg': 'var(--color-content-primary)',
// NO `panel-font-family` / `panel-line-height`: the panel carries
// `data-depth='overlay'` and the PLANE declares both at the same
// specificity, later in the cascade — it wins, so a token here would lie
// (measured 2026-08-23; it is the open question of PLAN §12.9).
'panel-font-size-sm': 'var(--size-sm-font-size)',
'panel-font-size-md': 'var(--size-md-font-size)',
'panel-font-size-lg': 'var(--size-lg-font-size)'
},
// ─────────────────────────────────────────────────────────────────────
// ContextMenu — same surface as DropdownMenu, with a span Trigger.
// ─────────────────────────────────────────────────────────────────────
'context-menu': {
// El cromo propio del panel y de su disparador. Dos limpiezas de paso: el
// `font-family` leía `var(--style-label-font-family, var(--style-label-font-family))`
// —respaldo de sí mismo— y el peso del encabezado llevaba un `500` muerto
// detrás del token que ya vale 500.
// NO `content-font-family`: the panel carries `data-depth=overlay` and the
// PLANE declares the family at the same specificity, later in the cascade —
// it wins, so a token here would lie (measured 2026-08-23; PLAN §12.9).
'content-fg': 'var(--color-content-primary)',
'item-fg': 'var(--color-content-primary)',
'item-line-height': 'var(--leading-ui)',
// El galón del submenú: dos bordes girados 45°, un solo trazo.
'chevron-stroke': 'var(--border-width-medium)',
'heading-fg': 'var(--color-content-muted)',
'heading-font-weight': 'var(--style-label-font-weight)',
'heading-letter-spacing': 'var(--tracking-caps)',
// El disparador abierto se marca con un contorno DISCONTINUO: el estilo es
// identidad de la afordancia, el grosor y el color son knobs.
'open-trigger-outline-width': 'var(--border-width)',
'open-trigger-outline': 'var(--color-border-default)',
'open-trigger-radius': 'var(--radius-sm)',
'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)',
'hover-item-bg': 'var(--color-primary-element)',
'hover-item-fg': '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': {
// El hueco ENTRE los dos campos y su separador. La clave se llamaba
// `row-gap` y alimentaba el `column-gap`: nombre corregido, valor idéntico.
'column-gap': 'var(--space-3)',
// Y el de VERDAD: etiqueta ↔ fila de campos, que leía `--space-1-5` a pelo.
'row-gap': 'var(--space-1-5)',
// La raíz del rango NO es un `[data-field]`, así que no puede heredar la
// etiqueta de field.css (sus tokens de etiqueta viven en ese ámbito). Los
// mismos valores, nombrados aquí. Cuando el mandato de composición de Field
// (§5.3-3) convierta la raíz en un campo, estas tres desaparecen.
'label-fg': 'var(--color-content-primary)',
'label-font-size': 'var(--size-sm-font-size)',
'label-font-weight': 'var(--font-weight-regular)',
'separator-fg': 'var(--color-content-muted)',
'separator-glyph': "'—'"
},
// ─────────────────────────────────────────────────────────────────────
// TimeRangeField — layout only; chrome inherits from time-field.
// ─────────────────────────────────────────────────────────────────────
'time-range-field': {
// Gemelo exacto de date-range-field, y con el mismo nombre equivocado: la
// clave `row-gap` alimentaba el `column-gap`. Dos claves, cada una con el
// nombre de lo que mueve, y el computado sin tocar.
'column-gap': 'var(--space-3)',
'row-gap': 'var(--space-1-5)',
// La raíz del rango NO es un `[data-field]`: no puede heredar la etiqueta de
// field.css. Las tres desaparecen cuando el mandato §5.3-3 la convierta en
// un campo.
'label-fg': 'var(--color-content-primary)',
'label-font-size': 'var(--size-sm-font-size)',
'label-font-weight': 'var(--font-weight-regular)',
'separator-fg': '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',
// NO `offset` / `z` here (retired 2026-08-15). Placement values belong to
// the shared viewport-placement layer, which declares them itself in
// `lib/viewport-placement.css` — it has no recipe key at all, and that is
// what lets it live outside any component's directory. The one value Fab
// differs on (the `sticky` band) rides `--_viewport-placement-z` in its
// bridge. Minting `--fab-offset` / `--fab-z` only created a parallel
// vocabulary whose sole reader was the bridge translating it back.
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)',
'trigger-radius': 'var(--radius-full)',
'trigger-fg': 'var(--color-content-on-solid)',
'sector-shadow': 'var(--shadow-raised)',
// ONE ring colour for the two states: the hover ring is the SAME colour
// as the selection ring, told apart by its opacity (below) — the recipe
// says so and the census counted it twice.
'sector-ring-fg': 'var(--color-content-on-solid)',
'sector-ring-width': '2.5',
'sector-hover-ring-width': '2',
'label-font-family': 'var(--style-label-font-family)',
'label-font-weight': 'var(--style-label-font-weight)',
// A RATIO of its geometry box, not pixels, so the trigger's +/x scales with
// the ring sizing. (The option icon has no twin: the rule that would size
// it is DROPPED — see the CSS.)
'glyph-size': '42%',
'glyph-bar-thickness': '2px',
'glyph-bar-radius': 'var(--radius-full)',
'muted-opacity': '0.85',
'hover-ring-opacity': '0.65',
// ── `luminous` look: the lit rim (a blurred pass under a crisp one) ──
'rim-width': '2',
'rim-glow-width': '9',
'rim-glow-opacity': 'var(--opacity-45)',
'rim-glow-blur': 'var(--blur-sm)'
},
// ─────────────────────────────────────────────────────────────────────
// 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.