uix(background): temable — 57 % → 95 %, y montar MÁS capas hacía medir MENOS

Seis claves nuevas (31 → 37), tres literales firmados, un privado adjudicado y
19 tokens preexistentes medidos uno a uno: no había pasado por R-5.4.

LAS SEIS CLAVES SON LA MISMA COSTURA, DOS VECES

El patrón mesh leía var(--gradient-aurora) y los cinco pasos de escarcha leían
var(--blur-*), los dos A PELO: el VALOR es del sistema, el KNOB es del
componente. Con scrim-blur-* hay además una razón dura — `blur` es familia
MÉTRICA del eje `scaling` (theming §23), así que un literal ahí sería ciego al
zoom global.

TRES LITERALES FIRMADOS, Y EL GRANO NO SE ACUÑA

El 100% de img/video es identidad (una capa de medio ES la caja de su capa). El
grano del ruido es un feTurbulence en un data-URI: la textura ES la técnica —el
único patrón que el UA no elimina bajo forced-colors— y re-escaparlo para
meterlo en el contrato cambiaría el valor serializado sin cambiar la imagen. Su
mando sigue siendo pattern-noise-opacity.

EL 5 % QUE QUEDA ES CANAL DE VALOR

--_background-gradient-image lo escribe background-gradient.svelte EN LÍNEA
desde la prop `colors`. Un público encima no lo alcanzaría —el inline gana— y
mentiría: el caso de --gp-current-gradient y del preview-z que F2-B retiró.

UNA SOSPECHA MÍA, REFUTADA MIDIENDO

La línea del mesh hace lo que un apunte da por roto: un gradiente MESH por el
longhand background-image (su color base final no es capa de imagen válida →
computa `none`). Medido aquí: --gradient-aurora PINTA, porque su serialización
empieza por un radial-gradient. No hay defecto. La nota vale para un mesh
serializado con color base final, no para este token.

EL GUARD: DE 3/37 A 18/37, Y UNA LECCIÓN AL REVÉS

Las partes de este componente son CAPAS OPT-IN independientes (velo, foco,
pausa…), cada una tras su control, y sólo un patrón se renderiza a la vez: sobre
el escenario por defecto casi todo el contrato no tiene nodo que pintar. Nace
`prepareWith` — enciende TODOS los controles que se le den, al contrario que
`openWith`, que para en el primero — y con el barrido de data-pattern sube a 18.

Y la lección: MONTAR MÁS PUEDE MEDIR MENOS. Encender además el foco puntual, la
velocidad de paralaje y la profundidad bajó la corrida de 17 a 9: los tres
repintan el background-image y el translate de la MISMA capa donde se miden los
patrones. prepareWith se dejó en lo que no tapa.

LAS 19 ADJUDICACIONES, FORZANDO SOBRE LA CAPA REAL

Nunca sobre un nodo fabricado. Las dos del mask (fade-*, que el guard no puede
ver porque no fotografía mask-image — hueco de instrumento, §13), las dos del
contexto de tinta (on=dark/light), los ocho pasos de las dos escalas (blur 4→33,
8→33, 12→33, 24→33 px; strength alpha 0.08 · 0.13 · 0.4 · 0.7 → 0.9), el
gradient-drift-duration (24s → 42s, en una capa gradient que EXCLUYE a la de
patrón), los tres del foco puntual (40% → 42px…, y spotlight-fg sólo cuando la
capa NO lleva data-color: con él manda --palette-solid, por diseño), el
parallax-travel (sólo entra por las keyframes ligadas al scroll) y los dos de la
pausa (la demo no monta ese control: cero nodos).

AVISO: LOS CHIPS DE ESTA DEMO SE APLICAN CON RETRASO

Una comparación por chips capturó cada estado con el patrón ANTERIOR —el estado
«mesh» guardaba el glow— y dio 0 diffs por estar desfasada IGUAL en las dos
corridas: un gate verde sobre dos medidas equivocadas. La prueba buena fue la de
EQUIVALENCIA, determinista: cada token nuevo resuelve al mismo valor que el
primitivo que sustituyó (pattern-mesh-image = --gradient-aurora; scrim-blur-{k}
= --blur-{k} = 4/8/12/16/24 px) y lo pintado al forzar el atributo es idéntico
(el mesh pinta el aurora, el ruido pinta su data-URI intacto).

VERIFICACIÓN

