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

6097 lines
295 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: {
// ── 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.
'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',
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',
'layout-column-gap': 'var(--space-4)',
'required-fg': 'var(--color-risk-text)',
'control-height-xs': 'var(--size-xs-control-height)',
'control-height-sm': 'var(--size-sm-control-height)',
'control-height-md': 'var(--size-md-control-height)',
'control-height-lg': 'var(--size-lg-control-height)',
'control-height-xl': 'var(--size-xl-control-height)',
// Segment height — derived from the SIZE axis at the FIELD family level
// (B.1 mandate, audit S6): the segmented fields (date/time/color + ranges)
// read `--field-segment-height` instead of each re-declaring `height-{k}`.
// The `size:` TSC scope emits `[data-field][data-size='{k}']`; host default
// = md (the field's default size). Inert until a field wraps in `[data-field]`.
'segment-height': {
declarations: [
{ value: 'calc(var(--field-control-height-md) - var(--space-2))', scope: 'host' },
{ value: 'calc(var(--field-control-height-xs) - var(--space-2))', scope: 'size:xs' },
{ value: 'calc(var(--field-control-height-sm) - var(--space-2))', scope: 'size:sm' },
{ value: 'calc(var(--field-control-height-lg) - var(--space-2))', scope: 'size:lg' },
{ value: 'calc(var(--field-control-height-xl) - var(--space-2))', scope: 'size:xl' }
]
},
'control-padding-inline-xs': 'var(--space-2)',
'control-padding-inline-sm': 'var(--space-2-5)',
'control-padding-inline-md': 'var(--space-3)',
'control-padding-inline-lg': 'var(--space-3-5)',
'control-padding-inline-xl': 'var(--space-4)',
'control-gap-xs': 'var(--space-1)',
'control-gap-sm': 'var(--space-1-5)',
'control-gap-md': 'var(--space-2)',
'control-gap-lg': 'var(--space-2-5)',
'control-gap-xl': 'var(--space-3)',
'control-font-size-xs': 'var(--size-xs-font-size)',
'control-font-size-sm': 'var(--size-sm-font-size)',
'control-font-size-md': 'var(--size-md-font-size)',
'control-font-size-lg': 'var(--size-lg-font-size)',
'control-font-size-xl': 'var(--size-xl-font-size)',
'control-line-height': 'var(--leading-ui)',
// Value/control text sits one weight step ABOVE the label: the value is
// `medium`, the label stays `regular` (the "label one step below the
// component" doctrine — the size offset is derived in field.css).
'control-font-weight': 'var(--font-weight-medium)',
'control-radius': 'var(--radius-md)',
'control-border-width': 'var(--border-width)',
'control-border': 'var(--color-border-default)',
'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)',
// 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: {
'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)',
'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': {
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)',
'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)'
},
'color-picker': {
// Popover content shell. The panel padding is the popover's canonical
// size-aware `--popover-padding-*` (shared by every floating panel) — the
// picker declares NO padding of its own. Inter-row gap is owned by
// [data-picker-body]; both kept the picker from being a padding outlier.
'content-width-sm': '17rem',
'content-width-md': '19.5rem',
'content-width-lg': '22rem',
// Trigger button (compact field-shaped pill with a swatch dot)
'trigger-height-sm': 'var(--size-sm-control-height)',
'trigger-height-md': 'var(--size-md-control-height)',
'trigger-height-lg': 'var(--size-lg-control-height)',
'trigger-padding-inline-sm': 'var(--space-2)',
'trigger-padding-inline-md': 'var(--space-2-5)',
'trigger-padding-inline-lg': 'var(--space-3)',
'trigger-gap': 'var(--space-2)',
'trigger-radius': 'var(--radius-md)',
'trigger-border-width': 'var(--border-width)',
'trigger-border': 'var(--color-border-default)',
'trigger-bg': 'var(--color-surface-default)',
'trigger-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)',
'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': {
'_palette-element': {
parts: ['root'],
declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }]
}
},
'tree-view': {
'_palette-element': {
parts: ['root'],
declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }]
}
},
// virtual-list / virtual-grid: their accent cascade was declaration-only (no CSS
// consumer) — pure virtualized containers with no colour-driven visual. The type
// stays open (guard/uniformity) + the wrapper stamps a harmless `data-color`, but
// there is nothing to forward, so NO recipe here (avoids an orphan palette token).
feed: {
'_palette-solid': {
parts: ['root'],
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
},
'_palette-element': {
parts: ['root'],
declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }]
}
},
'grid-list': {
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
},
// ─────────────────────────────────────────────────────────────────────
// 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)'
},
clipboard: {
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
},
carousel: {
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
},
listbox: {
'_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: {
'_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: {
'_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: {
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
label: {
'_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: {
'_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: {
'_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: {
'_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' },
// Host-scoped ON PURPOSE (audit B.2, 2026-07-07): both values reference the
// private `--_calendar-palette-border`, which lives on `[data-calendar]`
// (the recipe host default + the THM-2 forward). Emitted at `:root` they
// froze guaranteed-invalid (the private doesn't exist there) and the
// holiday/event marks NEVER painted. On the host, the custom property
// computes where the private (and its colour retunes) actually cascade.
'day-holiday-shadow': {
value: 'inset 0 -2px 0 var(--_calendar-palette-border)',
scope: 'host'
},
'day-unavailable-decoration': 'var(--color-risk-border)',
'event-shadow': {
value:
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--_calendar-palette-border)',
scope: 'host'
},
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
},
chronos: {
// Component-semantic dimensions only. Spacing, typography and colors come
// straight from the eidos primitives in chronos.css (no per-component
// font/color tokens — those belong to the eidos foundation).
'cell-min-block-sm': 'calc(var(--size-md-control-height) * 2.6)',
'cell-min-block-md': 'calc(var(--size-md-control-height) * 3.2)',
'cell-min-block-lg': 'calc(var(--size-md-control-height) * 4)',
'num-row': 'var(--space-7)',
'lane-height-sm': 'var(--space-5)',
'lane-height-md': 'var(--space-6)',
'lane-height-lg': 'var(--space-7)',
// Week/Day time-grid viewport height (scrolls internally).
'timegrid-block-size': 'min(70vh, 40rem)',
'disabled-opacity': 'var(--opacity-disabled)'
},
pagination: {
'gap-xs': 'var(--space-0-5)',
'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)',
'gap-lg': 'var(--space-2)',
'control-size-xs': 'var(--size-xxs-control-height)',
'control-size-sm': 'var(--size-xs-control-height)',
'control-size-md': 'var(--size-sm-control-height)',
'control-size-lg': 'var(--size-md-control-height)',
'control-padding-inline-xs': 'var(--space-1-5)',
'control-padding-inline-sm': 'var(--space-2)',
'control-padding-inline-md': 'var(--space-2-5)',
'control-padding-inline-lg': 'var(--space-3)',
'font-family': 'var(--style-label-font-family)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-md-font-size)',
'font-size-lg': 'var(--size-lg-font-size)',
'line-height': 'var(--leading-ui)',
'control-radius': 'var(--radius-md)',
'control-border-width': 'var(--border-width)',
'control-border': 'transparent',
'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: {
'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: 'var(--color-surface-overlay)',
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)'
},
'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)',
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': {
'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: {
// 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)'
},
'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)',
'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'
},
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).
'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.
'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)'
},
// ─────────────────────────────────────────────────────────────────────
// 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',
// 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: {
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'
},
// ─────────────────────────────────────────────────────────────────────
// 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)'
},
// ─────────────────────────────────────────────────────────────────────
// 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)',
'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: {
gap: 'var(--space-3)',
'actions-gap': 'var(--space-2)',
'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'
},
// ─────────────────────────────────────────────────────────────────────
// 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.
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 palette's own surface radius; inside
// <Command.Dialog> the recipe re-points that reference at the DIALOG's
// public radius - the panel the user sees there IS the dialog's, so
// reading a sibling's public token is the correct composition, not a
// second token of our own.
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': {
'preview-z': 'var(--z-index-overlay-detached)',
// Selection palette (THM-2) — `data-color` sits on `[data-drag-drop-root]`.
'_palette-solid': {
parts: ['root'],
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
},
'_palette-element': {
parts: ['root'],
declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }]
}
},
'navigation-menu': {
// Inline (non-portaled) dropdown panel: stacks above normal page flow.
'content-z': 'var(--z-index-overlay-inline)',
'indicator-w': '0px',
'indicator-x': '0px',
// 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)'
},
// ─────────────────────────────────────────────────────────────────────
// DropdownMenu — panel chrome + item rows. Mirror tokens to context-menu.
// ─────────────────────────────────────────────────────────────────────
'dropdown-menu': {
// Floating menu panel stacks in the top overlay band (matches
// `select` — both are click-triggered list overlays). Without this
// the portaled panel inherits `z-index: auto` and any positioned
// page element with a positive z-index (e.g. a selected
// `[data-toggle-group-item]`, z-index 1) paints THROUGH it.
'content-z': 'var(--z-index-overlay-floating)',
'content-min-width': '12rem',
'content-max-height': 'min(60vh, 22rem)',
'content-padding': 'var(--space-1)',
'content-radius': 'var(--radius-md)',
'content-font-size': 'var(--size-sm-font-size)',
'item-gap': 'var(--space-2)',
'item-height': '2rem',
'item-padding-inline': 'var(--space-2)',
'item-padding-block': 'var(--space-1)',
'item-radius': 'var(--radius-sm)',
'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 itself styles via foundation privates; these are the
// PANEL surface's public knobs (the role=dialog form surface a bar entry
// can drop instead of a role=menu Content).
// ─────────────────────────────────────────────────────────────────────
menubar: {
// Same overlay band as the sibling menu panels — both drop from the bar.
'panel-z': 'var(--z-index-overlay-floating)',
'panel-min-width': '16rem',
'panel-max-height': 'min(70vh, 28rem)',
'panel-padding': 'var(--space-3)',
'panel-radius': 'var(--radius-md)'
},
// ─────────────────────────────────────────────────────────────────────
// ContextMenu — same surface as DropdownMenu, with a span Trigger.
// ─────────────────────────────────────────────────────────────────────
'context-menu': {
'content-min-width': '12rem',
'content-max-height': 'min(60vh, 22rem)',
'content-padding': 'var(--space-1)',
'content-radius': 'var(--radius-md)',
'content-font-size': 'var(--size-sm-font-size)',
'item-gap': 'var(--space-2)',
'item-height': '2rem',
'item-padding-inline': 'var(--space-2)',
'item-padding-block': 'var(--space-1)',
'item-radius': 'var(--radius-sm)',
'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': {
'row-gap': 'var(--space-3)',
'separator-fg': 'var(--color-content-muted)',
'separator-glyph': "'—'"
},
// ─────────────────────────────────────────────────────────────────────
// TimeRangeField — layout only; chrome inherits from time-field.
// ─────────────────────────────────────────────────────────────────────
'time-range-field': {
'row-gap': 'var(--space-3)',
'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)',
'sector-shadow': 'var(--shadow-raised)',
'sector-ring-width': '2.5',
'sector-hover-ring-width': '2',
'glyph-bar-thickness': '2px',
'muted-opacity': '0.85',
'hover-ring-opacity': '0.65',
// ── `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.