| 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 |
| Parallax por puntero / spotlight | ✗ | ✗ | ✗ | ✗ | ✓ («Parallax Scroll mouse», Spotlight) | ✗ | sólo dentro de efectos WebGL del pack | ✓ `depth` por capa + `spotlight` (rAF-coalesced, `dom.writeProperty`) |
| 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 |
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.
| 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
| `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.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`)** —
- 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
Sólo `translate`/`opacity` en las capas; scroll-driven en el compositor donde
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 |
| **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 |
| **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 |
| **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 |
| **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 (`<Herobackground={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 |
| **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 |
| **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» |
| **F5 · Rollout tier** | cta · stats-band · testimonials · feature-split (banda 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`) |
| **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` →
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.
### 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
- **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