Sonda estándar antes/después 0 diffs (384 valores · 7 estados) · equivalencia
token↔primitivo exacta en las seis · R-5.4 18/37 con las 19 restantes
adjudicadas y medidas, cero STALE · component:audit PASS · censo 95 % ·
eidos-lint 3 morfo-backed / 63 eidos-only / 0 invalid / 0 class-hooks ·
rtl:check 0 · docs:check 0 · suite eidos 439/440 (el rojo es skin-media-player,
el conocido). Tras el cambio del guard, command, table, tooltip,
navigation-menu y waveform re-verificados sin regresión ni STALE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 3c6742b9c5
commit 9bdc5a5d5d

@ -10,8 +10,8 @@
- **Medido**: 2026-08-23 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/`
- **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**?
- **Alcance global**: **66%** — 3019 de 4601 knobs pasan por un token público del componente
- **Reparto**: público 3019 · privado 318 · global 844 · literal 420 · sistema transversal 509 · excepción firmada 111 _(los dos últimos, fuera del ratio)_
- **Alcance global**: **66%** — 3025 de 4598 knobs pasan por un token público del componente
- **Reparto**: público 3025 · privado 318 · global 838 · literal 417 · sistema transversal 509 · excepción firmada 114 _(los dos últimos, fuera del ratio)_
- **Sin token público propio**: 24 · **alcance < 20 %**: 12 · **alcance 100 %**: 26 · **con eje `size`**: 57
## Cómo se lee
@ -134,7 +134,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [toggle-group](./toggle-group.md) | 50% | 2 | 1 | 0 | 1 | 0 | 0 | 1 | – |
| [sidebar](./sidebar.md) | 55% | 45 | 23 | 0 | 14 | 5 | 3 | 50 | – |
| [audio-player](./audio-player.md) | 56% | 18 | 10 | 0 | 8 | 0 | 0 | 8 | – |
| [background](./background.md) | 57% | 26 | 13 | 1 | 6 | 3 | 3 | 31 | – |
| [s-text-virtual-list](./s-text-virtual-list.md) | 58% | 12 | 7 | 0 | 3 | 2 | 0 | 9 | – |
| [badge](./badge.md) | 59% | 22 | 13 | 6 | 1 | 2 | 0 | 65 | y |
| [drawer](./drawer.md) | 60% | 74 | 43 | 8 | 16 | 5 | 2 | 54 | y |
@ -218,6 +217,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [color-field](./color-field.md) | 93% | 15 | 14 | 0 | 1 | 0 | 0 | 26 | y |
| [scroll-area](./scroll-area.md) | 94% | 17 | 15 | 0 | 0 | 1 | 1 | 13 | – |
| [tree-grid](./tree-grid.md) | 95% | 42 | 37 | 1 | 0 | 1 | 3 | 47 | – |
| [background](./background.md) | 95% | 23 | 19 | 1 | 0 | 0 | 3 | 37 | – |
| [picker-shell](./picker-shell.md) | 95% | 21 | 20 | 0 | 1 | 0 | 0 | 31 | y |
| [nav-tree](./nav-tree.md) | 96% | 24 | 22 | 0 | 0 | 1 | 1 | 18 | – |
| [image](./image.md) | 96% | 23 | 22 | 1 | 0 | 0 | 0 | 23 | y |

@ -5,46 +5,39 @@
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-23 · **Alcance**: **57%** — 13 de 23 knobs por token público
- **Knobs de apariencia**: 26 — público 13 · privado 1 · global 6 · literal 3 · sistema 3 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 31 pública(s) — `pattern-rule`, `pattern-cell`, `pattern-dot-size`, `pattern-glow-size`, `pattern-glow-at`, `pattern-glow-strength`, `pattern-mesh-opacity`, `pattern-noise-opacity`, `pattern-vignette-strength`, `pattern-lines-width`, `pattern-lines-gap`, `pattern-lines-angle`, `pattern-rings-width`, `pattern-rings-gap`, `fade-size`, `fade-at`, `scrim-fg`, `scrim-fg-over-dark`, `scrim-fg-over-light`, `scrim-strength-xs`, `scrim-strength-sm`, `scrim-strength-md`, `scrim-strength-lg`, `scrim-strength-xl`, `gradient-drift-duration`, `pause-offset`, `pause-z`, `parallax-travel`, `spotlight-size`, `spotlight-strength`, `spotlight-fg`
- **Medido**: 2026-08-23 · **Alcance**: **95%** — 19 de 20 knobs por token público
- **Knobs de apariencia**: 23 — público 19 · privado 1 · global 0 · literal 0 · sistema 3 · excepción 3 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 37 pública(s) — `pattern-rule`, `pattern-cell`, `pattern-dot-size`, `pattern-glow-size`, `pattern-glow-at`, `pattern-glow-strength`, `pattern-mesh-image`, `pattern-mesh-opacity`, `pattern-noise-opacity`, `pattern-vignette-strength`, `pattern-lines-width`, `pattern-lines-gap`, `pattern-lines-angle`, `pattern-rings-width`, `pattern-rings-gap`, `fade-size`, `fade-at`, `scrim-fg`, `scrim-fg-over-dark`, `scrim-fg-over-light`, `scrim-blur-sm`, `scrim-blur-md`, `scrim-blur-lg`, `scrim-blur-xl`, `scrim-blur-xxl`, `scrim-strength-xs`, `scrim-strength-sm`, `scrim-strength-md`, `scrim-strength-lg`, `scrim-strength-xl`, `gradient-drift-duration`, `pause-offset`, `pause-z`, `parallax-travel`, `spotlight-size`, `spotlight-strength`, `spotlight-fg`
- **Eje `size`**: no · **ficheros**: `background.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (6)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `background.css:192` | `[data-background-layer][data-pattern='mesh']` | `background-image` | `var(--gradient-aurora)` |
| 2 | `background.css:379` | `[data-background-layer][data-kind='scrim'][data-blur='sm']` | `backdrop-filter` | `blur(var(--blur-sm))` |
| 3 | `background.css:382` | `[data-background-layer][data-kind='scrim'][data-blur='md']` | `backdrop-filter` | `blur(var(--blur-md))` |
| 4 | `background.css:385` | `[data-background-layer][data-kind='scrim'][data-blur='lg']` | `backdrop-filter` | `blur(var(--blur-lg))` |
| 5 | `background.css:388` | `[data-background-layer][data-kind='scrim'][data-blur='xl']` | `backdrop-filter` | `blur(var(--blur-xl))` |
| 6 | `background.css:391` | `[data-background-layer][data-kind='scrim'][data-blur='xxl']` | `backdrop-filter` | `blur(var(--blur-xxl))` |
_Ninguno._
### 1.2 A través de un privado (1)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `background.css:264` | `[data-background-layer][data-kind='gradient']` | `background` | `var(--_background-gradient-image, transparent)` |
| 1 | `background.css:265` | `[data-background-layer][data-kind='gradient']` | `background` | `var(--_background-gradient-image, transparent)` |
### 1.3 Literales (3)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `background.css:76` | `[data-background-layer] :where(img, video)` | `inline-size` | `100%` |
| 2 | `background.css:77` | `[data-background-layer] :where(img, video)` | `block-size` | `100%` |
| 3 | `background.css:239` | `[data-background-layer][data-pattern='noise']` | `background-image` | `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E")` |
_Ninguno._
### 1.4 Excepciones firmadas (0) — fuera del ratio
### 1.4 Excepciones firmadas (3) — fuera del ratio
Literales que llevan su anotación `/* literal: <razón> */` en la propia
declaración: la válvula de recipe-contract §3, la misma que honra
`component-audit`. **Una desviación firmada no es deuda** — se listan para que la
razón se lea, no para acuñarlas.
_Ninguno._
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `background.css:76` | `[data-background-layer] :where(img, video)` | `inline-size` | `100%` |
| 2 | `background.css:77` | `[data-background-layer] :where(img, video)` | `block-size` | `100%` |
| 3 | `background.css:240` | `[data-background-layer][data-pattern='noise']` | `background-image` | `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E")` |
## 2. Sistema transversal (3) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
@ -73,7 +66,7 @@ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inl
## 4. Propuesta de corrección
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (2)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (1)
Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién
puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla
@ -83,8 +76,7 @@ no distingue lo que debería — se marca `⚠`.
| token (`--background-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `layer-backdrop-filter` | `root` | ⚠ `blur(var(--blur-sm))` / `blur(var(--blur-md))` / `blur(var(--blur-lg))` / `blur(var(--blur-xl))` / `blur(var(--blur-xxl))` | 5 |
| `layer-bg-image` | `root` | ⚠ `var(--gradient-aurora)` / `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E")` | 2 |
| `layer-bg-image` | `root` | `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E")` | 1 |
### 4.2 Sin nombre mecánico (3)
@ -103,6 +95,75 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**EJECUTADO 2026-08-23 — 57 % → 95 %.** Seis claves nuevas (31 → 37), tres
literales firmados, un privado adjudicado y **19 tokens preexistentes medidos
uno a uno**: el componente no había pasado por R-5.4.
**Las seis claves son la MISMA costura, dos veces.** El patrón `mesh` leía
`var(--gradient-aurora)` y los cinco pasos de escarcha leían `var(--blur-*)`,
los dos a pelo: el VALOR es del sistema, el KNOB es del componente. Con
`scrim-blur-*` hay además una razón dura — `blur` es familia MÉTRICA del eje
`scaling` (theming §23), así que un literal ahí sería ciego al zoom global.
**Tres literales firmados.** El `100%` de `img` / `video` es identidad (una capa
de medio ES la caja de su capa). Y el **grano del ruido no se acuña**: es un
`feTurbulence` en un data-URI, la textura ES la técnica —el único patrón que el
UA no elimina bajo `forced-colors`— y re-escaparlo para meterlo en el contrato
cambiaría el valor serializado sin cambiar la imagen. Su mando sigue siendo
`pattern-noise-opacity`.
**El 5 % que queda es el privado, y es canal de valor.**
`--_background-gradient-image` lo escribe `background-gradient.svelte` EN LÍNEA
desde la prop `colors`. Un público encima no lo alcanzaría —el inline gana— y
mentiría: es el caso de `--gp-current-gradient` del gradient-picker y del
`preview-z` de drag-drop que la revisión de F2-B retiró.
**Una sospecha mía, refutada midiendo.** La línea del mesh hacía exactamente lo
que un apunte de referencia da por roto: un gradiente MESH consumido por el
longhand `background-image` (su color base final no es una capa de imagen
válida, así que computa `none`). Medido aquí: `--gradient-aurora` **pinta** —su
serialización empieza por un `radial-gradient` y el navegador la acepta—, así
que no hay defecto. La nota vale para un mesh serializado con color base
final, no para este token.
**El guard midió 3 de 37 y ahora mide 18, con un campo nuevo.** Las partes de
este componente son CAPAS OPT-IN independientes (velo, foco, pausa…), cada una
tras su control, y sólo un patrón se renderiza a la vez: sobre el escenario por
defecto casi todo el contrato no tiene nodo que pintar. `prepareWith` (nuevo:
enciende TODOS los controles que se le den, al contrario que `openWith`, que
para en el primero) más el barrido de `data-pattern` lo llevan a 18.
**Y una lección del propio guard: montar MÁS puede medir MENOS.** Encender
además el foco puntual, la velocidad de paralaje y la profundidad hizo que la
corrida bajase de **17 a 9**: los tres repintan el `background-image` y el
`translate` de la misma capa donde se medían los patrones. `prepareWith` se dejó
en lo que no tapa.
**Las 19 adjudicaciones, todas medidas forzando el atributo sobre la capa REAL**
(nunca sobre un nodo fabricado): las dos del mask (`fade-*`, que el guard no
puede ver porque no fotografía `mask-image` — hueco de instrumento, §13), las
dos del contexto de tinta (`on='dark'` / `on='light'`), los ocho pasos de las
dos escalas (blur 4→33, 8→33, 12→33, 24→33 px; strength alpha 0.08 · 0.13 · 0.4
· 0.7 → 0.9), el `gradient-drift-duration` (24s → 42s, en una capa `gradient`
que EXCLUYE a la de patrón), los tres del foco puntual (40% → 42px…, y
`spotlight-fg` sólo cuando la capa NO lleva `data-color`: con él manda
`--palette-solid`, por diseño), el `parallax-travel` (sólo entra por las
keyframes ligadas al scroll, que el escenario medido no tiene) y los dos de la
pausa (la demo no monta ese control: cero nodos en la página).
**Un aviso para quien mida aquí: los chips de esta demo se aplican con RETRASO.**
Una comparación por chips capturó cada estado con el patrón ANTERIOR —el estado
«mesh» guardaba el glow— y dio 0 diffs por estar desfasada igual en las dos
corridas. La prueba buena fue la de EQUIVALENCIA, determinista: cada token nuevo
resuelve al mismo valor que el primitivo que sustituyó
(`pattern-mesh-image` = `--gradient-aurora`; `scrim-blur-{k}` = `--blur-{k}` =
4/8/12/16/24 px), y lo pintado al forzar el atributo es idéntico (el mesh pinta
el aurora, el ruido pinta su data-URI intacto).
**Verificación**: sonda estándar antes/después **0 diffs** (384 valores · 7
estados) · equivalencia token↔primitivo exacta en las seis claves · R-5.4 18/37
con las 19 restantes adjudicadas y medidas, cero STALE · `component:audit` PASS
· censo 95 % · `eidos-lint` 3 morfo-backed / 63 eidos-only / **0 invalid, 0
class-hooks** · `docs:check` 0.
<!-- veredicto:end -->

@ -1282,6 +1282,25 @@ Lo que `chart` destapó (2026-08-22):
waveform, chronos, los pickers) puede pasarle primitivos crudos sin que el
censo ni `recipe-css-contract` lo vean. Medir cuántas hay antes de decidir si
D-TH.2 se ensancha.
- **El guard no fotografía `mask-image`** (medido 2026-08-23 en `background`).
Sus dos tokens de desvanecido (`fade-size`, `fade-at`) alimentan un
`mask-image` radial y leen muertos aunque están vivos — medidos a mano, el
mask cambia (`at 50% 35%` → `at 11% 50%`). Añadir `maskImage` a la lista de
propiedades del guard es una línea, pero toca el instrumento de TODOS: entra
con su re-verificación de los componentes con ledger.
- **Montar MÁS capas puede medir MENOS** (medido 2026-08-23 en `background`).
Al enseñar al guard a encender las capas opt-in del componente, añadir el foco
puntual, la velocidad de paralaje y la profundidad hizo BAJAR la corrida de 17
a 9 tokens: los tres repintan el `background-image` y el `translate` de la
misma capa donde se miden los patrones. La regla operativa: `prepareWith` sólo
debe encender lo que NO tapa lo ya medido, y eso hay que comprobarlo corriendo
el guard antes y después de cada control que se añada.
- **Los chips de la demo de `background` se aplican con RETRASO.** Una captura
por chips guardó cada estado con el patrón ANTERIOR (el estado «mesh» tenía el
glow) y dio 0 diffs por estar desfasada IGUAL en las dos corridas — un gate
verde sobre dos medidas equivocadas. Donde la demo no coopere, la prueba buena
es la de EQUIVALENCIA (el token nuevo resuelve al mismo valor que el primitivo
que sustituyó) más forzar el atributo en el DOM, las dos deterministas.
- **Un panel de HOVER se cierra por el propio guard.** `navigation-menu` abre
con `pointerenter` y `pointerleave` programa el cierre; el guard abre con
clic y luego aparca el puntero en (0,0), que es exactamente el gesto que lo

