You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/PLAN-background.md

1315 lines
111 KiB

uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
# PLAN — `Background`: el anfitrión de capas de fondo del canon (estudio + plan)
> **Estado: DECISIONES D-BG.1–11 y D-BG.13–15 FIRMADAS 2026-08-17 (D-BG.12
> superada por la 14) — NADA CONSTRUIDO.** Falta que el autor nombre la rama de
> trabajo (§10.1) y ordene el arranque del agente (F0). El estudio original,
> abajo, se conserva íntegro.
>
> ~~Estado: ESTUDIO ENTREGADO 2026-08-17 — NADA CONSTRUIDO, NADA FIRMADO.~~
> Este documento responde a la pregunta «¿conviene un componente `Background`
> rico, composable desde el resto del framework, a la altura o por encima de lo
> que hay hoy (parallax, capas, vídeo, patrones, scrim…)?». Contiene el
> veredicto razonado, la comparativa con las referencias, el inventario de lo
> que YA existe en el framework, la forma canónica propuesta, las decisiones
> que sólo el autor puede firmar (§6, D-BG) y el plan por fases con sus gates
> (§7). Es un documento de proceso: no es doctrina hasta que el autor firma.
>
> **Kickoff para sesión nueva**: _«Lee `docs/process/PLAN-background.md`; si
> las D-BG de §6 están firmadas, ejecuta la fase que toque; si no, PARA y
> preséntalas una a una.»_ Cada fase lista qué leer, qué producir y con qué
> guard se verifica.
>
> **Enmiendas de la sesión 2026-08-17 (conversación con el autor, tras el
> estudio)** — mandan sobre cualquier frase anterior de este documento:
>
> 1. **`Background` NO compone `Box` ni es una caja de flujo.** Es la PILA DE
> CAPAS que el padre renderiza como hijo (`position:absolute; inset:0;
> z-index:-1; border-radius: inherit; overflow: clip` en la propia pila). Un
> solo modo de colocación; el layout sigue siendo de `Box`/`Section`/`Card`
> («one axis, one primitive»). D-BG.12 queda superada por D-BG.14.
> 2. **El padre se convierte en anfitrión sin tocarlo**: una regla de foundation
> `:where(:has(> [data-background])) { position: relative; isolation: isolate }`
> (`:has()` ya se usa 71 veces en eidos; `:where` deja ganar al `position` de
> cualquier recipe). El bloque `data-on` gana el mismo gemelo `:has`.
> 3. **Cero props nuevas en `Box`** (Box es layout-only por doctrina; el
> tratamiento vive en primitivas aparte — Surface es el precedente).
> 4. **Composición, no struct**: el padre que quiera renderizar el fondo expone
> un snippet (`background`) donde el app compone `<Background>…</Background>`;
> un prop `background={struct}` contradice la regla compositional-not-data-driven
> del autor (OnionMenu 2026-06-21), B-5 y la regla 6 de eidos → D-BG.13.
> 5. Ejecución: **un agente Opus 5 construye; Fable supervisa** — brief en §10,
> protocolo de supervisión en §11.
---
## 0. La pregunta y la respuesta corta
**Pregunta** (usuario, 2026-08-17): estudiar el sistema ActiveUIX, su
documentación, filosofía y guías, y evaluar un componente `Background` rico,
composable en el resto de componentes, que permita las técnicas de fondo de hoy
(parallax, etc.), a la par o por encima de las referencias; evaluar idoneidad;
si conviene, plan de desarrollo.
**Respuesta corta — SÍ conviene, con una forma precisa**: no «un componente
más» sino **el anfitrión canónico de capas de fondo** que el tier `blocks` ya
está reclamando a gritos y que hoy se resuelve a mano. La evidencia es
mecánica, no de gusto:
- `src/uix/blocks/hero/hero.svelte` (layout `background`) monta **tres capas a
mano**: un `Box` absoluto para el media del app, un `Box` con
`style="background: var(--color-overlay); opacity: var(--opacity-scrim)"` de
scrim y un `<style>` scoped `/* justified: */` para el `object-fit: cover`
del `<img>/<video>` del app — la **excepción D-BLK.2** del contrato B, más el
`data-on='dark'` y las tintas explícitas puestos a mano. El README del hero
documenta dos huecos del canon que salieron de ahí. Es exactamente la regla
de admisión disparando: «si al construir un block hace falta un `data-attr`
que el CSS necesita seleccionar o una obligación a11y, esa pieza se construye
ANTES como componente canónico».
- `docs/process/PLAN-blocks-quality.md` §2 diagnosticó «no hay fondo decorativo
de sección» y resolvió `Backdrop` (glow/mesh/grid/dots) — **cuatro patrones
estáticos, una sola capa, sin README, sin demo, un consumidor** (hero). El
mismo plan deja «parallax suave» del hero como acabado que falta, y
`Surface` deja en Gaps «scrim de autoría para vívidos — componentizarlo sólo
si aparece el caso repetido». El caso ha aparecido.
- Ninguna referencia (Radix/Ark/Bits/React Aria/MUI/Chakra) tiene un
componente de fondo; Mantine (`BackgroundImage` + `Overlay`), Vuetify
(`v-parallax`) y `react-scroll-parallax` (`ParallaxBanner` por capas) son
los únicos con API de producto; el resto (Aceternity, Magic UI, shadcn.io,
React Bits) son colecciones de efectos copy-paste sin sistema. **El hueco de
«anfitrión de capas con contrato» está vacío en el sector** — y el framework
ya posee todas las piezas para llenarlo con ventaja (tokens+TSC, ActiveDom,
motion reducido, `data-on`, `Image`, `$scene`/`Ambient`, `Toggle`).
**Y con la misma precisión, lo que NO conviene**: meter en el canon los efectos
animados decorativos (aurora WebGL, beams, partículas…). Eso ya está decidido y
construido: son el **pack `Ambient`** (32 efectos sobre `$scene`) —
`docs/architecture/packs.md` + `decisions/design-text-effects.md` («backgrounds
went to the pack tier»). `Background` es el **anfitrión** (geometría, a11y,
tokens, parallax, scrim, media) donde el app monta esos efectos como una capa
más. Canon = la superficie de contrato; pack = la hoja decorativa. La doctrina
se cumple leyéndola entera: lo que fue al pack fue el EFECTO, no la ranura.
---
## 1. Lo que se ha leído (precondición cumplida)
Doctrina leída ENTERA para los ejes que toca esta pieza (arquitectura, canon,
morfo, soma §2 membresía, eidos, motion, theming, packs, blocks, guías):
`docs/README.md` · `architecture/overview.md` · `architecture/active-architecture.md`
· `CANON.md` · `architecture/morfo.md` · `architecture/soma.md` (membresía) ·
`architecture/eidos.md` · `src/uix/eidos/components/README.md` ·
`theming/motion.md` · `theming/motion-guide.md` · `src/arts/motion/README.md` ·
`theming/reference.md` (entera) · `canon/tsc.md` · `canon/recipe-contract.md` ·
`theming/gradient-finish.md` · `theming/channels.md` · `theming/notes.md` ·
`architecture/packs.md` · `architecture/blocks.md` · `building-a-component.md` ·
`guides/component-guide.md` (build contract + Before You Start §1–5) ·
`guides/completion-checklist.md` · `guides/demo-authoring.md` ·
`guides/component-audit.md` · `canon/vocabularies.md` ·
`decisions/design-text-effects.md` · `decisions.md` · `glossary.md` ·
`next-features.md` · `rfcs/rfc-depth.md` (alcance: scrim/frost/parallax) ·
`process/PLAN-blocks-quality.md` · `process/PLAN-blocks.md` (forma) ·
`process/CONTINUE-lectura-doctrina.md`.
Código leído: `eidos/components/{backdrop,surface,image,scroll-frames,motion}`
+ sus morfos · `blocks/hero/{README,hero.svelte}` · `packs/ambient/{README,
ambient.svelte}` · `arts/scene/{README,types}` · `arts/adom/README.md` ·
`eidos/lib/primitives/static.ts` (gradientes/scrim/opacity/blur) ·
`eidos/lib/themes/cristal.ts` · `web/routes/demos/heroscrolling` (semilla de
parallax) · `web/routes/demos/animations/background/*` (censo de semillas).
**No leído, y por qué no bloquea**: `architecture/sema.md` (1051 L) — la
pieza es pasiva (`scope: ['eidos']`, 0 eventos, patrón ScrollFrames/Backdrop);
si alguna decisión de §6 la hiciera emitir eventos, se lee ANTES de tocar el
morfo. `MOTION_SERVICE_RFC.md` — leído por sus síntesis en `motion.md` /
`motion-guide.md` y grepeado: **no contempla timelines de scroll ni parallax**
(sólo cita GSAP como lo que NO se reinventa) → el parallax es terreno no
cubierto por la doctrina de motion, y por eso es una decisión (D-BG.3), no una
presuposición.
---
## 2. Comparativa — qué hay hoy en el sector y dónde queda UIX
Leído en las fuentes (docs oficiales / repos), 2026-08-17.
| Capacidad | Mantine (`BackgroundImage`, `Overlay`) | Vuetify `v-parallax` | `react-scroll-parallax` (`ParallaxBanner`) | Motion/Framer (`useScroll`) | Aceternity · Magic UI · shadcn.io · React Bits | Tailwind Plus · Untitled · daisyUI | **UIX hoy** | **UIX con `Background`** |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Imagen de fondo cover con contenido encima | ✓ (`src`, `radius`) | ✓ (`src`, `height`) | ✓ (`image` por capa) | — | dentro de cada efecto | HTML + `bg-*` / `hero-overlay` | hero a mano (`<style>` justified) | ✓ `Background.Image` compone `Image` (fit/position/srcset/priority/fallback) |
| Vídeo de fondo (autoplay muted loop poster, pausa a11y) | ✗ | ✗ | ✗ | ✗ | ✗ | HTML crudo | ✗ | ✓ `Background.Video` + `Background.Pause` (WCAG 2.2.2), pausa fuera de vista/pestaña |
| Scrim / overlay (color, opacidad, gradiente, blur, fixed) | ✓ `Overlay` | ✗ | ✗ | — | ad hoc | `hero-overlay` (opaco fijo) | inline `style=` en hero | ✓ `Background.Scrim` sobre `--color-overlay` / `--opacity-*` / `--blur-*` (Cristal-compatible) |
| Capas apiladas con blend/opacidad/máscara | ✗ | ✗ | ✓ (orden = z; `expanded`) | manual | ✗ (un efecto = un wrapper) | ✗ | `Backdrop` = una capa `::before` | ✓ `Background.Layer` genérica (blend, opacity semántica, fade-mask, bleed) |
| Patrones estáticos (grid, dots, líneas, ruido, anillos, viñeta, glow, mesh, spotlight) | ✗ | ✗ | ✗ | — | ✓ (copy-paste, colores a mano) | Untitled `BackgroundPattern` (SVG), TW blobs `clip-path` | `Backdrop`: 4 sobre tokens | ✓ `Background.Pattern` (los 4 + lines/noise/rings/vignette/spotlight), **sobre tokens y modo** |
| Gradientes nombrados temables | ✗ | ✗ | ✗ | — | hex a mano | utilidades | `--gradient-{name}` (open cage) + `Surface gradient` | ✓ `Background.Gradient colors="aurora"\|[stops]` (patrón D-T3 de TextGradient) |
| Parallax por scroll (velocidad por capa) | ✗ | ✓ (una imagen) | ✓ (`speed`, easing, ranges) | ✓ (`useTransform`) | «Hero Parallax» (JS, GSAP-like) | ✗ | ✗ (semilla `heroscrolling`) | ✓ CSS scroll-driven (`animation-timeline: view()`) primero + fallback JS por ActiveDom |
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los ejes que F3 añade, y separarlas dejaría un estado que nunca se probó. Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la deriva contra el puntero), `spotlight` y `attach='fixed'`. El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca `ScrollProgress` y escribe `--background-progress`, que la rama `@supports not` mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll del propio lector, que es justo la clase que provoca síntomas vestibulares. **La decisión que no estaba prevista.** El scroll y el puntero quieren mover la MISMA capa, y una animación sobre `translate` gana a cualquier declaración estática: el puntero habría dejado de existir sin más. Así que el scroll anima una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único `translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`. El precio, dicho porque en la primera redacción escribí lo contrario tres veces: una custom property NO se puede compositar, así que el navegador recalcula estilo cada frame. Para una decoración es el intercambio correcto —una property por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era falso y ahora el código dice lo que ocurre. **Dos footguns cerrados por forma, no por disciplina:** - `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes había que escribirlo también en la pila, y olvidarlo dejaba la capa `position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae de vuelta). La prop de la pila desaparece: no hay nada que olvidar. - Un `speed` negativo —una capa que se mueve contra el scroll— invertía el bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora usa la magnitud. **Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones —`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio, `declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde el init, y seguir el prop sin escribir en la primera pasada. **La demo** (`/uix/components/background`, v2, nueve pestañas) monta un ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí solo y sin padre no se puede enseñar lo único que importa: que el padre se adopta y el layout no se mueve. Los chips son uniones completas verificadas por el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación. Y fue la demo la que destapó que, con A30, encender `animate` en caliente no hacía aparecer el control de pausa — el registro era un hecho de montaje. Invisible en una sonda, obvio con un interruptor. Registro documental (D-BG.11): `next-features.md` §11 · la frase en `design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) · `PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y `Aura` corregida (decía «Not built yet» y está construido) · y en `motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un preset nombra una transición discreta CON duración, y esto es modulación continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor. Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el `translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres de las cuatro clases de capa hasta la segunda auditoría). ⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60 fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con `animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome visible. Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 · `morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Parallax por puntero / spotlight | ✗ | ✗ | ✗ | ✗ | ✓ («Parallax Scroll mouse», Spotlight) | ✗ | sólo dentro de efectos WebGL del pack | ✓ `depth` por capa + `spotlight` (un listener por pila, rect cacheado, `dom.writeProperty`) |
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| Efectos animados (aurora, beams, partículas, mesh en deriva…) | ✗ | ✗ | ✗ | ✗ | ✓ decenas (canvas/CSS/WebGL) | ✗ | ✓ **pack `Ambient` (32)** + `$scene` | = pack; `Background.Layer` es la ranura; `Ambient` lee el contexto (pausa/reduce) |
| Reduced-motion / reduced-data / forced-colors por construcción | ✗ | ✗ | `disabled` manual | manual | rara vez | ✗ | Backdrop: `prefers-contrast` ✓; pack: P-1 reduce ✓ | ✓ política `reduce` en el host + poster-only bajo `saveData` + capas fuera en `forced-colors` |
| Contexto de tinta para el contenido (inversión) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | `data-on` (D12, mínimo) | ✓ prop `on` en el host (D12) + scrim: la legibilidad se compone, no se improvisa |
| Disciplina de plataforma (iframe-safe, sin reflow forzado, teardown) | ✗ | ✗ | ✗ | parcial | ✗ | ✗ | ActiveDom (`listen/raf/measure/observe*`) | ✓ heredada — la misma que ScrollFrames |
| Contrato declarado (partes, `aria-hidden`, tokens con TSC, guard) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | morfo pasivo (Backdrop/Surface/ScrollFrames) | ✓ morfo `scope:['eidos']` + `component:audit` + eidos-lint + recipe-contract |
**Lectura de la tabla.** Ninguna referencia ofrece la pieza entera; cada una
tiene un trozo. UIX iguala cada trozo con SU mecanismo (tokens, ActiveDom,
`Image`, `data-on`, pack) y supera en lo que ninguna tiene: contrato
declarado, a11y por construcción, theming vivo, y la ranura para el pack. Donde
las referencias seguirán ganando **a propósito**: amplitud de efectos exóticos
(shadcn.io lista 100) — la respuesta del framework es el tier de packs, no el
canon; y la orquestación de scroll tipo GSAP (pin + scrub + timelines) —
fuera de alcance de esta pieza (ScrollFrames ya cubre el scrub de media; el
storytelling pinned es otra iniciativa).
**Soporte de plataforma comprobado (2026-08)**: CSS scroll-driven animations
en Chrome/Edge 115+, Safari 26+ (threaded en 26.4), Firefox aún tras flag
(prioridad Interop 2026) — ~84 % global, no Baseline → **progressive
enhancement obligatoria** con `@supports (animation-timeline: view())` y
fallback JS (es lo que hace Motion/Framer: `ScrollTimeline` nativo donde hay,
JS donde no).
---
## 3. Inventario — lo que el framework YA tiene (y no se reinventa)
| Pieza | Qué aporta a `Background` | Cómo se usa |
| --- | --- | --- |
| `Backdrop` (canon, eidos-native; morfo `scope:['eidos']`, 1 parte, `data:[]`) | 4 patrones sobre tokens (glow/mesh/grid/dots), `fade` mask, `prefers-contrast: more` cae el bloom, `isolation:isolate` + `::before z:-1` | Se **absorbe** como `Background.Pattern` (D-BG.1); único consumidor: hero |
| `Surface` (Box + tratamiento; `on`, `gradient`, `rounded`) | El precedente de «lienzo temable» y de `data-on` (D12) | `Background` NO duplica: es la superficie de CAPAS; `Surface` sigue siendo el lienzo pintado por identidad. Se cierra su gap «scrim» |
| Gradientes nombrados `--gradient-{name}` (open cage; `aurora` es `background-image`-válido, los mesh con base final NO) | Capa de gradiente temable | `Background.Gradient` pinta con `background:` shorthand (válido en ambos casos) — la lección de `reference_mesh_gradient_not_background_image` |
| `Image` (fit/position responsive, srcset/sizes/fetchpriority/decoding, Fallback/Error, `data-status` de `ImageProvider`) | La imagen de fondo con ciclo de carga | `Background.Image` **compone** `<Image fit="cover">`; nunca un `<img>` crudo |
| `ScrollFrames` (progreso de scroll por `dom.listen` + `dom.raf` + `observeResize`, lecturas dentro del rAF) | El patrón JS de progreso de scroll disciplinado | Semilla del fallback de parallax; candidato a compartir un helper `$adom` (`ScrollProgress`, D-BG.7) |
| `Motion trigger="viewport"` + contexto `seen` · `Cascade` · `data-stagger` | La coreografía de entrada del contenido — no del fondo | El contenido encima sigue usando esto; `Background` publica su propio contexto (pausa/reduce) por el mismo patrón |
| `$scene` (engine con IO-pause, DPR cap, reduce obligatoria, context-loss, budget, `pause()/resume()` en el handle) + pack `Ambient` (32 efectos, P-1…P-6, re-tintado por tema) + `Aura` | Los fondos ANIMADOS ya resueltos | Se montan DENTRO de `Background.Layer` desde el app; el pack puede leer el contexto de `Background` (dirección pack→framework, legal por `packs.md` §Hard boundaries 1) |
| Tokens: `--color-overlay` (por modo), `--opacity-{scrim,overlay,muted,subtle,ghost}`, `--blur-*`, `--gradient-angle-*`, `--motion-distance-*`, `--radius-*`, `--z-index-*` | Todo lo que un scrim / frost / parallax necesita, ya temable | Consumidos por el recipe; el generador emite los `--background-*` (TSC) |
| Depth channel + `data-frost` + tema Cristal | El vidrio como cue de plano | `Background.Scrim blur` usa `--blur-*`; el fondo NO es elevación (`rfc-depth` excluye parallax 3D del canal a propósito) |
| `data-on` (foundation, D12) | Inversión mínima de tinta del subárbol | Prop `on` en el host; límites documentados (anidados/portales) — hero midió los dos huecos (`Display` ignora el contexto, `Link subtle` hereda) |
| `ActiveDom`: `listen`, `raf`, `measure`, `writeProperty`, `observeIntersection`, `observeResize`, `prefersReducedMotion`, `IsInViewport`, `IsDocumentVisible`, `ScrollState`, `ElementRect`, `getWindow` | Toda la ciudadanía DOM | Sin `window.*` crudos; `CSS.supports` vía `dom.getWindow(node)` |
| `Toggle` / `IconButton` (canon) | El control de pausa | `Background.Pause` compone `Toggle` (`pressed` = pausado); los eventos sema son de Toggle |
| `Box` (position/inset/overflow/minHeight…) | La caja anfitriona | `Background` compone `<Box>` como Backdrop/Surface (patrón Section) |
| Semillas: `web/routes/demos/heroscrolling` (parallax de columnas + giro 3D), `animations/background/{liquid-image,glass-window,…}` | Referencia comparativa (decisión D5 del plan scene: intactas, no se shippean) | Se citan en el README como baseline; nada se porta verbatim |
**Huecos reales del framework** (lo que `Background` construye o destapa):
anfitrión de capas composable · scrim/overlay canónico · imagen/vídeo cover
como capa con políticas (modo, reduce-motion, reduce-data, pausa) · parallax
por scroll y por puntero · patrones adicionales sobre tokens · política
`forced-colors` para decoración · puerto `prefersReducedData` en `$adom` (no
existe; `prefers-reduced-data` no es Baseline, `navigator.connection.saveData`
sólo Chromium) · helper `ScrollProgress` en `$adom` (no existe; ScrollFrames lo
lleva privado).
---
## 4. Idoneidad y tier — el veredicto razonado
**Regla de admisión** (`packs.md` / `blocks.md`): canon = lo que tiene
superficie de contrato que otros consumen. `Background` la tiene por tres vías:
1. **`data-*` que el CSS necesita seleccionar** — `data-background`,
`data-background-layer`, `data-background-pause`: un block NO puede escribir
CSS (B-3/B-11), así que la geometría de capas sólo puede vivir en el canon.
2. **Obligaciones a11y** — capas decorativas `aria-hidden` +
`pointer-events:none`; **pausa/parada** de movimiento automático > 5 s en
paralelo con contenido (WCAG 2.2.2) para vídeo y capas animadas;
**legibilidad del contenido sobre media** (1.4.3/1.4.11 → scrim + `on`);
reduced-motion (2.3.3) para parallax; `forced-colors`. «The moment an
artifact owns an accessibility obligation, it enters the canon»
(`design-text-effects.md`).
3. **Tokens que otros recipes/temas quieren** — `--background-scrim-*`,
`--background-parallax-travel`, `--background-pattern-*` (los de Backdrop),
temables por config (precedente state-layer §38/§40).
**Forma**: componente **eidos-native pasivo** — morfo `scope: ['eidos']`, 0
eventos, sin soma (membresía §2 de `soma.md`: no hay patrón APG ni máquina de
estados accesible; el fondo pinta y reacciona a scroll/puntero como
**modulación continua**, no como acto — el mismo criterio contract-level que
ScrollFrames («observación, no acto») y la regla «no emitir por frame»). El
único acto de usuario —pausar— pertenece al `Toggle` compuesto y a SU morfo
(`commit-toggle`). El estado `paused` es visual (`data-paused`, attr de
wrapper). Sema: `SemaPanel` en vacío justificado (D-4.1).
**Morfo mínimo (regla 2026-08-15, morfo `image.ts`)**: sólo partes + ARIA;
los knobs visuales (`data-kind`, `data-fit`, `data-blend`, `data-parallax`,
`data-attach`, `data-paused`, `data-pattern`, `data-color`, `data-strength`)
son attrs eidos-only de wrapper — ningún prop cruza la frontera de soma. Las
partes son lo que el consumidor COMPONE (`morfo.md` §renderAttrs: «if a
consumer can compose it or address it, it is a PART»):
| Parte | kebab | archetype | rol/aria | Nota |
| --- | --- | --- | --- | --- |
| Provider (host) | `provider` | `provider` | — | `Box` + `position:relative; isolation:isolate; overflow:clip` |
| Layer | `layer` | — (display puro; ver nota) | `aria-hidden="true"` (literal, como `scroll-frames.media`) | UNA parte para todas las capas; el `kind` (image/video/pattern/gradient/scrim/custom) es attr eidos-only |
| Pause | `pause` | `action` | ARIA del `Toggle` compuesto | Opcional; contenedor de colocación (patrón Fab-sobre-Button) |
> Nota archetype `layer`: `image` sólo cuando el elemento ES `<img>`; una capa
> genérica no encaja en ninguno de los 26 (`overlay` = «modal/dim backdrop» y
> arrastraría CSS de veil; `content` pisa `position`) → sin archetype
> («Omit it for a plain display part that pulls no shared styling»). Se
> confirma en F0 contra `ARCHETYPE_DESCRIPTIONS`.
**Textos** (`texts:` + `langs/components/background.ts`): `pause`
(`#?components.background.pause|Pause background`) y `play`
(`#?components.background.play|Play background`) para el `Toggle`; claves
IDÉNTICAS entre `texts` y catálogo (la trampa de chronos).
**Motion (doctrina aplicada)**: la deriva ambiental de un gradiente o el
travel de parallax NO son firmas de evento ni presets de estado → van como
`@keyframes` locales anotados `/* functional: … */` (R-4.5; precedente
TextGradient) con kill bajo reduce; el parallax por scroll con
`animation-timeline: view()` es la misma clase (maquinaria continua). La
alternativa —un eje `timeline` en el registro de presets— es una EXTENSIÓN de
contrato de `$motion`/eidos y sólo entra firmada (D-BG.3).
**Packs (doctrina aplicada)**: canon nunca importa `src/packs/`;
`Background.Layer` es una ranura; el app compone `<Ambient>` dentro; un block
NO puede (B-4) — recibe la escena del app por snippet (el hero ya tiene
`backdrop`). Un `Background.Scene effect=…` en canon importaría decoración al
canon → **rechazado** (queda registrado como alternativa desechada).
**Blocks (doctrina aplicada)**: al existir `Background`, el layout
`background` del hero deja de necesitar su `<style>` justificado y su scrim
inline; `Background.Pattern` releva a `Backdrop`; los blocks pendientes de
acabado (`cta`, `stats-band`, `testimonials`, `feature-split` «banda de fondo
uix(background): el velo tenía una escala que no ordenaba, y una tinta que le pone techo Cierra el eje salvo dos firmas. La escala `strength` dejaba de crecer a mitad de camino: tomaba prestada `--opacity-*`, que nombra cuán opaco es un ELEMENTO, y leída como pesos de velo salía `ghost` 0,30 · `scrim` 0,45 · `overlay` 0,65 · `muted` 0,65 · `subtle` 0,80 — es decir, un consumidor que pedía `subtle` recibía el velo MÁS pesado del conjunto, y `overlay` y `muted` eran el mismo número con dos nombres. Ahora el velo tiene su propia escala de cinco pasos (`--background-scrim-strength-xs`…`-xl`) y la unión pasa a `xs|sm|md|lg|xl`, ordenada por construcción: medido en Chrome, α 0,084 · 0,126 · 0,189 · 0,273 · 0,420, estrictamente creciente. `md` sostiene el 0,45 que shipeó el hero, así que el default pinta exactamente lo que pintaba. Era cambio de API y se hizo mientras el único consumidor era la demo: ningún block nombra `strength`. Y midiendo eso apareció que la tabla de contraste del README era falsa por la mitad, en la dirección incómoda. Estaba calculada sobre una tinta de α 0,66; `--color-overlay` resuelve a `rgba(28, 25, 23, 0.42)`. Los valores reales, ahora compuestos en un canvas y leídos por píxel en vez de calculados: el default da 1,48:1 sobre foto blanca —no 2,10— y el paso más fuerte 2,14 —no 4,42—. Lo que eso destapa no es un número peor sino un TECHO: `xl` gasta la tinta entera y llega a 2,66:1, de modo que ningún paso nuevo puede alcanzar AA, porque el límite es el alpha del token y no la escala. Con tinta opaca los mismos pesos dan 5,45:1 a 0,65 y 9,22:1 a 0,80. Queda como decisión del autor, reescrita con estas cifras, porque es sobre qué ES un scrim y no sobre subir un valor. La rama `@supports not (animation-timeline: view())` queda ejercitada, que era el último hueco de verificación. Chromium ya no puede desactivar scroll-driven —estable, flag de runtime retirado: seis candidatos probados, los seis siguen reportando soporte—, pero el Firefox de Playwright NO lo soporta por sí mismo, así que la rama está viva ahí sin emulación ninguna. Con el fichero de receta real: `--background-progress` 0 → −64px, 0,5 → 0, 1 → +64px, con `animation-name: none` y cero animaciones. Son los mismos extremos que el camino CSS medido en Chrome, o sea que los dos caminos concuerdan; y en Chromium la rama no aplica y escribir la var no mueve nada, que es la exclusión mutua que el README afirmaba sin haberla medido. Sin ejercitar queda un eslabón —que `ScrollProgress` escriba la var extremo a extremo—: desde el shell de este entorno no hay ruta a localhost, y Firefox no alcanza el dev server. El paseo de la checklist A–H, que estaba listado como lectura de F4 y nunca registrado, encontró tres cosas. Faltaba la excepción `A2.3` con su ID (las tres partes llevan `data: []` porque sus attrs son de wrapper y su único estado vive en el Button que compone) y faltaba `## Subset` (color: el conjunto completo, porque en decoración restringir la paleta sería arbitrario; intent: no se acepta). Y una deriva documental: el README decía en TRES sitios que el control de pausa «IS the canonical Toggle» y que dispara `commit-toggle`, cuando D-BG.18 lo cambió a `IconButton` con etiqueta que cambia y sin `aria-pressed` — el evento es `contact-activate` de `button.ts`. Vestigios de la era D-BG.4 que la enmienda no barrió; corregidos, incluido el comentario del propio morfo. La banda de `feature-split` queda firmada POR SECCIÓN, enmendando el plan en sus tres menciones y `PLAN-blocks-quality` §3 columna B, que la pedía por fila: por fila obliga a cada `Row` a poseer el estado de alternancia —su índice y el de sus hermanas— y un block que coordina deja de ser un block que no posee nada. Gates: audit PASS 0/0 · morfo:check (6 de 160 fallan, background no) · eidos-lint invalid 0 y class-hooks 0 · rtl:check 0/180 · smoke del componente PASS · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno en ficheros propios. Tres fantasmas costaron tiempo y son el mismo patrón: leer las vars tras un screenshot devuelve el `write(0,0)` de un `pointerleave`; dar por muerto el `strength` leyendo `backgroundColor` de un scrim GRADUADO, que pinta por `background-image`; y juzgar `fade` y `pause` sin la precondición que necesitan. El instrumento miente antes que el código, y aquí mintió tres veces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
por sección» —enmendado 2026-08-18, el plan de calidad la pedía por fila—, `banner`, `site-footer`) componen la misma pieza. **Un block sólo
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
puede ser tan expresivo como el canon que compone** — ésta es la palanca.
---
## 5. Forma canónica propuesta (API, recipe, tokens, a11y, perf)
Todo lo de esta sección es PROPUESTA sujeta a §6. Convención: disciplined
option C (root + partes atadas), children siempre; sin flat-snippet API.
### 5.1 API
```svelte
<!-- El PADRE es cualquier componente o elemento que acepte hijos: Section, Card,
Dialog.Content, un Box-columna de Grid/Flex, un <li>… No se envuelve nada:
Background es un HIJO más, y el padre se vuelve anfitrión por la regla de
foundation (`:has`) sin tocar su recipe. -->
<Section>
<Background on="dark" parallax bind:paused>
<!-- capas: orden de fuente = apilamiento; todas absolute/inset 0 DENTRO de la pila, aria-hidden -->
<Background.Image src={hero} srcset sizes fetchpriority="high" fit="cover" position="center"
speed={-0.3} sources={{ dark: heroDark }} />
<Background.Video src="/loop.mp4" poster="/loop.jpg" sources={[{ src, type, media }]}
speed={-0.2} />
<Background.Gradient colors="aurora" | colors={['--color-primary-solid', '--color-tertiary-solid']}
angle="to-b" animate />
<Background.Pattern pattern="dots" color="primary" fade /> <!-- ex-Backdrop -->
<Background.Layer speed={0.15} blend="screen" opacity="muted" fade="to-b">
<Ambient effect="mesh" params={{…}} /> <!-- el pack, desde el APP -->
</Background.Layer>
<Background.Scrim strength="scrim" gradient="to-t" blur="md" />
<Background.Pause position="top-end" /> <!-- opcional: reubica el control -->
</Background>
<!-- el contenido, hermano de la pila, en flujo normal: pinta encima solo -->
<Container>…</Container>
</Section>
<!-- Un componente que quiera RENDERIZAR el fondo desde dentro (hero) expone un
snippet — composición, nunca un struct (D-BG.13): -->
<Hero>
{#snippet background()}<Background>…</Background>{/snippet}
{#snippet title()}…{/snippet}
</Hero>
```
| Superficie | Props (v1) | Notas |
| --- | --- | --- |
| `Background` (root = la PILA; NO compone `Box`) | `on?: 'light'\|'dark'` · `parallax?: boolean \| 'scroll' \| 'pointer' \| ('scroll'\|'pointer')[]` · `attach?: 'scroll'\|'fixed'` · `reduce?: 'static'\|'hide'` (política bajo reduced-motion, default `static`) · `paused?: boolean` (bindable) · `children` (las capas) + `HTMLAttributes<HTMLDivElement>` | La pila es `position:absolute; inset:0; z-index:-1; border-radius: inherit; overflow: clip` — bleed, recorte, radio y `attach='fixed'` (`clip-path: inset(0)` en la PILA, nunca en el padre) no tocan al anfitrión. El padre queda posicionado + aislado por la regla de foundation `:where(:has(> [data-background]))` (D-BG.14). `on` re-vincula la tinta del PADRE vía el gemelo `:where(:has(> [data-background][data-on='dark']))` del bloque D12 (mismos límites: anidados/portales). Publica contexto `getBackgroundContext()` → `{ paused, reduced, seen }`. Sin `rounded` (hereda del padre), sin props de layout (son del padre) |
| `Background.Layer` | `speed?: number` (parallax scroll; 0 = fijo; negativo = más lento/opuesto — semántica react-scroll-parallax) · `depth?: number` (parallax puntero) · `blend?: MixBlend` · `opacity?: 'full'\|'subtle'\|'muted'\|'ghost'` (tokens `--opacity-*`) · `fade?: 'radial'\|'to-t'\|'to-b'\|'edges'` (mask) · `bleed?: boolean` (default `true` cuando hay `speed`: margen negativo = `--background-parallax-travel` para que no asomen bordes; el `expanded` de react-scroll-parallax) · `pointer?: boolean` (opt-in `pointer-events:auto` para escenas interactivas, P-5) · `children` | La capa genérica; las demás son azúcar sobre ella con `data-kind` |
| `Background.Image` | `ImageProps` (compone `<Image fit="cover">`) + layer props + `sources?: { dark?: string; light?: string }` | Decorativa por definición: `alt=""` + `aria-hidden` (si la imagen significa algo, es contenido, no fondo). Modo resuelto por `eidos.getThemeContext()` (D-BG.5). Default `loading="eager"` + `fetchpriority="high"` sólo si el app lo pide: un fondo bajo el pliegue debe seguir lazy |
| `Background.Video` | `src` · `sources?` · `poster` · `loop=true` · `autoplay=true` · `preload='metadata'` · layer props | Siempre `muted playsinline` (política de autoplay); pausa fuera de vista (`observeIntersection`) y con pestaña oculta (`IsDocumentVisible`); reduced-motion → no autoplay (poster); reduced-data → sólo poster; `paused` global |
| `Background.Pattern` | `pattern: 'glow'\|'mesh'\|'grid'\|'dots'\|'lines'\|'noise'\|'rings'\|'vignette'\|'spotlight'` · `color?: ComponentColorProp` · `cell?` · `fade?` + layer props | Los 4 de Backdrop migran 1:1 (tokens `--backdrop-*` → `--background-pattern-*`); `noise` = `feTurbulence` en `url()` data-URI (sobrevive forced-colors → por eso D-BG.6 oculta las capas allí); `spotlight` sigue el puntero |
| `Background.Gradient` | `colors: string \| string[]` (nombre canónico `--gradient-{name}` \| lista de stops con tokens) · `angle?: GradientAngle` · `animate?: boolean` + layer props | Discriminación por FORMA (D-T3 de TextGradient); pinta con `background:` shorthand; `animate` = deriva `background-position` funcional, estática bajo reduce |
| `Background.Scrim` | `color?: ComponentColorProp` (default `--color-overlay`) · `strength?: 'scrim'\|'overlay'\|'muted'\|'subtle'\|'ghost'` (tokens) · `gradient?: 'to-t'\|'to-b'\|'radial'` · `blur?: BlurKey` (`--blur-*`, `backdrop-filter`) | Paridad Mantine `Overlay`; el frost es Cristal-compatible |
| `Background.Pause` | `position?: LogicalPosition` (default `top-end`) · props del `Toggle` compuesto | Compone `Toggle` (`pressed` ↔ `paused`, textos del morfo). Regla de render por defecto: D-BG.4. **Colocación**: NO puede vivir dentro de la pila (`z-index:-1` la dejaría bajo el contenido) → `Background` la renderiza como **segundo nodo raíz** (fragmento hermano de la pila, `position:absolute; z-index: 1` sobre el contenido del padre); si el app compone `<Background.Pause>` fuera de `<Background>` (junto al contenido), esa instancia se registra en el contexto y el default no se renderiza |
Contexto publicado (patrón `getMotionContext`): `{ paused, reduced, seen }` —
`seen` por `IsInViewport` para que capas caras arranquen al entrar (misma
razón que Motion→CountUp).
### 5.2 Recipe (`background.css`) — sólo selectores morfo-backed + attrs de wrapper
- **Foundation (generador, `render-css.ts`, junto a `renderOnContextBlocks`)** —
la adopción del anfitrión (D-BG.14):
`:where(:has(> [data-background])) { position: relative; isolation: isolate; }`
y el gemelo de tinta `:where(:has(> [data-background][data-on='dark']))` /
`…='light'` añadido como segundo selector del bloque D12 (una regla, dos
selectores — sin duplicar declaraciones). `:where` = especificidad 0: el
`position` propio de cualquier recipe (Dialog.Content `fixed`, Affix…) gana;
`isolation` no altera layout. Un padre `display: contents` no tiene caja → se
documenta como no-anfitrión.
- `[data-background]` (la pila): `position:absolute; inset:0; z-index:-1;
border-radius: inherit; overflow: clip; pointer-events: none;` —
`[data-attach='fixed']` → `clip-path: inset(0)` (la capa fija dentro queda
recortada por la pila; documentar que un `transform`/`filter`/`contain: paint`
en ancestros rompe `fixed`). Dentro de un padre con `[data-shape]` /
`[data-shape-nest]` el recorte sigue al `corner-shape` heredado (verificar en
F1: Card `rounded` × shape `continuous`, en Chrome real).
- `[data-background-pause]` (segundo nodo raíz): `position:absolute;
inset-block-start/inset-inline-end` según `LogicalPosition`; `z-index: 1`
(intra-componente, entero crudo permitido por el recipe-contract) — sobre el
contenido del padre, alcanzable por puntero y teclado.
- `[data-background-layer]`: `position:absolute; inset:0; z-index:-1;
pointer-events:none;` + `[data-bleed]` inset negativo por
`--background-parallax-travel`; `[data-blend=…]`; `[data-opacity=…]` →
`opacity: var(--opacity-{k})` (R-4.2 sin literales); `[data-fade=…]` → mask
(Backdrop hoy); `[data-pointer]` → `pointer-events:auto`.
- Parallax scroll: `[data-background][data-parallax~='scroll'] [data-background-layer][data-speed]`
→ `animation: background-parallax linear both; animation-timeline: view();
animation-range: entry 0% exit 100%;` con `@keyframes background-parallax
{ from { translate: 0 calc(var(--_background-speed) * var(--background-parallax-travel)) } to { translate: 0 calc(-1 * …) } }`
`/* functional: scroll-linked travel, not an event signature */`; bajo
`@supports not (animation-timeline: view())` el wrapper escribe
`--background-progress` (0..1) por `dom.writeProperty` desde `ScrollProgress`
y el recipe traduce con `calc()`. Reduce → `animation:none; translate:none`.
- Parallax puntero: host escribe `--background-pointer-x/-y` (−1..1) en
`pointermove` coalescido por `dom.raf`; capa → `translate:
calc(var(--background-pointer-x) * var(--_background-depth) * var(--motion-distance-xl))`.
Físico, no espeja en RTL (sigue al puntero). Reduce → 0. Sin `will-change`
global (memoria: jitter a DPR fraccional; sólo si se mide necesidad).
- Scrim: `[data-kind='scrim']` → `background: color-mix(in oklch,
var(--_background-scrim-color) calc(var(--_background-scrim-opacity) * 100%), transparent)`
o gradiente `to-t/to-b/radial`; `[data-blur]` → `backdrop-filter: blur(var(--blur-{k}))`.
- Patrones: migración literal de `backdrop.css` con prefijo `--background-pattern-*`.
- `@media (forced-colors: active) { [data-background-layer] { display:none } }`
(D-BG.6); `@media (prefers-contrast: more)` cae glow/mesh (hoy).
- Cero `@keyframes` no anotados, cero color crudo, cero `!important`, cero
`--eidos-*`. Recipe tokens en `lib/recipes/base.ts` (`background-*`, TSC:
`root` los estables; `host` los que leen `--palette-*`).
### 5.3 a11y — el contrato, no la esperanza
- Capas: `aria-hidden` (morfo) + `pointer-events:none` (recipe). Un fondo NO
es contenido; si el media significa, el app lo pone como `Image` con `alt`
en el flujo.
- **Pausa** (WCAG 2.2.2): cualquier capa que se mueva sola > 5 s (vídeo
autoplay, `Gradient animate`, escena del pack) exige control accesible;
`Background.Pause` compone `Toggle` (foco, `aria-pressed`, textos por langs).
Reduced-motion NO sustituye al control (es preferencia, no mecanismo).
- **Legibilidad**: `Scrim` + `on` son la pareja documentada; la demo mide
contraste con el método del hero (píxeles pintados; el probe rgb/canvas
miente con `oklch()`).
- Reduced-motion: parallax off, vídeo sin autoplay, `animate` estático;
`reduce='hide'` oculta las capas decorativas enteras.
- Reduced-data: vídeo → poster; imágenes: se respeta `srcset/sizes` del app.
- `forced-colors`: capas fuera; el contenido cae al `Canvas` del UA.
- Sin foco atrapado, sin roles falsos (lección TextFocus).
### 5.4 Rendimiento
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los ejes que F3 añade, y separarlas dejaría un estado que nunca se probó. Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la deriva contra el puntero), `spotlight` y `attach='fixed'`. El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca `ScrollProgress` y escribe `--background-progress`, que la rama `@supports not` mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll del propio lector, que es justo la clase que provoca síntomas vestibulares. **La decisión que no estaba prevista.** El scroll y el puntero quieren mover la MISMA capa, y una animación sobre `translate` gana a cualquier declaración estática: el puntero habría dejado de existir sin más. Así que el scroll anima una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único `translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`. El precio, dicho porque en la primera redacción escribí lo contrario tres veces: una custom property NO se puede compositar, así que el navegador recalcula estilo cada frame. Para una decoración es el intercambio correcto —una property por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era falso y ahora el código dice lo que ocurre. **Dos footguns cerrados por forma, no por disciplina:** - `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes había que escribirlo también en la pila, y olvidarlo dejaba la capa `position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae de vuelta). La prop de la pila desaparece: no hay nada que olvidar. - Un `speed` negativo —una capa que se mueve contra el scroll— invertía el bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora usa la magnitud. **Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones —`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio, `declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde el init, y seguir el prop sin escribir en la primera pasada. **La demo** (`/uix/components/background`, v2, nueve pestañas) monta un ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí solo y sin padre no se puede enseñar lo único que importa: que el padre se adopta y el layout no se mueve. Los chips son uniones completas verificadas por el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación. Y fue la demo la que destapó que, con A30, encender `animate` en caliente no hacía aparecer el control de pausa — el registro era un hecho de montaje. Invisible en una sonda, obvio con un interruptor. Registro documental (D-BG.11): `next-features.md` §11 · la frase en `design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) · `PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y `Aura` corregida (decía «Not built yet» y está construido) · y en `motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un preset nombra una transición discreta CON duración, y esto es modulación continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor. Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el `translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres de las cuatro clases de capa hasta la segunda auditoría). ⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60 fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con `animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome visible. Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 · `morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Sólo `translate`/`opacity` en las capas; scroll-driven por CSS donde
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
hay soporte; el fallback lee layout dentro de `dom.raf` (nunca sync tras
escritura; el detector `uix.perf` de reflow lo verifica); vídeo pausado fuera
de vista y con pestaña oculta; `preload="metadata"`; imágenes lazy por
defecto; el presupuesto de escenas lo pone `$scene`; `attach='fixed'` con
`clip-path` (no `background-attachment: fixed`, roto en iOS y caro).
---
## 6. Decisiones D-BG — para firmar UNA A UNA (nada ejecutado)
| # | Decisión | Opciones | Recomendación (arquitecto de framework de referencia) |
| --- | --- | --- | --- |
| **D-BG.1** | Nombre y destino de `Backdrop` | (a) nuevo `Background` compound que ABSORBE Backdrop → `Background.Pattern`, hero migra, `backdrop/` se BORRA (sin shim); (b) crecer `Backdrop` como compound; (c) dos componentes | **FIRMADA 2026-08-17 — (a), nombre `Background`** (no `RichBackground`: los nombres del catálogo dicen QUÉ es la pieza, nunca cómo de buena es; ergonomía de `Background.Video` / `data-background` / `--background-*`; las referencias usan el sustantivo). Motivo: «Backdrop» colisiona con el veil modal (MUI/Vuetify y el archetype `overlay` = «modal/dim backdrop»); Backdrop tiene 1 consumidor, sin README ni demo (C8 de PLAN-blocks-quality). El borrado de `backdrop/` + su morfo sigue exigiendo tu orden explícita en F1 |
| **D-BG.2** | Tier y alcance | canon eidos-native pasivo (host + capas + a11y + tokens); efectos animados siguen en el pack; `Background.Layer` = ranura; SIN `Background.Scene` en canon | **FIRMADA 2026-08-17** — canon eidos-native pasivo (`scope: ['eidos']`, partes `provider`/`layer`/`pause`, 0 eventos, sin soma ni pack sema; la pausa la emite el `Toggle` compuesto); efectos animados = pack `Ambient`; `Background.Layer` = ranura; `Background.Scene` en canon RECHAZADO. Si una capa necesitara un evento propio algún día: leer `architecture/sema.md` y replantear, no improvisar |
| **D-BG.3** | Mecanismo de parallax | (A) local al recipe: `animation-timeline: view()` + keyframes `/* functional */` + fallback JS por ActiveDom; (B) eje `timeline` en el registro de presets de `$motion`/eidos (extensión de contrato) | **FIRMADA 2026-08-17 — (A)**: CSS scroll-driven primero (`animation-timeline: view()` + `animation-range`, keyframes locales anotados `/* functional: scroll-linked travel, not an event signature */`), fallback bajo `@supports not (animation-timeline: view())` con `ScrollProgress` (D-BG.7) escribiendo `--background-progress` vía `dom.writeProperty` dentro de `dom.raf`; reduce → sin travel; token `--background-parallax-travel` (config data). Sin cambios en `$motion` ni en el registro de presets; nota en `MOTION_SERVICE_RFC` (candidato «dominio scroll», ≥2 consumidores) y en `motion-guide.md` §8 |
uix(background): el media entra en la pila, y el fondo aprende a pararse F2 de PLAN-background.md. Entran las tres piezas que le faltaban al anfitrión: `Image` (compone el `<Image>` canónico, una fuente por modo de tema, `priority` para el caso LCP), `Video` (con las cinco políticas que deciden si suena una sola trama) y `Pause`, el control que WCAG 2.2.2 le debe al lector cuando algo se mueve solo. Ninguna de las cinco políticas del vídeo es del consumidor: fuera de vista, pestaña oculta, movimiento reducido, datos reducidos y la pausa. El elemento se gobierna con `play()`/`pause()` y no con `autoplay` porque cuatro de las cinco son estado que va y vuelve, y el atributo es un disparo único al parsear. El hero se queda SIN CSS. Su layout `background` era tres capas `Box` a mano, un scrim inline y una hoja con marcador `/* justified: */` para el cover-fit del media; las tres cosas son ya canon, así que el bloque pierde su única excepción D-BLK.2. También pierde el `data-on` manual: `<Background on="dark">` lo estampa en la pila y el gemelo de foundation se lo da al anfitrión — medido, el título y la bajada resuelven `rgb(255,255,255)` sin que el bloque lo recuerde. Tres decisiones que la ejecución obligó a tomar, todas firmadas: - **D-BG.17 — el control explícito viaja por un snippet.** La pila es `z-index: -1` con `pointer-events: none` y su propio contexto de apilamiento: cualquier control escrito DENTRO pinta detrás del contenido y deja de ser un control, pero registrarse en el contexto exige ser descendiente. El snippet `pause` rompe el nudo con el único mecanismo que ya tenía precedente (`Toggle.icon`, `Image.fallback`), y suministrarlo suprime el default igual que `Switch` elige entre su snippet y su propio thumb. - **D-BG.18 — la etiqueta CAMBIA y no hay `aria-pressed`.** El APG ofrece dos formas de nombrar un control de dos estados y hacer las dos anuncia el estado por partida doble, en dos lecturas que se contradicen. Es la forma que `MediaPlayer.PlayButton` ya usa para el mismo acto. - **D-BG.15 completa.** Un fondo no reporta su fallo: se aparta y la capa de debajo ES la composición. Pero si la capa todavía tiene algo que enseñar —un snippet `error` del app, o el póster de un vídeo— la capa SE QUEDA (`data-has-fallback`). Sin eso los snippets eran inalcanzables por construcción, y un `<video>` fallido se llevaba por delante el póster que la decisión promete conservar. Y tres defectos que sólo aparecieron al medirlos en Chrome: - **`effect_update_depth_exceeded`**: una capa que se registraba desde un `$effect` escribía el contador del padre en fase de efectos y el flush no cerraba. No era ruido — mataba el efecto raíz: el botón se pintaba y todo clic posterior en la superficie se ignoraba en silencio. La cura es la ley que el framework ya tenía escrita, A30 (`anchor-nav-provider.svelte.ts`): registrar desde el init, nunca desde un efecto reactivo. Bisecado: ni `untrack` solo ni mover la lectura a otro componente lo arreglaban. - **El control caía 18px fuera del anfitrión**: el `<Button>` compuesto declara `position: relative` en `[data-button]`, misma especificidad que un `[data-background-pause]` pelado, así que decidía el orden de hojas del bundler. Fijado a dos atributos. - **Un `<img>` pelado no se estiraba** dentro de una capa. La capa es ahora una rejilla de una celda —así cualquier hijo la llena sin que la receta escriba tamaños sobre contenido ajeno— y el media se dimensiona aparte, porque `stretch` no aplica a elementos reemplazados. Medido en Chrome real: el control por defecto y el de snippet alternan etiqueta, escriben `data-paused` y congelan de verdad la deriva; sin capa que se mueva no hay control; una imagen rota oculta su capa y el patrón de debajo sigue pintando; un vídeo roto conserva su póster; `Surface`, `<Image>` compuesto, `<img>` pelado y el backdrop real del hero llenan su capa. Queda por verificar con Chrome visible: el vídeo REPRODUCIÉNDOSE y las políticas de movimiento/datos reducidos. En un panel oculto el IntersectionObserver está suspendido y las media features no se pueden emular, así que las políticas 1 y 2 se comprobaron RETENIÉNDOLO y las otras no se dan por probadas. Dos decisiones del autor quedan abiertas en el README (§Gaps): la escala `strength` del scrim no está ordenada por peso (`subtle` 0.80 vela más que `overlay` 0.65, y `overlay` ≡ `muted`), y sobre una foto clara ningún peso llega a AA — 2.10:1 el default, 4.42:1 el más fuerte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-BG.4** | Control de pausa | (a) `Background.Pause` se renderiza POR DEFECTO cuando hay capa autoplay/animada; componerlo explícito lo reubica y suprime el default (precedente: thumb por defecto de `Switch`); (b) sólo por composición (N-7 estricto) + warning dev si autoplay sin control | **FIRMADA 2026-08-17 — (a)**: default cuando hay capa que se mueve sola > 5 s (vídeo autoplay, `Gradient animate`, escena del pack que lo declare por contexto); `Toggle` compuesto (`pressed` ↔ `paused`, textos `pause`/`play` por langs, `aria-pressed`, teclado) como segundo nodo raíz sobre el contenido, `top-end` (`LogicalPosition`); una instancia compuesta explícitamente por el app se registra en el contexto y suprime el default; reduced-motion NO sustituye al control. Sin `*Button` boolean props. **Enmendada por D-BG.18** (la forma es `IconButton` + `aria-label` que cambia, sin `aria-pressed`) y **por D-BG.17** (la reubicación explícita viaja por el snippet `pause`, no por un hijo dentro de la pila) |
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **D-BG.5** | Media según modo | `sources={{ dark }}` resuelto por `eidos.getThemeContext()` (reactivo, como el re-tintado de Ambient), NO `prefers-color-scheme` (el modo del framework no es el del SO) | **FIRMADA 2026-08-17** — `sources={{ light?, dark? }}` en `Background.Image`/`Background.Video`, `src` derivado en el wrapper por `getThemeContext().mode` (reactivo, sin remount; cae a `src` si falta la clave del modo activo). Motivo: `mode` es un source visual de `ActiveEidos` (toggle del app / pref persistida / `data-mode` local) que puede divergir del SO; `<picture media="(prefers-color-scheme)">` sólo ve el SO. Alternativa CSS (dos `<img>` + `[data-mode]`) descartada: doble descarga y DOM |
| **D-BG.6** | `forced-colors` | ocultar todas las capas decorativas (contenido sobre `Canvas`) | **FIRMADA 2026-08-17** — `@media (forced-colors: active) { [data-background-layer] { display: none } }`; el `Toggle` de pausa sigue visible (control, no decoración); se mantiene el `prefers-contrast: more` heredado de Backdrop (glow/mesh fuera, patrones estructurales se quedan); nunca `forced-color-adjust: none`. Motivo: los `url()` (imagen, vídeo, ruido data-URI) sobreviven al UA y el scrim (`background-color`) lo pinta el UA como `Canvas`, así que no protege |
| **D-BG.15** | **Fallo de carga de imagen / vídeo** — planteada por el autor 2026-08-17 («¿lo resolvía Image con su fallback?») | (a) **la pila ES el fallback**: las capas de media son transparentes hasta cargar y se ocultan al fallar; lo que haya debajo (padre, `Pattern`, `Gradient`, `Scrim`) se ve; `Background.Image` compone `<Image>` y hereda su ciclo `idle/loading/loaded/error` (`data-status`, `Fallback`/`Error` como snippets passthrough) con defaults de FONDO: `placeholder='none'` (nada de skeleton a sangre), sin icono de error (decorativo → capa transparente); `Background.Video`: `poster` nativo + estados propios (`loadeddata`/`error`/`stalled`/fuente no soportada → `data-status='error'` en la capa → el vídeo se oculta y queda el poster o la capa de debajo); (b) API de fallback propia (`fallback` snippet por capa) | **FIRMADA 2026-08-17 — (a)**: un fondo no muestra errores: degrada a lo que tiene debajo, y el orden de capas ya expresa «primero lo barato, encima el media». `Background.Image` = `<Image>` con `placeholder='none'`, sin icono de error, `Fallback`/`Error` passthrough. `Background.Video` = `poster` + `data-status` propio (`loadeddata`/`error`/`stalled`/no soportado → capa oculta). Para el vídeo NO existe contrato de estado en el framework (`ImageProvider` sólo cubre `<img>`; ScrollFrames escucha `loadedmetadata` a mano): con DOS consumidores (`ScrollFrames` + `Background.Video`) es candidato a capa compartida `soma/layers` («compose existing; flag gaps») — se evalúa en F2 y, si se extrae, entra por su propia decisión; en v1 `Background.Video` lo hace localmente vía `dom.listen` como ScrollFrames. Demo: una imagen rota y un vídeo roto |
| **D-BG.7** | Infra `$adom` | (i) helper reactivo `ScrollProgress` (patrón `IsInViewport`; fallback de parallax; ScrollFrames candidato a migrar); (ii) puerto `prefersReducedData` (`prefers-reduced-data` + `navigator.connection.saveData`, `false` en SSR) | **FIRMADA 2026-08-17 — ambos**: `src/arts/adom/scroll-progress.svelte.ts` (+ test happy-dom; progreso 0→1 de un elemento por viewport o `root`, medido en `dom.raf` sobre `listen('scroll', passive)` + `observeResize`) y `ActiveDom.prefersReducedData` (`matchMedia('(prefers-reduced-data: reduce)')` OR `navigator.connection?.saveData`, `false` en SSR, sobre `targetWindow`; + test); export en el barrel `$adom`; entrada fechada en «Backlog / Evolution decisions» del README de adom (what/why/trigger). `SceneDom`/`MotionDom` no cambian; ScrollFrames no se migra ahora (F6) |
| **D-BG.8** | Integración con el pack | `Background` publica `getBackgroundContext()` `{ paused, reduced, seen }`; `<Ambient>` lo lee opcionalmente y llama `pause()/resume()` del handle (`$scene` ya lo expone) | **FIRMADA 2026-08-17** — el canon publica `{ paused, reduced, seen, registerAnimated() }` (patrón `getMotionContext`); `<Ambient>` (pack) lo lee opcionalmente: `handle.pause()/resume()` con `paused`, respeta `reduced`, se declara animado (dispara la pausa por defecto de D-BG.4); un prop `paused` explícito en `<Ambient>` puede coexistir como override. Tarea DEL PACK, separada, después de F2 (`src/packs/ambient/ambient.svelte` + README del pack); nada en `$scene`, nada en el canon más allá del contexto; encapsulación intacta |
| **D-BG.9** | `attach='fixed'` | técnica `clip-path: inset(0)` + capa `position:fixed`; incompatibilidades documentadas | **FIRMADA 2026-08-17** — en F3: `[data-background][data-attach='fixed'] { clip-path: inset(0) }` + `[data-background-layer][data-attach='fixed'] { position: fixed; inset: 0 }` (el `overflow: clip` de la pila NO recorta fijos; el `clip-path` sí). Nunca `background-attachment: fixed`. Incompatibilidades documentadas (`transform`/`filter`/`perspective`/`contain: paint`/`will-change: transform` en un ancestro → el fijo se comporta como absoluto, sin efecto pero sin fallo); `Dialog.Content` = no-anfitrión de este modo (documentado). Sin JS, sin listeners; verificar en Chrome y Safari reales |
uix(background): el velo tenía una escala que no ordenaba, y una tinta que le pone techo Cierra el eje salvo dos firmas. La escala `strength` dejaba de crecer a mitad de camino: tomaba prestada `--opacity-*`, que nombra cuán opaco es un ELEMENTO, y leída como pesos de velo salía `ghost` 0,30 · `scrim` 0,45 · `overlay` 0,65 · `muted` 0,65 · `subtle` 0,80 — es decir, un consumidor que pedía `subtle` recibía el velo MÁS pesado del conjunto, y `overlay` y `muted` eran el mismo número con dos nombres. Ahora el velo tiene su propia escala de cinco pasos (`--background-scrim-strength-xs`…`-xl`) y la unión pasa a `xs|sm|md|lg|xl`, ordenada por construcción: medido en Chrome, α 0,084 · 0,126 · 0,189 · 0,273 · 0,420, estrictamente creciente. `md` sostiene el 0,45 que shipeó el hero, así que el default pinta exactamente lo que pintaba. Era cambio de API y se hizo mientras el único consumidor era la demo: ningún block nombra `strength`. Y midiendo eso apareció que la tabla de contraste del README era falsa por la mitad, en la dirección incómoda. Estaba calculada sobre una tinta de α 0,66; `--color-overlay` resuelve a `rgba(28, 25, 23, 0.42)`. Los valores reales, ahora compuestos en un canvas y leídos por píxel en vez de calculados: el default da 1,48:1 sobre foto blanca —no 2,10— y el paso más fuerte 2,14 —no 4,42—. Lo que eso destapa no es un número peor sino un TECHO: `xl` gasta la tinta entera y llega a 2,66:1, de modo que ningún paso nuevo puede alcanzar AA, porque el límite es el alpha del token y no la escala. Con tinta opaca los mismos pesos dan 5,45:1 a 0,65 y 9,22:1 a 0,80. Queda como decisión del autor, reescrita con estas cifras, porque es sobre qué ES un scrim y no sobre subir un valor. La rama `@supports not (animation-timeline: view())` queda ejercitada, que era el último hueco de verificación. Chromium ya no puede desactivar scroll-driven —estable, flag de runtime retirado: seis candidatos probados, los seis siguen reportando soporte—, pero el Firefox de Playwright NO lo soporta por sí mismo, así que la rama está viva ahí sin emulación ninguna. Con el fichero de receta real: `--background-progress` 0 → −64px, 0,5 → 0, 1 → +64px, con `animation-name: none` y cero animaciones. Son los mismos extremos que el camino CSS medido en Chrome, o sea que los dos caminos concuerdan; y en Chromium la rama no aplica y escribir la var no mueve nada, que es la exclusión mutua que el README afirmaba sin haberla medido. Sin ejercitar queda un eslabón —que `ScrollProgress` escriba la var extremo a extremo—: desde el shell de este entorno no hay ruta a localhost, y Firefox no alcanza el dev server. El paseo de la checklist A–H, que estaba listado como lectura de F4 y nunca registrado, encontró tres cosas. Faltaba la excepción `A2.3` con su ID (las tres partes llevan `data: []` porque sus attrs son de wrapper y su único estado vive en el Button que compone) y faltaba `## Subset` (color: el conjunto completo, porque en decoración restringir la paleta sería arbitrario; intent: no se acepta). Y una deriva documental: el README decía en TRES sitios que el control de pausa «IS the canonical Toggle» y que dispara `commit-toggle`, cuando D-BG.18 lo cambió a `IconButton` con etiqueta que cambia y sin `aria-pressed` — el evento es `contact-activate` de `button.ts`. Vestigios de la era D-BG.4 que la enmienda no barrió; corregidos, incluido el comentario del propio morfo. La banda de `feature-split` queda firmada POR SECCIÓN, enmendando el plan en sus tres menciones y `PLAN-blocks-quality` §3 columna B, que la pedía por fila: por fila obliga a cada `Row` a poseer el estado de alternancia —su índice y el de sus hermanas— y un block que coordina deja de ser un block que no posee nada. Gates: audit PASS 0/0 · morfo:check (6 de 160 fallan, background no) · eidos-lint invalid 0 y class-hooks 0 · rtl:check 0/180 · smoke del componente PASS · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno en ficheros propios. Tres fantasmas costaron tiempo y son el mismo patrón: leer las vars tras un screenshot devuelve el `write(0,0)` de un `pointerleave`; dar por muerto el `strength` leyendo `backgroundColor` de un scrim GRADUADO, que pinta por `background-image`; y juzgar `fade` y `pause` sin la precondición que necesitan. El instrumento miente antes que el código, y aquí mintió tres veces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-BG.10** | Rollout en blocks | hero `background` → `Background` (borra la excepción D-BLK.2), Backdrop→Pattern en todos; luego cta/stats-band/testimonials/feature-split/banner/site-footer | **FIRMADA 2026-08-17** — F5, sólo tras F4 PASS, en este orden: (1) hero (layout `background` compone `Background`: fuera el `<style>` justificado, el scrim inline y el `data-on` manual → `on="dark"`; `decor` → `Background.Pattern`; snippet `backdrop` → `background`; su README borra la excepción D-BLK.2) — es la prueba del listón; (2) cta · stats-band · testimonials · feature-split (banda por fila) · banner · site-footer (columna B de PLAN-blocks-quality §3); (3) página compuesta. Una entrada de `AUDIT-blocks-ledger.md` por block con medición (contraste en píxeles). El «parallax suave del mockup» del hero queda FUERA (contenido, no fondo → candidato del eje motion/scroll); los huecos `Display`/`Link` bajo `data-on` siguen como candidatos de canon aparte. **Enmendada 2026-08-18**: la banda de `feature-split` va por SECCIÓN y no por fila — una banda por fila obliga a cada `Row` a poseer el estado de alternancia (su índice y el de sus hermanas), y un block que coordina deja de ser un block que no posee nada; si se quisiera alternancia, el lugar es un `:nth-child` de la receta, decisión de eidos. `PLAN-blocks-quality.md` §3 col. B enmendado en el mismo acto |
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **D-BG.11** | Registro documental | entrada nueva en `docs/next-features.md` (§11); nota aclaratoria en `design-text-effects.md` («el HOST es canon; el EFECTO es pack»); `PLAN-blocks-quality.md` Q0.3 apunta aquí | **FIRMADA 2026-08-17** — en F4, tras el PASS (nunca antes: una nota que describe futuro es la deriva de `packs.md`/`glossary.md` con Aura): (1) `next-features.md` §11 (fecha, origen, alcance, deps, enlace a este plan); (2) frase en `design-text-effects.md`: el EFECTO fue al pack, el ANFITRIÓN de capas es canon por su contrato; (3) `PLAN-blocks-quality.md` Q0.3 → sucesor; (4) `surface/README.md` §Gaps «scrim» → cerrado por `Background.Scrim`; (5) `motion-guide.md` §8 una línea + nota en `MOTION_SERVICE_RFC` (D-BG.3); (6) `glossary.md`: entrada `Background` + corregir `Aura` («Not built yet» caduco, hallazgo nº 18 del registro de lectura). Regla `authoring.md` (leerlo entero antes): enlazar, no copiar; sin conteos a mano; `docs:check` verde |
| **D-BG.12** | ~~Uso dentro de CUALQUIER componente — dos modos (envoltorio + `fill`)~~ | — | **SUPERADA el mismo día por D-BG.14** (un solo modo: hijo). Sobreviven sus tres consecuencias como alcance/gate de F1: (1) herencia de radio y **shape** dentro de superficies redondeadas/anidadas (verificar Card `rounded` × shape `continuous` en Chrome real); (2) el **bug medido** en el hero (`Box flex/grow` no crece un hijo flex, README hero 2026-07-23) lo destapa el caso columna → se verifica y, si sigue, se arregla en `Box` ANTES (canon, no en Background); (3) **coste en partes repetidas** (celdas/items/cards de una rejilla): patrones/gradientes/scrim sí; vídeo/escenas NO (una por sección; el presupuesto de `$scene` avisa) — README y demo lo dicen |
| **D-BG.13** | **Cómo llega el fondo a un componente que lo renderiza desde dentro** (hero, y cualquier otro) — planteada por el autor 2026-08-17 (`<Hero background={struct}>`) | (a) **snippet** `background` en el componente; el app compone `<Background>…</Background>` dentro y el componente lo coloca (el hero ya tiene `backdrop` → se renombra); (b) prop `background={struct}` (árbol de datos que describe capas) | **FIRMADA 2026-08-17 — (a)**: snippet `background` (hero: `backdrop` → `background` en F5); ninguna API nueva. (b) rechazada: contradice la regla compositional-not-data-driven fijada por el autor (OnionMenu 2026-06-21: «los hijos son componentes reales, nunca `root={tree}`/`items={[...]}`»), B-5 de blocks y la regla 6 de eidos; además un struct no puede nombrar un efecto del pack sin canon→pack, y cada componente tendría que poseer su mapeo struct→capas |
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA, no un envoltorio — se renderiza como HIJO de la superficie que viste y se ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de foundation, `:where(:has(> [data-background]))`, así que ninguna receta se parchea y ningún consumidor reestructura su layout. Con la pila entran sus capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines, noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el `on` que reata la tinta del contenido de arriba. Dos cosas medidas en Chrome real que el plan no preveía: - La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css` declara `position: var(--box-position, revert-layer)` en `[data-box]`, que es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer` devuelve la propiedad al valor del UA. Una Section o una columna de Grid computaban `static` y la pila se escapaba al ancestro posicionado más cercano. La regla escribe ahora también `--box-position: relative`: se resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que un `position` por prop —que llega inline— sigue ganando y `Dialog.Content` conserva su `fixed`. 5/5 anfitriones cubiertos. - El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`: `box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el shorthand borraba aquel de los tres cuya var estuviera sin poner. `<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el shorthand desaparece de la receta; medido en una fila de 600px: `flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando. Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que la capa lleve su propio `color`. El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es de F5 por D-BG.13. Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 · recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 · 504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **D-BG.16** | **Enmienda medida de la regla de anfitrión (D-BG.14)** — destapada en F1, 2026-08-17 | (a) la regla escribe también `--box-position: relative` (resuelve dentro del mecanismo de Box sin subir especificidad; un `position` por prop, inline, sigue ganando; `Dialog.Content` conserva su `fixed` — medido); (b) subir la especificidad de la regla (rompe `Dialog.Content`/`Affix`); (c) que Box deje de usar `revert-layer` para `position` (cambio de Box con radio catálogo) | **FIRMADA 2026-08-17 — (a)**: la regla queda `:where(:has(> [data-background])) { --box-position: relative; position: relative; isolation: isolate }`. Medido antes/después en Chrome: sin la var, Section y Box-columna computaban `static` y la pila se escapaba; con ella, 5/5 anfitriones cubiertos y `Dialog.Content` conserva su `fixed`. El acoplamiento foundation → token PÚBLICO de Box es el precio declarado (precedente estructural: la foundation ya escribe `--motion-stagger-index` sobre `[data-stagger] > *`); `:where` se mantiene, así que nada sube de especificidad |
uix(background): el media entra en la pila, y el fondo aprende a pararse F2 de PLAN-background.md. Entran las tres piezas que le faltaban al anfitrión: `Image` (compone el `<Image>` canónico, una fuente por modo de tema, `priority` para el caso LCP), `Video` (con las cinco políticas que deciden si suena una sola trama) y `Pause`, el control que WCAG 2.2.2 le debe al lector cuando algo se mueve solo. Ninguna de las cinco políticas del vídeo es del consumidor: fuera de vista, pestaña oculta, movimiento reducido, datos reducidos y la pausa. El elemento se gobierna con `play()`/`pause()` y no con `autoplay` porque cuatro de las cinco son estado que va y vuelve, y el atributo es un disparo único al parsear. El hero se queda SIN CSS. Su layout `background` era tres capas `Box` a mano, un scrim inline y una hoja con marcador `/* justified: */` para el cover-fit del media; las tres cosas son ya canon, así que el bloque pierde su única excepción D-BLK.2. También pierde el `data-on` manual: `<Background on="dark">` lo estampa en la pila y el gemelo de foundation se lo da al anfitrión — medido, el título y la bajada resuelven `rgb(255,255,255)` sin que el bloque lo recuerde. Tres decisiones que la ejecución obligó a tomar, todas firmadas: - **D-BG.17 — el control explícito viaja por un snippet.** La pila es `z-index: -1` con `pointer-events: none` y su propio contexto de apilamiento: cualquier control escrito DENTRO pinta detrás del contenido y deja de ser un control, pero registrarse en el contexto exige ser descendiente. El snippet `pause` rompe el nudo con el único mecanismo que ya tenía precedente (`Toggle.icon`, `Image.fallback`), y suministrarlo suprime el default igual que `Switch` elige entre su snippet y su propio thumb. - **D-BG.18 — la etiqueta CAMBIA y no hay `aria-pressed`.** El APG ofrece dos formas de nombrar un control de dos estados y hacer las dos anuncia el estado por partida doble, en dos lecturas que se contradicen. Es la forma que `MediaPlayer.PlayButton` ya usa para el mismo acto. - **D-BG.15 completa.** Un fondo no reporta su fallo: se aparta y la capa de debajo ES la composición. Pero si la capa todavía tiene algo que enseñar —un snippet `error` del app, o el póster de un vídeo— la capa SE QUEDA (`data-has-fallback`). Sin eso los snippets eran inalcanzables por construcción, y un `<video>` fallido se llevaba por delante el póster que la decisión promete conservar. Y tres defectos que sólo aparecieron al medirlos en Chrome: - **`effect_update_depth_exceeded`**: una capa que se registraba desde un `$effect` escribía el contador del padre en fase de efectos y el flush no cerraba. No era ruido — mataba el efecto raíz: el botón se pintaba y todo clic posterior en la superficie se ignoraba en silencio. La cura es la ley que el framework ya tenía escrita, A30 (`anchor-nav-provider.svelte.ts`): registrar desde el init, nunca desde un efecto reactivo. Bisecado: ni `untrack` solo ni mover la lectura a otro componente lo arreglaban. - **El control caía 18px fuera del anfitrión**: el `<Button>` compuesto declara `position: relative` en `[data-button]`, misma especificidad que un `[data-background-pause]` pelado, así que decidía el orden de hojas del bundler. Fijado a dos atributos. - **Un `<img>` pelado no se estiraba** dentro de una capa. La capa es ahora una rejilla de una celda —así cualquier hijo la llena sin que la receta escriba tamaños sobre contenido ajeno— y el media se dimensiona aparte, porque `stretch` no aplica a elementos reemplazados. Medido en Chrome real: el control por defecto y el de snippet alternan etiqueta, escriben `data-paused` y congelan de verdad la deriva; sin capa que se mueva no hay control; una imagen rota oculta su capa y el patrón de debajo sigue pintando; un vídeo roto conserva su póster; `Surface`, `<Image>` compuesto, `<img>` pelado y el backdrop real del hero llenan su capa. Queda por verificar con Chrome visible: el vídeo REPRODUCIÉNDOSE y las políticas de movimiento/datos reducidos. En un panel oculto el IntersectionObserver está suspendido y las media features no se pueden emular, así que las políticas 1 y 2 se comprobaron RETENIÉNDOLO y las otras no se dan por probadas. Dos decisiones del autor quedan abiertas en el README (§Gaps): la escala `strength` del scrim no está ordenada por peso (`subtle` 0.80 vela más que `overlay` 0.65, y `overlay` ≡ `muted`), y sobre una foto clara ningún peso llega a AA — 2.10:1 el default, 4.42:1 el más fuerte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-BG.17** | **Dónde vive el control de pausa que el app compone** — destapada en F2, 2026-08-17 | (a) hijo registrador: `<Background.Pause>` dentro de la pila no pinta ahí, registra sus props y `<Background>` lo coloca; (b) **snippet** `pause` en `<Background>`, pintado como segundo nodo raíz; (c) hermano de `<Background>` con `bind:pressed` | **FIRMADA 2026-08-17 — (b)**. La pila es `z-index: -1` + `pointer-events: none` y forma su propio contexto de apilamiento: cualquier control escrito DENTRO pinta detrás del contenido del anfitrión y deja de ser un control — pero registrarse en el contexto exige ser descendiente. El snippet rompe el nudo con el único mecanismo que ya tiene precedente canónico (`Toggle.icon`, `Image.fallback`, y el propio D-BG.13): `<Background pause={…}>` recibe `{ paused, toggle }` y `<Background>` lo pinta donde la geometría funciona. Suministrar el snippet ES la composición explícita, así que suprime el default por la misma forma que `Switch` elige entre `bodyContent` y su `<SwitchThumb>` — sin prop booleana, que D-BG.4 prohíbe. (a) rechazada: inventa un mecanismo nuevo (un hijo que se re-parentiza) para un problema que un snippet resuelve; (c) rechazada: obliga al app a cablear el booleano y a una prop booleana para suprimir |
| **D-BG.18** | **Enmienda de D-BG.4: la FORMA del control de pausa** — destapada al comparar con el estado del arte, 2026-08-17 | (a) `IconButton` con `aria-label` que cambia play↔pause y SIN `aria-pressed` (el precedente propio: `MediaPlayer.PlayButton`); (b) `Toggle` con `aria-pressed` y etiqueta fija; (c) lo que decía D-BG.4: `Toggle` + `aria-pressed` + etiquetas que cambian | **FIRMADA 2026-08-17 — (a)**. (c) mezcla los dos patrones que el APG separa: etiqueta FIJA con `aria-pressed`, **o** etiqueta que CAMBIA sin él — nunca ambos, porque el lector anuncia dos veces el estado y se contradicen. El framework ya eligió en `MediaPlayer.PlayButton` (`IconButton` ghost, `aria-label` derivado del estado, sin `aria-pressed`), y coherencia C0–C10 manda que dos controles del mismo acto se comporten igual. Es además lo que hacen las dos referencias con equipo de accesibilidad (Apple y Microsoft ponen un botón persistente de pausa/reproducir en la esquina de cada hero animado; ninguna librería de componentes trae fondo de vídeo, y las colecciones copy-paste que sí — shadcn.io, `next-video` — lo sirven `autoplay` SIN control). Consecuencias: el morfo describe la parte como botón compuesto, no como Toggle; el control es persistente (jamás sólo-hover) y va ANTES del contenido en orden de DOM, que es el orden que el APG fija para el control de rotación de un carrusel |
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA, no un envoltorio — se renderiza como HIJO de la superficie que viste y se ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de foundation, `:where(:has(> [data-background]))`, así que ninguna receta se parchea y ningún consumidor reestructura su layout. Con la pila entran sus capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines, noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el `on` que reata la tinta del contenido de arriba. Dos cosas medidas en Chrome real que el plan no preveía: - La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css` declara `position: var(--box-position, revert-layer)` en `[data-box]`, que es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer` devuelve la propiedad al valor del UA. Una Section o una columna de Grid computaban `static` y la pila se escapaba al ancestro posicionado más cercano. La regla escribe ahora también `--box-position: relative`: se resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que un `position` por prop —que llega inline— sigue ganando y `Dialog.Content` conserva su `fixed`. 5/5 anfitriones cubiertos. - El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`: `box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el shorthand borraba aquel de los tres cuya var estuviera sin poner. `<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el shorthand desaparece de la receta; medido en una fila de 600px: `flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando. Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que la capa lleve su propio `color`. El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es de F5 por D-BG.13. Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 · recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 · 504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **D-BG.14** | **Colocación: hijo, sin `Box`** — decidido en conversación 2026-08-17 (autor: «componer una capa Box trastoca todo»; «¿no puede renderizar desde el padre?») | (a) `Background` = la PILA como hijo del padre; el padre se vuelve anfitrión por la regla de foundation `:where(:has(> [data-background])) { position: relative; isolation: isolate }` (+ gemelo `data-on`); pausa como segundo nodo raíz; cero props en `Box`; (b) igual pero SIN regla `:has` (el consumidor posiciona/aísla el padre a mano, estilo Mantine `Overlay`); (c) `Background` compone `Box` (el plan original) | **FIRMADA 2026-08-17 — (a)**: la pila como hijo (`absolute; inset:0; z:-1; border-radius: inherit; overflow: clip; pointer-events: none`); adopción del anfitrión por regla de FOUNDATION en el generador (`:where(:has(> [data-background])) { position: relative; isolation: isolate }`) + gemelo `data-on` como segundo selector del bloque D12 (test en `generated-css`); pausa = segundo nodo raíz; `Background` sin `BoxProps` ni `rounded`; **cero props nuevas en `Box`**. **Enmendada por D-BG.16** (la regla escribe además `--box-position: relative`; medido). `display: contents` = no-anfitrión (documentado). Verificación F1 en Chrome real: Section · Box-columna · Card rounded×shape · Dialog.Content. (b) rechazada: footgun «las capas desaparecen»; (c) rechazada: duplica la API de Box y contradice «one axis, one primitive» |
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
---
## 7. Plan por fases (cada una con lectura, producto y gate)
| Fase | Producto | Leer antes | Gate |
| --- | --- | --- | --- |
| **F0 · Firma + infra** | D-BG.1–14 firmadas · `$adom` `ScrollProgress` (+ test happy-dom, patrón `is-in-viewport.svelte.test.ts`) y `prefersReducedData` (+ README adom Backlog) · morfo `background.ts` (3 partes, `texts`, cabecera con la justificación pasiva y la regla de attrs de wrapper) + `langs/components/background.ts` · recipe tokens `background-*` en `lib/recipes/base.ts` · **regla de adopción del anfitrión + gemelo `data-on` en el generador** (`render-css.ts`, junto a `renderOnContextBlocks`; test en `generated-css`/`active-eidos-config`) · `generate:eidos-css` | `morfo.md` §Authoring · `canon/tsc.md` · `recipe-contract.md` §1 · `arts/adom/README.md` · `render-css.ts` (`renderOnContextBlocks`, `renderSharedPaletteLayer`) | `validateMorfo` · `npm run translations:check` · `recipe-css-contract` · `generated-css` · `npm run check` · suites adom verdes |
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA, no un envoltorio — se renderiza como HIJO de la superficie que viste y se ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de foundation, `:where(:has(> [data-background]))`, así que ninguna receta se parchea y ningún consumidor reestructura su layout. Con la pila entran sus capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines, noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el `on` que reata la tinta del contenido de arriba. Dos cosas medidas en Chrome real que el plan no preveía: - La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css` declara `position: var(--box-position, revert-layer)` en `[data-box]`, que es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer` devuelve la propiedad al valor del UA. Una Section o una columna de Grid computaban `static` y la pila se escapaba al ancestro posicionado más cercano. La regla escribe ahora también `--box-position: relative`: se resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que un `position` por prop —que llega inline— sigue ganando y `Dialog.Content` conserva su `fixed`. 5/5 anfitriones cubiertos. - El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`: `box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el shorthand borraba aquel de los tres cuya var estuviera sin poner. `<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el shorthand desaparece de la receta; medido en una fila de 600px: `flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando. Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que la capa lleve su propio `color`. El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es de F5 por D-BG.13. Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 · recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 · 504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **F1 · Núcleo** | `Background` root = la pila (hijo, sin Box — D-BG.14) · `Layer` · `Pattern` (4 migrados + lines/noise/rings/vignette) · `Gradient` · `Scrim` · `on` (gemelo `:has`) · contexto · hero: `Backdrop`→`Background.Pattern` (el renombre del snippet `backdrop`→`background` NO es de esta fase: D-BG.13 lo firmó para F5) · borrado de `backdrop/` (tras tu orden) · verificación previa del `Box flex/grow` (bug hero 2026-07-23; si persiste, arreglo en `Box` primero — **persistió: arreglado en F1.5.a**) · enmienda D-BG.16 (`--box-position` en la regla de anfitrión) | `eidos/components/README.md` (7 reglas + comparativa obligatoria) · `gradient-finish.md` §10 (fronteras) · `backdrop.css` (migración literal) · `box/README.md` + `card/README.md` + `dialog/dialog.css` (qué `position` propio traen los anfitriones habituales) | `component-api-contract` · `component-visual-attrs` · `eidos-lint` invalid 0 · `rtl:check` · `blocks:check` · verificación en Chrome REAL (el pane oculto suspende rAF) de CUATRO anfitriones: `Section`, **`Box`-columna de Grid/Flex**, **Card `rounded` × shape**, **`Dialog.Content`** (portal + `position: fixed` propio) |
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **F2 · Media** | `Image` (compone `<Image>`, `sources` por modo, prioridad) · `Video` (políticas: IO-pause, doc-hidden, reduce-motion, reduce-data, `paused`) · `Pause` (Toggle; regla D-BG.4) · hero layout `background` refactorizado (fuera `<style>` justified + scrim inline + `data-on` manual) | `image/README.md` + `image/types.ts` · `toggle/README.md` · WCAG 2.2.2 · `hero/README.md` (huecos medidos) | audit `--only background` · `blocks:check` · contraste medido en píxeles (método hero) · teclado: el control de pausa alcanzable y anunciado · reduce-motion probado en Chrome |
| **F3 · Parallax** | scroll (`animation-timeline: view()` + fallback `ScrollProgress`→`--background-progress`) · `speed`/`bleed`/`--background-parallax-travel` · puntero (`depth`, `--background-pointer-*`) · `spotlight` · `attach='fixed'` · reduce=static | `motion.md` §2/§10 · `motion-guide.md` §8 · `theming/reference.md` §14 (draggable lift: `translate` ≠ `transform`) · MDN scroll-driven · memoria `will-change` jitter | `@supports` en ambos caminos (Chrome con/sin flag) · `uix.perf` reflowDetector 0 violaciones · 60 fps medido · RTL: puntero físico, scroll vertical |
| **F4 · Demo + README + docs** | `web/routes/uix/components/background/+page.svelte` (v2 9 tabs LOCKED; Sema en vacío justificado; chips = uniones completas) · README (Baseline: Backdrop + hero background + semillas · Comparativa ≥3 · Decisiones · Gaps · Passive justification · Audit exceptions) · `next-features.md` §11 · nota en `design-text-effects.md` · `PLAN-blocks-quality.md` Q0.3 · Surface README (gap scrim → cerrado) | `demo-authoring.md` (entera) · `completion-checklist.md` | `component:audit --only background` **PASS 0 errores** · `morfo:check` · `SMOKE_SCOPE=/uix/components/background npm run smoke` · `docs:check` |
uix(background): el velo tenía una escala que no ordenaba, y una tinta que le pone techo Cierra el eje salvo dos firmas. La escala `strength` dejaba de crecer a mitad de camino: tomaba prestada `--opacity-*`, que nombra cuán opaco es un ELEMENTO, y leída como pesos de velo salía `ghost` 0,30 · `scrim` 0,45 · `overlay` 0,65 · `muted` 0,65 · `subtle` 0,80 — es decir, un consumidor que pedía `subtle` recibía el velo MÁS pesado del conjunto, y `overlay` y `muted` eran el mismo número con dos nombres. Ahora el velo tiene su propia escala de cinco pasos (`--background-scrim-strength-xs`…`-xl`) y la unión pasa a `xs|sm|md|lg|xl`, ordenada por construcción: medido en Chrome, α 0,084 · 0,126 · 0,189 · 0,273 · 0,420, estrictamente creciente. `md` sostiene el 0,45 que shipeó el hero, así que el default pinta exactamente lo que pintaba. Era cambio de API y se hizo mientras el único consumidor era la demo: ningún block nombra `strength`. Y midiendo eso apareció que la tabla de contraste del README era falsa por la mitad, en la dirección incómoda. Estaba calculada sobre una tinta de α 0,66; `--color-overlay` resuelve a `rgba(28, 25, 23, 0.42)`. Los valores reales, ahora compuestos en un canvas y leídos por píxel en vez de calculados: el default da 1,48:1 sobre foto blanca —no 2,10— y el paso más fuerte 2,14 —no 4,42—. Lo que eso destapa no es un número peor sino un TECHO: `xl` gasta la tinta entera y llega a 2,66:1, de modo que ningún paso nuevo puede alcanzar AA, porque el límite es el alpha del token y no la escala. Con tinta opaca los mismos pesos dan 5,45:1 a 0,65 y 9,22:1 a 0,80. Queda como decisión del autor, reescrita con estas cifras, porque es sobre qué ES un scrim y no sobre subir un valor. La rama `@supports not (animation-timeline: view())` queda ejercitada, que era el último hueco de verificación. Chromium ya no puede desactivar scroll-driven —estable, flag de runtime retirado: seis candidatos probados, los seis siguen reportando soporte—, pero el Firefox de Playwright NO lo soporta por sí mismo, así que la rama está viva ahí sin emulación ninguna. Con el fichero de receta real: `--background-progress` 0 → −64px, 0,5 → 0, 1 → +64px, con `animation-name: none` y cero animaciones. Son los mismos extremos que el camino CSS medido en Chrome, o sea que los dos caminos concuerdan; y en Chromium la rama no aplica y escribir la var no mueve nada, que es la exclusión mutua que el README afirmaba sin haberla medido. Sin ejercitar queda un eslabón —que `ScrollProgress` escriba la var extremo a extremo—: desde el shell de este entorno no hay ruta a localhost, y Firefox no alcanza el dev server. El paseo de la checklist A–H, que estaba listado como lectura de F4 y nunca registrado, encontró tres cosas. Faltaba la excepción `A2.3` con su ID (las tres partes llevan `data: []` porque sus attrs son de wrapper y su único estado vive en el Button que compone) y faltaba `## Subset` (color: el conjunto completo, porque en decoración restringir la paleta sería arbitrario; intent: no se acepta). Y una deriva documental: el README decía en TRES sitios que el control de pausa «IS the canonical Toggle» y que dispara `commit-toggle`, cuando D-BG.18 lo cambió a `IconButton` con etiqueta que cambia y sin `aria-pressed` — el evento es `contact-activate` de `button.ts`. Vestigios de la era D-BG.4 que la enmienda no barrió; corregidos, incluido el comentario del propio morfo. La banda de `feature-split` queda firmada POR SECCIÓN, enmendando el plan en sus tres menciones y `PLAN-blocks-quality` §3 columna B, que la pedía por fila: por fila obliga a cada `Row` a poseer el estado de alternancia —su índice y el de sus hermanas— y un block que coordina deja de ser un block que no posee nada. Gates: audit PASS 0/0 · morfo:check (6 de 160 fallan, background no) · eidos-lint invalid 0 y class-hooks 0 · rtl:check 0/180 · smoke del componente PASS · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno en ficheros propios. Tres fantasmas costaron tiempo y son el mismo patrón: leer las vars tras un screenshot devuelve el `write(0,0)` de un `pointerleave`; dar por muerto el `strength` leyendo `backgroundColor` de un scrim GRADUADO, que pinta por `background-image`; y juzgar `fade` y `pause` sin la precondición que necesitan. El instrumento miente antes que el código, y aquí mintió tres veces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **F5 · Rollout tier** | cta · stats-band · testimonials · feature-split (banda por SECCIÓN — enmienda 2026-08-18, antes «por fila») · banner · site-footer con `Background.*` (columna B de PLAN-blocks-quality §3) · página compuesta | `AUDIT-blocks-ledger.md` · cada README de block | `blocks:check` · ledger actualizado · comparación al lado de la referencia (la vara de `feedback_blocks_must_surpass`) |
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **F6 · Condicionado** | `Ambient` lee el contexto (pack) · ScrollFrames migra a `ScrollProgress` · `Gradient animate` variantes · patrones open-cage por config | — | sólo con consumidor real |
Orden fijo: **F0 → F1 → F2 → F3 → F4 → F5**; F4 puede solaparse con F2/F3
(la demo se construye con el componente). Nada de F1 empieza sin D-BG.1/2
firmadas; nada de F3 sin D-BG.3; F2 depende de D-BG.4/5.
### 7.bis Hallazgos de ejecución de F0 (2026-08-17) — medidos, no supuestos
Tres cosas que el plan no podía saber hasta tocar el árbol. Se registran aquí
porque son deuda de ESTE documento, no del código.
1. **F0.4 (tokens `background-*`) es IMPOSIBLE en F0 y se ejecuta en F1.** No
es un recorte: los dos guards que el propio F0 declara como gate lo
prohíben. Medido declarando un bloque `background: { 'parallax-travel': … }`
en `lib/recipes/base.ts` y corriendo `recipe-css-contract`:
- `does not declare recipes for missing component CSS files` →
`expected [ 'background' ] to deeply equal []` (el guard exige
`components/background/background.css`, producto de F1);
- `does not leave declared public recipe variables orphaned` → el token no
lo consume nadie hasta que exista la receta.
Y un tercero espera detrás: `loads every component CSS recipe exactly once`
exige que ese `.css` lo auto-importe `background.svelte` — el wrapper,
también de F1. **Los tokens viajan con la receta que los consume**; la
sonda se revirtió sin dejar rastro (`git diff` vacío en `base.ts`).
2. **La parte `pause` es el BOTÓN, no un contenedor de colocación.** La tabla
de §4 dice «contenedor de colocación (patrón Fab-sobre-Button)», pero el
patrón que cita hace lo contrario: `morfo/components/fab.ts:31-33` declara
*«Structurally a `<Button>`: the same `<button>` carries `data-button` +
`data-fab`»*. Y el archetype importa: `[data-archetype='action']` arrastra
`cursor: pointer` + `user-select: none` (`archetypes.css:169-172`), el
anillo de foco y un suelo de 44px bajo `pointer: coarse`
(`archetypes.css:293-301`) — sobre un `div` de colocación eso es la clase
de defecto que `reference_archetype_item_pulls_interactive_styling` fichó.
El morfo declara `defaultElement: 'button'` + `archetype: 'action'`: UN
nodo, el `Toggle` compuesto llevando ambos markers. La colocación absoluta
vivirá en el recipe sobre ese mismo nodo, como en `Fab`.
3. **«test happy-dom» (F0 / D-BG.7) no existe como proyecto.**
`vite.config.ts:73-107` define dos: `client` (Playwright chromium,
`*.svelte.{test,spec}.ts`) y `server` (node). Los helpers con runas de
`$adom` se prueban TODOS en `client` (`is-in-viewport.svelte.test.ts` y sus
once hermanos). Los dos tests nuevos siguen esa convención.
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA, no un envoltorio — se renderiza como HIJO de la superficie que viste y se ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de foundation, `:where(:has(> [data-background]))`, así que ninguna receta se parchea y ningún consumidor reestructura su layout. Con la pila entran sus capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines, noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el `on` que reata la tinta del contenido de arriba. Dos cosas medidas en Chrome real que el plan no preveía: - La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css` declara `position: var(--box-position, revert-layer)` en `[data-box]`, que es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer` devuelve la propiedad al valor del UA. Una Section o una columna de Grid computaban `static` y la pila se escapaba al ancestro posicionado más cercano. La regla escribe ahora también `--box-position: relative`: se resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que un `position` por prop —que llega inline— sigue ganando y `Dialog.Content` conserva su `fixed`. 5/5 anfitriones cubiertos. - El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`: `box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el shorthand borraba aquel de los tres cuya var estuviera sin poner. `<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el shorthand desaparece de la receta; medido en una fila de 600px: `flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando. Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que la capa lleve su propio `color`. El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es de F5 por D-BG.13. Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 · recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 · 504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
### 7.ter Supervisión de F1 (Fable, 2026-08-17) — veredicto y correcciones
**Estado de F1 al evaluarla**: los cinco wrappers, el recipe, los tokens, el
README y la migración del hero existen y pasan los gates de contrato
(`recipe-css-contract` · `component-api-contract` · `component-visual-attrs` ·
`eidos-lint` invalid 0 · `rtl:check` · `blocks:check` · `docs:check` · `check`
0 errores propios) y la verificación en Chrome de CINCO anfitriones (Section ·
Box-columna · Card `rounded`×shape · panel `fixed` · `Dialog.Content` real).
Sin commitear. Cinco desviaciones/hallazgos, ninguna oculta — todas están en la
entrega del constructor o las destapó el supervisor al re-ejecutar:
1. **La regla firmada en D-BG.14 no bastaba y el constructor la amplió** con
`--box-position: relative` (medido: los anfitriones basados en Box
computaban `static` porque `box.css` declara `position: var(--box-position,
revert-layer)` a (0,1,0), y sin `@layer` `revert-layer` cae al UA — la
trampa que `affix/types.ts:19` ya documentaba). La ampliación es correcta y
está medida, pero es una ENMIENDA del texto firmado y acopla la foundation
a un token público de Box → **D-BG.16**, a firmar.
2. **El bug de `Box flex` existe, está diagnosticado y NO arreglado.** F1
firmaba «si persiste, arreglo en Box PRIMERO»; el constructor lo reprodujo
(`<Box flex={2}>` → `0 1 auto`, 71px de 992) y diagnosticó el mecanismo
(`box.css:268-271`: el shorthand `flex:` va ANTES de los longhands, que
revierten al UA cuando su var está sin poner y borran su aportación;
inyectando `--box-grow: 2` en el mismo nodo → `2 1 auto`, 823px), pero se
detuvo por §10.3 («cero cambios en recipes ajenos») y lo escaló. Conflicto
entre dos textos del plan; el firmado (D-BG.12→F1) manda: **se arregla en
F1.5**, con guard.
3. **`Scrim` sin `color` hereda la paleta del anfitrión** (por semántica CSS,
no medido aún): `--palette-*` NO están registradas `inherits:false` (0
`@property --palette-` en `generated/base.css`) y la capa compartida las
pone en `[data-color='x']` — un `<Card color="teal">` que hospede un
`<Background.Scrim>` sin `color` le pasa `--palette-solid` por herencia y
la tinta del velo deja de ser `--color-overlay`. Es exactamente el «nesting
gap» que THM-2 cierra con una guarda de PRESENCIA (`[data-{c}][data-color],
[data-{c}][data-color-custom]`, ver el forward de `card` en
`generated/base.css`). `Pattern` no lo sufre porque SIEMPRE estampa
`data-color` (default `primary`), pero merece la misma guarda.
4. **El audit clasifica `background` como INTERACTIVE** (la parte `pause`
declara `role: 'button'` + `defaultElement: 'button'`; heurística de
`component-audit.ts:1471-1477`) → pide `apg` (A-1.4) y un tratamiento de
foco (R-1.5). El precedente exacto es `fab` (mismo caso, PASS): `apg` del
patrón button + `## Audit exceptions` con `R-1.5 exception:` (el foco es del
`Toggle` compuesto) + `## Sema events` («0 eventos propios; interactivo POR
COMPOSICIÓN»). Falta añadirlos.
5. **Docs desincronizadas por la migración**: el README del hero sigue diciendo
`Backdrop` en el mapa de composición (línea 18) y en la fila `backdrop`
(27); y la fila F1 de §7 pedía renombrar el snippet `backdrop`→`background`
mientras D-BG.13 lo firmó para F5 — el constructor siguió el texto FIRMADO
(bien), pero la fila de §7 queda corregida aquí: **el renombrado es de F5**.
Fuera de desviación, anotado: `[data-background-layer] > :where(img, video)`
(cover-fit del media del app) entró en F1 sin estar en su fila — es el receptor
del `<style>` justificado del hero y F2 lo consume; se acepta y se registra.
uix(background): el media entra en la pila, y el fondo aprende a pararse F2 de PLAN-background.md. Entran las tres piezas que le faltaban al anfitrión: `Image` (compone el `<Image>` canónico, una fuente por modo de tema, `priority` para el caso LCP), `Video` (con las cinco políticas que deciden si suena una sola trama) y `Pause`, el control que WCAG 2.2.2 le debe al lector cuando algo se mueve solo. Ninguna de las cinco políticas del vídeo es del consumidor: fuera de vista, pestaña oculta, movimiento reducido, datos reducidos y la pausa. El elemento se gobierna con `play()`/`pause()` y no con `autoplay` porque cuatro de las cinco son estado que va y vuelve, y el atributo es un disparo único al parsear. El hero se queda SIN CSS. Su layout `background` era tres capas `Box` a mano, un scrim inline y una hoja con marcador `/* justified: */` para el cover-fit del media; las tres cosas son ya canon, así que el bloque pierde su única excepción D-BLK.2. También pierde el `data-on` manual: `<Background on="dark">` lo estampa en la pila y el gemelo de foundation se lo da al anfitrión — medido, el título y la bajada resuelven `rgb(255,255,255)` sin que el bloque lo recuerde. Tres decisiones que la ejecución obligó a tomar, todas firmadas: - **D-BG.17 — el control explícito viaja por un snippet.** La pila es `z-index: -1` con `pointer-events: none` y su propio contexto de apilamiento: cualquier control escrito DENTRO pinta detrás del contenido y deja de ser un control, pero registrarse en el contexto exige ser descendiente. El snippet `pause` rompe el nudo con el único mecanismo que ya tenía precedente (`Toggle.icon`, `Image.fallback`), y suministrarlo suprime el default igual que `Switch` elige entre su snippet y su propio thumb. - **D-BG.18 — la etiqueta CAMBIA y no hay `aria-pressed`.** El APG ofrece dos formas de nombrar un control de dos estados y hacer las dos anuncia el estado por partida doble, en dos lecturas que se contradicen. Es la forma que `MediaPlayer.PlayButton` ya usa para el mismo acto. - **D-BG.15 completa.** Un fondo no reporta su fallo: se aparta y la capa de debajo ES la composición. Pero si la capa todavía tiene algo que enseñar —un snippet `error` del app, o el póster de un vídeo— la capa SE QUEDA (`data-has-fallback`). Sin eso los snippets eran inalcanzables por construcción, y un `<video>` fallido se llevaba por delante el póster que la decisión promete conservar. Y tres defectos que sólo aparecieron al medirlos en Chrome: - **`effect_update_depth_exceeded`**: una capa que se registraba desde un `$effect` escribía el contador del padre en fase de efectos y el flush no cerraba. No era ruido — mataba el efecto raíz: el botón se pintaba y todo clic posterior en la superficie se ignoraba en silencio. La cura es la ley que el framework ya tenía escrita, A30 (`anchor-nav-provider.svelte.ts`): registrar desde el init, nunca desde un efecto reactivo. Bisecado: ni `untrack` solo ni mover la lectura a otro componente lo arreglaban. - **El control caía 18px fuera del anfitrión**: el `<Button>` compuesto declara `position: relative` en `[data-button]`, misma especificidad que un `[data-background-pause]` pelado, así que decidía el orden de hojas del bundler. Fijado a dos atributos. - **Un `<img>` pelado no se estiraba** dentro de una capa. La capa es ahora una rejilla de una celda —así cualquier hijo la llena sin que la receta escriba tamaños sobre contenido ajeno— y el media se dimensiona aparte, porque `stretch` no aplica a elementos reemplazados. Medido en Chrome real: el control por defecto y el de snippet alternan etiqueta, escriben `data-paused` y congelan de verdad la deriva; sin capa que se mueva no hay control; una imagen rota oculta su capa y el patrón de debajo sigue pintando; un vídeo roto conserva su póster; `Surface`, `<Image>` compuesto, `<img>` pelado y el backdrop real del hero llenan su capa. Queda por verificar con Chrome visible: el vídeo REPRODUCIÉNDOSE y las políticas de movimiento/datos reducidos. En un panel oculto el IntersectionObserver está suspendido y las media features no se pueden emular, así que las políticas 1 y 2 se comprobaron RETENIÉNDOLO y las otras no se dan por probadas. Dos decisiones del autor quedan abiertas en el README (§Gaps): la escala `strength` del scrim no está ordenada por peso (`subtle` 0.80 vela más que `overlay` 0.65, y `overlay` ≡ `muted`), y sobre una foto clara ningún peso llega a AA — 2.10:1 el default, 4.42:1 el más fuerte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.quater F2 ejecutada (2026-08-17) — medido en Chrome real
Entregado: `Background.Image` (compone `<Image>`, `sources` por modo, `priority`,
sin `alt` porque la capa es `aria-hidden`), `Background.Video` (las cinco
políticas), `Background.Pause` (D-BG.18), el snippet `pause` (D-BG.17), la
receta y sus dos tokens, y el layout `background` del hero refactorizado — el
bloque **se queda sin `<style>`**: su única excepción D-BLK.2 (el cover-fit) es
ya canon, y el `data-on` manual sobra porque el gemelo de foundation se lo da al
anfitrión (medido: título y bajada resuelven `rgb(255,255,255)` sin él).
**Tres defectos destapados por la verificación, los tres arreglados:**
1. **`effect_update_depth_exceeded`** — una capa que se registraba desde un
`$effect` escribía el contador del padre en fase de efectos, invalidaba al
hermano que lo lee y el flush no cerraba. No sólo hacía ruido: **mataba el
efecto raíz**, así que el botón se pintaba y todo clic posterior en la
superficie se ignoraba en silencio. Arreglado aplicando la ley que el propio
framework ya tenía escrita, **A30** (`anchor-nav-provider.svelte.ts:204`:
«register with the parent from the constructor, not a reactive effect»), más
`untrack` en la mutación y la decisión leída desde un componente aparte.
2. **El control de pausa computaba `position: relative`** y caía 18px fuera del
anfitrión: el `<Button>` compuesto declara `position: relative` en
`[data-button]`, misma (0,1,0), así que decidía el orden de hojas del
bundler. Clavado con `[data-background-pause][data-placement]` — (0,2,0), sin
`!important` ni envoltorio. Medido después: `absolute`, 12px/12px, dentro.
3. **Mi `size='sm'` inventado** en el control: retirado, se queda el del canon
(`md`, 36×36), que es el par que compone `MediaPlayer.PlayButton`.
**Comprobado en navegador**: control por defecto y de snippet alternan etiqueta
ES pausa↔reanudar, escriben `data-paused` y **congelan de verdad** la deriva
(`animation-play-state: paused`) · sin capa que se mueva no hay control · una
imagen rota oculta su capa (`data-status='error'`) y el patrón de debajo sigue
pintando · la buena carga con `object-fit: cover` a 192px · el control es
`<button type="button">`, enfocable, fuera de todo `aria-hidden` · el anfitrión
del hero es `relative` y la tinta sale a `rgb(255,255,255)`.
**DOS decisiones tuyas, medidas, sin tocar** (README §Gaps):
- **La escala de `strength` del scrim no está ordenada por peso**: `ghost` 0.30 ·
`scrim` 0.45 · `overlay` 0.65 · `muted` **0.65 (duplicado)** · `subtle`
**0.80 — el más pesado de todos, y su nombre dice lo contrario**. La causa es
que esos tokens nombran cuán opaco es un ELEMENTO, no cuánto vela un scrim.
Renombrar o renumerar es cambio de API, no arreglo de pasada.
- **Sobre una foto CLARA ningún peso llega a AA**: 2.10:1 el default, 4.42:1 el
más fuerte, con texto blanco. O entra un peso por encima de `--opacity-subtle`,
o se doctrina que la respuesta es el scrim graduado.
Gates: audit `--only background` **PASS 0/0** · eidos-lint invalid 0 ·
`blocks:check` 0/15 · `rtl:check` 0/180 · 441/442 en eidos+morfo (el fallo es el
`skin-media-player` de siempre) · `check` 0 errores propios · prettier limpio.
Sin commitear: falta tu orden.
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los ejes que F3 añade, y separarlas dejaría un estado que nunca se probó. Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la deriva contra el puntero), `spotlight` y `attach='fixed'`. El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca `ScrollProgress` y escribe `--background-progress`, que la rama `@supports not` mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll del propio lector, que es justo la clase que provoca síntomas vestibulares. **La decisión que no estaba prevista.** El scroll y el puntero quieren mover la MISMA capa, y una animación sobre `translate` gana a cualquier declaración estática: el puntero habría dejado de existir sin más. Así que el scroll anima una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único `translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`. El precio, dicho porque en la primera redacción escribí lo contrario tres veces: una custom property NO se puede compositar, así que el navegador recalcula estilo cada frame. Para una decoración es el intercambio correcto —una property por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era falso y ahora el código dice lo que ocurre. **Dos footguns cerrados por forma, no por disciplina:** - `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes había que escribirlo también en la pila, y olvidarlo dejaba la capa `position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae de vuelta). La prop de la pila desaparece: no hay nada que olvidar. - Un `speed` negativo —una capa que se mueve contra el scroll— invertía el bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora usa la magnitud. **Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones —`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio, `declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde el init, y seguir el prop sin escribir en la primera pasada. **La demo** (`/uix/components/background`, v2, nueve pestañas) monta un ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí solo y sin padre no se puede enseñar lo único que importa: que el padre se adopta y el layout no se mueve. Los chips son uniones completas verificadas por el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación. Y fue la demo la que destapó que, con A30, encender `animate` en caliente no hacía aparecer el control de pausa — el registro era un hecho de montaje. Invisible en una sonda, obvio con un interruptor. Registro documental (D-BG.11): `next-features.md` §11 · la frase en `design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) · `PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y `Aura` corregida (decía «Not built yet» y está construido) · y en `motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un preset nombra una transición discreta CON duración, y esto es modulación continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor. Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el `translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres de las cuatro clases de capa hasta la segunda auditoría). ⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60 fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con `animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome visible. Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 · `morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.quinquies F4 ejecutada (2026-08-18)
**La demo** (`web/routes/uix/components/background/+page.svelte`) sigue el v2 de
`demo-authoring.md`: las 9 pestañas, el harness compartido sin inventar paneles,
y la entrada en el nav bajo **Layout**, junto a `Section` — su anfitrión
canónico (ni `Surface` ni `Backdrop` tenían demo, así que no había precedente).
Dos cosas que este componente obliga a hacer distinto, y ambas son el punto:
- **El escenario muestra un ANFITRIÓN de verdad** (una `Section` con copia
encima), no el componente aislado. Background es invisible por sí solo: sin
anfitrión la vista previa sería un rectángulo vacío y lo que hay que enseñar
—que el padre se ADOPTA y el layout no se mueve— sería intestable.
- **Los chips son uniones completas por el TIPO, no a mano.** Cada fila se
deriva de un `Record<Union, 0>`: un miembro que falte es error de compilación
y uno inventado también, así que una fila de chips no puede derivar en
silencio del tipo que enseña.
**Un defecto que sólo la demo podía destapar, arreglado.** Con A30 el registro
de «esta capa se mueve» era un hecho de MONTAJE, así que cambiar `animate` en
caliente no hacía aparecer el control de pausa — invisible en una sonda, obvio
en cuanto hay un interruptor. `background-gradient.svelte` registra ahora al
init Y sigue el prop después, con un efecto que **no escribe nada en su primera
pasada**: el flush de montaje sigue limpio y un cambio posterior es una
escritura suelta en un grafo ya asentado. Medido en Chrome: el control aparece
al encender `animate`, sin `effect_update_depth_exceeded`.
**Las entradas documentales de D-BG.11**: (1) `next-features.md` §11 · (2) la
frase en `decisions/design-text-effects.md` (el mismo corte canon/pack leído
desde el otro lado) · (3) `PLAN-blocks-quality.md` Q0.3 → sucesor · (4)
`surface/README.md` §Gaps «scrim de autoría» → **CERRADO** por
`Background.Scrim` · (6) `glossary.md`: entrada `Background` **y** `Aura`
corregida — decía «Not built yet» y está construido, con sus familias
`delegate`/`sustain`.
**(5) queda DIFERIDA a F3 a propósito**: la nota de `motion-guide.md` §8 y la de
`MOTION_SERVICE_RFC` describen el parallax (D-BG.3), que no existe todavía. La
propia D-BG.11 lo prohíbe — «una nota que describe futuro es la deriva de
`packs.md`/`glossary.md` con Aura», que es exactamente el error que esta fase
acaba de corregir en el glosario. Se escriben cuando el parallax aterrice.
Gates: `component:audit --only background` **PASS 0 errores** (2 warnings
`D-1.5`/`D-4.3` idénticos a los del canario `button` — el harness v2 encapsula
el `MutationObserver` y el emit, así que el grep literal del audit no los ve) ·
`morfo:check` **PASS background** · `SMOKE_SCOPE=/uix/components/background`
**PASS** · `docs:check` **0 errores, 0 warnings** en 633 docs.
### 7.sexies F3 ejecutada (2026-08-18) — parallax, puntero, `attach`
Entregado sobre D-BG.3 (A) y D-BG.9, sin tocar `$motion` ni el registro de
presets: `speed` · `bleed` · `depth` · `spotlight` · `attach='fixed'`, el
fallback `--background-progress`, y los tokens `--background-parallax-travel` /
`--background-spotlight-*`. La demo expone los cuatro ejes.
**La decisión de diseño que no estaba prevista.** Dos ejes quieren mover la
MISMA capa —el scroll y el puntero— y una animación sobre `translate` gana a
cualquier declaración estática: el puntero habría dejado de existir sin más. Así
que el scroll anima una custom property REGISTRADA (`@property`, o interpolaría
a saltos) y un único `translate` compone los dos términos. Medido: parallax solo
→ `0px 30px`; con el puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es
`translate`, nunca el shorthand `transform`, que es la misma ley que sigue el
lift del draggable con `scale`.
**Tres cosas que costaron medición:**
1. El shorthand `animation` pone `animation-duration: 0s`, y una línea de tiempo
de progreso necesita el `auto` inicial para mapear su intervalo sobre el
rango de scroll. Con `0s` la animación es instantánea y la capa no se mueve
nunca. Van longhands, y el porqué queda escrito en la receta.
2. Mi listener de puntero pedía un frame y, si el rect salía degenerado, **no lo
liberaba nunca**: un anfitrión sin caja en el primer movimiento (transición,
cambio de display) mataba el puntero para siempre. Reescrito sin frame: el
rect se CACHEA (invalidado por `pointerenter` y `observeResize`), la lectura
sale del camino caliente y el fallo desaparece por forma.
3. Las cuatro registraciones (`animated`/`pointer`/`scroll`/`fixed`) comparten
un solo sitio (`declare.svelte.ts`) con la regla A30 y su segunda mitad
—seguir el prop sin escribir en la primera pasada—, en vez de una copia del
baile por fichero.
**Verificado en Chrome real**: el puntero mueve `depth` y `spotlight` con los
valores exactos y ambos vuelven al centro al salir · `attach='fixed'` da
`position: fixed` en la capa y `clip-path: inset(0)` en la pila, y una pila sin
`attach` conserva su `overflow: clip` sin tocar · **RTL**: el `translate` del
puntero se mantiene FÍSICO (no espeja, que es lo correcto para un dispositivo
físico) y el bleed vive en el eje de bloque · la demo estampa `data-parallax`,
`--_background-speed`, `animation-timeline: view()` y el bleed al cambiar los
chips, con paridad en el snippet.
⚠️ **Lo que NO pude verificar, y por qué**: el panel del navegador va oculto y
con viewport 0×0, así que **las animaciones scroll-driven declaradas en CSS no
se activan** ahí — comprobado que es del ENTORNO y no del código con un caso
mínimo inyectado (un `div` pelado con `animation-timeline: view()` también sale
inactivo, mientras una `ViewTimeline` creada por API sobre el mismo sujeto marca
68%). Quedan por medir con Chrome visible: el travel real al hacer scroll, los
60 fps, el detector de reflow, y la rama `@supports not` (Chrome la soporta, así
que no se puede ejercitar aquí). Lo verificable —la composición de los dos ejes,
el puntero, `attach`, RTL— está medido arriba.
Gates: audit **PASS 0 errores** · eidos-lint invalid 0 · `rtl:check` 0/180 ·
`docs:check` 0/0 en 634 docs · `blocks:check` 0/18 · `morfo:check` PASS ·
`smoke` PASS · 441/442 (el fallo es el `skin-media-player` de siempre) ·
`check` 0 errores propios · prettier limpio.
### 7.septies F3.5 — correcciones de la segunda auditoría (2026-08-18)
Nueve hallazgos, dos de diseño. Ninguno cambia el mecanismo firmado; cambian
quién declara qué, y que lo escrito sea verdad.
**Diseño:**
1. **`attach='fixed'` era un footgun**: había que escribirlo en la capa Y en la
pila, y olvidar el segundo dejaba la capa `position: fixed` **pintando a
sangre por todo el viewport**, detrás de todo, sin error. Ahora la capa lo
DECLARA (`registerFixed`) y la pila se recorta sola — la prop de la pila
desaparece, así que no hay nada que olvidar. La forma CSS de D-BG.9 no
cambia; cambia quién pone el atributo. Medido: al poner `fixed` la pila
estampa `data-attach` y `clip-path: inset(0)`, y al volver a `scroll` los
retira.
2. **Un `speed` negativo invertía el bleed.** El bleed por defecto ES el travel,
y con travel negativo el `inset-block` salía positivo: la capa ENCOGÍA y
enseñaba justo los bordes que el bleed existe para tapar. Ahora usa la
magnitud (`max(t, -t)`). Medido: `-64px` con speed 1 y con speed −1.
**Verdad de lo escrito** (lo más importante de esta pasada): afirmé tres veces
—README, wrapper y plan— que el travel «va en el compositor / nada en el hilo
principal». **Es falso**: animar una custom property REGISTRADA no se puede
compositar, el navegador recalcula estilo cada frame. Es el precio de componer
los dos ejes en un `translate`, y para una decoración es el intercambio correcto
— pero había que decirlo, no lo contrario. Corregidas las tres frases y el
comentario del `will-change`, que se apoyaba en la premisa falsa. También decía
que el puntero iba «coalescido en un frame» cuando yo mismo había quitado ese
frame, y que las registraciones compartían un sitio cuando gradient y video
seguían con su copia — ahora **sí** lo comparten las cuatro.
**Menores:** el fallback JS se gatea por reduced-motion (el CSS ya se paraba
solo, el JS seguía midiendo) · el spotlight gana `--background-spotlight-color`
con guarda de paleta, como el scrim · documentados los cuatro límites (`speed`
muere bajo `attach='fixed'`; el `clip-path` cuadra las esquinas de una capa fija
en un anfitrión redondeado; `Dialog.Content` no es anfitrión de ese modo; el
fallback mide contra la ventana y `view()` contra el scroller más cercano) y las
vars de runtime `--background-pointer-*` / `--background-progress`.
**Demo:** `spotlight` y `depth` llegan ya a las cuatro clases de capa (antes el
interruptor **no hacía nada** con gradiente, imagen y vídeo), entra el control
`bleed`, las tablas API y Recipe dejan de mentir —fila de ejes compartidos y los
selectores de parallax/attach— y el snippet `pause` pierde el `onclick` que el
componente pisaba y un `aria-pressed` inerte.
Gates: audit **PASS 0** · eidos-lint invalid 0 · rtl 0/180 · docs 0/0 en 634 ·
blocks 0/18 · smoke PASS · 441/442 · `check` 0 propios · prettier limpio.
blocks(background): seis fondos, y el que parecía mejor era el que no se podía leer F5 de PLAN-background.md: los seis blocks de D-BG.10 (2) reciben `Background.*`. El hero cayó en F2 y la página compuesta ya existía — no había que construirla, había que verificarla con la decoración puesta. La forma es uniforme y tiene un solo desvío: cada block gana un prop `decor` que monta `<Background.Pattern>` como HIJA de su `Section`, la misma composición que el hero ya probó. El desvío es `cta`, donde la decoración va dentro del PANEL, que es donde está el ojo. **Dos hallazgos que sólo la medición podía dar.** Dentro del panel sólido del `cta`, las dos tramas TINTADAS hunden el titular bajo AA: `glow` lleva la copia blanca de 5,18:1 a **3,06:1** y `mesh` a **2,27:1**. Pintan un lavado claro anclado en `50% 0%` — exactamente donde se asienta el titular. Las de línea toman la tinta de regla, oscurecen, y la copia sube a **14,35:1** mientras la textura sigue leyéndose a 3,19:1 contra el panel. Por eso el default es `rings`, que además irradia del mismo anclaje superior que usaba el glow. Las tintadas se ofrecen igual: el app decide, pero ya con el número delante, en el tipo y en el README. Y una trama tintada dentro del panel era invisible por construcción: `--_background-tint` cae en `--color-primary-solid`, que es EL MISMO valor que pinta el panel — un halo del color del panel sobre el color del panel. El block pasa ahora `color="var(--color-content-on-solid)"`, el token que ya usa para su propia copia; medido después, el tinte resuelve a `#ffffff`. **Los defaults, y por qué dos están apagados.** `cta` `rings` · `stats-band` `grid`, porque una banda de cifras se lee como medida y la rejilla es la trama que lo dice sin decorar por decorar · `testimonials` y `feature-split` `glow`. `site-footer` y `banner` reciben la capacidad APAGADA: la columna B del plan de calidad no pide acabado en ninguno de los dos, y encenderlo por nuestra cuenta sería inventar un aspecto que nadie pidió. **Un desvío de alcance, declarado.** La columna B pedía «`Backdrop` alterno» POR FILA en `feature-split`; se resuelve en la SECCIÓN. Una banda por fila obligaría a cada `Row` a poseer el estado de alternancia, y eso es coordinación — un block que coordina deja de ser un block que no posee nada. Medido en Chrome sobre la página compuesta real: cinco pilas, los cinco anfitriones adoptados (`position: relative` + `isolation: isolate`) sin que ningún block los posicione; la copia mide 15,88:1 sobre las secciones decoradas y 5,18:1 sobre el panel del cta — AA en todas. Sobre lienzo oscuro, medido aparte: 17,06 → 13,23:1 con `glow` y 12,37:1 con `grid`/`dots`. Ledger: seis filas nuevas, A-100…A-105, cada una con su mecanismo y su evidencia. Gates: `blocks:check` 0/18 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · 434/435 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.octies F5 ejecutada (2026-08-18) — rollout al tier
Los seis blocks de D-BG.10 (2) reciben `Background.*`. El (1) —el hero— aterrizó
en F2, y el (3) —la página compuesta— **ya existía** (`/blocks/landing`): no
había que construirla, había que verificarla con la decoración puesta, y eso es
lo que se hizo.
**La forma, uniforme y con un solo desvío**: cada block gana un prop `decor` que
monta `<Background.Pattern>` como HIJA de su `Section` — la misma que el hero ya
probó. El desvío es `cta`, donde la decoración va dentro del PANEL (`Surface`),
que es donde está el ojo.
**Dos hallazgos que sólo la medición podía dar, y que habría shippeado a ciegas:**
1. **Dentro del panel sólido del `cta`, las dos tramas TINTADAS hunden el titular
bajo AA**: `glow` lleva la copia blanca de 5,18:1 a **3,06:1** y `mesh` a
**2,27:1**, porque pintan un lavado claro anclado en `50% 0%` — exactamente
donde se asienta el titular. Las de línea toman la tinta de regla, oscurecen,
y la copia sube a **14,35:1** mientras la textura sigue leyéndose a 3,19:1
contra el panel. El default es `rings`, que irradia del mismo anclaje que
usaba el glow. Las tintadas se ofrecen igual, con el número en el tipo.
2. **Una trama tintada dentro del panel es invisible por construcción**:
`--_background-tint` cae en `--color-primary-solid`, que es EL MISMO valor que
pinta el panel — un halo del color del panel sobre el color del panel. El
block pasa `color="var(--color-content-on-solid)"`, el token que ya usa para
su copia; medido después, el tinte resuelve a `#ffffff`.
**Los defaults, y por qué dos están apagados.** `cta` `rings` · `stats-band`
`grid` (una banda de cifras se lee como medida) · `testimonials` y
`feature-split` `glow`. `site-footer` y `banner` reciben la capacidad **APAGADA**:
la columna B del plan de calidad no pide acabado en ninguno de los dos, y
encenderlo por nuestra cuenta sería inventar un aspecto que nadie pidió.
**Un desvío de alcance, declarado**: la columna B pedía «`Backdrop` alterno» POR
FILA en `feature-split`; se resuelve en la SECCIÓN. Una banda por fila obligaría
a cada `Row` a poseer el estado de alternancia, que es coordinación — y un block
que coordina deja de ser un block que no posee nada.
**Medido en Chrome sobre la página compuesta real** (`/blocks/landing`): cinco
pilas, los cinco anfitriones adoptados (`position: relative` + `isolation:
isolate`) sin que ningún block los posicione; la copia en modo claro mide
15,88:1 sobre las secciones decoradas y 5,18:1 sobre el panel del cta — AA en
todas. Sobre lienzo oscuro, medido aparte: 17,06 → 13,23:1 con `glow` y
12,37:1 con `grid`/`dots`.
Ledger: seis filas nuevas, **A-100…A-105**, con su mecanismo y su evidencia.
Gates: `blocks:check` 0/18 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 ·
434/435 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores
propios · prettier limpio.
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA, no un envoltorio — se renderiza como HIJO de la superficie que viste y se ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de foundation, `:where(:has(> [data-background]))`, así que ninguna receta se parchea y ningún consumidor reestructura su layout. Con la pila entran sus capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines, noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el `on` que reata la tinta del contenido de arriba. Dos cosas medidas en Chrome real que el plan no preveía: - La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css` declara `position: var(--box-position, revert-layer)` en `[data-box]`, que es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer` devuelve la propiedad al valor del UA. Una Section o una columna de Grid computaban `static` y la pila se escapaba al ancestro posicionado más cercano. La regla escribe ahora también `--box-position: relative`: se resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que un `position` por prop —que llega inline— sigue ganando y `Dialog.Content` conserva su `fixed`. 5/5 anfitriones cubiertos. - El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`: `box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el shorthand borraba aquel de los tres cuya var estuviera sin poner. `<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el shorthand desaparece de la receta; medido en una fila de 600px: `flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando. Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que la capa lleve su propio `color`. El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es de F5 por D-BG.13. Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 · recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 · 504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
### F1.5 — correcciones de supervisión (para el agente, ANTES del commit de F1)
| # | Qué | Cómo (exacto) | Gate |
| --- | --- | --- | --- |
| F1.5.a | **Box `flex`** (canon, `eidos/components/box`) | El wrapper expande el shorthand: `flex` prop → `--box-grow` / `--box-shrink` / `--box-basis` (número `n` → `n 1 0%`; `'none'` → `0 0 auto`; `'auto'` → `1 1 auto`; string de 1–3 tokens → asignación CSS estándar); `box.css` deja de declarar el shorthand `flex:` y conserva los tres longhands. Un test browser en `box` que monte `<Box display="flex">` con hijos `flex={1}`/`flex={2}` y afirme `computedStyle.flexGrow` y anchos (discriminante: hoy falla). README de Box: nota fechada. Verificar que Surface/Section/Container (componen Box) no cambian de aspecto: `check` + suite eidos + un vistazo a `/uix/components/{box,flex,surface}` | suite eidos verde · test nuevo rojo→verde · `check` 0 errores propios |
| F1.5.b | **Guarda de presencia de paleta** en `background.css` | `--_background-tint` y `--_background-scrim-ink` se resuelven desde `--palette-solid` SÓLO bajo `[data-background-layer][data-color], [data-background-layer][data-color-custom]`; el default (`--color-primary-solid` / `--background-scrim-color`) en la regla base. Medir en Chrome: `<Card color="teal">` + `<Background.Scrim/>` → `--_background-scrim-ink` == `--color-overlay` (hoy: teal) | `eidos-lint` invalid 0 · medición en navegador anotada |
| F1.5.c | **Audit** | morfo `background.ts`: `apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/button/'` (el `Toggle` compuesto es un botón; precedente `fab.ts:25`); README: `## Audit exceptions` (`R-1.5 exception:` foco del `Toggle` compuesto; `E-2.2` NO aplica: la receta se auto-importa) + `## Sema events` (0 propios; interactivo por composición) | `component-audit --only background` → 0 errores (queda el `Demo —` hasta F4) |
| F1.5.d | **README del hero** | Fila `decoration` → `Background.Pattern` (hijo de `Section`, no envoltorio; patrones `glow·mesh·grid·dots·lines·noise·rings·vignette·none`); fila `backdrop` intacta (F2 la refactoriza); nota fechada en Decisiones | `blocks:check` verde |
| F1.5.e | **Plan** | Fila F1 de §7: quitar «snippet backdrop→background» (es F5, D-BG.13); D-BG.14: añadir la enmienda `--box-position` si el autor firma D-BG.16 | `docs:check` |
Orden: a → b → c → d → e; después los gates de F1 completos otra vez y el
commit de F1 (sólo lo propio) por orden del autor.
**Ejecutada 2026-08-17 — a·b·c·d·e hechas, medidas en Chrome real** (sonda
`/uix/__probe-f15`, creada y borrada; el layout de `/uix` es el que carga el CSS
de componentes, el raíz no):
- **a — `Box flex`, ARREGLADO.** Antes: `<Box flex={2}>` computaba `0 1 auto` y
71 px de 992. Ahora, en una fila de 600 px: `flex={1}` → `1 1 0%` **146 px** ·
`flex={2}` → `2 1 0%` **292 px** (exactamente el doble) · `Surface flex={1}`
(compone Box) crece igual, 146 px · `flex="none"` → `0 0 auto` ·
`flex="0 0 120px"` → `0 0 120px`, 120 px · `grow={3}` junto a `flex={1}` →
`3 1 0%`, o sea el prop explícito sigue ganando. Suma: 146+146+292+2 gaps = 600.
- **b — guarda de paleta, CONFIRMADA.** Dentro de `<Card color="teal">`
(`--palette-solid` = teal): el scrim resuelve `rgb(0 0 0 / 0.66)` =
`--color-overlay` y el patrón `oklch(0.5556 0.1829 305.86)` =
`--color-primary-solid` — ninguno hereda el teal del anfitrión. Con
`<Background.Scrim color="teal">` sí resuelve teal: la adhesión sigue siendo
explícita. De paso, la adopción del anfitrión (D-BG.16) se re-midió en los dos
Cards: `position: relative` + `isolation: isolate`.
- **No es defecto**: el scrim pinta alfa efectiva **0.297** (`--color-overlay`
0.66 × `--opacity-scrim` 0.45). Es EXACTAMENTE lo que pintaba el scrim inline
del hero que sustituye, así que la migración es fiel; queda dicho en el README
que los pesos son relativos a una tinta ya translúcida.
- **c** — `component-audit --only background` → **PASS, 0 errores, 0 warnings**.
- **d** — README del hero: fila `decoration` reescrita (hijo de la Section, ocho
tramas) + nota fechada; el hallazgo `Box flex` de 2026-07-23 queda marcado
«Resuelto 2026-08-17» con la causa.
- **e** — fila F1 de §7 corregida (el renombre del snippet es F5) y D-BG.14
enlazada con su enmienda D-BG.16.
uix(background): la línea de tiempo la capturaba un overflow, y el puntero se iba con el scroll El travel llevaba dos días escrito y sin medir, porque el panel oculto nunca activa una animación scroll-driven declarada en CSS. Con el Chrome del autor delante se pudo medir, y lo que apareció no fue una confirmación: fueron dos defectos que ninguna sonda había estado en posición de ver. La puerta es `document.visibilityState` — con la pestaña oculta la `ViewTimeline` existe, con `source` y `subject` correctos y `playState: 'running'`, y `currentTime` es `null` para siempre; ahí ni un `await requestAnimationFrame` resuelve. El primero estaba en la demo y el límite es del componente. `[data-uix-stage]` declaraba `overflow: hidden`, y eso es un scroll container aunque no pueda scrollear nunca: `view()` se ancló al stage y el progreso quedó clavado en 52,63% en toda posición de scroll, sin error, sin aviso y con un `translate` de aspecto perfectamente razonable. Con `overflow: clip` —recorta igual, respeta el radio, y no es scroll container— el travel aparece: progreso 43,5% → 95,5%, `translate` `0px -8,34px` → `0px 58,19px`, monótono, y los `4rem` completos de `--background-parallax-travel` en el extremo. Entra en el README como quinto límite porque el caso real no es un harness de demos: es el `overflow-x: hidden` con el que cualquier landing contiene su decoración, que convierte al elemento en scroll container en los DOS ejes. El segundo era del componente. El rect del anfitrión se cacheaba en coordenadas de viewport y sólo lo invalidaban `pointerenter` y un resize; el scroll mueve el anfitrión sin disparar ninguno de los dos. Medido con ratón real: un tick de rueda sobre un anfitrión de 288px dejaba `--background-pointer-y` 0,77 fuera de sitio —los 111px de scroll sobre media altura, a la centésima—, o sea fuera del rango −1…1 que la variable promete, con el spotlight despegado del cursor unos 115px y sin recuperarse hasta salir y volver a entrar. Cacheada la caja en coordenadas de DOCUMENTO y normalizando contra `pageX`/`pageY`, que en un evento real ya traen el scroll incorporado: error 0 en los dos movimientos, halo a 1px del píxel pedido, sin lectura de layout en el camino caliente y sin el listener de scroll que este componente existe para no tener. Queda dicho el residuo: un scroller anidado vuelve a desviar, porque `pageY` no lo ve, y se autocura al reentrar. Y lo que era el encargo, medido: 60 fps con mediana de 16,7ms, p95 17,0, máximo 17,1 y 0 de 200 frames por encima de 20ms con el travel vivo —la base con `speed: 0` salió peor, que es ruido de entorno—; y 0ms de reflow forzado en esos mismos 200 frames moviendo los DOS ejes, con el observador de Long Animation Frames que usa `arts/perf`. El cero está validado por mutación: un thrash deliberado de 65ms en la misma página se reporta con 43ms forzados y el script atribuido, porque un instrumento que no ve nada y un cero real se leen igual. Sigue pendiente la rama `@supports not`, que sólo se ejercita en Firefox. Las 174 demos comparten ese stage, así que el cambio se auditó: `affix` idéntico y `sticky` idéntico —cuatro pasadas alternando `clip` y `hidden`, porque la primera miente—, y `anchor-nav` cambia a mejor: su rail `position: sticky` es hermano del scroller interno, con `hidden` no se pegaba nunca y se escapaba por arriba perdiendo media lista, y ahora se mantiene a la vista. Ninguna demo tocada. Dos avisos para quien vuelva. Un `PointerEvent` construido reporta `pageY === clientY`, sin sumar el scroll, así que los eventos sintéticos sirven para DESCUBRIR este defecto pero no para verificarlo: con ellos el arreglo bueno mide −1,389 donde el ratón real da −0,008. Y leer las variables después de un screenshot puede devolver el `write(0, 0)` de un `pointerleave` en vez de la medición; el valor que vale es el del último `pointermove`, trazado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.nonies La verificación con Chrome VISIBLE (2026-08-18) — y los dos defectos que destapó
Lo que F3 dejó sin medir porque el panel oculto no activa una animación
scroll-driven declarada en CSS. Hecho con el Chrome real del autor, pestaña al
frente. **`document.visibilityState` es la puerta**: con la pestaña oculta la
`ViewTimeline` existe, con `source` y `subject` correctos y `playState:
'running'`, y `currentTime` es `null` para siempre; en cuanto la pestaña se ve,
marca. Un `await requestAnimationFrame` en esa pestaña ni resuelve (CDP timeout
a 45s), así que ninguna medición por frames es posible ahí.
**Medido (los tres pendientes):**
- **El travel es real.** Anfitrión de 288px, `speed: 1`, scroll de página:
progreso 43,5% → 95,5%, `translate` `0px -8,34px` → `0px 58,19px`, monótono y
lineal, y `0px 64px` = `4rem` completos en el extremo. Los keyframes van
−travel → +travel, así que el punto medio es 0 (`-0,03px` a 49,98%) y la
amplitud pico a pico es el doble del token. Las cifras de §7.sexies (`0px 30px`,
`20px 10px`) NO eran falsas: probaban la COMPOSICIÓN, y la aritmética de ahora
las ratifica (52,6% → −64 + 128×0,526 = 3,37px, medido 3,37px).
- **60 fps sin un frame caído.** 200 frames de scroll continuo con el travel
vivo: mediana 16,7ms · p95 17,0 · **max 17,1** · 0 frames > 20ms. La base con
`speed: 0` salió PEOR (max 63,6ms, 6 > 20ms): ruido de entorno, no del
componente.
- **0 violaciones de reflow.** Mismos 200 frames moviendo LOS DOS ejes (scroll +
puntero con `depth`): el observador de Long Animation Frames que usa
`arts/perf` no reportó ni un frame largo, 0ms forzados. **Instrumento validado
por mutación** — un thrash deliberado de 65ms en la misma página sí se reporta
(43ms forzados, script atribuido); sin esa prueba el cero sería el de un guard
ciego.
- **`@supports not` sigue pendiente**: Chrome soporta scroll-driven, así que la
rama del fallback sólo se ejercita en Firefox. No verificable en esta sesión.
**Defecto 1 — un ancestro `overflow: hidden` mata el travel en silencio.**
`view()` se ancla al scroll container más cercano, y `hidden` lo es aunque no
pueda scrollear nunca. La demo de este componente lo sufría: `[data-uix-stage]`
(`web/routes/uix/uix.css`) declaraba `overflow: hidden`, la timeline se ancló al
stage y el progreso quedó clavado en 52,63% en TODA posición de scroll — sin
error, con un `translate` de aspecto plausible. Arreglado con `overflow: clip`,
que recorta igual, respeta el radio y no es scroll container: `timeline.source`
pasa a ser el `scrollingElement` y el travel aparece, con las cifras de arriba.
El límite es del COMPONENTE, no de la demo (un envoltorio con `overflow-x:
hidden` para contener decoración es el patrón más común de una landing, y hace
scroll container en ambos ejes), así que entra en el README como **quinto
límite**. Efecto colateral verificado en las 174 demos que comparten el stage:
`affix` idéntico (`position: fixed`, A/B `identical: true`), `sticky` idéntico
(4 pasadas alternando `clip`/`hidden`: `top 13`, `stuck ""` las cuatro — el
sujeto tiene su scroller interno ANTES del stage, así que el stage no
participaba); `anchor-nav` sí cambia y **a mejor**: su rail `position: sticky` es
HERMANO del scroller interno, con `hidden` no se pegaba nunca y se escapaba por
arriba perdiendo media lista, con `clip` se mantiene a la vista (offset 47px) y
se ve el item activo. Ninguna demo se toca.
**Defecto 2 — el eje del puntero se desviaba exactamente lo scrolleado.** El
rect del anfitrión se cacheaba en coordenadas de VIEWPORT y sólo se invalidaba
con `pointerenter` y `observeResize`; el scroll mueve el anfitrión sin disparar
ninguno de los dos. Medido con ratón real: un tick de rueda sobre un anfitrión
de 288px dejó `--background-pointer-y` en 0,062 donde la geometría pedía 0,831 —
error 0,769, que es 111px de scroll sobre media altura (0,771) a la centésima —
sacando el valor del rango −1…1 que las vars prometen, y despegando el
`spotlight` del cursor unos 115px a la vista. No se recuperaba hasta salir y
volver a entrar. Arreglado cacheando la caja en coordenadas de DOCUMENTO y
normalizando contra `pageX`/`pageY`, que en un evento REAL ya traen el scroll:
cero lecturas de layout en el camino caliente y cero listeners nuevos, que es la
propiedad que este componente defiende. Verificado con ratón real: error **0** en
los dos movimientos (−0,008 y 0,833 contra su geometría), secuencia
`pointerenter > pointermove > pointermove` sin `leave` de por medio, y el centro
del halo del spotlight a **1px** del píxel pedido. Residuo documentado: un
scroller ANIDADO vuelve a desviar (`pageY` no lo ve) y se autocura al reentrar.
⚠️ **Un `PointerEvent` sintético no puede verificar esto**: un evento construido
reporta `pageY === clientY`, sin sumar el scroll, así que el handler nuevo mide
mal por culpa del instrumento y parece roto (medido: −1,389 donde el ratón real
da −0,008). Descubrí el defecto con eventos sintéticos, pero sólo el ratón real
lo verifica. Segundo fantasma de la sesión: el primero fue leer `0,0` / `50%`
tras un `computer{screenshot}`, que es el `write(0,0)` del `pointerleave`, no una
medición.
---
uix(background): el velo tenía una escala que no ordenaba, y una tinta que le pone techo Cierra el eje salvo dos firmas. La escala `strength` dejaba de crecer a mitad de camino: tomaba prestada `--opacity-*`, que nombra cuán opaco es un ELEMENTO, y leída como pesos de velo salía `ghost` 0,30 · `scrim` 0,45 · `overlay` 0,65 · `muted` 0,65 · `subtle` 0,80 — es decir, un consumidor que pedía `subtle` recibía el velo MÁS pesado del conjunto, y `overlay` y `muted` eran el mismo número con dos nombres. Ahora el velo tiene su propia escala de cinco pasos (`--background-scrim-strength-xs`…`-xl`) y la unión pasa a `xs|sm|md|lg|xl`, ordenada por construcción: medido en Chrome, α 0,084 · 0,126 · 0,189 · 0,273 · 0,420, estrictamente creciente. `md` sostiene el 0,45 que shipeó el hero, así que el default pinta exactamente lo que pintaba. Era cambio de API y se hizo mientras el único consumidor era la demo: ningún block nombra `strength`. Y midiendo eso apareció que la tabla de contraste del README era falsa por la mitad, en la dirección incómoda. Estaba calculada sobre una tinta de α 0,66; `--color-overlay` resuelve a `rgba(28, 25, 23, 0.42)`. Los valores reales, ahora compuestos en un canvas y leídos por píxel en vez de calculados: el default da 1,48:1 sobre foto blanca —no 2,10— y el paso más fuerte 2,14 —no 4,42—. Lo que eso destapa no es un número peor sino un TECHO: `xl` gasta la tinta entera y llega a 2,66:1, de modo que ningún paso nuevo puede alcanzar AA, porque el límite es el alpha del token y no la escala. Con tinta opaca los mismos pesos dan 5,45:1 a 0,65 y 9,22:1 a 0,80. Queda como decisión del autor, reescrita con estas cifras, porque es sobre qué ES un scrim y no sobre subir un valor. La rama `@supports not (animation-timeline: view())` queda ejercitada, que era el último hueco de verificación. Chromium ya no puede desactivar scroll-driven —estable, flag de runtime retirado: seis candidatos probados, los seis siguen reportando soporte—, pero el Firefox de Playwright NO lo soporta por sí mismo, así que la rama está viva ahí sin emulación ninguna. Con el fichero de receta real: `--background-progress` 0 → −64px, 0,5 → 0, 1 → +64px, con `animation-name: none` y cero animaciones. Son los mismos extremos que el camino CSS medido en Chrome, o sea que los dos caminos concuerdan; y en Chromium la rama no aplica y escribir la var no mueve nada, que es la exclusión mutua que el README afirmaba sin haberla medido. Sin ejercitar queda un eslabón —que `ScrollProgress` escriba la var extremo a extremo—: desde el shell de este entorno no hay ruta a localhost, y Firefox no alcanza el dev server. El paseo de la checklist A–H, que estaba listado como lectura de F4 y nunca registrado, encontró tres cosas. Faltaba la excepción `A2.3` con su ID (las tres partes llevan `data: []` porque sus attrs son de wrapper y su único estado vive en el Button que compone) y faltaba `## Subset` (color: el conjunto completo, porque en decoración restringir la paleta sería arbitrario; intent: no se acepta). Y una deriva documental: el README decía en TRES sitios que el control de pausa «IS the canonical Toggle» y que dispara `commit-toggle`, cuando D-BG.18 lo cambió a `IconButton` con etiqueta que cambia y sin `aria-pressed` — el evento es `contact-activate` de `button.ts`. Vestigios de la era D-BG.4 que la enmienda no barrió; corregidos, incluido el comentario del propio morfo. La banda de `feature-split` queda firmada POR SECCIÓN, enmendando el plan en sus tres menciones y `PLAN-blocks-quality` §3 columna B, que la pedía por fila: por fila obliga a cada `Row` a poseer el estado de alternancia —su índice y el de sus hermanas— y un block que coordina deja de ser un block que no posee nada. Gates: audit PASS 0/0 · morfo:check (6 de 160 fallan, background no) · eidos-lint invalid 0 y class-hooks 0 · rtl:check 0/180 · smoke del componente PASS · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno en ficheros propios. Tres fantasmas costaron tiempo y son el mismo patrón: leer las vars tras un screenshot devuelve el `write(0,0)` de un `pointerleave`; dar por muerto el `strength` leyendo `backgroundColor` de un scrim GRADUADO, que pinta por `background-image`; y juzgar `fade` y `pause` sin la precondición que necesitan. El instrumento miente antes que el código, y aquí mintió tres veces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.decies El cierre: escala del scrim, la rama del fallback y el paseo A–H (2026-08-18)
**La escala `strength` deja de tomar prestada `--opacity-*`.** Esos tokens nombran
cuán opaco es un ELEMENTO y, leídos como pesos de velo, no ordenaban: `subtle`
(0,80) velaba MÁS que `overlay` (0,65), que empataba con `muted`. Ahora cinco
pasos propios (`--background-scrim-strength-xs`…`-xl`) y la unión pasa a
`xs|sm|md|lg|xl`, con `md` sosteniendo el 0,45 que shipeó el hero. Medido en
Chrome tras el cambio: α 0,084 · 0,126 · 0,189 · 0,273 · 0,420, **estrictamente
creciente**, y el default pinta 0,189 — idéntico a antes, así que la paridad con
el hero se conserva. Era cambio de API y se hizo mientras el único consumidor era
la demo: ningún block nombra `strength`.
⚠️ **La tabla de contraste del README estaba mal, y el problema era PEOR de lo
escrito.** Decía α 0,297 / 0,429 / 0,528 y 2,10:1 para el default, calculados
sobre una tinta de α 0,66. `--color-overlay` resuelve hoy a
`rgba(28, 25, 23, 0.42)`, así que los valores reales son 0,189 / 0,273 / 0,336 y
**1,48:1** el default sobre foto blanca. Re-medido componiendo el velo en un
canvas y leyendo el píxel, no calculándolo. La consecuencia es un TECHO: `xl`
gasta la tinta entera y llega a **2,66:1**; ningún paso adicional puede pasar de
ahí porque el límite es el alpha del token. Con tinta opaca, los mismos pesos dan
5,45:1 (0,65) y 9,22:1 (0,80). **Sigue siendo decisión del autor** y es sobre qué
ES un scrim, no sobre un número: el gap queda reescrito con estas cifras.
**La rama `@supports not`, ejercitada de verdad.** Chromium ya no puede
desactivar scroll-driven (estable, flag de runtime retirado — probados seis
candidatos, todos siguen reportando soporte), así que se ejerció en el **Firefox
de Playwright, que reporta `CSS.supports('animation-timeline: view()') === false`
por sí mismo**: la rama está VIVA ahí, sin emulación. Con el fichero de receta
real cargado, `--background-progress` 0 → **−64px**, 0,25 → −32px, 0,5 → **0px**,
0,75 → +32px, 1 → **+64px**, con `animation-name: none` y cero animaciones. Son
exactamente los extremos del camino CSS medido en Chrome (−64…+64, punto medio 0):
**los dos caminos concuerdan**. Y en Chromium la rama no aplica y escribir la var
no mueve nada (18,65px constante), que es la **exclusión mutua** que el README
afirmaba sin haberla medido.
Lo que NO se pudo ejercitar y por qué: que `ScrollProgress` escriba la var
extremo a extremo en un motor sin soporte. Desde el shell de este entorno no hay
ruta a `localhost:5173` (`curl` devuelve 000 con y sin sandbox; el Chromium de
Playwright llega, Firefox no), así que la app no se puede cargar en Firefox aquí.
El gate JS queda cubierto por lectura (`background.svelte`: `CSS.supports` +
`prefersReducedMotion` + `scrollTravellers > 0`) y por los tres tests de
`ScrollProgress` (0, 1, rango medio); el eslabón que los une, sin ejercitar.
**Paseo de la checklist A–H** (estaba listada como lectura de F4 y nunca
registrada como paseo). Mecánicos: audit **PASS 0/0** · `morfo:check` (6 de 160
fallan, background no está) · `eidos-lint background` **invalid 0, class-hooks 0**
· `rtl:check` 0/180 · smoke del componente PASS · `check` sin errores en ficheros
propios · vitest eidos 434/435 (el único rojo es `skin-media-player`, el de
siempre). Manuales, con lo que encontraron:
- **A-2.2** — `role` sólo lo declara `pause`. No es omisión: es el patrón de facto
(surface, section, box, image, separator declaran CERO), y la única parte con
semántica aquí es el botón, que sí lo lleva.
- **A-2.3** — las tres partes llevan `data: []`. Añadida al README la excepción con
su ID (`A2.3 exception:`, el formato que ya usa `chart`): los attrs que estampa
son de WRAPPER, y el único estado de contrato —la pausa— vive en el `Button`.
- **G-1.1** — faltaba `## Subset`. Añadida: `color` es el conjunto COMPLETO (en
decoración, restringir la paleta sería arbitrario) e `intent` no se acepta.
- **Deriva documental cazada**: el README decía en TRES sitios que el control de
pausa «IS the canonical `<Toggle>`» y que dispara `commit-toggle`. Compone
`IconButton` (D-BG.18) y el evento es `contact-activate` de `button.ts`. Eran
vestigios de la era D-BG.4 que la enmienda D-BG.18 no barrió. Corregidos, más el
comentario del propio morfo.
- **D-7.1** — 11 controles, todos producen cambio salvo `intent`, cuyo único efecto
es SUPRIMIR `color` (el `<PalettePicker>` es un control compartido de dos ejes y
este componente no toma intent). Justificado, no defecto.
⚠️ **Tres fantasmas en una sesión, todos del mismo patrón**: leer `0,0`/`50%` tras
un screenshot (era el `write(0,0)` de un `pointerleave`), dar por muerto el
`strength` leyendo `backgroundColor` de un scrim GRADUADO (pinta por
`background-image`), y creer que `fade`/`pause` no hacían nada por juzgarlos sin
la precondición que necesitan (scrim encendido, capa que se mueva). **El
instrumento miente antes que el código**, y aquí mintió tres veces.
uix(background): un fondo nunca suena, y no estaba resuelto sino evitado D-BG.19, planteada por el autor: qué pasa con un `Background.Video` cuyo clip trae pista de audio, y cómo se relaciona con el interruptor de sonido del framework o con un «no sound» de la app. La respuesta que había en el código era que no había respuesta. `muted` era una prop con default `true` y la razón escrita al lado no era doctrinal sino mecánica —un clip sin mutear tiene el autoplay rechazado en todos los navegadores—, de modo que el silencio era un efecto secundario de la política del navegador y no una decisión de nadie. Lo que eso significaba en cuanto alguien escribiera `muted={false}`: el clip sonaba FUERA del grafo de audio. `sound.buses.content.setMuted(true)` no lo silenciaba; no participaba del audio focus, así que hablaba por encima de un `MediaPlayer` en vez de cederle el paso; no atenuaba el bus `ui` mientras sonaba; no se proyectaba a MediaSession; y el slot `sound` de `$prefs` —el interruptor «sin sonido» de la app, el que se proyecta al DOM como `data-sound`— no lo alcanzaba nunca. Un `AudioContext` por documento es la razón de ser de ese art, y el componente no tenía ni una referencia a él. De WCAG 1.4.2 se salvaba de rebote: el control de pausa que el stack ya debía lo para, por carambola. Así que `muted` deja de ser prop y se escribe `true` incondicionalmente: la API ya no puede expresar un fondo audible. Tres razones, de más a menos vinculante. La doctrina propia del componente —toda capa es `aria-hidden` y la decoración no es contenido, así que un clip de aquí no puede portar significado que el audio entregue; una capa audible sería contenido vestido de fondo—. WCAG 1.4.2: una decoración no puede pedir un consentimiento que nadie le dio. Y la fuga de `arts/sound`, que es la parte que hace de esto un asunto de framework y no de gusto. Un clip que DEBE oírse es contenido: `MediaPlayer`, que ya es ciudadano de `sound.media()` y obtiene focus, ducking y MediaSession gratis, o montarlo por `Background.Layer` y registrarlo uno mismo. La alternativa —hacerlo ciudadano con `sound.media(el, { focus: 'duck' })` cuando el consumidor desmutea— queda rechazada por lo mismo que la hacía atractiva: abre la puerta a que un fondo compita con el contenido real, y ese territorio es de `Ambient` o de un reproductor. Verificado en Chrome con la capa en `video`: `el.muted === true`, `paused: false`, `currentTime` avanzando, `readyState: 4` —sigue reproduciéndose— y el control de pausa presente. Queda anotado en el código un detalle que invita a un arreglo equivocado: `hasAttribute('muted')` lee false mientras la propiedad es true, porque Svelte fija `muted` como PROPIEDAD sin reflejarlo al atributo. No es un agujero: la propiedad se escribe en el mismo efecto y dos líneas antes del `play()`, y no hay nada que pueda colarse entre las dos. Ningún consumidor pasaba `muted` —el `HeroSite` ya lo decía en prosa, «DECORATIVE — muted»—, así que el cambio de API no rompe a nadie. Gates: audit PASS 0/0 · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno propio · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.undecies D-BG.19 — un fondo NUNCA suena (2026-08-18)
**Planteada por el autor**: qué pasa con un `Background.Video` cuyo clip trae
pista de audio, y cómo se relaciona con el interruptor de sonido del framework o
con un «no sound» de la app. La respuesta, medida en el código, era que **no
estaba resuelto: estaba evitado**. `muted` era una prop con default `true`, y la
razón escrita no era doctrinal sino mecánica («un clip sin mutear tiene el
autoplay rechazado en todos los navegadores»). Nada conectaba el vídeo con
`arts/sound`: cero referencias en todo el componente.
**Lo que eso significaba con `muted={false}`**: el clip sonaba FUERA del grafo.
`sound.buses.content.setMuted(true)` no lo silenciaba; no participaba del audio
focus, así que hablaba por encima de un `MediaPlayer` en vez de cederle el paso;
no atenuaba el bus `ui`; no se proyectaba a MediaSession; y el slot `sound` de
`` —el interruptor «sin sonido» de la app, proyectado como `data-sound`—
no lo alcanzaba. Un `AudioContext` por documento es la razón de ser de ese art, y
un fondo cantando por fuera es exactamente la fuga que existe para impedir.
De WCAG 1.4.2 se salvaba de rebote: el control de pausa lo para, por carambola.
**FIRMADA (a): el fondo nunca suena.** `muted` deja de ser prop y se escribe
`true` incondicionalmente, así que **la API no puede expresar un fondo audible**.
Tres razones, de más a menos vinculante: la doctrina propia del componente (toda
capa es `aria-hidden`, la decoración no es contenido, así que un clip de aquí no
puede portar significado que el audio entregue); WCAG 1.4.2 (una decoración no
puede pedir un consentimiento que nadie le dio); y la fuga de `arts/sound` de
arriba, que es la parte de framework. Un clip que DEBE oírse es contenido:
`MediaPlayer`, que ya es ciudadano de `sound.media()`, o montarlo por
`Background.Layer` y registrarlo uno mismo.
(b) rechazada —hacerlo ciudadano con `sound.media(el, { focus: 'duck' })` cuando
`muted === false`— por más potente y por eso mismo: abre la puerta a que un fondo
compita con el contenido real, y ese territorio es de `Ambient` o de un reproductor.
**Verificado en Chrome**: con la capa en `video`, `el.muted === true`,
`paused: false`, `currentTime` avanzando y `readyState: 4` — sigue reproduciéndose
—, y el control de pausa presente. ⚠️ Anotado en el código: `hasAttribute('muted')`
lee **false** mientras la propiedad es true, porque Svelte lo fija como PROPIEDAD
sin reflejarlo; no es un agujero, porque la propiedad se escribe en el mismo
efecto y dos líneas antes del `play()`. Ningún consumidor pasaba `muted` (el
`HeroSite` ya lo decía en prosa: «DECORATIVE — muted»), así que el cambio de API
no rompe a nadie. Gates: audit PASS 0/0 · vitest eidos 434/435 (el de siempre) ·
`check` sin errores propios · prettier limpio.
uix(background): el velo era el dim del modal, y con on="light" combatía a su propia tinta D-BG.20. El scrim tomaba prestado `--color-overlay`, que es `surface.backdrop`: el dim de los modales (MD3/Radix/Vaul según el changelog), afinado POR MODO porque una página clara necesita menos atenuación que una oscura. Es una herramienta de ATENUACIÓN, y el scrim la usaba como tinta de LEGIBILIDAD — la misma enfermedad que la escala `--opacity-*` prestada que se arregló esta misma mañana, y que trajo tres defectos. El primero, que la misma fotografía se leía distinto según el modo: 1,48:1 en claro y 2,10:1 en oscuro. Una fotografía no cambia con el modo, y la legibilidad no es un eje que el modo pueda mover. El segundo, un TECHO: el alpha propio de la tinta (0,42 / 0,66) acotaba la escala entera, de modo que ni el peso máximo alcanzaba AA y ningún paso nuevo podría haberlo alcanzado. El tercero es el que no había visto y es el peor: con `on="light"` el velo era OSCURO bajo tinta oscura. El scrim combatía a la tinta que existía para sostener. La tinta del velo pasa a ser el SUELO de la tinta que `on` puso en vigor, que es lo que D12 ya define: `--color-content-on-solid-contrast` bajo `on="dark"`, `--color-content-on-solid` bajo `on="light"`, y `--color-surface-default` sin `on`, porque ahí la copia toma la tinta de la página. Tres roles que ya existían —ninguno inventado, §16.C— consumidos como capa 4 → capa 3 (§3) y sin alias. Con la tinta opaca el peso ES el alpha, así que `strength` significa una sola cosa; antes significaba dos, porque con `color="teal"` el ink ya era `--palette-solid`, opaco, y sólo el default era translúcido. La escala se re-afina por el TRABAJO de cada paso, medida con la matemática del propio framework (`$color`: `apcaLc` + `wcagContrastRatio`) sobre la peor obra de cada contexto: `xs` 0,08 · `sm` 0,13 · `md` 0,19 · `lg` 0,40 · `xl` 0,70. `xl` es el único paso que promete legibilidad sobre CUALQUIER fotografía, y lo promete contra los cuatro suelos que el framework ya usa —el criterio del par on-solid (`lib/on-solid.ts`: APCA |Lc| ≥ 60 ∧ WCAG ≥ 3) y el AA 4,5 de §40— en ambos contextos: `on="dark"` sobre foto blanca 6,45:1 con Lc 85, `on="light"` sobre foto negra 8,29:1 con Lc 61. 0,70 es el mínimo que cierra los cuatro; el más exigente, el APCA de `on="light"`, pedía 0,695. Los pasos por debajo son atmósfera y el README lo dice: no son garantías. Verificado en Chrome, no calculado: sin `on` el velo sale `oklch(0.9911 0 0 / 0.19)`, el suelo de la página en claro; con `on="dark"`, `#1c1917` al 19%; con `on="light"`, BLANCO al 19% bajo tinta oscura, que es el arreglo; `xl` en `on="light"` sobre foto negra mide 8,25:1 por píxel donde `$color` predijo 8,29. Y el consumidor real: el hero en layout `background` pinta `#1c1917 / 0.19` con titular blanco y da 1,48 / 5,17 — exactamente lo que daba antes en modo claro, paridad a la cifra. Los heroes en modo oscuro se aclaran de 0,297 a 0,19, que es el modo soltando una decisión que nunca fue suya. El selector de contexto va envuelto en `:where()`, y no por estética: a pelo llega a (0,4,0) y le gana a `[data-color]`, de modo que un `<Background.Scrim color="teal">` dentro de un stack `on="dark"` pintaba el suelo en vez de teal — el sistema de color abierto de §25 derrotado por un selector de conveniencia. Envuelto, la familia baja a (0,2,0): el contexto gana al bloque base por orden y pierde contra el color explícito. Medido antes y después. Queda anotado que el vignette sigue consumiendo `--color-overlay`, y ahí es correcto: es una trama que atenúa bordes, no un velo de legibilidad. Esta decisión salió de leer la doctrina entera después de que el autor preguntara si mi recomendación era conforme al sistema de color. No lo era: yo proponía una tinta opaca única, que servía sólo a `on="dark"` e ignoraba los otros dos contextos, y medía el contraste con un canvas WCAG-only en vez de con los suelos del framework. Los tres puntos los corrigió la documentación. Gates: audit PASS 0/0 · eidos-lint invalid 0, class-hooks 0 · rtl:check 0/180 · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno propio · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.duodecies D-BG.20 — la tinta del scrim es el SUELO de la tinta en vigor (2026-08-18)
Firmada tras leer la doctrina entera (`reference.md` §3/§4/§25/§29/§39/§40,
`gradient-finish.md` §10 + D11/D12, el changelog de la escala de opacidad y del
cue `scrim` podado, `on-solid.ts`, `renderOnContextBlocks`). **Mi recomendación
anterior no era conforme y la doctrina la corrigió en tres puntos.**
**El diagnóstico que sí se sostuvo**: `--color-overlay` ES `surface.backdrop`, el
dim modal (MD3/Radix/Vaul según el changelog), afinado POR MODO porque una página
clara necesita menos atenuación que una oscura. Es una herramienta de ATENUACIÓN,
y el scrim la tomaba prestada como tinta de LEGIBILIDAD — la misma enfermedad que
la escala `--opacity-*` prestada que se arregló horas antes.
**Lo que la doctrina corrigió:**
1. **No es «una tinta opaca» elegida por mí: es el SUELO del `on`.** D12 ya define
el contexto de tinta (`on='dark'` → `--color-content-on-solid`; `on='light'` →
`--color-content-on-solid-contrast`), así que el velo es el OTRO miembro de ese
par, y sin `on` es el suelo de la tinta de la página
(`--color-surface-default`). Tres roles EXISTENTES, ninguno inventado (§16.C),
capa 4 → capa 3 (§3), sin alias (`no-token-aliases`). Mi token único servía
sólo a `on='dark'` e ignoraba los otros dos casos — y con `on='light'` el velo
era OSCURO bajo tinta oscura: **el scrim combatía a su propia tinta**. Ése era
el defecto más grave de los dos, y no lo había visto.
2. **El contraste se mide con el criterio del framework**: `on-solid.ts` (APCA
|Lc| ≥ 60 ∧ WCAG ≥ 3) y §40 (AA 4,5 para texto), con la matemática de `$color`,
no con un canvas WCAG-only como el que usé antes.
3. **`--opacity-scrim` sigue existiendo** (0,45, rol de opacidad de ELEMENTO, lo
leen `chart` y el mesh): el `md: 0.45` que shipeé por la mañana colisionaba con
él por valor. Con el suelo firmado, `md` pasa a 0,19 y la colisión se disuelve.
**La escala, por trabajo y medida** (`$color` sobre la peor obra de cada
contexto): `xs` 0,08 · `sm` 0,13 · **`md` 0,19** (default) · `lg` 0,40 · **`xl`
0,70**. Con tinta opaca el peso ES el alpha. `xl` es **el único paso que promete
legibilidad sobre CUALQUIER fotografía**, y lo promete contra los cuatro suelos a
la vez: `on='dark'` sobre foto blanca **6,45:1 / Lc 85**; `on='light'` sobre foto
negra **8,29:1 / Lc 61**. 0,70 es el peso mínimo que cierra los cuatro (el más
exigente, el APCA de `on='light'`, pedía 0,695).
**Verificado en Chrome, no calculado**: sin `on` el velo es
`oklch(0.9911 0 0 / 0.19)` (el suelo de la página en claro) · `on='dark'` →
`#1c1917 / 0.19` · `on='light'` → **blanco** `/ 0.19` con tinta oscura encima
(el arreglo) · `xl` en `on='light'` sobre foto negra medido **8,25:1** por píxel,
donde `$color` predijo 8,29 · un `data-color='teal'` dentro de un stack
`on='light'` pinta teal y no el suelo (**la precedencia**). Y el consumidor real:
el hero en layout `background` (`on='dark'`, `md`) pinta `#1c1917 / 0.19` con
titular blanco y da **1,48 / 5,17** — idéntico a lo que daba antes en modo claro,
**paridad exacta**. Los heroes en modo OSCURO se aclaran de 0,297 a 0,19, que es
el modo soltando una decisión que nunca fue suya.
⚠️ **La especificidad casi rompe §25.** El selector de contexto a pelo llega a
(0,4,0) y ganaba a `[data-color]` (0,3,0): un `<Background.Scrim color="teal">`
dentro de un stack `on='dark'` pintaba el suelo en vez de teal — es decir, el
sistema de color abierto (§25) derrotado por un selector de conveniencia.
Envuelto en `:where()` la familia entera baja a (0,2,0): el contexto gana al
bloque base por ORDEN y pierde contra el color explícito. Medido antes y después.
Fuera de alcance, anotado: el **vignette** sigue consumiendo `--color-overlay`
(`background.css`), y ahí es correcto — es una trama que atenúa bordes, no un
velo de legibilidad.
Gates: audit PASS 0/0 · eidos-lint invalid 0 / class-hooks 0 · `rtl:check` 0/180 ·
vitest eidos 434/435 (el `skin-media-player` de siempre) · `check` con los 72
errores preexistentes y ninguno propio.
uix(background): se va `Backdrop`, que llevaba desde F1 absorbido y esperando tu palabra D-BG.1 firmó la absorción y el borrado el 2026-08-17, pero el borrado exigía orden explícita del autor y no la tenía; llegó hoy. Se van cinco ficheros: `eidos/components/backdrop/` entero —`backdrop.css`, `backdrop.svelte`, `index.ts`, `types.ts`— y su morfo `morfo/components/backdrop.ts`. Con `git rm`, no con `rm`, para que el índice lo registre. El censo se repitió justo antes de tocar nada, no se dio por bueno el de ayer: ningún import del componente ni del morfo fuera de su propia carpeta, no lo exporta el índice de eidos, no aparece en ningún índice de morfos, sin langs, sin demo, y ningún `import.meta.glob` que lo cargara por convención. Los gates lo confirman por aritmética, que es mejor que por ausencia de ruido: `morfo:check` pasa de 168 morfos a 167 y de 8 saltados sin demo a 7 — exactamente uno, el que se fue. `check` conserva sus 72 errores preexistentes y no menciona backdrop en ninguno (el recuento de ficheros baja de 6985 a 6981). vitest de `eidos` + `morfo` 653/654, con el rojo de siempre (`skin-media-player`). `docs:check` 0/635. `smoke` 320/320. Lo que NO se borra son las citas. La §Baseline del README de `Background` sigue nombrando a `Backdrop` —ahora fechada, con la nota de que sus cuatro tramas (`glow · mesh · grid · dots`), su máscara de desvanecido y su caída bajo `prefers-contrast` se migraron valor por valor, y de que la historia guarda el original— porque esa fila explica algo que el código ya no puede: por qué este componente se llama `Background` y no `Backdrop`. El nombre colisionaba con el velo modal (MUI, Vuetify, y el propio archetype `overlay`), y esa colisión es la razón del nombre que hoy llevan `data-background`, `--background-*` y `Background.Video`. Con esto el eje queda cerrado: las cuatro firmas del autor ejecutadas, la verificación en navegador completa —travel, 60 fps, reflow y la rama `@supports not`— y el paseo A–H registrado. Vivo sólo queda lo que pertenece a otros ejes: `Ambient` honrando la pausa (del pack) y el puerto de estado para `<video>`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.terdecies `backdrop/` borrado (2026-08-18) — D-BG.1 completada
La orden explícita del autor llegó el 2026-08-18 («ok borra»), que es lo que
D-BG.1 llevaba esperando desde que se firmó: la absorción estaba hecha desde F1,
pero el borrado exigía su palabra y no la tenía.
**Cinco ficheros**, todos rastreados: `eidos/components/backdrop/`
(`backdrop.css`, `backdrop.svelte`, `index.ts`, `types.ts`) y su morfo
`morfo/components/backdrop.ts`. Censo previo, repetido justo antes de tocar
nada: ningún import del componente ni del morfo fuera de su propia carpeta, no
lo exporta el índice de eidos, no está en ningún índice de morfos, sin langs,
sin demo, y ningún descubrimiento por `import.meta.glob` que lo cargara. Borrado
con `git rm`, no con `rm`, para que el índice lo registre.
**Gates después**: `check` con los mismos **72 errores preexistentes** y ninguna
mención a backdrop (el conteo de ficheros baja de 6985 a 6981) · vitest
`eidos` + `morfo` **653/654** (el rojo sigue siendo `skin-media-player`) ·
`docs:check` **0/635** · `smoke` **320/320** · `morfo:check` sin cambios en los
fallos y con **167 morfos donde había 168**, y los saltados sin demo bajan de 8 a
7 — la aritmética confirma que lo que se fue era exactamente lo que se pretendía.
Lo que NO se borra: las citas. La §Baseline del README de `Background` conserva a
`Backdrop` como su baseline —ahora fechada, con la nota de que sus cuatro tramas
se migraron valor por valor y que la historia guarda el original— y las tres
menciones en docs de proceso se quedan donde están. Una fila de registro nunca es
basura: es la huella de por qué el componente se llama `Background` y no
`Backdrop` (el nombre colisionaba con el velo modal).
---
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
---
## 8. Riesgos y cómo se acotan
- **Sobre-alcance**: la pieza podría crecer hacia GSAP. Frontera escrita: el
fondo NO orquesta contenido (pin/scrub/timelines fuera; ScrollFrames sigue
siendo el scrub de media).
- **Deuda de contraste**: un fondo hace fácil poner texto ilegible. La demo y
el README ENSEÑAN scrim+`on` y miden; el hero ya midió los dos huecos de
`data-on` (Display/Link) — candidatos de canon aparte, no de esta pieza.
- **Firefox sin scroll-driven**: el fallback JS existe desde F3; nunca «no
hay parallax en Firefox».
- **`will-change`/compositing**: no se añade a ciegas (memoria de jitter);
sólo si una medición lo pide.
- **Pack ↔ canon**: la integración va por contexto opcional (pack lee); el
canon jamás importa el pack — `npm run check` verde al borrar `src/packs/`.
- **Naming**: renombrar/borrar `Backdrop` exige orden explícita del autor
(regla dura); hasta entonces `Background.Pattern` puede convivir con
`Backdrop` sin shim y el hero migra en F1.
---
## 9. Fuentes externas consultadas (2026-08-17)
Mantine `BackgroundImage` / `Overlay` (docs) · Vuetify `v-parallax` (**no verificado en sesión**: la página devolvió sólo el título y el JSON de la API dio 429; la columna de la tabla §2 va por conocimiento previo — `src` + altura, una sola imagen) · react-scroll-parallax
(`ParallaxBanner` layers, props) · Motion/Framer `useScroll`/`useTransform` (+ ScrollTimeline
nativo con fallback) · Aceternity UI (backgrounds, parallax, spotlight) · Magic UI (Backgrounds)
· shadcn.io/background (100 fondos canvas/CSS) · Chakra v3 `Bleed`/layout helpers · daisyUI
`hero`/`hero-overlay` · MDN «CSS scroll-driven animations» + soporte 2026 (Chrome/Edge 115+,
Safari 26+, Firefox flag/Interop 2026, ~84 %) · WCAG 2.2.2 / 2.3.3 / 1.4.3.
---
## 10. Brief de ejecución para el agente constructor (Opus 5)
> El agente lo ejecuta; **Fable supervisa** (§11). El brief es autosuficiente:
> el agente NO descubre la doctrina por arqueología — la lee en el orden de
> abajo. Todo lo que no esté aquí ni en los docs enlazados es una PREGUNTA al
> supervisor, nunca una decisión propia.
### 10.1 Precondiciones (no arranca sin ellas)
1. **D-BG.1–14 (§6) FIRMADAS por el autor** — una fila sin «FIRMADA» bloquea la
fase que dependa de ella (F1 ← D-BG.1/2/13/14 · F2 ← D-BG.4/5/6 · F3 ←
D-BG.3/7/9 · F5 ← D-BG.10). El agente empieza por la fase más baja cuyas
decisiones estén firmadas y PARA en la primera que no.
2. **Rama propia** para el trabajo (`alpha-0.1-background` o la que el autor
nombre): el árbol actual (`alpha-0.1-dir-prefs`) lleva cambios sin commitear
ajenos a esta pieza (`cta`, docs, `scripts/__scratch-*`, `web/routes/alpha/`)
— **no tocarlos, no incluirlos, no hacer `git stash`/`reset`/`checkout` de
nada**. Crear la rama la ordena el autor.
3. Dev server: el que ya corra en el árbol (`npm run dev`); no reiniciarlo
(`reference_dev_server_restart_orphans_browser`), no arrancar otro en un
worktree para `morfo:check` (403 por `fs.allow`).
### 10.2 Lectura obligatoria (el «paquete mínimo» de `component-audit.md` §0, más los ejes de esta pieza)
Orden y TODO entero, no «la parte relevante»:
1. `docs/building-a-component.md` → `docs/guides/component-guide.md` (Build
contract + Before You Start §1–5) → `docs/guides/completion-checklist.md` →
`docs/guides/demo-authoring.md` (LOCKED) → `docs/guides/component-audit.md`
→ `docs/CANON.md` → `docs/architecture/morfo.md` + `soma.md` (§2 membresía)
→ `docs/canon/vocabularies.md` → `src/uix/eidos/components/README.md`.
2. Ejes de la pieza: `docs/theming/reference.md` (§3, §6, §25, §39, §40) ·
`docs/canon/tsc.md` · `docs/canon/recipe-contract.md` ·
`docs/theming/gradient-finish.md` (§10 fronteras, D8, D12) ·
`docs/theming/motion.md` + `motion-guide.md` (§8 límites) ·
`docs/architecture/packs.md` · `docs/architecture/blocks.md` ·
`docs/decisions/design-text-effects.md` · `src/arts/adom/README.md`.
3. Código de referencia (los precedentes que se imitan, no se reinventan):
`eidos/components/backdrop/*` (se absorbe) · `surface/*` (Box+tratamiento,
`on`) · `image/*` (se compone) · `scroll-frames/*` (progreso de scroll por
ActiveDom) · `motion/*` (contexto `seen`) · `toggle/*` (se compone para la
pausa) · `blocks/hero/{README.md,hero.svelte}` (el consumidor que motiva
todo) · `packs/ambient/ambient.svelte` + `arts/scene/{README.md,types.ts}`
(el pack que se montará DENTRO) · `eidos/lib/render-css.ts`
(`renderOnContextBlocks`) · `eidos/lib/primitives/static.ts` (gradientes,
scrim/opacity/blur) · `morfo/components/{surface,scroll-frames,image}.ts`.
4. Este plan entero, incluidas las enmiendas de cabecera.
### 10.3 Reglas duras del agente (además de `CLAUDE.md`)
- **Nunca commitear ni pushear**; nunca borrar ficheros (incluido
`eidos/components/backdrop/`) sin orden escrita del autor transmitida por el
supervisor. Nunca `--no-verify`.
- **Morfo-first** y morfo MÍNIMO: partes `provider` (la pila) · `layer`
(`aria-hidden` literal) · `pause`; `texts` `pause`/`play` con claves
IDÉNTICAS al catálogo; **ningún knob visual en el morfo** (regla 2026-08-15,
`image.ts`); `as const satisfies Morfo`; `validateMorfo` en test.
- **Sin soma**, sin pack sema, `scope: ['eidos']`, 0 eventos + `## Passive
justification` en el README. Si en algún punto parece necesitar un evento →
PARAR y preguntar (antes se lee `architecture/sema.md`).
- **`Background` NO compone `Box`** ni acepta props de layout; es un hijo del
padre; el anfitrión se adopta por la regla de foundation `:where(:has(>
[data-background]))` emitida por el generador (no por el recipe). La pausa
es un segundo nodo raíz.
- **Cero props nuevas en `Box`**, cero cambios en recipes ajenos (Card,
Section, Dialog…) para «hacer sitio»: si un anfitrión no funciona, se
reporta con medición, no se parchea desde fuera.
- **Canon nunca importa `src/packs/`**; `Background.Layer` es una ranura. La
integración con `Ambient` (D-BG.8) es una tarea DEL PACK, separada, después.
- Recipe: tokens `background-*` en `lib/recipes/base.ts` (TSC: `root` /
`host`), `generate:eidos-css`; sin color crudo, sin `opacity` literal
(`--opacity-*`), sin `box-shadow` literal, sin `@keyframes` sin
`/* functional: … */`, sin `!important` sin `/* important: … */`, sin
`--eidos-*`, sin `will-change` (memoria: jitter a DPR fraccional). Migrar
`backdrop.css` LITERALMENTE con prefijo `--background-pattern-*`.
- DOM sólo por `ActiveEidos.require().dom` (`listen`, `raf`, `measure`,
`writeProperty`, `observeIntersection`, `observeResize`, `getWindow`,
`prefersReducedMotion`); lecturas de layout SIEMPRE dentro de `dom.raf`/
`dom.measure`, nunca síncronas tras una escritura. `CSS.supports` vía
`dom.getWindow(node).CSS`.
- Composición: `Background.Image` compone `<Image>`; `Background.Pause`
compone `<Toggle>`; nada de `<img>`/`<button>` crudos.
- Demo: plantilla v2 de 9 tabs, chips = uniones completas, `SemaPanel` en
vacío justificado, snippet con paridad; canario `button`.
- README con las secciones que el audit exige (Baseline · Comparativa ≥3 ·
Decisiones · Gaps con disposición · Passive justification · Audit
exceptions) — Baseline = `Backdrop` + layout `background` del hero + semillas
`web/routes/demos/{heroscrolling,animations/background}` (referencia, no se
portan).
- Comentarios en inglés; tabs; comillas simples; sin `console.log`; sin
español nuevo en código.
- **Ante cualquier contradicción entre docs, o entre docs y código, o ante
una decisión no cubierta por §6: PARAR, escribirla con cita
(`fichero:línea`) y devolverla al supervisor.** No inventar campos, tipos,
tokens ni mecanismos.
### 10.4 Entregable por fase (lo que el supervisor recibe)
Al cerrar cada fase el agente entrega, en su mensaje final: (a) lista de
ficheros creados/modificados; (b) salida LITERAL de cada gate de la fase
(comando + resultado); (c) qué verificó en navegador y cómo (ruta, qué midió,
valores) — para F1+ una captura por anfitrión (Section · Box-columna · Card
rounded×shape · Dialog.Content) y para F2 la medición de contraste con el
método del hero (píxeles pintados, no `rgb()` parseado a mano); (d) dudas y
contradicciones encontradas, con cita; (e) lo que quedó fuera y por qué. Sin
adjetivos: números y rutas.
### 10.5 Prompt de arranque (para el `Agent`, `model: opus`)
> «Lee ENTERO `docs/process/PLAN-background.md` (incluidas las enmiendas de
> cabecera y §10) y después, en ese orden, toda la lectura de §10.2. Comprueba
> en §6 qué D-BG están FIRMADAS. Ejecuta SOLO la fase más baja de §7 cuyas
> decisiones estén firmadas, respetando §10.3, y PARA al terminarla entregando
> §10.4. No commitees, no borres, no toques ficheros ajenos a la pieza. Si
> algo no está decidido o contradice la doctrina, para y devuélvelo con cita.»
---
## 11. Protocolo de supervisión (Fable)
Por cada fase entregada, en este orden y sin saltarse ninguno:
1. **Firma**: comprobar que la fase sólo usó decisiones FIRMADAS; si el agente
decidió algo por su cuenta → se retira antes de revisar nada más.
2. **Gates, re-ejecutados por el supervisor** (no se acepta el pegado del
agente): `npm run check` (filtrado a los paths de la pieza + hero) ·
`npx vitest run src/uix/eidos` (recipe-css-contract, component-api-contract,
component-visual-attrs, generated-css, gradient-finish-guard) ·
`npx vitest run src/arts/adom` (F0) · `node --import tsx/esm
scripts/component-audit.ts --only background` (F4: PASS 0 errores) ·
`node scripts/eidos-lint.ts background` (invalid 0) · `npm run rtl:check` ·
`npm run blocks:check` (F1+, hero) · `npm run translations:check` ·
`npm run docs:check` (F4) · `morfo:check` + `SMOKE_SCOPE=/uix/components/background npm run smoke` (F4, con dev server).
3. **Diff contra doctrina** (lectura del diff entero, no de resumen): morfo
sin knobs · sin Box · regla `:has` en el generador y no en el recipe ·
tokens con TSC · anotaciones R-4.x · ActiveDom sin globales · lecturas
post-layout · Image/Toggle compuestos · nada en `src/packs/` · nada en
recipes ajenos · nada borrado sin orden · sin shims.
4. **Navegador REAL** (Chrome; el pane oculto suspende rAF y miente): los
cuatro anfitriones de F1; en F2 el vídeo (autoplay muted, pausa por
teclado, `aria-pressed`, pausa fuera de vista y con pestaña oculta,
reduced-motion emulado → poster, reduced-data → poster) y el contraste en
píxeles; en F3 el parallax con y sin soporte de `animation-timeline`
(`@supports` forzado / flag de Chrome), el detector `uix.perf` de reflow a
0, RTL con `dir="rtl"` (puntero físico, scroll vertical) y reduced-motion →
estático.
5. **Encapsulación**: el pack sigue fuera del canon (grep de imports); borrar
`src/packs/` en un worktree de prueba deja `npm run check` verde (F1+).
6. **Informe al autor**: 3–6 líneas — qué entregó el agente, qué verifiqué,
qué falló (con `fichero:línea`), qué decide él. Si hay decisión nueva → las
5 preguntas, UNA por mensaje, y PARAR.
7. Sólo tras el «ok» del autor: siguiente fase al agente. Los commits los
ordena el autor; el supervisor los prepara (staged verificado, mensaje) y
no los ejecuta sin orden.

Powered by TurnKey Linux.