@ -64,6 +64,9 @@
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 )
36. [Gap canónico trigger→panel — offset token-driven (2026-06-22) ](#36-gap-canónico-triggerpanel--offset-token-driven-2026-06-22 )
37. [Touch-target — 44px en táctil, gated por puntero (2026-06-28) ](#37-touch-target--44px-en-táctil-gated-por-puntero-2026-06-28 )
38. [Capa de estado (state-layer) — feedback neutro unificado (2026-06-28) ](#38-capa-de-estado-state-layer--feedback-neutro-unificado-2026-06-28 )
---
@ -2160,6 +2163,34 @@ consumir `accent-border` en el anillo del campo. **Hoy** ambos usan el `--focus-
genérico (consistente con el resto), así que `data-color` no tiñe su foco — decisión
deliberada de la unificación.
### Outline en superficies — `box-shadow` muere en HCM/overflow (2026-06-29)
El anillo `box-shadow` tiene tres fragilidades en **controles autónomos sobre una superficie**
(no campos): (1) `box-shadow` **desaparece bajo forced-colors / HCM** (§28); (2) lo **recorta**
un ancestro `overflow: hidden` ; (3) su capa de hueco hardcodea `var(--color-surface-default)`
(arriba), así que sobre un plano `raised` / `overlay` / relleno el hueco **no casa** con el
fondo → halo desalineado.
Por eso el tier de **controles de superficie** usa `outline` :
```
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset); /* o 0 flush para input / scrollbar */
```
`outline` sigue el `border-radius` en todo navegador evergreen, **no** lo recorta `overflow` ,
y el fallback de forced-colors ya mapea `outline` (§28). Es el patrón de `button` / `card` y
~40 componentes. Los ** últimos 5** en box-shadow se migraron en `ab62cca7` : `command-input` ,
`collapsible-trigger` , `scroll-area-scrollbar` , `toggle` , `splitter` .
**Los campos (input / textarea / segmentos) SÍ siguen en box-shadow** — necesitan el anillo
INTERIOR parametrizable (`--focus-ring-inner-width`) que `outline` , al ser una sola línea, no
puede dar. El modelo de dos anillos de arriba es para ellos.
**Orphan**: tras migrar los 5, el token `--focus-ring` (box-shadow) quedó **sin consumidores
en CSS** (solo lo citan docs). Se deja como token público de foundation; podarlo es decisión
de API aparte (junto con los `--{x}-bg-hover` huérfanos, mismo blocker de `base.css` ).
## 33. Glifos de stepper themeables (`spin-field`) — 2026-06-11
Los botones increment/decrement pintan su glifo desde un **token** , no desde markup
@ -2376,6 +2407,72 @@ Commits `a265d39a` (controles) · `c0904fc6` (filas) · `2750b9ce` (fila de radi
---
**Última revisión**: 2026-06-28. Si algo en este doc no coincide con
## 38. Capa de estado (state-layer) — feedback neutro unificado (2026-06-28)
El feedback neutro de interacción (hover / press / selected de un control **sin valencia** )
estaba implementado de ** ~5 maneras distintas** en ~60 archivos: surface-swap
(`background: var(--color-surface-raised)`), `color-mix(currentColor X%)` ad-hoc, opacity-dim,
tokens `--{x}-bg-hover` bespoke por componente… El `ARCHETYPE_COHERENCE_AUDIT` (Arq. 8) y el
plugin de diseño marcaron lo mismo 🔴: ~168 reglas `:hover` resolviendo el MISMO efecto de
cinco formas.
**Solución — un único state-layer (modelo MD3), parametrizado a nivel de tema**
(`archetypes.css > :root`):
```
--state-hover: color-mix(in srgb, currentColor 8%, transparent);
--state-press: color-mix(in srgb, currentColor 12%, transparent);
--state-selected: color-mix(in srgb, currentColor 12%, transparent);
```
Basado en `currentColor` → **theme-adaptive sin valores por-tema** : el mismo token tiñe
correcto en light y en dark (donde el surface-swap fijo perdía contraste — ganancia neta).
Es el state-layer de Material Design 3 (hover 8% · press/selected 12%).
### Doctrina — qué tier usa el state-layer
- **Tier neutro / ghost** (la mayoría: triggers, items, ghost, controles sin valencia) →
**state-layer** . Es el dueño del feedback neutro.
- **Tier valenced** (solid / soft por color) → **mantiene su hover de paleta** en el recipe
(sistema de Button: `solid→solid-hover` , `soft→element` ). NO se toca — es el patrón de
referencia (Radix/Material), no bespoke.
- El shift de **color de TEXTO** (`--{x}-color-hover`) **se conserva** por componente — el
state-layer solo reemplaza el idioma de **fondo** .
### Mecanismo — overlay, no replace
- **Base transparente** (la mayoría): `background: var(--state-hover)` .
- **Base rellena** (tiene `background-color` propio): overlay en capa →
`background-image: linear-gradient(var(--state-hover), var(--state-hover))` — pinta el tinte
SOBRE el fondo base sin perderlo (filled-safe). Es la forma usada en el rollout.
### Cobertura
- **Pilot** : `accordion-trigger` (`a13ca387`).
- **Rollout** : 19 componentes neutros (`7de6c76b`) — collapsible, breadcrumb, calendar,
pagination, toolbar, file-upload, tag-group, editable, stepper, spin-field, select,
radio-cards, … (cada `--{x}-bg-hover` neutro → `--state-hover` ).
- **Fold transversal** (`e7e4870d`) — las **dos reglas neutras canónicas de `archetypes.css` **
pasan al state-layer:
- `[data-archetype='trigger']:hover` — el opacity-dim (`0.85`) → tinte `--state-hover` (el
dim atenuaba también el texto; el tinte no).
- El highlight de `item` / `option` (hover/focus/highlighted/selected en dropdown · context ·
select · combobox · listbox · command) — `surface-raised` → `--state-hover` (conservando
`color: content-primary` ).
### Pendiente
- Poda de los `--{x}-bg-hover` huérfanos en `lib/recipes/base.ts` (sin uso tras el rollout) +
guard test (ningún recipe neutro declara su propio `--{x}-bg-hover` ). Diferido por un
entanglement de `base.css` con trabajo concurrente.
- `tabs` + `color-picker` (hover bespoke) — diferidos por el mismo motivo.
**Doctrina**: el feedback neutro es **un concepto de tema, no de componente** — paralelo
exacto al focus ring (§32). Un state-layer definido una vez y parametrizado; los componentes
no reinventan su hover.
---
**Última revisión**: 2026-06-29. Si algo en este doc no coincide con
el código, el código gana — pero abre un issue para que actualicemos
el doc.