@ -18,6 +18,54 @@
*/
export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
background: {
// 2026-08-23. Its parts are independent OPT-IN layers and only ONE pattern
// renders at a time, so the default stage moved 3 of 37 tokens. The guard
// now switches the scrim on and sweeps `data-pattern` (18/37); what is
// left is measured here, forcing the attribute on the REAL layer — never
// on a node built by hand.
//
// The mask: the guard does not read `mask-image`, so both read dead while
// being perfectly alive (instrument gap, next-features §13).
'fade-size': 'the guard does not snapshot mask-image; measured by hand → reaches (the radial mask changes)',
'fade-at':
'the guard does not snapshot mask-image; measured by hand → reaches (at 50% 35% → at 11% 50%)',
// The ink context: only under `on='dark'` / `on='light'`.
'scrim-fg-over-dark': "only under on='dark'; forced → reaches (the veil colour changes)",
'scrim-fg-over-light': "only under on='light'; forced → reaches (the veil colour changes)",
// Two scales where only the step in force paints (the `section` / `qr-code`
// class): every step measured one by one on the real scrim layer.
'scrim-blur-sm': 'only the step in force paints; forced data-blur=sm → reaches (4px → 33px)',
'scrim-blur-md': 'only the step in force paints; forced data-blur=md → reaches (8px → 33px)',
'scrim-blur-lg': 'only the step in force paints; forced data-blur=lg → reaches (12px → 33px)',
'scrim-blur-xxl': 'only the step in force paints; forced data-blur=xxl → reaches (24px → 33px)',
'scrim-strength-xs':
'only the step in force paints; forced data-strength=xs → reaches (alpha 0.08 → 0.9)',
'scrim-strength-sm':
'only the step in force paints; forced data-strength=sm → reaches (alpha 0.13 → 0.9)',
'scrim-strength-lg':
'only the step in force paints; forced data-strength=lg → reaches (alpha 0.4 → 0.9)',
'scrim-strength-xl':
'only the step in force paints; forced data-strength=xl → reaches (alpha 0.7 → 0.9)',
// A gradient layer EXCLUDES a pattern layer, so the sweep cannot have both.
'gradient-drift-duration':
'the gradient layer is exclusive with the pattern one the sweep needs; forced data-kind=gradient → reaches (24s → 42s)',
// The spotlight is off on the default stage and, once on, it repaints the
// pattern layer it shares a node with — measured on its own pass.
'spotlight-size': 'spotlight is off in the demo; forced data-spotlight → reaches (40% 40% → 42px)',
'spotlight-strength':
'spotlight is off in the demo; forced data-spotlight → reaches (the mix stop moves)',
'spotlight-fg':
"spotlight is off AND the demo layer carries data-color, where --palette-solid wins by design; forced data-spotlight with no data-color → reaches",
// Structural, not conditional: the travel only enters through the
// scroll-linked `background-parallax` keyframes, which need a scroll
// timeline the measured stage does not have.
'parallax-travel':
'only enters via the scroll-linked keyframes (--_background-parallax-offset); the measured stage has no scroll timeline',
// No node at all: the demo never mounts the pause control.
'pause-offset': 'the pause control is not mounted in the demo (0 nodes on the page)',
'pause-z': 'the pause control is not mounted in the demo (0 nodes on the page)'
},
'color-field': {
// 2026-08-23. Thirteen tokens, none of them a lie: the demo boots with a
// value, in `md`, editable, with three formats allowed — so every state

@ -139,6 +139,16 @@ const COMPONENT_OVERRIDES: Record<
urls?: string[];
/** Nodes to measure that carry NO `data-{c}-*` attr (prose styles bare HTML). */
extraNodes?: string;
/**
* Demo controls to switch ON before measuring — ALL of them, unlike
* `openWith`, which stops at the first that works because it opens ONE
* surface. A component whose parts are independent OPT-IN layers
* (background: scrim, spotlight, pause… each behind its own control)
* measured 3 of 37 tokens on the default stage: what the demo does not
* mount has no node to paint. Failures are ignored — a control that is
* not there is not an error, it is one layer this route cannot show.
*/
prepareWith?: string[];
/**
* How to tell the surface is ALREADY open, when it is not
* `[data-{c}-content]`. Without it the re-open guard that runs before every
@ -199,6 +209,25 @@ const COMPONENT_OVERRIDES: Record<
// pointer parking the guard does after a click (`mouse.move(0, 0)`) shut it
// MID-RUN — two runs of the same code disagreed on one token.
'navigation-menu': { openBy: 'hover', extraNodes: '[data-navigation-menu-content] a' },
// Its parts are independent OPT-IN layers, each behind its own demo control,
// and only ONE pattern renders at a time: on the default stage the guard saw
// a single `glow` layer and 3 of 37 tokens moved. Switch the extra layers on,
// then sweep the pattern axis.
background: {
// ONLY the layers that do not COVER what is already being measured:
// switching the spotlight, the parallax speed and the pointer depth on
// repaints the pattern layer's own `background-image` and its transform,
// and the run went BACKWARDS (17 → 9 tokens). A guard that mounts more
// can measure less.
prepareWith: [
"[data-uix-control]:has([data-uix-control-label]:text-matches('^scrim')) input[type=checkbox]",
"[data-uix-control]:has([data-uix-control-label]:text-matches('^blur')) [data-uix-chip]:text-is('xl')"
],
sweepAttr: {
attr: 'data-pattern',
values: ['glow', 'mesh', 'grid', 'dots', 'noise', 'vignette', 'lines', 'rings']
}
},
// Same class: its chrome is an embedded Slider the recipe re-tints, and the
// playhead IS that slider's thumb — `data-waveform*` matched 4 nodes and none
// of them was it.
@ -393,6 +422,17 @@ async function main() {
// A surface that boots OPEN must not be clicked SHUT: onion-menu renders its
// sectors from the start, and the opening click toggled the whole recipe out
// of the document (12 of its 20 tokens read dead, 2026-08-23).
for (const sel of override.prepareWith ?? []) {
const el = page.locator(sel).first();
if (!(await el.count())) continue;
try {
await el.click({ timeout: 1500 });
await page.waitForTimeout(350);
} catch {
/* one layer this route cannot show */
}
}
const alreadyOpen = override.openMarker
? (await page.locator(override.openMarker).count()) > 0
: false;

