feat(eidos): canonize theming scales + tokenize magic numbers

Audit-driven canonization of the eidos theming layer: a canonical scale that
components bypass with literals drifts into N variants. Every axis is now
retunable-by-theme AND consumed via token, never a bare literal.

Bloque A/B (scales): --blur-* (Tailwind-aligned, depth planes consume it);
inner-shadow --shadow-inset-* (mode-aware) for the recessed plane; gradient
angle/named tokens; --breakpoint-* sourced from ActiveDom (runtime, dev-settable)
+ container queries; opacity dual numeric+semantic scale; border-width
none/thin/medium/thick/heavy (adds real 3px); tracking caps/widest. Fase 7:
3 size archetypes documented + coherence guard (no px/rem in recipe
font-size/icon-size).

Bloque C (magic numbers): z-index 80/50/99 (combobox/nav-menu/drag-drop) ->
recipe content-z/preview-z tokens (overlay micro-band soma mirrors); on-scale
durations -> var(--duration-*) (dialog fast/slow, card slow); 12 single
border/ring widths -> var(--border-width-*), value-preserving. Off-scale kept
only as justified recipe tokens (continuous spinner/loading periods, sub-fast
press). Self-contained dimensional scales (avatar ring, ring-thickness) left whole.

Docs: THEMING.md SS35 + SS6 + SS29, THEMING_NOTES, README, TSC. Excludes
words/palabras/chronos (active dev tracks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 02016f8ab5
commit dc62a0268b

@ -554,6 +554,7 @@ Resumen rápido de lo que cubre, para no duplicar aquí:
| **Theme builder en runtime: `eidos.applyColorScheme(seed)` (motor `uix.color`)** | §26 |
| **Salida wide-gamut OKLCH default-on (hex fallback + `oklch()` sibling)** | §27 |
| **a11y forced-colors (focus outline fallback) + ramp de bordes (slot 6→7)** | §28 |
| **Canon de escalas — auditoría de theming: blur · inset-shadow/ring · gradientes · breakpoints+container · opacidad · border-width · tracking (2026-06-15)** | §35 |
Lo que sigue en este README son las decisiones operativas de la **capa
visual como módulo** (typography sourcing, picker patterns, API

@ -63,6 +63,7 @@
32. [Focus ring — modelo de dos anillos parametrizado](#32-focus-ring--modelo-de-dos-anillos-parametrizado-2026-06-11)
33. [Glifos de stepper themeables (`spin-field`)](#33-glifos-de-stepper-themeables-spin-field--2026-06-11)
34. [`spin-field` — visual compartido del stepper-field](#34-spin-field--visual-compartido-del-stepper-field-number-field--css-field--2026-06-11)
35. [Canon de escalas — auditoría de theming (2026-06-15)](#35-canon-de-escalas--auditoría-de-theming-2026-06-15)
---
@ -511,15 +512,21 @@ Cada size canónico genera tokens coordinados:
```
--size-md-control-height: 36px
--size-md-font-size: 14px
--size-md-font-line-height: 1.5
--size-md-icon-size: 16px
--size-md-font-size: 14px /* = --font-size-sm (control compacto), NO 16px */
--size-md-font-line-height: 1.45
--size-md-icon-size: 18px /* = --icon-size-md */
--size-md-padding-inline: 12px
--size-md-padding-block: 8px
--size-md-gap: 8px
--size-md-radius: 6px
```
> **Dos `md` distintos** (la confusión que la auditoría señaló): el **bundle de
> control** `--size-md-font-size` = 14px (fuente `sm`, porque un control de 36px lleva
> texto compacto — paridad Radix/Material). La **escala tipográfica** `--font-size-md`
> = 16px (body). Comparten el nombre `md` pero son ejes distintos: control-size vs
> tipo. Ver los arquetipos size→fuente abajo.
### Regla clave: `md` NO cambia por viewport
Lo responsive decide **qué size activo se usa**, NO redefine los
@ -532,7 +539,7 @@ tokens están disponibles y el wrapper elige cuál.
<!-- Incorrecto -->
@media (max-width: 768px) {
:root { --toggle-size-md-control-height: 32px; }
:root { --toggle-height-md: 32px; } /* NO redefinas el token del size por viewport */
}
```
@ -550,6 +557,33 @@ recipe soporta. Categorías:
La categorización vive en
[`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md §12.8`](../../../web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md).
### Arquetipos size→fuente (Fase 7 — 2026-06-15)
El bundle `--size-*` (arriba) es la **referencia del arquetipo `control`**: empareja
`md`-control con fuente `sm` (14px) e icono `md`. Pero los componentes NO consumen el
bundle directamente — cada recipe declara su `font-size-{size}` apuntando a `--font-size-*`
(la auditoría encontró que esa indirección, hand-rolled, derivó en varias convenciones).
Las **convenciones canónicas** (cada size → token de fuente) son:
| Arquetipo | sm | md | lg | xl | Componentes |
|---|---|---|---|---|---|
| **control** | sm | sm | md | lg | button, toggle\*, switch |
| **compact** | xs | sm | md | lg | badge, breadcrumb, select, editable, file-upload, stepper, tooltip |
| **dense** | xs | sm | sm | md | field, form, calendar, pagination, spin/date/time/color-field, radio-group |
(\* toggle/avatar son una variante extra-compacta — un escalón más abajo.) Todas
comparten `md→sm` (14px, decisión 2 = paridad Radix/Material).
**Regla dura (guard de coherencia)** — `recipe-css-contract.test.ts`:
> Ningún token `font-size-*` / `icon-size-*` de recipe puede ser un **literal px/rem**
> — DEBE referenciar `--font-size-*` / `--icon-size-*` (si no, el texto deja de seguir
> la escala tipográfica, `--scaling` y `applyTypeScale()`).
Cierra el hueco que el guard anterior (solo-CSS) dejaba — los valores de recipe-token
se vuelven custom properties, no la propiedad `font-size:`. (`words`/`palabras`/`chronos`
excluidos — track activo.) El refactor a *consumir* el bundle del arquetipo (en vez de
re-declarar el mapeo) queda como follow-up; el guard es lo que **impide la deriva**.
### Escala tipográfica ↔ escala de iconos
Son dos escalas paralelas — `--font-size-{name}` (`lib/primitives/typography.ts`)
@ -613,9 +647,18 @@ Donde `{prefix}` es uno de:
| `--size-{key}-{slot}` | Size primitive | `--size-md-control-height` |
| `--space-{n}` | Spacing scale | `--space-3` |
| `--radius-{key}` | Radius scale | `--radius-md` |
| `--shadow-{n}` | Shadow scale | `--shadow-3` |
| `--border-width-{key}` | Border-width scale (lineal `none·thin·medium·thick·heavy` = 0/1/2/3/4) — §35 | `--border-width-thick` |
| `--ring-inset-width` | Default ancho del inset-ring (anillo interior `box-shadow`) — §35 | `--ring-inset-width` |
| `--shadow-{n}` | Shadow scale (gota) | `--shadow-3` |
| `--shadow-inset-{key}` | Inner-shadow / recessed (`subtle·deep`, mode-aware) — §35 | `--shadow-inset-subtle` |
| `--blur-{key}` | Blur scale (backdrop/frost, `none·sm·md·lg·xl·xxl`) — §35 | `--blur-lg` |
| `--gradient-{name}` | Gradiente nombrado (themeable, role-composed) — §35 | `--gradient-shimmer` |
| `--gradient-angle-{dir}` | Dirección de gradiente (8 brújulas) — §35 | `--gradient-angle-to-r` |
| `--breakpoint-{key}` | Breakpoint responsive (fuente = ActiveDom) — §35 | `--breakpoint-md` |
| `--z-index-{key}` | Z-index layer | `--z-index-modal` |
| `--opacity-{key}` | Opacity | `--opacity-disabled` |
| `--opacity-{key}` | Opacidad — escala dual numérica (`0..100`) + semántica (`disabled·muted·…`) — §35 | `--opacity-disabled` |
| `--tracking-{key}` | Letter-spacing scale (incl. `caps` para MAYÚSCULAS) — §35 | `--tracking-caps` |
| `--leading-{key}` | Line-height scale | `--leading-ui` |
| `--duration-{key}` | Motion duration | `--duration-fast` |
| `--ease-{key}` | Motion ease | `--ease-out` |
| `--style-{name}-*` | Typography named style | `--style-h1-font-size` |
@ -1750,6 +1793,127 @@ sigue opaco salvo que pida `data-frost`). El builder runtime `ActiveEidos.applyD
en vivo — hermano de `applyColorScheme` / `applyTypeScale`. Demo: `/temas/profundidad`
§Materiales.
**Tier de sombra interior** (`--shadow-inset-*`, 2026-06-15): la escala de sombra
gana un tier **inset** mode-aware, distinto de las sombras de gota (exteriores) y
de los *inset-rings* (anillo nítido `inset 0 0 0 Npx`, otro eje):
| Token | Light | Dark |
|---|---|---|
| `--shadow-inset-subtle` | `inset 0 1px 2px rgb(15 23 42 / 0.08)` | `inset 0 1px 2px rgb(0 0 0 / 0.30)` |
| `--shadow-inset-deep` | `inset 0 2px 4px rgb(15 23 42 / 0.12)` | `inset 0 2px 4px rgb(0 0 0 / 0.45)` |
El plano `recessed` lo consume (`--depth-recessed-shadow: var(--shadow-inset-subtle)`),
sustituyendo el `color-mix(neutral-contrast …)` inline previo — que en dark daba un
borde claro (embossado) en vez de un hundido; ahora es mode-correcto (inset oscuro en
ambos modos). Referencias: Tailwind `inset-shadow-{2xs,xs,sm}`, Bootstrap `shadow-inset`,
Chakra `inner`. Disponible además para estados *pressed* / wells.
**Inset-ring** (`--ring-inset-width`, 2026-06-15): eje hermano pero **distinto** —
un anillo interior **nítido** (no difuminado), como el `inset-ring` de Tailwind
(`inset 0 0 0 Npx <color>`). El ancho sale de la escala `--border-width-*`
(`--ring-inset-width` por defecto `thick`=2px, retunable por tema / override por uso);
el color va por el hook `--ring-inset-color`. **No** se puede hacer un token único
`--ring-inset` pre-resuelto: CSS hornea los `var()` anidados en el scope donde se
declara (`:root`), así que el color/ancho por-elemento no propagaría — la expresión
vive en el punto de uso: `box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor)`.
Consumidores: date-field (focus de segmento), drag-drop (accepting 1px / dragover 2px),
float-panel (focus + grabbed + resize-grip), select (item checked+highlighted). Las
marcas laterales de un solo lado (range-calendar `inset ±2px 0 0 0`) **no** son anillos
→ se quedan. De paso, este eje da el primer uso real a los pasos `thin`/`thick` de
`--border-width-*`.
**Escala de blur canónica** (`--blur-*`, 2026-06-15): el desenfoque es un
**primitivo** (`STATIC_BLUR` → `lib/primitives/static.ts`), no un px disperso.
Valores alineados a Tailwind, escalados por `--scaling` como `--icon-size-*`:
| Token | px | = Tailwind |
|---|---|---|
| `--blur-none` | 0 | — |
| `--blur-sm` | 4 | xs |
| `--blur-md` | 8 | sm |
| `--blur-lg` | 12 | md |
| `--blur-xl` | 16 | lg |
| `--blur-xxl` | 24 | xl |
Dos modelos de referencia: **Tailwind** (escala numérica cruda) y **Apple** (materiales
semánticos `ultraThin…thick` que acoplan blur+translucidez). Eidos toma el de Tailwind como
**eje crudo** y lo compone en la capa semántica de **profundidad**: los planos consumen
`--blur-*` (`--depth-overlay-blur: var(--blur-lg)`, `--depth-modal-blur: var(--blur-xl)`),
igual que color separa `--scale-*` (crudo) de los roles. Un futuro tema "cristal" acopla
blur+alpha por plano (el modelo Apple) sobre esta escala. Consumidores ya migrados:
planos overlay/modal, `tooltip` (frost), `dialog`/`drawer` (`overlay-blur`). Nada
inventa px de blur a mano.
**Gradientes themeables** (`--gradient-angle-*` + `gradients`, 2026-06-15): eje de
dos capas, espejo de Tailwind (que declara **0 gradientes nombrados** — solo
maquinaria):
- **Direcciones** (`--gradient-angle-*`): las 8 brújulas de Tailwind como ángulos CSS
(`to-t 0deg · to-tr 45 · to-r 90 · to-br 135 · to-b 180 · to-bl 225 · to-l 270 · to-tl 315`).
- **Nombrados** (`gradients` config → `--gradient-*`): **extensibles** (jaula abierta:
`extendEidosConfig({ primitives: { gradients: {…} } })`), **role/surface-composed**
→ mode-aware vía los tokens que referencian. Default fuerte mínimo: **un solo**
nombrado, `--gradient-shimmer` (barrido de carga; lo consume `image`). Un tema añade
sus gradientes de marca aquí.
Los gradientes **funcionales** (color-picker HSV/checkerboard, `conic` de progress/meter,
líneas 1px de cropper/tree-grid, máscara de scroll de tabs, split bicolor de
range-calendar, grip de float-panel, barra de carga de command) **no** son de tema y
siguen crudos — no son decorativos. `skeleton` tinta su shimmer por variante de color
(data-driven), así que conserva su gradiente local pero dogfoodea `var(--gradient-angle-to-r)`.
**Breakpoints — fuente única + container queries** (`EidosConfig.breakpoints`,
`--breakpoint-*`, 2026-06-15): la **fuente de verdad de los breakpoints es el servicio
runtime** `ActiveDom` (el dev los setea vía `createActiveUix({ dom: { breakpoints } })`;
`BREAKPOINTS_DEFAULT` es solo el seed). `ActiveEidos` threadea `dom.breakpoints.current`
a `renderStaticCss`, que los emite como tokens `--breakpoint-{sm..xxl}` **y** los usa en
los `@media` de tipografía responsive — así el CSS generado deja de congelarse en un const
duplicado y sigue los breakpoints configurados. **Container queries**: una recipe declara
overrides por breakpoint en la key reservada `container` (hermana de `composition`):
```ts
recipes: { card: { container: { md: { 'pad': 'var(--space-6)' } } } }
// → @container (min-width: 768px) { [data-card] { --card-pad: var(--space-6) } }
```
El generador (`emitContainerQueries`) usa los **mismos** breakpoints configurados (px
literal — CSS prohíbe `var()` en condiciones `@container`/`@media`, así que la sincronía
solo es posible generándolo). Opt-in: un ancestro con `data-container` activa
`container-type: inline-size`. Eje themeable, 0 consumidores hoy (jaula abierta).
**Opacidad — escala coordinada de dos capas** (`--opacity-*`, 2026-06-15): mismo
patrón dual que la sombra (numérico + semántico).
- **Numérico** (`--opacity-{0,5,…,100}`, Tailwind step-5): granularidad fina para
interfaces etéreas / cristal (capas translúcidas en el tramo bajo).
- **Semántico** (los roles que consumen los recipes): `ghost 0.3 · disabled 0.4 ·
scrim 0.45 · muted 0.65 · overlay 0.65 · subtle 0.8 · press 0.85 · hover 0.9 · full 1`.
`disabled = 0.4` (estándar moderno ≈ Material 38%).
**Unificación**: el estado `disabled` se renderizaba con ~10 valores distintos
(0.45–0.72) en recipes (`disabled-opacity`) + CSS (`[data-disabled]`/`:disabled`).
Ahora TODOS consumen `var(--opacity-disabled)`. La deriva ad-hoc de CSS
(muted/ghost/subtle) migrada a sus roles. Quedan crudos solo los de animación
(`spinner` keyframe) y `scroll-frames` (rol no semántico). Retunable por tema, como
size/sombra/superficie (decisión del usuario).
**Border-width — escala lineal** (`--border-width-*`, 2026-06-15): adoptada la
**lineal de Bootstrap** (`none 0 · thin 1 · medium 2 · thick 3 · heavy 4`) — la única
escala de referencia que tiene el **3px** que los componentes usan (Tailwind salta
1/2/4/8). Podados los pasos muertos `hairline`(0.5) y el viejo `medium`(1.5) (0
consumidores); `medium` retuneado a 2, `thick` a 3, `heavy`(4) nuevo. Los 3
consumidores de `thick`(2px) — focus-ring de select, quote-border, separator — +
el default de `--ring-inset-width` movidos a `medium`(2px, sin cambio visual). **Todos
los anchos crudos tokenizados** (consumo completo de la escala — la tesis de la
auditoría): `3px→thick`, `2px→medium`, `1px→var(--border-width)`, `1.5px→medium`
(chevron de navigation-menu) en ~35 ficheros. Así un tema retunea el ancho de borde
de una vez (p. ej. `--border-width` denso) y todos los bordes lo siguen.
**Tracking — `caps` para mayúsculas** (`--tracking-*`, 2026-06-15): añadidos
`caps 0.04em` (micro-tracking canónico de etiquetas en MAYÚSCULAS — el patrón
dominante en menús/headings) y `widest 0.1em`. Los 11 `letter-spacing` crudos de
CSS migrados a sus roles (`0.04→caps · 0.05→wider · 0.02→wide · 0.1→widest`). El
`letterSpacing` óptico por-tamaño de la escala tipográfica (xxs/xs…) NO se migra:
es la corrección óptica intrínseca de cada paso.
**Pendiente** (menor): el cue `scrim` está disponible como token (`--depth-{plane}-scrim`) pero
sin regla cableada — el backdrop dim de los modales lo gestiona hoy cada componente.
@ -1927,6 +2091,80 @@ refactor aparte.
---
**Última revisión**: 2026-06-11. Si algo en este doc no coincide con
## 35. Canon de escalas — auditoría de theming (2026-06-15)
Sprint de auditoría que llevó las escalas del theming a paridad con las referencias
(Tailwind · Material 3 · Apple HIG · Bootstrap · Radix) y, sobre todo, **forzó su
consumo**: la tesis de la auditoría es que *una escala canónica que los componentes
no consumen (la bypassan con literales) deriva en N variantes del mismo valor*. Cada
eje es ahora **retunable por tema** (igual que size/sombra/superficie) y los recipes
**consumen el token, nunca un literal**. Detalle por-eje en el addendum de §29; tokens
en la tabla de §6.
| Eje | Token(s) | Canon | Decisión clave |
|---|---|---|---|
| **Blur** | `--blur-{none,sm,md,lg,xl,xxl}` | 0/4/8/12/16/24 (Tailwind) | numérico crudo; los planos de depth lo consumen (`--depth-*-blur`) |
| **Inner-shadow** | `--shadow-inset-{subtle,deep}` | mode-aware (light slate / dark negro) | lo usa el plano `recessed`; ≠ inset-ring |
| **Inset-ring** | `--ring-inset-width` + `--ring-inset-color` | `inset 0 0 0 var(width) var(color)` **en el punto de uso** | un token único pre-resuelto es imposible (CSS hornea el `var()` anidado en `:root`) |
| **Gradientes** | `--gradient-angle-*` + `gradients`→`--gradient-*` | 8 direcciones + nombrados role-composed | Tailwind ship 0 nombrados → solo `shimmer`; los funcionales (HSV, conic, líneas) NO son de tema |
| **Breakpoints** | `--breakpoint-{sm..xxl}`, `EidosConfig.breakpoints` | **fuente = `ActiveDom`** (runtime, dev-settable) | `ActiveEidos` threadea `dom.breakpoints` al generador; los `@media` dejan de congelarse |
| **Container queries** | recipe key `container` → `@container` + `[data-container]` | px literal **generado** (CSS prohíbe `var()` en `@container`) | jaula abierta, 0 consumidores hoy |
| **Opacidad** | `--opacity-{0..100}` + semánticos | dual numérico + semántico; `disabled 0.4` (≈ Material 38%) | unificado (~10 valores de disabled → 1); numérico fino = glass-friendly |
| **Border-width** | `--border-width-{none,thin,medium,thick,heavy}` | **lineal Bootstrap** 0/1/2/3/4 (única ref con el 3px real) | **todos** los anchos crudos tokenizados (~35 ficheros) |
| **Tracking** | `--tracking-{…,caps,widest}` | + `caps 0.04em` (MAYÚSCULAS) + `widest 0.1em` | 11 `letter-spacing` crudos migrados; el óptico por-tamaño NO |
**Incidente registrado**: una reescritura masiva por PowerShell (`WriteAllText`)
corrompió 11 ficheros (`o→p`); recuperados con `git checkout` + rehechos con la
herramienta Edit. Regla: **modificar ficheros del repo SOLO con Edit/Write**, nunca
PowerShell en bloque.
**Fase 7 (size→fuente — parcial)**: documentados los **3 arquetipos** canónicos
(`control · compact · dense`, §5) + `--size-*` como referencia del `control`. **Guard
de coherencia** activo: ningún `font-size-*`/`icon-size-*` de recipe puede ser literal
px/rem (cierra el hueco del guard solo-CSS). Arreglados los últimos hardcodes
(`toggle`, `avatar` → `--font-size-*`, valores preservados). El `icon-size` de
`password-field` desde `--control-height-*` es **correcto** (es el tamaño del botón
reveal, no del glifo) — falso positivo de la auditoría. **Deferido**: el refactor a
*consumir* el bundle del arquetipo (en vez de re-declarar el mapeo) — grande, con
edge-cases (fuentes semánticas por-parte, sistema `--text-N` de accordion) + edición
en paralelo; el guard es lo que impide la deriva mientras tanto.
### Bloque C — números mágicos sueltos (z-index · duración · border/ring)
Cierre de los literales que bypasseaban una escala ya existente. **Regla**: un
literal que iguala un paso de escala DEBE consumir el token; nada de "intencionales".
- **z-index de overlays flotantes** — `combobox` (era `80`), `navigation-menu`
(era `50`) y `drag-drop` preview (era `99`) hardcodeaban su z. Ahora declaran un
token de recipe (`content-z` / `preview-z`) como ya hacían sus 5 hermanos
(`popover`/`select`/`tooltip`/`link-preview`/`dropdown-menu`). Estos NO usan la
escala global `--z-index-*` a propósito: son una **micro-banda** baja (75-80) que
la capa flotante de soma **espeja** leyendo el z computado del wrapper; subirlos a
300/400 rompería el espejo. El fallback redundante `, 80` de `dropdown-menu` se
eliminó (el valor vive una vez, en el recipe). Los `z-index: 0..5` de apilado
local (avatar, tabs, sticky cells) son ordenación relativa, NO mágicos — se quedan.
- **Duración** — los que igualaban un paso de la escala la consumen: `dialog` enter
`120ms`→`var(--duration-fast)`, exit `280ms`→`var(--duration-slow)` (asimetría
rápida-entra/lenta-sale preservada, ya 100% en escala); `card` emerge
`320ms`→`var(--duration-slow)`; banner/code-block/link `120ms`→`fast`. Los
fallbacks muertos `, 220ms`/`, 720ms` (checkbox/button, cuyo recipe ya declaraba
el token) se quitaron. **Excepción razonada**: `press-duration 80ms`,
`spinner-duration 720ms`, `loading-indicator-duration 900ms` se quedan como token
de recipe — son **periodos de animación continua** (giro / shimmer) o un press
deliberadamente sub-`fast`, NO transiciones de interacción; la escala de 5 pasos
(`instant..slow`) es para interacciones, no tiene sitio para ellos.
- **Border / ring width** — los anchos **únicos** de borde/ring que igualaban un
paso (`2px`=`medium`, `3px`=`thick`, `1px`=`thin`) se tokenizaron a
`var(--border-width-*)` (avatar border + badge + carve, drawer drag-ring, slider
thumb, grid-list focus-ring, toast accent-stripe → `thick`, table/menu cell/content
border → `var(--border-width)`). Valor-preservante, cero cambio visual. Lo que se
**queda como escala dimensional propia** (NO es el concepto border-width
re-derivado): el ring del avatar `sm/md/lg = 1.5/2/3px` (el `1.5` quedó fuera de la
escala global al podar el `0.5/1.5`), `ring-thickness 3..10px`, `track-width`,
`content-width`, offsets — escalas locales coherentes, no literales sueltos.
---
**Última revisión**: 2026-06-16. Si algo en este doc no coincide con
el código, el código gana — pero abre un issue para que actualicemos
el doc.

@ -37,13 +37,20 @@ source: extracted from src/uix/eidos/THEMING.md (was §15 + §17)
| **Cross-axis collision** | ✅ explícito | ❌ | ⚠️ partial | ❌ | N/A | N/A |
| **Composite scopes** | ✅ `[axis:v, …]` | ❌ | ✅ conditional pairs | ❌ | N/A | N/A |
| **9 color roles canónicos** | ✅ libro | ⚠️ 6 accents | ❌ open | ❌ open | ⚠️ 4 roles | ❌ open |
| **6 size canon coordinated** | ✅ | ⚠️ 1-3 | ⚠️ 5 | ⚠️ 5 | ❌ | N/A |
| **6 size canon coordinated** | ⚠️ canon + guard¹ | ⚠️ 1-3 | ⚠️ 5 | ⚠️ 5 | ❌ | N/A |
| **Density runtime** | ✅ 3 levels | ❌ | ❌ | ⚠️ partial | ❌ | ❌ |
| **Contract introspection** | ✅ typed | ⚠️ docs | ✅ Panda | ⚠️ docs | ❌ | ❌ |
| **Runtime override** | ✅ contract-aware | ⚠️ via CSS vars | ❌ | ✅ CSSVarsProvider | ⚠️ via CSS | ❌ |
| **Persistence versioned** | ✅ envelope | ❌ | ❌ | ❌ | ❌ | N/A |
| **Perceptual layer (sema)** | ✅ unique | ❌ | ❌ | ❌ | ❌ | ❌ |
¹ El bundle `--size-*` existe + emite, pero los componentes NO lo consumen
directamente (re-declaran su mapeo size→fuente). La auditoría 2026-06-15 formalizó
los **3 arquetipos** (`control · compact · dense`, THEMING.md §5) y añadió un **guard
de coherencia** (sin literales px/rem en `font-size-*`/`icon-size-*` de recipe). El
refactor a *consumir* el bundle queda deferido; el guard impide la deriva. Por eso ⚠️
(canon + enforcement) y no ✅ (consumo pleno).
### Mental model
| Sistema | Token philosophy |

@ -269,6 +269,25 @@ Reglas:
sus items). Pattern reutilizable para futuros wrappers compositivos
(button-group, nav-menu).
### Container queries — `container: { ... }` (2026-06-15)
Segunda key reservada sibling de los tokens (como `composition`):
overrides de token por breakpoint, emitidos dentro de `@container`:
```ts
recipes: { card: { container: { md: { 'pad': 'var(--space-6)' } } } }
// → @container (min-width: 768px) { [data-card] { --card-pad: var(--space-6) } }
```
- Usa los **mismos breakpoints configurados** que `@media` (de
`EidosConfig.breakpoints`, threaded por `ActiveEidos` desde `ActiveDom`)
— px **literal**, porque CSS prohíbe `var()` en condiciones `@container`.
- Opt-in del contenedor: un ancestro con `[data-container]` activa
`container-type: inline-size`.
- `stripCompositionKey` (render-css) salta `composition` **y** `container`;
`contract.ts` los excluye del contrato público de tokens.
- Eje themeable, 0 consumidores hoy (jaula abierta). Detalle: THEMING.md §35.
### Pipeline de defensas (5 capas)
```

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { renderThemeCss } from './lib/render-css'
import { renderStaticCss, renderThemeCss } from './lib/render-css'
import {
COLOR_ALPHA_STEPS,
COLOR_SCALE_STEPS,
@ -521,8 +521,12 @@ describe('ActiveEidos config', () => {
it('emits the Fase 4 frost atmosphere (opt-in [data-frost] + blur tokens)', () => {
const css = createThemeBaseEidos().renderStaticCss();
expect(css).toContain('--depth-overlay-blur: 10px;');
expect(css).toContain('--depth-modal-blur: 16px;');
// Blur is a canonical primitive (`--blur-*`); the planes consume it instead
// of hardcoding px. See THEMING.md "Escala de blur".
expect(css).toContain('--blur-lg: calc(12px * var(--scaling));');
expect(css).toContain('--blur-xl: calc(16px * var(--scaling));');
expect(css).toContain('--depth-overlay-blur: var(--blur-lg);');
expect(css).toContain('--depth-modal-blur: var(--blur-xl);');
// frost is opt-in (gated on data-frost) so it never turns an opaque overlay translucent
expect(css).toContain("[data-depth='overlay'][data-frost] {");
expect(css).toContain('backdrop-filter: blur(var(--depth-overlay-blur));');
@ -541,6 +545,46 @@ describe('ActiveEidos config', () => {
eidos.clearDepth(); // reverts cleanly, no throw
});
it('emits --breakpoint-* tokens; configured breakpoints override the defaults', () => {
// Default (no breakpoints in config) → canonical fallback.
const dflt = renderStaticCss(THEME_BASE_OPTIONS);
expect(dflt).toContain('--breakpoint-md: 768px;');
expect(dflt).toContain('--breakpoint-xxl: 1536px;');
// Developer-configured breakpoints (as ActiveEidos threads from ActiveDom)
// flow into the emitted `--breakpoint-*` tokens (and the responsive `@media`
// blocks, which the base theme's fluid type scale doesn't currently emit).
const custom = renderStaticCss({
...THEME_BASE_OPTIONS,
breakpoints: { base: 0, sm: 480, md: 900, lg: 1024, xl: 1280, xxl: 1536 }
});
expect(custom).toContain('--breakpoint-md: 900px;');
expect(custom).not.toContain('--breakpoint-md: 768px;');
});
it('emits @container blocks from recipe `container` overrides + the data-container opt-in', () => {
// Opt-in rule is always emitted.
expect(renderStaticCss(THEME_BASE_OPTIONS)).toContain('[data-container] {');
// A recipe's `container` overrides emit `@container` blocks scoped to the
// component, using the configured breakpoints (literal px — var() is illegal
// in @container conditions).
const cfg = {
...THEME_BASE_OPTIONS,
recipes: { widget: { container: { md: { gap: 'var(--space-6)' } } } }
};
const css = renderStaticCss(cfg);
expect(css).toContain('@container (min-width: 768px)');
expect(css).toContain('--widget-gap: var(--space-6)');
// Configured breakpoints flow into the @container condition too.
const custom = renderStaticCss({
...cfg,
breakpoints: { base: 0, sm: 480, md: 900, lg: 1024, xl: 1280, xxl: 1536 }
});
expect(custom).toContain('@container (min-width: 900px)');
});
it('applyTheme composes all five builders from one seed (the runtime capstone)', () => {
const eidos = createThemeBaseEidos();
const result = eidos.applyTheme({
@ -720,7 +764,7 @@ describe('ActiveEidos config', () => {
delete (cfg.primitives.border!.width as Record<string, string>)['thin']
;(cfg.primitives.border as { defaultWidth: string }).defaultWidth = 'missing-width'
;(cfg.primitives.opacity as Record<string, string>)['ghost'] = '0.4'
;(cfg.primitives.opacity as Record<string, string>)['bogus'] = '0.4'
delete (cfg.primitives.zIndex as Record<string, number>)['toast']
delete (cfg.themes!['base-light'].shadow as Record<string, string>)['6']
@ -731,7 +775,7 @@ describe('ActiveEidos config', () => {
expect(report.issues.some((issue) => issue.path === 'primitives.border.defaultWidth')).toBe(
true
)
expect(report.issues.some((issue) => issue.path === 'primitives.opacity.ghost')).toBe(true)
expect(report.issues.some((issue) => issue.path === 'primitives.opacity.bogus')).toBe(true)
expect(report.issues.some((issue) => issue.path === 'primitives.zIndex.toast')).toBe(true)
expect(report.issues.some((issue) => issue.path === 'themes.base-light.shadow.6')).toBe(true)
})
@ -1019,7 +1063,7 @@ describe('ActiveEidos config', () => {
expect(css).toContain('--scaling-110: 1.1;')
expect(css).toContain('--scaling: var(--scaling-100);')
expect(css).toContain("[data-scaling='110'] {")
expect(css).toContain('--opacity-disabled: 0.48;')
expect(css).toContain('--opacity-disabled: 0.4;')
expect(css).toContain('--z-index-modal: 700;')
expect(css).toContain('--size-md-control-height: var(--control-height-md);')
expect(css).toContain('--size-md-font-size: var(--font-size-sm);')

@ -448,7 +448,11 @@ export class ActiveEidos {
renderStaticCss(): string {
this.assertValid();
return renderEidosStaticCss(this.#config);
// Thread the runtime-configured breakpoints (developer-set on ActiveDom) into
// the generator so the emitted `@media` blocks + `--breakpoint-*` tokens match
// the live `ResponsiveProp` resolution. Falls back to defaults when no DOM.
const breakpoints = this.#dom?.breakpoints.current;
return renderEidosStaticCss(breakpoints ? { ...this.#config, breakpoints } : this.#config);
}
/**

@ -26,7 +26,7 @@
display: block;
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
background: var(--color-surface-default);
font-size: var(--font-size-sm);
line-height: var(--leading-ui);

@ -167,7 +167,7 @@
background: transparent;
color: inherit;
cursor: pointer;
opacity: 0.75;
opacity: var(--opacity-subtle);
transition:
opacity var(--duration-fast) var(--ease-default),
background var(--duration-fast) var(--ease-default);

@ -129,7 +129,7 @@
[data-banner][data-variant='outline'] {
--_banner-bg: transparent;
--_banner-fg: var(--_banner-text);
--_banner-border-width: 1px;
--_banner-border-width: var(--border-width);
--_banner-border-color: var(--_banner-border);
}
@ -154,8 +154,8 @@
border: 0;
border-radius: var(--radius-sm, var(--space-1));
cursor: pointer;
opacity: 0.8;
transition: opacity 120ms ease, background-color 120ms ease;
opacity: var(--opacity-subtle);
transition: opacity var(--duration-fast) ease, background-color var(--duration-fast) ease;
}
[data-banner-close]:hover {
@ -164,7 +164,7 @@
}
[data-banner-close]:focus-visible {
outline: 2px solid currentColor;
outline: var(--border-width-medium) solid currentColor;
outline-offset: 1px;
opacity: 1;
}

@ -85,7 +85,7 @@
}
[data-button]:focus-visible {
outline: 2px solid var(--button-palette-border);
outline: var(--border-width-medium) solid var(--button-palette-border);
outline-offset: 2px;
}
@ -294,7 +294,7 @@
.eidos-button-spinner-ring {
inline-size: 100%;
block-size: 100%;
animation: button-spinner-spin var(--button-spinner-duration, 720ms) linear infinite;
animation: button-spinner-spin var(--button-spinner-duration) linear infinite;
}
@keyframes button-spinner-spin {

@ -105,21 +105,21 @@
/* ── Variant — surface (default) ─ */
[data-carousel][data-variant='surface'] {
padding: var(--space-2);
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--_carousel-radius);
background: var(--color-surface-default);
}
[data-carousel][data-variant='outline'] {
padding: var(--space-2);
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--_carousel-radius);
background: transparent;
}
[data-carousel][data-variant='ghost'] {
padding: 0;
border: 1px solid transparent;
border: var(--border-width) solid transparent;
background: transparent;
}
@ -173,7 +173,7 @@
}
[data-carousel][data-disabled] {
opacity: 0.55;
opacity: var(--opacity-disabled);
pointer-events: none;
}
@ -257,7 +257,7 @@
[data-carousel-prev-trigger][data-disabled],
[data-carousel-next-trigger][data-disabled] {
opacity: 0.4;
opacity: var(--opacity-disabled);
cursor: not-allowed;
}
@ -350,6 +350,6 @@
}
[data-carousel-indicator][data-disabled] {
opacity: 0.4;
opacity: var(--opacity-disabled);
cursor: not-allowed;
}

@ -176,7 +176,7 @@
[data-checkbox-indicator] svg[data-svg='dash'] path {
stroke-dasharray: 100;
stroke-dashoffset: 100;
transition: stroke-dashoffset var(--checkbox-stroke-duration, 220ms) var(--checkbox-stroke-ease, cubic-bezier(0.4, 0, 0.2, 1));
transition: stroke-dashoffset var(--checkbox-stroke-duration) var(--checkbox-stroke-ease);
}
[data-checkbox][data-state='checked'] [data-checkbox-indicator] svg[data-svg='check'] path,

@ -18,12 +18,12 @@
[data-code-block][data-variant='surface'] {
background: var(--color-neutral-element);
border: 1px solid var(--color-neutral-border);
border: var(--border-width) solid var(--color-neutral-border);
}
[data-code-block][data-variant='outline'] {
background: transparent;
border: 1px solid var(--color-neutral-border);
border: var(--border-width) solid var(--color-neutral-border);
}
[data-code-block][data-variant='ghost'] {
@ -37,7 +37,7 @@
gap: var(--space-2);
padding-inline: var(--space-3);
padding-block: var(--space-1);
border-bottom: 1px solid var(--color-neutral-border);
border-bottom: var(--border-width) solid var(--color-neutral-border);
background: var(--color-neutral-track);
}
@ -62,7 +62,7 @@
font-size: var(--font-size-xxs);
color: var(--color-content-muted);
text-transform: lowercase;
letter-spacing: 0.04em;
letter-spacing: var(--tracking-caps);
white-space: nowrap;
}
@ -80,16 +80,16 @@
font-size: var(--font-size-xs);
color: var(--color-content-secondary);
background: transparent;
border: 1px solid var(--color-neutral-border);
border: var(--border-width) solid var(--color-neutral-border);
border-radius: var(--radius-sm);
padding-inline: var(--space-2);
padding-block: 2px;
cursor: pointer;
white-space: nowrap;
transition:
background 120ms ease,
border-color 120ms ease,
color 120ms ease;
background var(--duration-fast) ease,
border-color var(--duration-fast) ease,
color var(--duration-fast) ease;
}
[data-code-block-copy]:hover {
@ -98,7 +98,7 @@
}
[data-code-block-copy]:focus-visible {
outline: 2px solid var(--color-primary-solid);
outline: var(--border-width-medium) solid var(--color-primary-solid);
outline-offset: 2px;
}

@ -32,7 +32,7 @@
}
[data-code][data-variant='outline'] {
border: 1px solid var(--color-neutral-border);
border: var(--border-width) solid var(--color-neutral-border);
padding-inline: 0.32em;
padding-block: 0.1em;
}

@ -178,7 +178,7 @@
inline-size: var(--_color-picker-trigger-swatch-size);
block-size: var(--_color-picker-trigger-swatch-size);
border-radius: var(--color-picker-trigger-swatch-radius);
border: 1px solid var(--color-picker-trigger-swatch-border);
border: var(--border-width) solid var(--color-picker-trigger-swatch-border);
background-image: var(--cp-checker);
background-size: var(--cp-checker-cell) var(--cp-checker-cell);
background-position:
@ -215,7 +215,7 @@
inline-size: var(--_color-picker-swatch-size);
block-size: var(--_color-picker-swatch-size);
border-radius: var(--color-picker-swatch-radius);
border: 1px solid var(--color-picker-swatch-border);
border: var(--border-width) solid var(--color-picker-swatch-border);
background-image: var(--cp-checker);
background-size: var(--cp-checker-cell) var(--cp-checker-cell);
background-position:
@ -451,7 +451,7 @@
/* Reinforce thumb visibility against any colour: bright ring + dark shadow.
We tweak the slider thumb only within the colour-picker scope. */
[data-color-picker-channel-slider] [data-slider-thumb] {
border: 2px solid var(--color-surface-default);
border: var(--border-width-medium) solid var(--color-surface-default);
box-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);
@ -508,7 +508,7 @@
inline-size: auto;
block-size: auto;
padding: 0;
border: 1px solid var(--color-picker-swatch-border);
border: var(--border-width) solid var(--color-picker-swatch-border);
border-radius: var(--color-picker-swatch-radius);
background-image: var(--cp-checker);
background-size: var(--cp-checker-cell) var(--cp-checker-cell);

@ -259,7 +259,7 @@
}
[data-floating-wrapper]:has(> [data-combobox-content]) {
z-index: 80;
z-index: var(--combobox-content-z);
}
[data-combobox-content] {
@ -369,7 +369,7 @@
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
line-height: var(--leading-ui);
letter-spacing: 0.04em;
letter-spacing: var(--tracking-caps);
text-transform: uppercase;
}
@ -497,7 +497,7 @@
}
[data-combobox-selected-tag][data-disabled] {
opacity: 0.56;
opacity: var(--opacity-disabled);
}
[data-combobox-selected-tag-remove] {
@ -511,7 +511,7 @@
border-radius: var(--radius-sm);
background: transparent;
color: inherit;
opacity: 0.7;
opacity: var(--opacity-muted);
cursor: pointer;
transition:
background var(--duration-fast) var(--ease-default),
@ -532,5 +532,5 @@
[data-combobox-selected-tag-remove][data-disabled],
[data-combobox-selected-tag-remove]:disabled {
cursor: not-allowed;
opacity: 0.4;
opacity: var(--opacity-disabled);
}

@ -33,7 +33,7 @@
flex-direction: column;
inline-size: 100%;
min-inline-size: 0;
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-surface-overlay);
box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo);
@ -67,7 +67,7 @@
block-size: var(--_command-input-height);
padding-inline: var(--_command-input-px);
border: 0;
border-block-end: 1px solid var(--color-border-subtle);
border-block-end: var(--border-width) solid var(--color-border-subtle);
background: transparent;
color: var(--color-content-primary);
font-family: var(--font-ui);
@ -134,7 +134,7 @@
font-size: var(--font-size-xs);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
letter-spacing: var(--tracking-caps);
}
[data-command-group-items] {
@ -173,7 +173,7 @@
[data-command-item][data-disabled],
[data-command-link-item][data-disabled] {
cursor: default;
opacity: 0.55;
opacity: var(--opacity-disabled);
}
/* ── Item sub-parts (eidos-only) ──────────────────────────────────── */

@ -94,10 +94,10 @@
margin-inline-start: auto;
inline-size: 0.625rem;
block-size: 0.625rem;
border-inline-end: 2px solid currentColor;
border-block-start: 2px solid currentColor;
border-inline-end: var(--border-width-medium) solid currentColor;
border-block-start: var(--border-width-medium) solid currentColor;
transform: rotate(45deg);
opacity: 0.65;
opacity: var(--opacity-muted);
}
/* ── Group / GroupHeading / Separator ─────────────────────────────── */
@ -113,7 +113,7 @@
font-size: var(--context-menu-heading-font-size, var(--font-size-xs));
font-weight: var(--style-label-font-weight, 500);
text-transform: uppercase;
letter-spacing: 0.04em; /* literal: micro-tracking for caps */
letter-spacing: var(--tracking-caps);
}
[data-context-menu-separator] {

@ -12,7 +12,7 @@
}
[data-cropper][data-disabled] {
opacity: 0.6;
opacity: var(--opacity-disabled);
pointer-events: none;
}
@ -169,7 +169,7 @@
flex: 0 0 auto;
inline-size: var(--cropper-zoom-btn-size);
block-size: var(--cropper-zoom-btn-size);
border: 1px solid var(--cropper-zoom-btn-border);
border: var(--border-width) solid var(--cropper-zoom-btn-border);
border-radius: var(--radius-md);
background: var(--cropper-zoom-btn-bg);
color: var(--cropper-zoom-btn-fg);

@ -211,7 +211,8 @@
background: var(--_date-field-accent-element, var(--color-primary-element));
color: var(--_date-field-accent-text);
font-weight: 600;
box-shadow: inset 0 0 0 2px var(--_date-field-accent-border);
--ring-inset-color: var(--_date-field-accent-border);
box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor);
outline: none;
}

@ -25,7 +25,7 @@
justify-content: center;
height: var(--control-height-md);
padding-inline: var(--space-3);
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-surface-raised);
color: var(--color-content-primary);
@ -50,7 +50,7 @@
[data-dialog-trigger]:disabled,
[data-dialog-trigger][data-disabled] {
cursor: default;
opacity: 0.5;
opacity: var(--opacity-disabled);
}
/* ── Overlay ───────────────────────────────────────────────────────────── */
@ -69,12 +69,12 @@
/* Overlay + content enter/exit motion comes from the motion system (presets
* `fade` / `scale-fade` via `data-animation-style`). `data-state` here is only
* steady styling. The prior recipe timing (120ms in / 280ms out) is preserved
* via the per-component duration override. See `eidos-motion.md`. */
* steady styling. Asymmetric timing — `fast` in / `slow` out — drawn from the
* canonical duration scale. See `eidos-motion.md`. */
[data-dialog-overlay],
[data-dialog-content] {
--motion-duration-enter: 120ms;
--motion-duration-exit: 280ms;
--motion-duration-enter: var(--duration-fast);
--motion-duration-exit: var(--duration-slow);
}
/* ── Content ───────────────────────────────────────────────────────────── */
@ -390,7 +390,7 @@
[data-dialog-close]:disabled,
[data-dialog-close][data-disabled] {
cursor: default;
opacity: 0.5;
opacity: var(--opacity-disabled);
}
/* ── data-last-action exit tinting ─────────────────────────────────────────

@ -46,12 +46,12 @@
}
[data-drag-drop-draggable][data-dragging] {
opacity: 0.4;
opacity: var(--opacity-ghost);
}
[data-drag-drop-draggable][data-disabled] {
cursor: not-allowed;
opacity: 0.55;
opacity: var(--opacity-disabled);
}
[data-drag-drop-draggable]:focus-visible {
@ -69,18 +69,21 @@
/* Highlight all compatible drop targets while ANY drag is active. */
[data-drag-drop-droppable][data-accepting] {
box-shadow: inset 0 0 0 1px var(--_drag-drop-accent-soft);
--ring-inset-color: var(--_drag-drop-accent-soft);
--ring-inset-width: var(--border-width-thin);
box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor);
}
/* Pointer / focus is over THIS droppable. */
[data-drag-drop-droppable][data-dragover] {
background: var(--_drag-drop-accent-soft);
box-shadow: inset 0 0 0 2px var(--_drag-drop-accent);
--ring-inset-color: var(--_drag-drop-accent);
box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor);
}
[data-drag-drop-droppable][data-disabled] {
cursor: not-allowed;
opacity: 0.55;
opacity: var(--opacity-disabled);
}
[data-drag-drop-droppable]:focus-visible {
@ -91,10 +94,10 @@
/* ── Preview (floating pointer follower) ─ */
[data-drag-drop-preview] {
pointer-events: none;
z-index: 99;
z-index: var(--drag-drop-preview-z);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
border: 1px solid var(--_drag-drop-accent);
border: var(--border-width) solid var(--_drag-drop-accent);
background: var(--color-surface-default);
box-shadow:
var(--shadow-raised),

@ -28,7 +28,7 @@
[data-drawer-trigger]:disabled,
[data-drawer-trigger][data-disabled] {
cursor: default;
opacity: 0.5;
opacity: var(--opacity-disabled);
}
/* ── Overlay ────────────────────────────────────────────────────────────── */
@ -106,16 +106,16 @@
* etc. (the edge facing the user before closing). Only meaningful in
* overlay/inline variants where the drawer animates out. */
[data-drawer-content][data-side='right'] {
border-left: 3px solid transparent;
border-left: var(--border-width-thick) solid transparent;
}
[data-drawer-content][data-side='left'] {
border-right: 3px solid transparent;
border-right: var(--border-width-thick) solid transparent;
}
[data-drawer-content][data-side='top'] {
border-bottom: 3px solid transparent;
border-bottom: var(--border-width-thick) solid transparent;
}
[data-drawer-content][data-side='bottom'] {
border-top: 3px solid transparent;
border-top: var(--border-width-thick) solid transparent;
}
/* ── Content sizing (variant-shared) ────────────────────────────────────── */

@ -30,7 +30,7 @@
* content (e.g. selected toggle-group chips with z-index 1). The soma
* floating layer reads this computed z-index and mirrors it onto the
* positioner wrapper. */
z-index: var(--dropdown-menu-content-z, 80);
z-index: var(--dropdown-menu-content-z);
min-inline-size: var(--dropdown-menu-content-min-width, 12rem);
max-block-size: var(--dropdown-menu-content-max-height, min(60vh, 22rem));
overflow-y: auto;

@ -122,18 +122,18 @@
[data-feed-root][data-variant='surface'] [data-feed-article] {
background: var(--color-surface-default);
border: 1px solid var(--_feed-divider);
border: var(--border-width) solid var(--_feed-divider);
}
[data-feed-root][data-variant='outline'] [data-feed-article] {
background: transparent;
border: 1px solid var(--_feed-divider);
border: var(--border-width) solid var(--_feed-divider);
}
[data-feed-root][data-variant='ghost'] [data-feed-article] {
background: transparent;
border: 0;
border-block-end: 1px solid var(--_feed-divider);
border-block-end: var(--border-width) solid var(--_feed-divider);
border-radius: 0;
padding-inline: 0;
}
@ -171,12 +171,12 @@
gap: var(--_feed-gap);
margin-block-start: var(--space-2);
padding-inline-start: var(--_feed-thread-indent);
border-inline-start: 2px solid var(--_feed-accent-soft);
border-inline-start: var(--border-width-medium) solid var(--_feed-accent-soft);
}
[data-feed-root][dir='rtl'] [data-feed-thread] {
border-inline-start: 0;
border-inline-end: 2px solid var(--_feed-accent-soft);
border-inline-end: var(--border-width-medium) solid var(--_feed-accent-soft);
}
/* ── Sentinel ─
@ -200,7 +200,7 @@
inline-size: 0.75rem;
block-size: 0.75rem;
margin-inline-end: var(--space-2);
border: 2px solid var(--_feed-accent-soft);
border: var(--border-width-medium) solid var(--_feed-accent-soft);
border-block-start-color: var(--_feed-accent);
border-radius: var(--radius-full);
animation: feed-spin 0.7s linear infinite;

@ -120,7 +120,7 @@
padding-inline: var(--float-panel-header-px);
background: var(--float-panel-header-bg);
/* Accent rule under the header — reflects `data-color` visibly. */
border-block-end: 2px solid var(--_float-panel-accent);
border-block-end: var(--border-width-medium) solid var(--_float-panel-accent);
cursor: grab;
touch-action: none;
user-select: none;
@ -133,10 +133,12 @@
ring, and a steady ring while in keyboard "grab mode" (data-grabbed). */
[data-float-panel-header]:focus-visible {
outline: none;
box-shadow: inset 0 0 0 2px var(--_float-panel-accent);
--ring-inset-color: var(--_float-panel-accent);
box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor);
}
[data-float-panel-header][data-grabbed] {
box-shadow: inset 0 0 0 2px var(--_float-panel-accent);
--ring-inset-color: var(--_float-panel-accent);
box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor);
cursor: grabbing;
}
@ -253,7 +255,7 @@
background-size: 10px 10px;
background-position: bottom right;
background-repeat: no-repeat;
opacity: 0.65;
opacity: var(--opacity-muted);
}
[data-float-panel-resize-grip]:hover,
[data-float-panel-resize-grip][data-grabbed] {
@ -263,7 +265,8 @@
[data-float-panel-resize-grip]:focus-visible {
outline: none;
opacity: 1;
box-shadow: inset 0 0 0 2px var(--_float-panel-accent);
--ring-inset-color: var(--_float-panel-accent);
box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor);
border-radius: var(--radius-sm);
}

@ -32,7 +32,7 @@
flex-direction: column;
gap: 1px;
padding: var(--space-1);
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-surface-default);
color: var(--color-content-primary);
@ -115,7 +115,7 @@
}
[data-grid-list][data-disabled] {
opacity: 0.55;
opacity: var(--opacity-disabled);
cursor: not-allowed;
}
@ -158,7 +158,7 @@
[data-grid-list-row][data-disabled] {
color: var(--color-content-muted);
cursor: default;
opacity: 0.6;
opacity: var(--opacity-disabled);
}
[data-grid-list-row]:focus-visible {
@ -199,7 +199,7 @@
flex: 0 0 auto;
inline-size: var(--_grid-list-checkbox-size);
block-size: var(--_grid-list-checkbox-size);
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: 0.2rem;
background: transparent;
cursor: pointer;

@ -16,7 +16,7 @@
}
[data-image-adjustments][data-disabled] {
opacity: 0.55;
opacity: var(--opacity-disabled);
}
[data-image-adjustments-item] {
@ -74,7 +74,7 @@
}
[data-image-adjustments-reset][disabled] {
opacity: 0.4;
opacity: var(--opacity-disabled);
cursor: default;
text-decoration: none;
}

@ -16,7 +16,7 @@
}
[data-image-picker][data-disabled] {
opacity: 0.6;
opacity: var(--opacity-disabled);
}
[data-image-picker-preview] {
@ -24,7 +24,7 @@
aspect-ratio: var(--image-picker-preview-aspect);
border-radius: var(--image-picker-preview-radius);
background: var(--image-picker-preview-bg);
border: 1px solid var(--image-picker-preview-border);
border: var(--border-width) solid var(--image-picker-preview-border);
overflow: hidden;
}
@ -71,7 +71,7 @@
justify-content: center;
inline-size: var(--image-picker-button-size);
block-size: var(--image-picker-button-size);
border: 1px solid var(--image-picker-button-border);
border: var(--border-width) solid var(--image-picker-button-border);
border-radius: var(--image-picker-button-radius);
background: var(--image-picker-button-bg);
color: var(--image-picker-button-fg);
@ -93,7 +93,7 @@
[data-image-picker-rotate][disabled],
[data-image-picker-remove][disabled] {
opacity: 0.5;
opacity: var(--opacity-disabled);
cursor: default;
}

@ -148,12 +148,7 @@
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
var(--image-placeholder-base) 0%,
var(--image-placeholder-highlight) 50%,
var(--image-placeholder-base) 100%
);
background: var(--gradient-shimmer);
background-size: 200% 100%;
animation: image-shimmer var(--image-shimmer-duration) linear infinite;
z-index: -1;

@ -27,19 +27,19 @@
vertical-align: baseline;
user-select: none;
white-space: nowrap;
letter-spacing: 0.02em;
letter-spacing: var(--tracking-wide);
}
[data-kbd][data-variant='surface'] {
background: var(--color-neutral-element);
border: 1px solid var(--color-neutral-border);
border-bottom-width: 2px;
border: var(--border-width) solid var(--color-neutral-border);
border-bottom-width: var(--border-width-medium);
box-shadow: 0 1px 0 var(--color-neutral-border) inset;
}
[data-kbd][data-variant='outline'] {
background: transparent;
border: 1px solid var(--color-neutral-border);
border: var(--border-width) solid var(--color-neutral-border);
}
[data-kbd][data-variant='ghost'] {

@ -60,7 +60,7 @@
min-inline-size: var(--_link-preview-min-width);
max-inline-size: var(--_link-preview-max-width);
padding: var(--_link-preview-py) var(--_link-preview-px);
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-surface-overlay);
color: var(--color-content-primary);

@ -27,8 +27,8 @@
cursor: pointer;
text-decoration: none;
transition:
color 120ms ease,
text-decoration-color 120ms ease;
color var(--duration-fast) ease,
text-decoration-color var(--duration-fast) ease;
}
[data-link][data-color='primary'] {
@ -90,7 +90,7 @@
[data-link][data-variant='subtle']:hover,
[data-link][data-variant='plain']:hover {
color: inherit;
opacity: 0.85;
opacity: var(--opacity-subtle);
}
[data-link]:active {
@ -98,13 +98,13 @@
}
[data-link]:focus-visible {
outline: 2px solid var(--color-primary-solid);
outline: var(--border-width-medium) solid var(--color-primary-solid);
outline-offset: 2px;
}
[data-link][data-disabled] {
cursor: not-allowed;
opacity: 0.5;
opacity: var(--opacity-disabled);
pointer-events: none;
}

@ -30,7 +30,7 @@
flex-direction: column;
gap: var(--space-1);
padding: var(--space-1);
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-surface-default);
color: var(--color-content-primary);
@ -136,7 +136,7 @@
}
[data-listbox][data-disabled] {
opacity: 0.55;
opacity: var(--opacity-disabled);
cursor: not-allowed;
}
@ -183,7 +183,7 @@
[data-listbox-item][data-disabled] {
color: var(--color-content-muted);
cursor: default;
opacity: 0.6;
opacity: var(--opacity-disabled);
}
/* ── ItemIndicator (✓ glyph on selected items) ─
@ -221,5 +221,5 @@
font-size: var(--font-size-xs);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
letter-spacing: var(--tracking-caps);
}

@ -34,7 +34,7 @@
align-items: center;
gap: var(--_menubar-gap);
padding: var(--_menubar-padding);
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--_menubar-radius);
background: var(--color-surface-raised);
}

@ -135,10 +135,10 @@
inline-size: 0.5rem;
block-size: 0.5rem;
margin-inline-start: var(--space-1);
border-block-end: 1.5px solid currentColor;
border-inline-end: 1.5px solid currentColor;
border-block-end: var(--border-width-medium) solid currentColor;
border-inline-end: var(--border-width-medium) solid currentColor;
transform: translateY(-1px) rotate(45deg);
opacity: 0.65;
opacity: var(--opacity-muted);
transition: transform var(--duration-fast) var(--ease-default);
}
@ -161,11 +161,11 @@
position: absolute;
inset-block-start: calc(100% + var(--space-1));
inset-inline-start: 0;
z-index: 50;
z-index: var(--navigation-menu-content-z);
min-inline-size: 14rem;
padding: var(--space-2);
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-lg);
background: var(--color-surface-overlay);
color: var(--color-content-primary);

@ -204,7 +204,7 @@
font: inherit;
outline: none;
/* Slightly wider tracking helps recognise the · placeholder mask. */
letter-spacing: 0.02em;
letter-spacing: var(--tracking-wide);
}
[data-password-field-input]::placeholder {

@ -9,7 +9,7 @@
justify-content: center;
height: var(--control-height-md);
padding-inline: var(--space-3);
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-surface-raised);
color: var(--color-content-primary);
@ -229,25 +229,25 @@
* Tint the trigger-facing edge (opposite to data-side) during the exit
* animation so the user gets a brief tinted flash by causa of the close.
*/
[data-popover-content][data-state='closed'][data-side='top'][data-last-action='saved'] { border-bottom: 2px solid var(--color-fulfill-element); }
[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='saved'] { border-top: 2px solid var(--color-fulfill-element); }
[data-popover-content][data-state='closed'][data-side='right'][data-last-action='saved'] { border-left: 2px solid var(--color-fulfill-element); }
[data-popover-content][data-state='closed'][data-side='left'][data-last-action='saved'] { border-right: 2px solid var(--color-fulfill-element); }
[data-popover-content][data-state='closed'][data-side='top'][data-last-action='saved'] { border-bottom: var(--border-width-medium) solid var(--color-fulfill-element); }
[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='saved'] { border-top: var(--border-width-medium) solid var(--color-fulfill-element); }
[data-popover-content][data-state='closed'][data-side='right'][data-last-action='saved'] { border-left: var(--border-width-medium) solid var(--color-fulfill-element); }
[data-popover-content][data-state='closed'][data-side='left'][data-last-action='saved'] { border-right: var(--border-width-medium) solid var(--color-fulfill-element); }
[data-popover-content][data-state='closed'][data-side='top'][data-last-action='failed'] { border-bottom: 2px solid var(--color-threat-element); }
[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='failed'] { border-top: 2px solid var(--color-threat-element); }
[data-popover-content][data-state='closed'][data-side='right'][data-last-action='failed'] { border-left: 2px solid var(--color-threat-element); }
[data-popover-content][data-state='closed'][data-side='left'][data-last-action='failed'] { border-right: 2px solid var(--color-threat-element); }
[data-popover-content][data-state='closed'][data-side='top'][data-last-action='failed'] { border-bottom: var(--border-width-medium) solid var(--color-threat-element); }
[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='failed'] { border-top: var(--border-width-medium) solid var(--color-threat-element); }
[data-popover-content][data-state='closed'][data-side='right'][data-last-action='failed'] { border-left: var(--border-width-medium) solid var(--color-threat-element); }
[data-popover-content][data-state='closed'][data-side='left'][data-last-action='failed'] { border-right: var(--border-width-medium) solid var(--color-threat-element); }
[data-popover-content][data-state='closed'][data-side='top'][data-last-action='cancelled'],
[data-popover-content][data-state='closed'][data-side='top'][data-last-action='dismissed'],
[data-popover-content][data-state='closed'][data-side='top'][data-last-action='dismissed-outside'] { border-bottom: 2px solid var(--color-neutral-element); }
[data-popover-content][data-state='closed'][data-side='top'][data-last-action='dismissed-outside'] { border-bottom: var(--border-width-medium) solid var(--color-neutral-element); }
[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='cancelled'],
[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed'],
[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed-outside'] { border-top: 2px solid var(--color-neutral-element); }
[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed-outside'] { border-top: var(--border-width-medium) solid var(--color-neutral-element); }
[data-popover-content][data-state='closed'][data-side='right'][data-last-action='cancelled'],
[data-popover-content][data-state='closed'][data-side='right'][data-last-action='dismissed'],
[data-popover-content][data-state='closed'][data-side='right'][data-last-action='dismissed-outside'] { border-left: 2px solid var(--color-neutral-element); }
[data-popover-content][data-state='closed'][data-side='right'][data-last-action='dismissed-outside'] { border-left: var(--border-width-medium) solid var(--color-neutral-element); }
[data-popover-content][data-state='closed'][data-side='left'][data-last-action='cancelled'],
[data-popover-content][data-state='closed'][data-side='left'][data-last-action='dismissed'],
[data-popover-content][data-state='closed'][data-side='left'][data-last-action='dismissed-outside'] { border-right: 2px solid var(--color-neutral-element); }
[data-popover-content][data-state='closed'][data-side='left'][data-last-action='dismissed-outside'] { border-right: var(--border-width-medium) solid var(--color-neutral-element); }

@ -33,7 +33,7 @@
inset-block-end: 1.5rem;
font-size: var(--font-size-xs);
line-height: 1;
letter-spacing: 0.1em;
letter-spacing: var(--tracking-widest);
color: var(--color-neutral-text);
opacity: 0.5;
}

@ -224,6 +224,6 @@
[data-search-field-loading-indicator]::before {
animation: none;
border-top-color: var(--search-field-loading-indicator-track);
opacity: 0.6;
opacity: var(--opacity-muted);
}
}

@ -304,7 +304,9 @@
/* Highlighted + checked: keep accent track but layer an inset focus shadow
so the row reads as "this checked item is the keyboard focus". */
[data-select-item][data-highlighted][data-state='checked'] {
box-shadow: inset 0 0 0 var(--select-item-focus-ring-width, 2px) var(--_select-accent-border);
--ring-inset-color: var(--_select-accent-border);
--ring-inset-width: var(--select-item-focus-ring-width, var(--border-width-medium));
box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor);
}
[data-select-item][data-disabled] {
@ -319,12 +321,12 @@
color: var(--select-item-description-color, var(--color-content-muted));
font-size: var(--select-item-description-font-size, 0.85em);
line-height: var(--select-line-height);
opacity: 0.8;
opacity: var(--opacity-subtle);
}
[data-select-item][data-state='checked'] [data-select-item-description] {
color: inherit;
opacity: 0.85;
opacity: var(--opacity-subtle);
}
[data-select-item-indicator] {

@ -101,7 +101,7 @@
[data-skeleton][data-animated] {
background-image: linear-gradient(
90deg,
var(--gradient-angle-to-r),
var(--_skeleton-bg) 0%,
var(--_skeleton-highlight) 50%,
var(--_skeleton-bg) 100%
@ -117,7 +117,7 @@
[data-skeleton][data-shape='text'][data-animated] [data-skeleton-line] {
background-image: linear-gradient(
90deg,
var(--gradient-angle-to-r),
var(--_skeleton-bg) 0%,
var(--_skeleton-highlight) 50%,
var(--_skeleton-bg) 100%

@ -54,17 +54,17 @@
/* ── Variant — surface (default) ─ */
[data-table-root][data-variant='surface'] {
border: 1px solid var(--_table-divider);
border: var(--border-width) solid var(--_table-divider);
background: var(--color-surface-default);
}
[data-table-root][data-variant='outline'] {
border: 1px solid var(--_table-divider);
border: var(--border-width) solid var(--_table-divider);
background: transparent;
}
[data-table-root][data-variant='ghost'] {
border: 1px solid transparent;
border: var(--border-width) solid transparent;
background: transparent;
}
@ -158,7 +158,7 @@
}
[data-table-root][data-disabled] {
opacity: 0.55;
opacity: var(--opacity-disabled);
pointer-events: none;
}
@ -199,7 +199,7 @@
text-align: start;
font-weight: var(--font-weight-semibold, 600);
color: var(--color-content-primary);
border-block-end: 1px solid var(--_table-divider);
border-block-end: var(--border-width) solid var(--_table-divider);
white-space: nowrap;
vertical-align: middle;
user-select: none;
@ -267,7 +267,7 @@
box-sizing: border-box;
padding-block: var(--_table-cell-py);
padding-inline: var(--_table-cell-px);
border-block-end: 1px solid var(--_table-divider);
border-block-end: var(--border-width) solid var(--_table-divider);
vertical-align: middle;
color: inherit;
}
@ -289,7 +289,7 @@
[data-table-footer] [data-table-cell] {
font-weight: var(--font-weight-semibold, 600);
border-block-end: 0;
border-block-start: 1px solid var(--_table-divider);
border-block-start: var(--border-width) solid var(--_table-divider);
}
/* ── RowDetail (disclosure) ─ */
@ -303,7 +303,7 @@
[data-table-row-detail] > td {
padding: var(--space-3) var(--_table-cell-px);
border-block-end: 1px solid var(--_table-divider);
border-block-end: var(--border-width) solid var(--_table-divider);
color: var(--color-content-secondary);
}
@ -338,7 +338,7 @@
justify-content: center;
inline-size: 1em;
block-size: 1em;
opacity: 0.55;
opacity: var(--opacity-disabled);
transition: opacity var(--duration-fast) var(--ease-default);
}

@ -141,7 +141,7 @@
[data-textarea-input][data-disabled] {
cursor: not-allowed;
opacity: var(--opacity-disabled, 0.6);
opacity: var(--opacity-disabled);
}
[data-textarea-input]::placeholder {
@ -167,7 +167,7 @@
}
[data-textarea-count-sep] {
opacity: 0.5;
opacity: var(--opacity-muted);
}
@media (prefers-reduced-motion: reduce) {

@ -178,6 +178,20 @@
inline-size: 100%;
}
/* Channel caption naming each slider (Hour / Minute / Second). Visual
only — the soma Slider already carries the same text as `aria-label`,
so the caption is `aria-hidden`. */
.time-picker-slider-label {
margin-block-end: var(--space-1);
color: var(--color-content-secondary);
font-family: var(--font-ui);
font-size: var(--font-size-xxs);
font-weight: var(--font-weight-medium);
letter-spacing: var(--tracking-caps);
line-height: 1;
text-transform: uppercase;
}
.time-picker-scale {
position: relative;
block-size: 0.875rem;

@ -196,7 +196,7 @@
font-size: var(--font-size-xxs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
letter-spacing: var(--tracking-wider);
color: var(--color-content-muted);
}
@ -298,7 +298,7 @@
display: inline-flex;
gap: var(--space-1);
padding: 2px;
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-surface-default);
}
@ -340,7 +340,7 @@
[data-time-range-picker][data-readonly] [data-time-range-picker-trigger],
[data-time-range-picker][data-readonly] [data-time-range-picker-clear] {
pointer-events: none;
opacity: 0.7;
opacity: var(--opacity-muted);
cursor: default;
}

@ -227,7 +227,7 @@
* extra child element. */
[data-toast-item][data-loading] [data-toast-status] {
background: transparent;
border: 2px solid color-mix(in srgb, var(--_toast-status-color) 30%, transparent);
border: var(--border-width-medium) solid color-mix(in srgb, var(--_toast-status-color) 30%, transparent);
border-block-start-color: var(--_toast-status-color);
animation: toast-spinner 0.9s linear infinite;
}

@ -69,7 +69,7 @@
--_tooltip-bg: color-mix(in srgb, var(--color-surface-overlay) 70%, transparent);
--_tooltip-border: transparent;
--_tooltip-shadow: none;
backdrop-filter: blur(8px);
backdrop-filter: blur(var(--blur-md));
}
/* ── Arrow ──────────────────────────────────────────────────────────────── */

@ -50,15 +50,15 @@
/* ── Variant ─ */
[data-tree-grid-root][data-variant='surface'] {
border: 1px solid var(--_tree-grid-divider);
border: var(--border-width) solid var(--_tree-grid-divider);
background: var(--color-surface-default);
}
[data-tree-grid-root][data-variant='outline'] {
border: 1px solid var(--_tree-grid-divider);
border: var(--border-width) solid var(--_tree-grid-divider);
background: transparent;
}
[data-tree-grid-root][data-variant='ghost'] {
border: 1px solid transparent;
border: var(--border-width) solid transparent;
background: transparent;
}
@ -137,7 +137,7 @@
}
[data-tree-grid-root][data-disabled] {
opacity: 0.55;
opacity: var(--opacity-disabled);
pointer-events: none;
}
@ -178,7 +178,7 @@
/* ── Header ─ */
[data-tree-grid-header] {
background: var(--_tree-grid-header-bg);
border-block-end: 1px solid var(--_tree-grid-divider);
border-block-end: var(--border-width) solid var(--_tree-grid-divider);
font-weight: var(--font-weight-semibold, 600);
}
@ -197,7 +197,7 @@
/* ── Body row ─ */
[data-tree-grid-row] {
background: var(--color-surface-default);
border-block-end: 1px solid var(--_tree-grid-divider);
border-block-end: var(--border-width) solid var(--_tree-grid-divider);
transition: background var(--duration-fast) var(--ease-default);
outline: none;
}
@ -225,7 +225,7 @@
[data-tree-grid-row][data-disabled] {
color: var(--color-content-muted);
cursor: not-allowed;
opacity: 0.6;
opacity: var(--opacity-disabled);
}
[data-tree-grid-row]:focus-visible {

@ -46,19 +46,19 @@
/* ── Variant ─ */
[data-tree-view-root][data-variant='surface'] {
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
background: var(--color-surface-default);
padding: var(--space-2);
}
[data-tree-view-root][data-variant='outline'] {
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
background: transparent;
padding: var(--space-2);
}
[data-tree-view-root][data-variant='ghost'] {
border: 1px solid transparent;
border: var(--border-width) solid transparent;
background: transparent;
padding: 0;
}
@ -178,7 +178,7 @@
[data-tree-view-item][data-disabled] {
color: var(--color-content-muted);
cursor: not-allowed;
opacity: 0.6;
opacity: var(--opacity-disabled);
}
[data-tree-view-branch-control]:focus-visible,

@ -46,15 +46,15 @@
}
[data-virtual-grid-root][data-variant='surface'] {
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
background: var(--color-surface-default);
}
[data-virtual-grid-root][data-variant='outline'] {
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
background: transparent;
}
[data-virtual-grid-root][data-variant='ghost'] {
border: 1px solid transparent;
border: var(--border-width) solid transparent;
background: transparent;
}

@ -51,15 +51,15 @@
/* ── Variant ─ */
[data-virtual-list-root][data-variant='surface'] {
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
background: var(--color-surface-default);
}
[data-virtual-list-root][data-variant='outline'] {
border: 1px solid var(--color-border-default);
border: var(--border-width) solid var(--color-border-default);
background: transparent;
}
[data-virtual-list-root][data-variant='ghost'] {
border: 1px solid transparent;
border: var(--border-width) solid transparent;
background: transparent;
}

@ -124,6 +124,11 @@
}
:root {
--breakpoint-sm: 480px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-xxl: 1536px;
--space-0: 0px;
--space-1: calc(4px * var(--density-space-scale) * var(--scaling));
--space-2: calc(8px * var(--density-space-scale) * var(--scaling));
@ -157,16 +162,17 @@
--radius-xxl: 20px;
--radius-full: 9999px;
--border-width-none: 0px;
--border-width-hairline: 0.5px;
--border-width-thin: 1px;
--border-width-medium: 1.5px;
--border-width-thick: 2px;
--border-width-medium: 2px;
--border-width-thick: 3px;
--border-width-heavy: 4px;
--border-style-solid: solid;
--border-style-dashed: dashed;
--border-style-dotted: dotted;
--border-width: var(--border-width-thin);
--border-style: var(--border-style-solid);
--border: var(--border-width) var(--border-style) var(--color-border-default);
--ring-inset-width: var(--border-width-medium);
--focus-ring-offset: 1px;
--focus-ring-width: 2px;
--focus-ring-inner-width: 0px;
@ -242,13 +248,50 @@
--icon-stroke-width-sm: 1.25;
--icon-stroke-width-md: 1.5;
--icon-stroke-width-lg: 1.75;
--blur-none: 0px;
--blur-sm: calc(4px * var(--scaling));
--blur-md: calc(8px * var(--scaling));
--blur-lg: calc(12px * var(--scaling));
--blur-xl: calc(16px * var(--scaling));
--blur-xxl: calc(24px * var(--scaling));
--gradient-angle-to-t: 0deg;
--gradient-angle-to-tr: 45deg;
--gradient-angle-to-r: 90deg;
--gradient-angle-to-br: 135deg;
--gradient-angle-to-b: 180deg;
--gradient-angle-to-bl: 225deg;
--gradient-angle-to-l: 270deg;
--gradient-angle-to-tl: 315deg;
--gradient-shimmer: linear-gradient(var(--gradient-angle-to-r), var(--color-surface-raised) 0%, color-mix(in srgb, var(--color-surface-default) 70%, var(--color-surface-overlay)) 50%, var(--color-surface-raised) 100%);
--opacity-0: 0;
--opacity-muted: 0.72;
--opacity-disabled: 0.48;
--opacity-scrim: 0.42;
--opacity-overlay: 0.66;
--opacity-hover: 0.92;
--opacity-press: 0.84;
--opacity-5: 0.05;
--opacity-10: 0.1;
--opacity-15: 0.15;
--opacity-20: 0.2;
--opacity-25: 0.25;
--opacity-30: 0.3;
--opacity-35: 0.35;
--opacity-40: 0.4;
--opacity-45: 0.45;
--opacity-50: 0.5;
--opacity-55: 0.55;
--opacity-60: 0.6;
--opacity-65: 0.65;
--opacity-70: 0.7;
--opacity-75: 0.75;
--opacity-80: 0.8;
--opacity-85: 0.85;
--opacity-90: 0.9;
--opacity-95: 0.95;
--opacity-100: 1;
--opacity-ghost: 0.3;
--opacity-disabled: 0.4;
--opacity-scrim: 0.45;
--opacity-muted: 0.65;
--opacity-overlay: 0.65;
--opacity-subtle: 0.8;
--opacity-press: 0.85;
--opacity-hover: 0.9;
--opacity-full: 1;
--z-index-base: 0;
--z-index-raised: 1;
@ -294,7 +337,9 @@
--tracking-tight: -0.02em;
--tracking-normal: 0;
--tracking-wide: 0.02em;
--tracking-caps: 0.04em;
--tracking-wider: 0.05em;
--tracking-widest: 0.1em;
--leading-none: 1;
--leading-tight: 1.15;
--leading-snug: 1.3;
@ -454,14 +499,14 @@
--depth-overlay-shadow: var(--shadow-overlay);
--depth-overlay-halo: inset 0 1px 0 0 color-mix(in oklab, white 7%, transparent);
--depth-overlay-z: var(--z-index-popover);
--depth-overlay-blur: 10px;
--depth-overlay-blur: var(--blur-lg);
--depth-modal-surface: var(--color-surface-overlay);
--depth-modal-shadow: var(--shadow-overlay);
--depth-modal-halo: inset 0 1px 0 0 color-mix(in oklab, white 8%, transparent);
--depth-modal-z: var(--z-index-modal);
--depth-modal-blur: 16px;
--depth-modal-blur: var(--blur-xl);
--depth-recessed-surface: var(--color-surface-muted);
--depth-recessed-shadow: inset 0 1px 2px color-mix(in srgb, var(--color-neutral-contrast) 12%, transparent);
--depth-recessed-shadow: var(--shadow-inset-subtle);
--depth-recessed-z: var(--z-index-base);
--shape-smoothing: 2;
--shape-nest-gap: var(--space-2);
@ -594,18 +639,18 @@
--avatar-size-xl: 64px;
--avatar-size-xxl: 96px;
--avatar-font-family: var(--font-ui);
--avatar-font-size-xs: 10px;
--avatar-font-size-sm: 12px;
--avatar-font-size-md: 14px;
--avatar-font-size-lg: 16px;
--avatar-font-size-xl: 20px;
--avatar-font-size-xxl: 28px;
--avatar-font-size-xs: var(--font-size-xxs);
--avatar-font-size-sm: var(--font-size-xs);
--avatar-font-size-md: var(--font-size-sm);
--avatar-font-size-lg: var(--font-size-md);
--avatar-font-size-xl: var(--font-size-lg);
--avatar-font-size-xxl: var(--font-size-xl);
--avatar-font-weight: var(--font-weight-medium);
--avatar-radius-full: var(--radius-full);
--avatar-radius-md: var(--radius-md);
--avatar-radius-sm: var(--radius-sm);
--avatar-radius-none: 0;
--avatar-border-width: 2px;
--avatar-border-width: var(--border-width-medium);
--avatar-primary-solid-bg: var(--color-primary-solid);
--avatar-primary-solid-fg: var(--color-primary-contrast);
--avatar-primary-soft-bg: var(--color-primary-surface);
@ -660,7 +705,7 @@
--avatar-color-custom: currentColor;
--avatar-color-custom-contrast: white;
--avatar-ring-color-custom: currentColor;
--avatar-badge-border-width: 2px;
--avatar-badge-border-width: var(--border-width-medium);
--avatar-badge-color-custom: currentColor;
--avatar-badge-color-custom-contrast: white;
--avatar-group-gap-xs: var(--space-1);
@ -668,7 +713,7 @@
--avatar-group-gap-md: var(--space-3);
--avatar-group-gap-lg: var(--space-4);
--avatar-group-overlap: -0.6em;
--avatar-group-carve-width: 2px;
--avatar-group-carve-width: var(--border-width-medium);
--avatar-group-carve-color: var(--color-surface-default);
--avatar-group-reverse-z-max: 20;
--avatar-group-max: 99;
@ -820,15 +865,15 @@
--card-footer-gap: var(--space-3);
--card-footer-padding-start: var(--space-3);
--card-footer-border-color: var(--color-border-subtle);
--card-emerge-duration: 320ms;
--card-emerge-duration: var(--duration-slow);
--card-emerge-ease: var(--ease-out);
--card-emerge-distance: 8px;
--card-hover-lift: -2px;
--card-hover-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo);
--card-press-scale: 0.985;
--card-press-duration: 80ms;
--card-selected-ring-width: 2px;
--card-disabled-opacity: 0.6;
--card-selected-ring-width: var(--border-width-medium);
--card-disabled-opacity: var(--opacity-disabled);
--card-color-custom: currentColor;
--card-color-custom-contrast: white;
--card-primary-solid: var(--color-primary-solid);
@ -903,7 +948,7 @@
--checkbox-border-off-hover: var(--color-border-strong);
--checkbox-bg-disabled: color-mix( in srgb, var(--color-surface-overlay) 78%, var(--color-surface-default) );
--checkbox-border-disabled: color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) );
--checkbox-disabled-opacity: 0.72;
--checkbox-disabled-opacity: var(--opacity-disabled);
--checkbox-indicator-color: var(--color-content-on-solid);
--checkbox-indicator-color-disabled: color-mix( in srgb, var(--color-content-on-solid) 82%, var(--color-surface-default) );
--checkbox-primary-solid: var(--color-primary-solid);
@ -932,11 +977,11 @@
--collapsible-content-padding: var(--space-3);
--collapsible-transition-duration: var(--duration-normal);
--collapsible-transition-ease: var(--ease-default);
--collapsible-disabled-opacity: 0.5;
--collapsible-disabled-opacity: var(--opacity-disabled);
--dialog-trigger-font-size: var(--font-size-sm);
--dialog-overlay-bg: var(--color-overlay);
--dialog-overlay-opacity: 62%;
--dialog-overlay-blur: 10px;
--dialog-overlay-blur: var(--blur-lg);
--dialog-overlay-z: 70;
--dialog-content-bg: var(--color-surface-overlay);
--dialog-content-color: var(--color-content-primary);
@ -1047,7 +1092,7 @@
--field-error-color: var(--color-risk-text);
--field-transition-duration: var(--duration-fast);
--field-transition-ease: var(--ease-default);
--field-disabled-opacity: 0.56;
--field-disabled-opacity: var(--opacity-disabled);
--form-gap-xs: var(--space-3);
--form-gap-sm: var(--space-4);
--form-gap-md: var(--space-5);
@ -1101,9 +1146,9 @@
--form-widget-border-width: var(--border-width);
--form-transition-duration: var(--duration-fast);
--form-transition-ease: var(--ease-default);
--form-disabled-opacity: 0.56;
--form-disabled-opacity: var(--opacity-disabled);
--drawer-overlay-bg: var(--color-overlay);
--drawer-overlay-blur: 10px;
--drawer-overlay-blur: var(--blur-lg);
--drawer-overlay-opacity: 62%;
--drawer-overlay-z: 72;
--drawer-inline-z: 64;
@ -1122,7 +1167,7 @@
--drawer-content-radius: var(--radius-xl);
--drawer-content-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo);
--drawer-content-shadow-dragging: var(--shadow-overlay-strong, var(--depth-overlay-shadow)), var(--depth-overlay-halo);
--drawer-content-ring-dragging-width: 2px;
--drawer-content-ring-dragging-width: var(--border-width-medium);
--drawer-content-ring-dragging-color: color-mix( in srgb, var(--color-content-secondary) 32%, transparent );
--drawer-content-ring-dragging-offset: 2px;
--drawer-content-inset: var(--space-3);
@ -1179,7 +1224,6 @@
--image-fade-duration: var(--duration-normal);
--image-fade-ease: var(--ease-out);
--image-placeholder-base: var(--color-surface-raised);
--image-placeholder-highlight: color-mix(in srgb, var(--color-surface-default) 70%, var(--color-surface-overlay));
--image-shimmer-duration: 1.8s;
--image-placeholder-color-custom: var(--color-neutral-track);
--image-font-family: var(--font-ui);
@ -1269,7 +1313,7 @@
--spin-field-control-disabled-opacity: 0.48;
--spin-field-transition-duration: var(--duration-fast);
--spin-field-transition-ease: var(--ease-default);
--spin-field-disabled-opacity: 0.56;
--spin-field-disabled-opacity: var(--opacity-disabled);
--float-panel-bg: var(--color-surface-overlay);
--float-panel-border: var(--color-border-default);
--float-panel-border-width: var(--border-width);
@ -1388,7 +1432,7 @@
--date-field-segment-hover-bg: color-mix(in srgb, var(--color-content-primary) 6%, transparent);
--date-field-transition-duration: var(--duration-fast);
--date-field-transition-ease: var(--ease-default);
--date-field-disabled-opacity: 0.56;
--date-field-disabled-opacity: var(--opacity-disabled);
--time-field-stack-gap: var(--space-1-5);
--time-field-height-xs: var(--control-height-xs);
--time-field-height-sm: var(--control-height-sm);
@ -1439,7 +1483,7 @@
--time-field-segment-focus-shadow: inset 0 0 0 var(--border-width) var(--_time-field-accent-border);
--time-field-transition-duration: var(--duration-fast);
--time-field-transition-ease: var(--ease-default);
--time-field-disabled-opacity: 0.56;
--time-field-disabled-opacity: var(--opacity-disabled);
--color-field-stack-gap: var(--space-1-5);
--color-field-height-xs: var(--control-height-xs);
--color-field-height-sm: var(--control-height-sm);
@ -1499,7 +1543,7 @@
--color-field-format-select-px: var(--space-1);
--color-field-transition-duration: var(--duration-fast);
--color-field-transition-ease: var(--ease-default);
--color-field-disabled-opacity: 0.56;
--color-field-disabled-opacity: var(--opacity-disabled);
--color-picker-content-gap: var(--space-3);
--color-picker-content-padding-sm: var(--space-3);
--color-picker-content-padding-md: var(--space-3-5);
@ -1533,7 +1577,7 @@
--color-picker-area-border: var(--color-border-subtle);
--color-picker-area-thumb-size: 16px;
--color-picker-area-thumb-radius: var(--radius-full);
--color-picker-area-thumb-border-width: 2px;
--color-picker-area-thumb-border-width: var(--border-width-medium);
--color-picker-area-thumb-border: var(--color-surface-default);
--color-picker-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);
--color-picker-transparency-cell: var(--color-content-muted);
@ -1561,7 +1605,7 @@
--color-picker-saturation-floor: hsl(0, 0%, 50%);
--color-picker-transition-duration: var(--duration-fast);
--color-picker-transition-ease: var(--ease-default);
--color-picker-disabled-opacity: 0.56;
--color-picker-disabled-opacity: var(--opacity-disabled);
--button-height-xs: var(--control-height-xs);
--button-height-sm: var(--control-height-sm);
--button-height-md: var(--control-height-md);
@ -1604,7 +1648,7 @@
--button-transition-duration: var(--duration-fast);
--button-transition-ease: var(--ease-default);
--button-spinner-duration: 720ms;
--button-disabled-opacity: 0.5;
--button-disabled-opacity: var(--opacity-disabled);
--button-primary-track: var(--color-primary-track);
--button-primary-element: var(--color-primary-element);
--button-primary-border: var(--color-primary-border);
@ -1730,7 +1774,7 @@
--calendar-event-shadow: 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-accent-border);
--calendar-transition-duration: var(--duration-fast);
--calendar-transition-ease: var(--ease-default);
--calendar-disabled-opacity: 0.52;
--calendar-disabled-opacity: var(--opacity-disabled);
--chronos-cell-min-block-sm: calc(var(--control-height-md) * 2.6);
--chronos-cell-min-block-md: calc(var(--control-height-md) * 3.2);
--chronos-cell-min-block-lg: calc(var(--control-height-md) * 4);
@ -1739,7 +1783,7 @@
--chronos-lane-height-md: var(--space-6);
--chronos-lane-height-lg: var(--space-7);
--chronos-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--chronos-disabled-opacity: 0.52;
--chronos-disabled-opacity: var(--opacity-disabled);
--pagination-gap-xs: var(--space-0-5);
--pagination-gap-sm: var(--space-1);
--pagination-gap-md: var(--space-1-5);
@ -1775,7 +1819,7 @@
--pagination-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--pagination-transition-duration: var(--duration-fast);
--pagination-transition-ease: var(--ease-default);
--pagination-disabled-opacity: 0.56;
--pagination-disabled-opacity: var(--opacity-disabled);
--popover-content-z: 75;
--popover-overlay-z: 60;
--popover-overlay-bg: var(--color-overlay);
@ -1905,7 +1949,7 @@
--radio-group-focus-ring: 0 0 0 var(--radio-group-focus-ring-offset) var(--radio-group-focus-ring-surface), 0 0 0 calc(var(--radio-group-focus-ring-offset) + var(--radio-group-focus-ring-width)) var(--radio-group-focus-ring-color);
--radio-group-transition-duration: var(--duration-fast);
--radio-group-transition-ease: var(--ease-default);
--radio-group-disabled-opacity: 0.72;
--radio-group-disabled-opacity: var(--opacity-disabled);
--radio-group-primary-solid: var(--color-primary-solid);
--radio-group-primary-solid-hover: var(--color-primary-solid-hover);
--radio-group-neutral-solid: var(--color-neutral-solid);
@ -1969,7 +2013,7 @@
--radio-cards-focus-ring: var(--radio-group-focus-ring);
--radio-cards-transition-duration: var(--duration-fast);
--radio-cards-transition-ease: var(--ease-default);
--radio-cards-disabled-opacity: 0.6;
--radio-cards-disabled-opacity: var(--opacity-disabled);
--rating-group-gap-xs: var(--space-0-5);
--rating-group-gap-sm: var(--space-1);
--rating-group-gap-md: var(--space-1-5);
@ -1995,7 +2039,7 @@
--rating-group-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--rating-group-transition-duration: var(--duration-fast);
--rating-group-transition-ease: var(--ease-default);
--rating-group-disabled-opacity: 0.56;
--rating-group-disabled-opacity: var(--opacity-disabled);
--search-field-height-xs: var(--control-height-xs);
--search-field-height-sm: var(--control-height-sm);
--search-field-height-md: var(--control-height-md);
@ -2046,7 +2090,7 @@
--search-field-loading-indicator-duration: 900ms;
--search-field-transition-duration: var(--duration-fast);
--search-field-transition-ease: var(--ease-default);
--search-field-disabled-opacity: 0.56;
--search-field-disabled-opacity: var(--opacity-disabled);
--password-field-height-xs: var(--control-height-xs);
--password-field-height-sm: var(--control-height-sm);
--password-field-height-md: var(--control-height-md);
@ -2105,7 +2149,7 @@
--password-field-strength-radius: var(--radius-sm);
--password-field-transition-duration: var(--duration-fast);
--password-field-transition-ease: var(--ease-default);
--password-field-disabled-opacity: 0.56;
--password-field-disabled-opacity: var(--opacity-disabled);
--select-stack-gap: var(--space-2);
--select-font-family: var(--font-ui);
--select-font-size-xs: var(--font-size-xs);
@ -2203,7 +2247,7 @@
--select-item-bg-highlighted: var(--color-surface-raised);
--select-item-color-highlighted: var(--color-content-primary);
--select-item-indicator-size: var(--icon-size-sm);
--select-item-focus-ring-width: 2px;
--select-item-focus-ring-width: var(--border-width-medium);
--select-item-description-mt: 0.125rem;
--select-item-description-color: var(--color-content-muted);
--select-item-description-font-size: 0.85em;
@ -2276,7 +2320,7 @@
--toolbar-separator-radius: var(--radius-full);
--toolbar-transition-duration: var(--duration-fast);
--toolbar-transition-ease: var(--ease-default);
--toolbar-disabled-opacity: 0.48;
--toolbar-disabled-opacity: var(--opacity-disabled);
--file-upload-gap-sm: var(--space-2);
--file-upload-gap-md: var(--space-3);
--file-upload-gap-lg: var(--space-4);
@ -2336,7 +2380,7 @@
--file-upload-progress-track-bg: var(--color-neutral-track);
--file-upload-transition-duration: var(--duration-fast);
--file-upload-transition-ease: var(--ease-default);
--file-upload-disabled-opacity: 0.52;
--file-upload-disabled-opacity: var(--opacity-disabled);
--file-upload-primary-solid: var(--color-primary-solid);
--file-upload-primary-solid-hover: var(--color-primary-solid-hover);
--file-upload-primary-text: var(--color-primary-text);
@ -2422,7 +2466,7 @@
--tag-group-remove-bg-hover: color-mix(in srgb, currentColor 12%, transparent);
--tag-group-transition-duration: var(--duration-fast);
--tag-group-transition-ease: var(--ease-default);
--tag-group-disabled-opacity: 0.5;
--tag-group-disabled-opacity: var(--opacity-disabled);
--tag-group-primary-solid-bg: var(--color-primary-solid);
--tag-group-primary-solid-bg-hover: var(--color-primary-solid-hover);
--tag-group-primary-soft-bg: var(--color-primary-surface);
@ -2524,7 +2568,7 @@
--editable-submit-color: var(--color-content-on-solid);
--editable-transition-duration: var(--duration-fast);
--editable-transition-ease: var(--ease-default);
--editable-disabled-opacity: 0.5;
--editable-disabled-opacity: var(--opacity-disabled);
--editable-primary-solid: var(--color-primary-solid);
--editable-secondary-solid: var(--color-secondary-solid);
--editable-neutral-solid: var(--color-neutral-solid);
@ -2553,7 +2597,7 @@
--words-font-size-lg: var(--font-size-md);
--words-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--words-invalid-border: var(--color-risk-border);
--words-disabled-opacity: 0.5;
--words-disabled-opacity: var(--opacity-disabled);
--words-font-size-xs: 0.6875rem;
--words-wash-faint: 6%;
--words-wash-subtle: 16%;
@ -2593,7 +2637,7 @@
--words-code-py: var(--space-0-5);
--words-code-px: var(--space-1);
--words-quote-px: var(--space-3);
--words-quote-border-width: var(--border-width-thick);
--words-quote-border-width: var(--border-width-medium);
--words-quote-color: var(--color-content-secondary);
--words-list-padding-inline-start: var(--space-5);
--words-transition-duration: var(--duration-fast);
@ -2688,7 +2732,7 @@
--tags-input-delete-radius: var(--radius-full);
--tags-input-transition-duration: var(--duration-fast);
--tags-input-transition-ease: var(--ease-default);
--tags-input-disabled-opacity: 0.52;
--tags-input-disabled-opacity: var(--opacity-disabled);
--tags-input-primary-text: var(--color-primary-text);
--tags-input-secondary-text: var(--color-secondary-text);
--tags-input-neutral-text: var(--color-neutral-text);
@ -2743,7 +2787,7 @@
--stepper-indicator-font-weight: var(--font-weight-semibold);
--stepper-complete-color: var(--color-content-on-solid);
--stepper-separator-min-size: 3rem;
--stepper-separator-thickness: var(--border-width-thick);
--stepper-separator-thickness: var(--border-width-medium);
--stepper-separator-radius: var(--radius-full);
--stepper-separator-bg: var(--color-border-subtle);
--stepper-content-padding: var(--space-4);
@ -2763,7 +2807,7 @@
--stepper-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--stepper-transition-duration: var(--duration-fast);
--stepper-transition-ease: var(--ease-default);
--stepper-disabled-opacity: 0.45;
--stepper-disabled-opacity: var(--opacity-disabled);
--stepper-primary-solid: var(--color-primary-solid);
--stepper-primary-text: var(--color-primary-text);
--stepper-secondary-solid: var(--color-secondary-solid);
@ -2822,7 +2866,7 @@
--slider-tick-bg-active: var(--color-primary-solid);
--slider-transition-duration: var(--duration-fast);
--slider-transition-ease: var(--ease-default);
--slider-disabled-opacity: 0.56;
--slider-disabled-opacity: var(--opacity-disabled);
--switch-size-xs-track-width: 26px;
--switch-size-xs-track-height: 15px;
--switch-size-xs-thumb-size: 11px;
@ -2863,7 +2907,7 @@
--switch-track-border-disabled: color-mix( in srgb, var(--color-border-default) 70%, var(--color-border-subtle) );
--switch-thumb-bg-disabled: color-mix( in srgb, var(--color-surface-default) 88%, var(--color-surface-raised) );
--switch-thumb-border-disabled: color-mix( in srgb, var(--color-border-subtle) 88%, var(--color-surface-overlay) );
--switch-disabled-opacity: 0.72;
--switch-disabled-opacity: var(--opacity-disabled);
--switch-thumb-bg: var(--color-surface-default);
--switch-thumb-border: color-mix( in srgb, var(--color-border-default) 82%, var(--color-surface-overlay) );
--switch-thumb-icon-color: var(--color-content-muted);
@ -2945,7 +2989,7 @@
--tabs-surface-content-border: color-mix( in srgb, var(--tabs-provider-border) 84%, transparent );
--tabs-surface-content-bg: color-mix( in srgb, var(--color-surface-overlay) 92%, transparent );
--tabs-pills-content-padding-block-start: var(--space-2);
--tabs-disabled-opacity: 0.5;
--tabs-disabled-opacity: var(--opacity-disabled);
--tabs-indicator-transition-duration: var(--duration-normal);
--tabs-indicator-transition-ease: cubic-bezier(0.32, 0.72, 0, 1);
--tabs-indicator-line-thickness: 2px;
@ -2975,7 +3019,7 @@
--toast-provider-border-width: var(--border-width);
--toast-provider-radius: var(--radius-lg);
--toast-provider-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo);
--toast-accent-width: 3px;
--toast-accent-width: var(--border-width-thick);
--toast-transition-duration: var(--duration-fast);
--toast-transition-ease: var(--ease-default);
--toast-closed-offset: 10px;
@ -3077,11 +3121,11 @@
--toggle-font-family: var(--font-ui);
--toggle-line-height: var(--leading-ui);
--toggle-letter-spacing: var(--tracking-normal);
--toggle-font-size-xs: 10px;
--toggle-font-size-sm: 12px;
--toggle-font-size-md: 14px;
--toggle-font-size-lg: 16px;
--toggle-font-size-xl: 18px;
--toggle-font-size-xs: var(--font-size-xxs);
--toggle-font-size-sm: var(--font-size-xs);
--toggle-font-size-md: var(--font-size-sm);
--toggle-font-size-lg: var(--font-size-md);
--toggle-font-size-xl: var(--font-size-lg);
--toggle-font-weight-xs: var(--font-weight-medium);
--toggle-font-weight-sm: var(--font-weight-medium);
--toggle-font-weight-md: var(--font-weight-medium);
@ -3151,7 +3195,7 @@
--toggle-ghost-bg: transparent;
--toggle-ghost-border: transparent;
--toggle-ghost-hover-border: transparent;
--toggle-disabled-opacity: 0.45;
--toggle-disabled-opacity: var(--opacity-disabled);
--tooltip-content-z: 76;
--tooltip-max-width: 22rem;
--tooltip-padding-y-sm: var(--space-1);
@ -3290,7 +3334,7 @@
--separator-min-length: var(--space-4);
--toggle-group-gap: var(--space-1);
--pin-input-gap: var(--space-2);
--pin-input-disabled-opacity: 0.55;
--pin-input-disabled-opacity: var(--opacity-disabled);
--pin-input-cell-size-sm: 2rem;
--pin-input-cell-size-md: 2.5rem;
--pin-input-cell-size-lg: 3rem;
@ -3301,7 +3345,7 @@
--pin-input-cell-font-size-xl: var(--font-size-xxl);
--pin-input-cell-font-weight: 500;
--pin-input-cell-radius: var(--radius-md);
--pin-input-cell-border-width: 1px;
--pin-input-cell-border-width: var(--border-width);
--pin-input-cell-border-color: var(--color-border-default);
--pin-input-cell-bg: var(--color-surface-default);
--pin-input-cell-filled-bg: var(--color-primary-element);
@ -3324,7 +3368,7 @@
--splitter-radius: var(--radius-md);
--splitter-border-width: var(--border-width);
--splitter-border-color: var(--color-border-default);
--splitter-disabled-opacity: 0.6;
--splitter-disabled-opacity: var(--opacity-disabled);
--splitter-panel-bg: transparent;
--splitter-handle-size: var(--space-2);
--splitter-handle-bg: var(--color-border-subtle);
@ -3336,13 +3380,16 @@
--splitter-grip-cross: 2px;
--splitter-transition-duration: var(--duration-fast);
--splitter-transition-ease: var(--ease-default);
--combobox-content-z: 80;
--drag-drop-preview-z: 99;
--navigation-menu-content-z: 50;
--navigation-menu-indicator-w: 0px;
--navigation-menu-indicator-x: 0px;
--dropdown-menu-content-z: 80;
--dropdown-menu-content-min-width: 12rem;
--dropdown-menu-content-max-height: min(60vh, 22rem);
--dropdown-menu-content-padding: var(--space-1);
--dropdown-menu-content-border-width: 1px;
--dropdown-menu-content-border-width: var(--border-width);
--dropdown-menu-content-border-color: var(--color-border-default);
--dropdown-menu-content-radius: var(--radius-md);
--dropdown-menu-content-bg: var(--color-surface-overlay);
@ -3364,7 +3411,7 @@
--context-menu-content-min-width: 12rem;
--context-menu-content-max-height: min(60vh, 22rem);
--context-menu-content-padding: var(--space-1);
--context-menu-content-border-width: 1px;
--context-menu-content-border-width: var(--border-width);
--context-menu-content-border-color: var(--color-border-default);
--context-menu-content-radius: var(--radius-md);
--context-menu-content-bg: var(--color-surface-overlay);
@ -5246,6 +5293,10 @@
}
}
[data-container] {
container-type: inline-size;
}
:root,
[data-theme='base-light'],
[data-theme='light'] {
@ -5914,6 +5965,8 @@
--shadow-subtle: var(--shadow-1);
--shadow-raised: var(--shadow-3);
--shadow-overlay: var(--shadow-5);
--shadow-inset-subtle: inset 0 1px 2px rgb(15 23 42 / 0.08);
--shadow-inset-deep: inset 0 2px 4px rgb(15 23 42 / 0.12);
}
[data-theme='base-dark'],
@ -6583,4 +6636,6 @@
--shadow-subtle: var(--shadow-1);
--shadow-raised: var(--shadow-3);
--shadow-overlay: var(--shadow-5);
--shadow-inset-subtle: inset 0 1px 2px rgb(0 0 0 / 0.30);
--shadow-inset-deep: inset 0 2px 4px rgb(0 0 0 / 0.45);
}

@ -1,6 +1,7 @@
import type { ResponsiveProp, Size, SizePrimitiveKey } from './types';
import type { MotionConfig } from '$motion';
import { DENSITIES, type Density } from '$libs/density';
import type { Breakpoint, Breakpoints } from '$libs/dom';
import { INTENTS, type Intent } from '$uix/intent';
export { SIZE_PRIMITIVE_KEYS, SIZES } from './types';
@ -150,6 +151,25 @@ export type ControlHeightKey = Exclude<Size, 'full'>;
export const RADIUS_KEYS = ['none', 'sm', 'md', 'lg', 'xl', 'xxl', 'full'] as const;
export type RadiusKey = (typeof RADIUS_KEYS)[number];
// Blur scale — backdrop / frost radii. Numeric primitive (Tailwind-aligned values);
// the depth planes consume it (`--depth-{plane}-blur` → `var(--blur-*)`). No `full`.
export const BLUR_KEYS = ['none', 'sm', 'md', 'lg', 'xl', 'xxl'] as const;
export type BlurKey = (typeof BLUR_KEYS)[number];
// Gradient direction tokens (Tailwind's 8 compass directions, as CSS angles where
// 0deg = "to top"). The raw axis; named gradients (`gradients`) compose these.
export const GRADIENT_ANGLE_KEYS = [
'to-t',
'to-tr',
'to-r',
'to-br',
'to-b',
'to-bl',
'to-l',
'to-tl'
] as const;
export type GradientAngleKey = (typeof GRADIENT_ANGLE_KEYS)[number];
export const CONTAINER_WIDTH_KEYS = ['sm', 'md', 'lg', 'xl', 'xxl', 'full'] as const;
export const CONTENT_WIDTH_KEYS = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl', 'prose'] as const;
export const ASPECT_RATIO_KEYS = [
@ -203,7 +223,11 @@ export interface SizePrimitive {
export type SizePrimitiveSet = Record<SizePrimitiveKey, SizePrimitive>;
export const BORDER_WIDTH_KEYS = ['none', 'hairline', 'thin', 'medium', 'thick'] as const;
// Linear width ladder (Bootstrap-style 1/2/3/4 — the only reference scale that has
// the 3px components actually use; Tailwind's 1/2/4/8 skips it). The dead `hairline`
// (0.5) + old `medium` (1.5) were pruned (0 consumers); `medium` retuned to 2, `thick`
// to 3, `heavy` (4) added for strong dividers/accents.
export const BORDER_WIDTH_KEYS = ['none', 'thin', 'medium', 'thick', 'heavy'] as const;
export const BORDER_STYLE_KEYS = ['solid', 'dashed', 'dotted'] as const;
export type BorderWidthKey = (typeof BORDER_WIDTH_KEYS)[number];
@ -225,23 +249,63 @@ export interface FocusRingPrimitiveSet {
export const SHADOW_PHYSICAL_KEYS = ['1', '2', '3', '4', '5', '6'] as const;
export const SHADOW_SEMANTIC_KEYS = ['none', 'subtle', 'raised', 'overlay'] as const;
export const SHADOW_KEYS = [...SHADOW_SEMANTIC_KEYS, ...SHADOW_PHYSICAL_KEYS] as const;
// Inner-shadow (recessed/well) tier — soft, mode-aware, drawn `inset`. Distinct
// from the outer drop shadows above and from inset-RINGS (crisp `inset 0 0 0 Npx`,
// a separate axis). Consumed by the `recessed` depth plane + pressed/well states.
export const SHADOW_INSET_KEYS = ['inset-subtle', 'inset-deep'] as const;
export const SHADOW_KEYS = [
...SHADOW_SEMANTIC_KEYS,
...SHADOW_PHYSICAL_KEYS,
...SHADOW_INSET_KEYS
] as const;
export type ShadowPhysicalKey = (typeof SHADOW_PHYSICAL_KEYS)[number];
export type ShadowSemanticKey = (typeof SHADOW_SEMANTIC_KEYS)[number];
export type ShadowInsetKey = (typeof SHADOW_INSET_KEYS)[number];
export type ShadowKey = (typeof SHADOW_KEYS)[number];
export type ShadowScale = Record<ShadowKey, string>;
export const OPACITY_KEYS = [
// Two-tier scale (mirrors shadow: numeric `--shadow-1..6` + semantic
// `--shadow-{subtle,raised,overlay}`). The numeric tier (Tailwind step-5) gives
// the fine low-end granularity ethereal / glass interfaces need for layered
// translucency; the semantic tier names the perceptual roles consumed by recipes.
export const OPACITY_NUMERIC_KEYS = [
'0',
'muted',
'5',
'10',
'15',
'20',
'25',
'30',
'35',
'40',
'45',
'50',
'55',
'60',
'65',
'70',
'75',
'80',
'85',
'90',
'95',
'100'
] as const;
export const OPACITY_SEMANTIC_KEYS = [
'ghost',
'disabled',
'scrim',
'muted',
'overlay',
'hover',
'subtle',
'press',
'hover',
'full'
] as const;
export const OPACITY_KEYS = [...OPACITY_NUMERIC_KEYS, ...OPACITY_SEMANTIC_KEYS] as const;
export type OpacityNumericKey = (typeof OPACITY_NUMERIC_KEYS)[number];
export type OpacitySemanticKey = (typeof OPACITY_SEMANTIC_KEYS)[number];
export type OpacityKey = (typeof OPACITY_KEYS)[number];
export type OpacityPrimitiveSet = Record<OpacityKey, string | number>;
@ -475,9 +539,25 @@ export interface RecipeComposition {
* and `tokenEntries` in the recipe contract test).
*/
type RecipeCompositionMap = Readonly<Record<string, RecipeComposition>>;
/**
* Container-query overrides — per-breakpoint token overrides emitted inside
* `@container (min-width: <configured-bp>px) { [data-{component}] { … } }`. The
* breakpoints are the SAME configured source as `@media` (`EidosConfig.breakpoints`),
* so a container-relative override tracks the runtime `ResponsiveProp` scale.
* CSS forbids `var()` in `@container` conditions, so the generator emits literal
* px — this is the only way to keep the condition synced with the configured set.
* Reserved sibling key `container`, like `composition`. The element opts in to
* being a query container via `data-container` (`container-type: inline-size`).
*/
export type ContainerBreakpoint = Exclude<Breakpoint, 'base'>;
export type RecipeContainerQueries = Partial<
Record<ContainerBreakpoint, Readonly<Record<string, string>>>
>;
export type RecipeTokenMap = Readonly<{
[name: string]: RecipeTokenValue | RecipeCompositionMap | undefined;
[name: string]: RecipeTokenValue | RecipeCompositionMap | RecipeContainerQueries | undefined;
composition?: RecipeCompositionMap;
container?: RecipeContainerQueries;
}>;
export type RecipeTokenSet = Readonly<Record<string, RecipeTokenMap>>;
@ -722,6 +802,12 @@ export interface PrimitiveSet {
readonly space?: ScaleMap;
readonly controlHeight?: Record<ControlHeightKey, string>;
readonly radius?: Record<RadiusKey, string>;
readonly blur?: Record<BlurKey, string>;
// Gradient direction tokens (`--gradient-angle-*`) + named gradients
// (`--gradient-*`). Named gradients compose role/surface color tokens, so they
// retint per theme + mode. Extensible via config (the "open cage").
readonly gradientAngle?: Record<GradientAngleKey, string>;
readonly gradients?: Record<string, string>;
readonly border?: BorderPrimitiveSet;
readonly focusRing?: FocusRingPrimitiveSet;
readonly layout?: LayoutPrimitiveSet;
@ -762,6 +848,15 @@ export interface EidosConfig {
readonly semantics: SemanticSet;
readonly themes?: ThemeMap;
readonly recipes?: RecipeTokenSet;
/**
* Responsive breakpoints (px), the SINGLE source of truth shared with the
* runtime responsive service. The developer configures them on
* `ActiveDom`/`ActiveUix`; `ActiveEidos` threads `dom.breakpoints.current`
* here so the generated `@media` blocks + `--breakpoint-*` tokens match the
* runtime `ResponsiveProp` resolution. Absent (build-time / no service) →
* the generator falls back to the canonical defaults.
*/
readonly breakpoints?: Breakpoints;
/**
* Motion system: named `@keyframes` + presets (the composition layer).
* Distinct from `primitives.motion` (duration/ease/distance/scale tokens).

@ -515,11 +515,11 @@ function appendRecipeContractTokens(
): void {
for (const [component, recipeTokens] of Object.entries(recipes ?? {})) {
for (const name of Object.keys(recipeTokens)) {
// Reserved TSC v2.2 sibling keys (currently `composition`) are
// not tokens — they're structural blocks describing cross-recipe
// overrides. Skip them so they don't leak into the public CSS
// contract as fake `--{component}-composition` knobs.
if (name === 'composition') continue
// Reserved sibling keys (`composition` — cross-recipe overrides;
// `container` — container-query overrides) are not tokens but
// structural blocks. Skip them so they don't leak into the public
// CSS contract as fake `--{component}-{composition,container}` knobs.
if (name === 'composition' || name === 'container') continue
// Private tokens (key starts with '_') are recipe-internal
// machinery emitted as `--_{c}-{rest}` — not part of the
// public CSS contract. External consumers should not see

@ -47,13 +47,49 @@ export const STATIC_RADIUS = {
full: '9999px'
} as const
// Blur scale — backdrop / frost radii. Numeric primitive, values aligned to
// Tailwind's blur scale (xs→sm 4 · sm→md 8 · md→lg 12 · lg→xl 16 · xl→xxl 24).
// The depth planes consume it (`--depth-{plane}-blur` → `var(--blur-*)`); a
// "cristal" theme retunes blur+alpha per plane on top. Density-scaled (× --scaling)
// at generation, like the icon scale. See THEMING.md "Escala de blur".
export const STATIC_BLUR = {
none: '0px',
sm: '4px',
md: '8px',
lg: '12px',
xl: '16px',
xxl: '24px'
} as const
// Gradient direction tokens — Tailwind's 8 compass directions as CSS angles
// (0deg = "to top"). The raw axis; named gradients compose these.
export const STATIC_GRADIENT_ANGLE = {
'to-t': '0deg',
'to-tr': '45deg',
'to-r': '90deg',
'to-br': '135deg',
'to-b': '180deg',
'to-bl': '225deg',
'to-l': '270deg',
'to-tl': '315deg'
} as const
// Named gradients — the semantic layer. Compose direction tokens + role/surface
// color tokens, so they retint per theme + mode. The base ships ONE (`shimmer`,
// the loading sweep consumed by `image`); apps/themes add more via config (open
// cage), like Tailwind ships directions but no named gradients of its own.
export const STATIC_GRADIENTS = {
shimmer:
'linear-gradient(var(--gradient-angle-to-r), var(--color-surface-raised) 0%, color-mix(in srgb, var(--color-surface-default) 70%, var(--color-surface-overlay)) 50%, var(--color-surface-raised) 100%)'
} as const
export const STATIC_BORDER = {
width: {
none: '0px',
hairline: '0.5px',
thin: '1px',
medium: '1.5px',
thick: '2px'
medium: '2px',
thick: '3px',
heavy: '4px'
},
style: {
solid: 'solid',
@ -205,13 +241,38 @@ export const STATIC_ICON = {
} as const
export const STATIC_OPACITY = {
// Numeric tier (Tailwind step-5) — fine granularity for ethereal / glass layering.
'0': 0,
muted: 0.72,
disabled: 0.48,
scrim: 0.42,
overlay: 0.66,
hover: 0.92,
press: 0.84,
'5': 0.05,
'10': 0.1,
'15': 0.15,
'20': 0.2,
'25': 0.25,
'30': 0.3,
'35': 0.35,
'40': 0.4,
'45': 0.45,
'50': 0.5,
'55': 0.55,
'60': 0.6,
'65': 0.65,
'70': 0.7,
'75': 0.75,
'80': 0.8,
'85': 0.85,
'90': 0.9,
'95': 0.95,
'100': 1,
// Semantic tier — the perceptual roles recipes consume (de-emphasis ladder +
// veils + interaction). Disabled = 0.4 (modern standard, ≈ Material 38%).
ghost: 0.3,
disabled: 0.4,
scrim: 0.45,
muted: 0.65,
overlay: 0.65,
subtle: 0.8,
press: 0.85,
hover: 0.9,
full: 1
} as const
@ -242,19 +303,19 @@ export const STATIC_DEPTH: DepthPrimitiveSet = {
surface: 'var(--color-surface-overlay)',
shadow: 'var(--shadow-overlay)',
halo: 'inset 0 1px 0 0 color-mix(in oklab, white 7%, transparent)',
blur: '10px',
blur: 'var(--blur-lg)',
z: 'var(--z-index-popover)'
},
modal: {
surface: 'var(--color-surface-overlay)',
shadow: 'var(--shadow-overlay)',
halo: 'inset 0 1px 0 0 color-mix(in oklab, white 8%, transparent)',
blur: '16px',
blur: 'var(--blur-xl)',
z: 'var(--z-index-modal)'
},
recessed: {
surface: 'var(--color-surface-muted)',
shadow: 'inset 0 1px 2px color-mix(in srgb, var(--color-neutral-contrast) 12%, transparent)',
shadow: 'var(--shadow-inset-subtle)',
z: 'var(--z-index-base)'
}
}
@ -347,6 +408,9 @@ export const STATIC_PRIMITIVES: Pick<
| 'space'
| 'controlHeight'
| 'radius'
| 'blur'
| 'gradientAngle'
| 'gradients'
| 'border'
| 'focusRing'
| 'layout'
@ -363,6 +427,9 @@ export const STATIC_PRIMITIVES: Pick<
space: STATIC_SPACE,
controlHeight: STATIC_CONTROL_HEIGHT,
radius: STATIC_RADIUS,
blur: STATIC_BLUR,
gradientAngle: STATIC_GRADIENT_ANGLE,
gradients: STATIC_GRADIENTS,
border: STATIC_BORDER,
focusRing: STATIC_FOCUS_RING,
layout: STATIC_LAYOUT,

@ -172,12 +172,16 @@ export const STATIC_TYPOGRAPHY: TypographyPrimitiveSet = {
}
},
// Tracking scale (letter-spacing) → --tracking-{key}. (Fase 3)
// `caps` (0.04em) is the canonical micro-tracking for UPPERCASE labels (the
// dominant raw-value pattern across menus/headings); `widest` (0.1em) for wide caps.
tracking: {
tighter: '-0.04em',
tight: '-0.02em',
normal: '0',
wide: '0.02em',
wider: '0.05em'
caps: '0.04em',
wider: '0.05em',
widest: '0.1em'
},
// Leading scale (line-height) → --leading-{key}.
leading: {

@ -144,18 +144,18 @@ export const THEME_BASE_RECIPE_TOKENS = {
'size-xl': '64px',
'size-xxl': '96px',
'font-family': 'var(--font-ui)',
'font-size-xs': '10px',
'font-size-sm': '12px',
'font-size-md': '14px',
'font-size-lg': '16px',
'font-size-xl': '20px',
'font-size-xxl': '28px',
'font-size-xs': 'var(--font-size-xxs)',
'font-size-sm': 'var(--font-size-xs)',
'font-size-md': 'var(--font-size-sm)',
'font-size-lg': 'var(--font-size-md)',
'font-size-xl': 'var(--font-size-lg)',
'font-size-xxl': 'var(--font-size-xl)',
'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': '2px',
'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)',
@ -215,7 +215,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'color-custom-contrast': 'white',
'ring-color-custom': 'currentColor',
// ── Badge ─────────────────────────────────────────────────────────
'badge-border-width': '2px',
'badge-border-width': 'var(--border-width-medium)',
'badge-color-custom': 'currentColor',
'badge-color-custom-contrast': 'white',
// ── AvatarGroup ──────────────────────────────────────────────────
@ -224,7 +224,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'group-gap-md': 'var(--space-3)',
'group-gap-lg': 'var(--space-4)',
'group-overlap': '-0.6em',
'group-carve-width': '2px',
'group-carve-width': 'var(--border-width-medium)',
'group-carve-color': 'var(--color-surface-default)',
'group-reverse-z-max': '20',
'group-max': '99',
@ -518,15 +518,15 @@ export const THEME_BASE_RECIPE_TOKENS = {
'footer-padding-start': 'var(--space-3)',
'footer-border-color': 'var(--color-border-subtle)',
// ── Animations ────────────────────────────────────────────────────
'emerge-duration': '320ms',
'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': '0.985',
'press-duration': '80ms',
'selected-ring-width': '2px',
'disabled-opacity': '0.6',
'selected-ring-width': 'var(--border-width-medium)',
'disabled-opacity': 'var(--opacity-disabled)',
// ── Custom color (set inline by consumer via style) ───────────────
// The Svelte file emits `style="--card-color-custom: <value>;"`
// when the consumer passes a non-canonical CSS color. Declared
@ -676,7 +676,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'color-mix( in srgb, var(--color-surface-overlay) 78%, var(--color-surface-default) )',
'border-disabled':
'color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) )',
'disabled-opacity': '0.72',
'disabled-opacity': 'var(--opacity-disabled)',
'indicator-color': 'var(--color-content-on-solid)',
'indicator-color-disabled':
'color-mix( in srgb, var(--color-content-on-solid) 82%, var(--color-surface-default) )',
@ -729,13 +729,13 @@ export const THEME_BASE_RECIPE_TOKENS = {
'content-padding': 'var(--space-3)',
'transition-duration': 'var(--duration-normal)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.5'
'disabled-opacity': 'var(--opacity-disabled)'
},
dialog: {
'trigger-font-size': 'var(--font-size-sm)',
'overlay-bg': 'var(--color-overlay)',
'overlay-opacity': '62%',
'overlay-blur': '10px',
'overlay-blur': 'var(--blur-lg)',
'overlay-z': '70',
'content-bg': 'var(--color-surface-overlay)',
'content-color': 'var(--color-content-primary)',
@ -849,7 +849,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'error-color': 'var(--color-risk-text)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
form: {
'gap-xs': 'var(--space-3)',
@ -905,11 +905,11 @@ export const THEME_BASE_RECIPE_TOKENS = {
'widget-border-width': 'var(--border-width)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
drawer: {
'overlay-bg': 'var(--color-overlay)',
'overlay-blur': '10px',
'overlay-blur': 'var(--blur-lg)',
'overlay-opacity': '62%',
'overlay-z': '72',
'inline-z': '64',
@ -929,7 +929,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'content-shadow': 'var(--depth-overlay-shadow), var(--depth-overlay-halo)',
'content-shadow-dragging':
'var(--shadow-overlay-strong, var(--depth-overlay-shadow)), var(--depth-overlay-halo)',
'content-ring-dragging-width': '2px',
'content-ring-dragging-width': 'var(--border-width-medium)',
'content-ring-dragging-color':
'color-mix( in srgb, var(--color-content-secondary) 32%, transparent )',
'content-ring-dragging-offset': '2px',
@ -995,8 +995,6 @@ export const THEME_BASE_RECIPE_TOKENS = {
'fade-ease': 'var(--ease-out)',
// ── Skeleton placeholder shimmer ──────────────────────────────────
'placeholder-base': 'var(--color-surface-raised)',
'placeholder-highlight':
'color-mix(in srgb, var(--color-surface-default) 70%, var(--color-surface-overlay))',
'shimmer-duration': '1.8s',
// ── Custom placeholder color (set inline by Svelte file) ─────────
'placeholder-color-custom': 'var(--color-neutral-track)',
@ -1108,7 +1106,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'control-disabled-opacity': '0.48',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
'float-panel': {
bg: 'var(--color-surface-overlay)',
@ -1258,7 +1256,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'segment-hover-bg': 'color-mix(in srgb, var(--color-content-primary) 6%, transparent)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
'time-field': {
'stack-gap': 'var(--space-1-5)',
@ -1312,7 +1310,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'segment-focus-shadow': 'inset 0 0 0 var(--border-width) var(--_time-field-accent-border)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
'color-field': {
'stack-gap': 'var(--space-1-5)',
@ -1375,7 +1373,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'format-select-px': 'var(--space-1)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
'color-picker': {
// Popover content shell
@ -1417,7 +1415,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'area-border': 'var(--color-border-subtle)',
'area-thumb-size': '16px',
'area-thumb-radius': 'var(--radius-full)',
'area-thumb-border-width': '2px',
'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)',
@ -1465,7 +1463,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
// Misc
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
button: {
// Size tokens
@ -1515,7 +1513,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'spinner-duration': '720ms',
'disabled-opacity': '0.5',
'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
@ -1738,7 +1736,22 @@ export const THEME_BASE_RECIPE_TOKENS = {
'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-accent-border)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.52'
'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(--control-height-md) * 2.6)',
'cell-min-block-md': 'calc(var(--control-height-md) * 3.2)',
'cell-min-block-lg': 'calc(var(--control-height-md) * 4)',
'num-row': 'var(--space-7)',
'lane-height-sm': 'var(--space-5)',
'lane-height-md': 'var(--space-6)',
'lane-height-lg': 'var(--space-7)',
'focus-shadow':
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'disabled-opacity': 'var(--opacity-disabled)'
},
pagination: {
'gap-xs': 'var(--space-0-5)',
@ -1777,7 +1790,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
popover: {
'content-z': '75',
@ -1920,7 +1933,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'0 0 0 var(--radio-group-focus-ring-offset) var(--radio-group-focus-ring-surface), 0 0 0 calc(var(--radio-group-focus-ring-offset) + var(--radio-group-focus-ring-width)) var(--radio-group-focus-ring-color)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.72',
'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)',
@ -1969,6 +1982,8 @@ export const THEME_BASE_RECIPE_TOKENS = {
'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)'
},
@ -2028,7 +2043,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'focus-ring': 'var(--radio-group-focus-ring)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.6'
'disabled-opacity': 'var(--opacity-disabled)'
},
'rating-group': {
'gap-xs': 'var(--space-0-5)',
@ -2057,7 +2072,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
'search-field': {
'height-xs': 'var(--control-height-xs)',
@ -2111,7 +2126,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'loading-indicator-duration': '900ms',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
'password-field': {
'height-xs': 'var(--control-height-xs)',
@ -2174,7 +2189,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'strength-radius': 'var(--radius-sm)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
select: {
'stack-gap': 'var(--space-2)',
@ -2279,7 +2294,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'item-bg-highlighted': 'var(--color-surface-raised)',
'item-color-highlighted': 'var(--color-content-primary)',
'item-indicator-size': 'var(--icon-size-sm)',
'item-focus-ring-width': '2px',
'item-focus-ring-width': 'var(--border-width-medium)',
'item-description-mt': '0.125rem',
'item-description-color': 'var(--color-content-muted)',
'item-description-font-size': '0.85em',
@ -2408,7 +2423,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'separator-radius': 'var(--radius-full)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.48'
'disabled-opacity': 'var(--opacity-disabled)'
},
'file-upload': {
'gap-sm': 'var(--space-2)',
@ -2471,7 +2486,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'progress-track-bg': 'var(--color-neutral-track)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.52',
'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)',
@ -2622,7 +2637,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'remove-bg-hover': 'color-mix(in srgb, currentColor 12%, transparent)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.5',
'disabled-opacity': 'var(--opacity-disabled)',
'primary-solid-bg': 'var(--color-primary-solid)',
'primary-solid-bg-hover': 'var(--color-primary-solid-hover)',
'primary-soft-bg': 'var(--color-primary-surface)',
@ -2816,7 +2831,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'submit-color': 'var(--color-content-on-solid)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.5',
'disabled-opacity': 'var(--opacity-disabled)',
'primary-solid': 'var(--color-primary-solid)',
'secondary-solid': 'var(--color-secondary-solid)',
'neutral-solid': 'var(--color-neutral-solid)',
@ -2884,7 +2899,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'focus-shadow':
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'invalid-border': 'var(--color-risk-border)',
'disabled-opacity': '0.5',
'disabled-opacity': 'var(--opacity-disabled)',
// Smallest doc-chrome size (breadcrumb, cell labels) — one step under `sm`.
'font-size-xs': '0.6875rem',
// Wash scale: ONE canonical set of color-mix opacities for the editor's
@ -2944,7 +2959,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'code-py': 'var(--space-0-5)',
'code-px': 'var(--space-1)',
'quote-px': 'var(--space-3)',
'quote-border-width': 'var(--border-width-thick)',
'quote-border-width': 'var(--border-width-medium)',
'quote-color': 'var(--color-content-secondary)',
'list-padding-inline-start': 'var(--space-5)',
'transition-duration': 'var(--duration-fast)',
@ -3044,7 +3059,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'delete-radius': 'var(--radius-full)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.52',
'disabled-opacity': 'var(--opacity-disabled)',
'primary-text': 'var(--color-primary-text)',
'secondary-text': 'var(--color-secondary-text)',
'neutral-text': 'var(--color-neutral-text)',
@ -3141,7 +3156,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'indicator-font-weight': 'var(--font-weight-semibold)',
'complete-color': 'var(--color-content-on-solid)',
'separator-min-size': '3rem',
'separator-thickness': 'var(--border-width-thick)',
'separator-thickness': 'var(--border-width-medium)',
'separator-radius': 'var(--radius-full)',
'separator-bg': 'var(--color-border-subtle)',
'content-padding': 'var(--space-4)',
@ -3162,7 +3177,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.45',
'disabled-opacity': 'var(--opacity-disabled)',
'primary-solid': 'var(--color-primary-solid)',
'primary-text': 'var(--color-primary-text)',
'secondary-solid': 'var(--color-secondary-solid)',
@ -3270,7 +3285,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'tick-bg-active': 'var(--color-primary-solid)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
'disabled-opacity': 'var(--opacity-disabled)'
},
switch: {
'size-xs-track-width': '26px',
@ -3318,7 +3333,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'color-mix( in srgb, var(--color-surface-default) 88%, var(--color-surface-raised) )',
'thumb-border-disabled':
'color-mix( in srgb, var(--color-border-subtle) 88%, var(--color-surface-overlay) )',
'disabled-opacity': '0.72',
'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) )',
@ -3429,7 +3444,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'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': '0.5',
'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',
@ -3465,7 +3480,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'provider-border-width': 'var(--border-width)',
'provider-radius': 'var(--radius-lg)',
'provider-shadow': 'var(--depth-overlay-shadow), var(--depth-overlay-halo)',
'accent-width': '3px',
'accent-width': 'var(--border-width-thick)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'closed-offset': '10px',
@ -3581,11 +3596,11 @@ export const THEME_BASE_RECIPE_TOKENS = {
'font-family': 'var(--font-ui)',
'line-height': 'var(--leading-ui)',
'letter-spacing': 'var(--tracking-normal)',
'font-size-xs': '10px',
'font-size-sm': '12px',
'font-size-md': '14px',
'font-size-lg': '16px',
'font-size-xl': '18px',
'font-size-xs': 'var(--font-size-xxs)',
'font-size-sm': 'var(--font-size-xs)',
'font-size-md': 'var(--font-size-sm)',
'font-size-lg': 'var(--font-size-md)',
'font-size-xl': 'var(--font-size-lg)',
'font-weight-xs': 'var(--font-weight-medium)',
'font-weight-sm': 'var(--font-weight-medium)',
'font-weight-md': 'var(--font-weight-medium)',
@ -3806,7 +3821,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
value: 'color-mix( in srgb, var(--toggle-palette-border) 88%, transparent )',
scope: 'host'
},
'disabled-opacity': '0.45'
'disabled-opacity': 'var(--opacity-disabled)'
},
tooltip: {
'content-z': '76',
@ -4097,7 +4112,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
// ─────────────────────────────────────────────────────────────────────
'pin-input': {
gap: 'var(--space-2)',
'disabled-opacity': '0.55',
'disabled-opacity': 'var(--opacity-disabled)',
'cell-size-sm': '2rem',
'cell-size-md': '2.5rem',
'cell-size-lg': '3rem',
@ -4108,7 +4123,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'cell-font-size-xl': 'var(--font-size-xxl)',
'cell-font-weight': '500',
'cell-radius': 'var(--radius-md)',
'cell-border-width': '1px',
'cell-border-width': 'var(--border-width)',
'cell-border-color': 'var(--color-border-default)',
'cell-bg': 'var(--color-surface-default)',
'cell-filled-bg': 'var(--color-primary-element)',
@ -4143,7 +4158,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
radius: 'var(--radius-md)',
'border-width': 'var(--border-width)',
'border-color': 'var(--color-border-default)',
'disabled-opacity': '0.6',
'disabled-opacity': 'var(--opacity-disabled)',
'panel-bg': 'transparent',
'handle-size': 'var(--space-2)',
'handle-bg': 'var(--color-border-subtle)',
@ -4161,7 +4176,23 @@ export const THEME_BASE_RECIPE_TOKENS = {
// NavigationMenu — soma writes the active-trigger rect into these
// indicator vars; the recipe paints the underline.
// ─────────────────────────────────────────────────────────────────────
// Combobox — floating result list. Same overlay micro-band as `select` /
// `dropdown-menu` (the soma floating layer mirrors this computed z-index onto
// the positioner wrapper, so it must stay a low, themeable value — not the
// global `--z-index-*` scale).
combobox: {
'content-z': '80'
},
// DragDrop — the drag preview/ghost that tracks the cursor. Floats above
// local content during an active drag.
'drag-drop': {
'preview-z': '99'
},
'navigation-menu': {
// Inline (non-portaled) dropdown panel: stacks above normal page flow.
'content-z': '50',
'indicator-w': '0px',
'indicator-x': '0px'
},
@ -4179,7 +4210,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'content-min-width': '12rem',
'content-max-height': 'min(60vh, 22rem)',
'content-padding': 'var(--space-1)',
'content-border-width': '1px',
'content-border-width': 'var(--border-width)',
'content-border-color': 'var(--color-border-default)',
'content-radius': 'var(--radius-md)',
'content-bg': 'var(--color-surface-overlay)',
@ -4207,7 +4238,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'content-min-width': '12rem',
'content-max-height': 'min(60vh, 22rem)',
'content-padding': 'var(--space-1)',
'content-border-width': '1px',
'content-border-width': 'var(--border-width)',
'content-border-color': 'var(--color-border-default)',
'content-radius': 'var(--radius-md)',
'content-bg': 'var(--color-surface-overlay)',

@ -32,6 +32,7 @@ import {
type LayoutPrimitiveSet,
type PrimitiveSet,
type RecipeComposition,
type RecipeContainerQueries,
type RecipeTokenSet,
type RecipeTokenValue,
type RecipeTokenDeclaration,
@ -144,6 +145,16 @@ export function renderStaticCss(options: EidosConfig): string {
const { primitives } = options
const declarations: string[] = []
// Responsive breakpoints — the configured set (threaded from the runtime
// service via `ActiveEidos`, which reads `dom.breakpoints.current`) merged over
// the fallback defaults. Emitted as `--breakpoint-*` tokens AND used for the
// responsive `@media` blocks below, so both track ONE source instead of a
// frozen const drifting from the developer-configured breakpoints.
const breakpoints = { ...STYLE_BREAKPOINT_WIDTHS, ...options.breakpoints }
for (const bp of STYLE_BREAKPOINT_ORDER) {
declarations.push(cssVar(`breakpoint-${bp}`, `${breakpoints[bp]}px`))
}
// Spacing + control heights scale with BOTH the active density and the global
// `--scaling` zoom (multiplicative): `calc(value * density-scale * scaling)`.
// The `[data-density]` / `[data-scaling]` blocks rebind those vars. At
@ -213,6 +224,19 @@ export function renderStaticCss(options: EidosConfig): string {
appendRecordDeclarations(declarations, 'icon-stroke-width', primitives.icon.strokeWidth)
}
if (primitives.blur) {
// Blur radii scale with --scaling (zoom-consistent), like icon-size.
appendScaledMetricDeclarations(declarations, 'blur', primitives.blur)
}
if (primitives.gradientAngle) {
appendRecordDeclarations(declarations, 'gradient-angle', primitives.gradientAngle)
}
if (primitives.gradients) {
// Named gradients compose role/surface tokens → mode-aware via the references.
appendRecordDeclarations(declarations, 'gradient', primitives.gradients)
}
appendRecordDeclarations(declarations, 'opacity', primitives.opacity)
appendRecordDeclarations(declarations, 'z-index', primitives.zIndex)
@ -238,7 +262,7 @@ export function renderStaticCss(options: EidosConfig): string {
}
appendTransitionAliasDeclarations(declarations, options)
const scopedRecipeBlocks = appendRecipeDeclarations(declarations, options.recipes)
const scopedRecipeBlocks = appendRecipeDeclarations(declarations, options.recipes, breakpoints)
const depthBlocks = primitives.depth ? renderDepthBlocks(primitives.depth) : []
const shapeBlocks = primitives.shape ? renderShapeBlocks(primitives.shape) : []
@ -261,7 +285,7 @@ export function renderStaticCss(options: EidosConfig): string {
for (const bp of STYLE_BREAKPOINT_ORDER) {
const lines = styleResponsive[bp]
if (!lines || lines.length === 0) continue
const width = STYLE_BREAKPOINT_WIDTHS[bp]
const width = breakpoints[bp]
const rootBlock = renderBlock(':root', lines)
blocks.push(`@media (min-width: ${width}px) {\n${indentBlock(rootBlock)}\n}`)
}
@ -291,6 +315,11 @@ export function renderStaticCss(options: EidosConfig): string {
blocks.push(renderForcedColorsBlock())
blocks.push(renderPrefersContrastBlock())
// Container-query opt-in: marking an element `data-container` makes it a query
// container, so its descendants' recipe `@container` overrides resolve against
// its inline size. (The `@container` blocks themselves are emitted per-recipe.)
blocks.push(renderBlock('[data-container]', ['container-type: inline-size;']))
return blocks.join('\n\n')
}
@ -565,6 +594,13 @@ function appendBorderDeclarations(declarations: string[], border: BorderPrimitiv
declarations.push(
cssVar('border', 'var(--border-width) var(--border-style) var(--color-border-default)')
)
// Inset-ring width — default for crisp inner rings drawn as `box-shadow`
// (`inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color)`). Defaults to the
// `medium` (2px) border step; a theme retunes all inset rings here, a component overrides
// per-use. The full expression lives at the use site (CSS resolves nested var() in
// the declaring scope, so a single pre-baked `--ring-inset` shorthand can't pick up
// per-element color/width). Distinct from the soft `--shadow-inset-*` tier.
declarations.push(cssVar('ring-inset-width', 'var(--border-width-medium)'))
}
function appendLayoutDeclarations(declarations: string[], layout: LayoutPrimitiveSet): void {
@ -980,9 +1016,12 @@ function renderShapeBlocks(shape: ShapePrimitiveSet): string[] {
}
/**
* Breakpoint min-width thresholds matching `$libs/dom/responsive`.
* `base` is the implicit 0 fallback — emitted into `:root`, not as a
* media query.
* Fallback breakpoint min-width thresholds (mirrors `$libs/dom/responsive`
* `BREAKPOINTS_DEFAULT`). Used only when `options.breakpoints` is absent
* (build-time / no runtime service). At runtime `ActiveEidos` passes the
* developer-configured `dom.breakpoints.current`, which overrides these — so the
* emitted `--breakpoint-*` tokens + `@media` blocks track the live source.
* `base` is the implicit 0 fallback — not emitted as a media query.
*/
const STYLE_BREAKPOINT_WIDTHS: Record<string, number> = {
sm: 480,
@ -1216,7 +1255,8 @@ function appendTransitionAliasDeclarations(
*/
function appendRecipeDeclarations(
declarations: string[],
recipes: RecipeTokenSet | undefined
recipes: RecipeTokenSet | undefined,
breakpoints: Record<string, number>
): string[] {
if (!recipes) return []
@ -1230,6 +1270,9 @@ function appendRecipeDeclarations(
// this iteration so it composes onto the same `blocks` array.
const composition = (tokens as { composition?: Readonly<Record<string, RecipeComposition>> })
.composition
// `container` is the reserved sibling key for container-query overrides
// (emitted inside `@container`); stripped before normalizing, like `composition`.
const container = (tokens as { container?: RecipeContainerQueries }).container
const tokensOnly = stripCompositionKey(tokens)
// Phase 1: normalize every token to a list of declarations.
@ -1294,6 +1337,13 @@ function appendRecipeDeclarations(
if (composition) {
blocks.push(...emitComposition(component, composition))
}
// Phase 6: container queries. Per-breakpoint token overrides emitted
// inside `@container (min-width: <configured-bp>px)`, scoped to the
// component. Uses the SAME configured breakpoints as `@media`.
if (container) {
blocks.push(...emitContainerQueries(component, container, breakpoints))
}
}
if (allViolations.length) {
@ -1315,12 +1365,44 @@ function stripCompositionKey(
): Readonly<Record<string, RecipeTokenValue>> {
const out: Record<string, RecipeTokenValue> = {}
for (const [key, value] of Object.entries(tokens)) {
if (key === 'composition') continue
if (key === 'composition' || key === 'container') continue
out[key] = value as RecipeTokenValue
}
return out
}
/**
* Emit `@container` blocks for a recipe's container-query overrides.
*
* For each breakpoint with token entries, emits
* `@container (min-width: <configured-bp>px) { [data-{c}] { --{c}-{token}: value } }`.
* The breakpoints are the SAME configured source as `@media` (threaded from
* `EidosConfig.breakpoints`), so a container override tracks the runtime scale.
* CSS forbids `var()` in `@container` conditions, so the px is emitted literally —
* the generator is the only place this can stay synced with the configured set.
*
* The queried element (`[data-{c}]`) responds to its nearest ANCESTOR query
* container — an ancestor opts in via `data-container` (`container-type: inline-size`).
*/
function emitContainerQueries(
component: string,
container: RecipeContainerQueries,
breakpoints: Record<string, number>
): string[] {
const blocks: string[] = []
for (const bp of STYLE_BREAKPOINT_ORDER) {
const tokens = container[bp]
if (!tokens) continue
const lines = Object.entries(tokens).map(([name, value]) =>
cssVar(name.startsWith('_') ? `_${component}-${name.slice(1)}` : `${component}-${name}`, value)
)
if (!lines.length) continue
const inner = renderBlock(`[data-${component}]`, lines)
blocks.push(`@container (min-width: ${breakpoints[bp]}px) {\n${indentBlock(inner)}\n}`)
}
return blocks
}
/**
* Emit blocks for a recipe's cross-recipe composition (TSC v2.2).
*

@ -27,6 +27,9 @@ export const THEME_BASE_COLOR_ROLES: ColorRoleMap = {
// and the gray neutral. Indigo is purple's cool ~60° neighbor (Material 3's
// tertiary-hue rule) and sits in a hue band no intent occupies, so it reads as
// hierarchy, not a signal. (Was `gray` — collided with neutral.)
// RESERVED (2026-06-15 audit): defined + emitted (`--color-tertiary-*`) but no
// component consumes it yet — a deliberate third-hierarchy slot apps/themes can
// reach for. Not dead code; not to be removed.
tertiary: 'indigo',
// `gray-9` (el sólido neutral) es un gris medio: ni blanco ni negro
// contrastan por defecto. El slot `contrast` se fija al step 12 — casi-negro
@ -459,7 +462,10 @@ export const THEME_BASE_LIGHT_SHADOW: ShadowScale = {
'3': '0 8px 24px rgb(15 23 42 / 0.12)',
'4': '0 14px 36px rgb(15 23 42 / 0.15)',
'5': '0 18px 48px rgb(15 23 42 / 0.18)',
'6': '0 28px 72px rgb(15 23 42 / 0.22)'
'6': '0 28px 72px rgb(15 23 42 / 0.22)',
// Inner-shadow tier (recessed wells). Same slate base as the drop shadows; soft + inset.
'inset-subtle': 'inset 0 1px 2px rgb(15 23 42 / 0.08)',
'inset-deep': 'inset 0 2px 4px rgb(15 23 42 / 0.12)'
}
export const THEME_BASE_DARK_SHADOW: ShadowScale = {
@ -472,7 +478,11 @@ export const THEME_BASE_DARK_SHADOW: ShadowScale = {
'3': '0 10px 30px rgb(0 0 0 / 0.42)',
'4': '0 16px 44px rgb(0 0 0 / 0.48)',
'5': '0 22px 60px rgb(0 0 0 / 0.55)',
'6': '0 34px 88px rgb(0 0 0 / 0.62)'
'6': '0 34px 88px rgb(0 0 0 / 0.62)',
// Inner-shadow tier (recessed wells). Dark wells read as a darker inset (not a
// light rim) — black base, deeper alpha, mode-correct unlike a contrast-mix.
'inset-subtle': 'inset 0 1px 2px rgb(0 0 0 / 0.30)',
'inset-deep': 'inset 0 2px 4px rgb(0 0 0 / 0.45)'
}
export const THEME_BASE_OPTIONS: EidosConfig = defineEidosConfig({

@ -196,6 +196,33 @@ describe('Eidos recipe CSS contract', () => {
expect(violations).toEqual([])
})
// Coherence guard (Fase 7 — theming audit): the type/size canon is only worth
// having if components CONSUME it. Recipe `font-size-*` / `icon-size-*` tokens
// MUST reference the `--font-size-*` / `--icon-size-*` scale (or another token),
// never a raw `px`/`rem` literal — else the text/icon stops responding to the
// type scale, `--scaling` and `applyTypeScale()`. The earlier CSS-only guard
// missed this (recipe-token values become custom properties, not `font-size:`).
// `words` / `palabras` / `chronos` are excluded (active dev tracks, per the audit).
it('keeps raw px/rem literals out of recipe font-size / icon-size tokens', () => {
const EXCLUDED = new Set(['words', 'palabras', 'chronos'])
const RAW_LENGTH = /^-?[0-9.]+(px|rem)$/
const violations: string[] = []
for (const [component, tokens] of Object.entries(RECIPE_TOKENS)) {
if (EXCLUDED.has(component)) continue
for (const [key, value] of tokenEntries(tokens)) {
if (!/(^|-)(font-size|icon-size)(-|$)/.test(key)) continue
for (const resolved of entryValues(value)) {
if (typeof resolved === 'string' && RAW_LENGTH.test(resolved.trim())) {
violations.push(`${component}.${key}: ${resolved}`)
}
}
}
}
expect(violations).toEqual([])
})
it('does not leave declared public recipe variables orphaned', () => {
const recipeValueReferences = readRecipeValueReferences()
const orphaned: string[] = []

Loading…
Cancel
Save

Powered by TurnKey Linux.