@ -298,6 +298,37 @@ never attached.** Parallax is motion tied to the reader's own scrolling, which
is the exact class that triggers vestibular symptoms. The layer stays where it
is: `reduce="static"`, not `hide`.
## Talla y tema
37 claves públicas en `lib/recipes/base.ts` (bloque `background`). El componente
no tiene eje `size`: sus escalas propias son **la fuerza del velo**
(`scrim-strength-{xs…xl}`) y **la escarcha** (`scrim-blur-{sm…xxl}`), cada una
resuelta por su atributo (`data-strength`, `data-blur`).
Lo que entró el **2026-08-23** (57 % → 95 %), y por qué:
- **`pattern-mesh-image`** — el patrón `mesh` leía `var(--gradient-aurora)` a
pelo. La costura: el VALOR es del catálogo de gradientes, el KNOB es de este
componente, así que un tema puede dar otro mesh sin mover el aurora en todas
partes. Verificado: el token resuelve al mismo valor que el primitivo.
- **`scrim-blur-{sm,md,lg,xl,xxl}`** — los cinco pasos leían `var(--blur-*)` a
pelo. Misma costura, y con una razón extra: `blur` es familia MÉTRICA del eje
`scaling` (theming §23), así que un literal ahí sería ciego al zoom global.
Medido paso a paso: 4 · 8 · 12 · 16 · 24 px, idénticos antes y después.
**Lo que NO se acuña**, y queda firmado en su declaración:
- **El grano del ruido** es un `feTurbulence` en un data-URI: la textura ES la
técnica —el único patrón que el UA no elimina bajo `forced-colors`— y
re-escaparlo para meterlo en el contrato cambiaría el valor serializado sin
cambiar la imagen. `pattern-noise-opacity` sigue siendo su mando.
- **El `100%` de `img` / `video`** es identidad: una capa de medio ES la caja de
su capa.
- **`--_background-gradient-image` es CANAL DE VALOR, no superficie de tema**:
lo escribe `background-gradient.svelte` en línea, desde la prop `colors`. Un
público encima no lo alcanzaría (el inline gana) y mentiría — el mismo caso
que `--gp-current-gradient` del gradient-picker.
## Preferences
| Preference | What happens |

@ -73,8 +73,8 @@
* of the app's own. */
[data-background-layer] :where(img, video) {
display: block;
inline-size: 100%;
block-size: 100%;
inline-size: 100%; /* literal: identity — a media layer IS its layer's box */
block-size: 100%; /* literal: identity — a media layer IS its layer's box */
object-fit: cover;
}
@ -189,7 +189,7 @@
}
[data-background-layer][data-pattern='mesh'] {
background-image: var(--gradient-aurora);
background-image: var(--background-pattern-mesh-image);
opacity: var(--background-pattern-mesh-opacity);
}
@ -236,7 +236,8 @@
only pattern the UA does NOT strip under forced colors — which is why the
forced-colors block below drops every layer rather than trusting it. */
[data-background-layer][data-pattern='noise'] {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
/* prettier-ignore */
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E"); /* literal: the grain IS the technique — an inline feTurbulence, the one pattern the UA does not strip under forced colors; re-escaping it into the contract would change the serialised value, not the picture */
opacity: var(--background-pattern-noise-opacity);
}
@ -376,19 +377,19 @@
}
[data-background-layer][data-kind='scrim'][data-blur='sm'] {
backdrop-filter: blur(var(--blur-sm));
backdrop-filter: blur(var(--background-scrim-blur-sm));
}
[data-background-layer][data-kind='scrim'][data-blur='md'] {
backdrop-filter: blur(var(--blur-md));
backdrop-filter: blur(var(--background-scrim-blur-md));
}
[data-background-layer][data-kind='scrim'][data-blur='lg'] {
backdrop-filter: blur(var(--blur-lg));
backdrop-filter: blur(var(--background-scrim-blur-lg));
}
[data-background-layer][data-kind='scrim'][data-blur='xl'] {
backdrop-filter: blur(var(--blur-xl));
backdrop-filter: blur(var(--background-scrim-blur-xl));
}
[data-background-layer][data-kind='scrim'][data-blur='xxl'] {
backdrop-filter: blur(var(--blur-xxl));
backdrop-filter: blur(var(--background-scrim-blur-xxl));
}
/* ── Parallax, pointer depth, spotlight ──────────────────────────────────────

@ -3511,6 +3511,7 @@
--background-pattern-glow-size: 60% 55%;
--background-pattern-glow-at: 50% 0%;
--background-pattern-glow-strength: 28%;
--background-pattern-mesh-image: var(--gradient-aurora);
--background-pattern-mesh-opacity: var(--opacity-scrim);
--background-pattern-noise-opacity: var(--opacity-ghost);
--background-pattern-vignette-strength: 38%;
@ -3524,6 +3525,11 @@
--background-scrim-fg: var(--color-surface-default);
--background-scrim-fg-over-dark: var(--color-content-on-solid-contrast);
--background-scrim-fg-over-light: var(--color-content-on-solid);
--background-scrim-blur-sm: var(--blur-sm);
--background-scrim-blur-md: var(--blur-md);
--background-scrim-blur-lg: var(--blur-lg);
--background-scrim-blur-xl: var(--blur-xl);
--background-scrim-blur-xxl: var(--blur-xxl);
--background-scrim-strength-xs: 0.08;
--background-scrim-strength-sm: 0.13;
--background-scrim-strength-md: 0.19;

@ -5462,6 +5462,14 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'pattern-glow-strength': '28%',
// A mesh is a veil at scrim weight — the same 0.45 Backdrop shipped, now
// tied to the semantic scale instead of a literal (R-4.2).
// The mesh pattern IS a system gradient — the seam: the VALUE belongs to
// the gradient catalogue, the KNOB belongs to this component, so a theme
// can give the mesh another image without moving `--gradient-aurora`
// everywhere it is used. (Measured 2026-08-23: it paints as a longhand
// `background-image` here — this token's serialisation starts with a
// radial-gradient, so the mesh-through-`background-image` trap does not
// apply to it.)
'pattern-mesh-image': 'var(--gradient-aurora)',
'pattern-mesh-opacity': 'var(--opacity-scrim)',
'pattern-noise-opacity': 'var(--opacity-ghost)',
'pattern-vignette-strength': '38%',
@ -5495,6 +5503,16 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
// legibility over ANY photograph: it clears BOTH framework floors in BOTH
// contexts (on-solid's APCA ≥ 60 ∧ WCAG ≥ 3, and §40's AA 4.5 for text) —
// measured `on='dark'` 6.45:1 / Lc 85, `on='light'` 8.29:1 / Lc 61.
// The frost behind the scrim, one step per `blur` value. Same seam as the
// mesh above: the scale is the system's (`--blur-*`), the knob is the
// component's — a theme can soften the scrim's frost without touching
// every blur in the app. `blur` is a METRIC family of the `scaling` axis
// (theming §23), so these must stay tokens, never literals.
'scrim-blur-sm': 'var(--blur-sm)',
'scrim-blur-md': 'var(--blur-md)',
'scrim-blur-lg': 'var(--blur-lg)',
'scrim-blur-xl': 'var(--blur-xl)',
'scrim-blur-xxl': 'var(--blur-xxl)',
'scrim-strength-xs': '0.08',
'scrim-strength-sm': '0.13',
'scrim-strength-md': '0.19',

@ -31,6 +31,7 @@
import { compileMorfo } from '$uix/morfo';
import { backgroundMorfo } from '@/uix/morfo/components/background';
import { getActiveUix } from '$active-uix';
import TokensPanel from '../../lib/TokensPanel.svelte';
import PalettePicker from '../../lib/PalettePicker.svelte';
import SystemAxes from '../../lib/SystemAxes.svelte';
@ -49,6 +50,7 @@
| 'api'
| 'morfo'
| 'recipe'
| 'tokens'
| 'a11y';
let tab = $state<Tab>('live');
@ -354,6 +356,9 @@
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}
>Recipe</button
>
<button data-uix-tab data-active={tab === 'tokens'} onclick={() => (tab = 'tokens')}>
Tokens <span data-uix-tab-count>37</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -886,6 +891,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="background" stage={stageRef ?? undefined} />
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>

Loading…
Cancel
Save

Powered by TurnKey Linux.