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

160 KiB

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
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)
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 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 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

<!-- 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).

Anotación 2026-08-26 — las cinco vars de runtime se llaman --_background-* (vale para las NUEVE menciones de esta sección, tres arriba y seis abajo). El codemod de los 14 canales de valor (ley del espacio cerrado) renombró --background-{pointer-x,pointer-y,pointer-px,pointer-py,progress} a --_background-*: son estado por instancia que el envoltorio escribe, no superficie de tema, y el prefijo privado es lo que esa clase (channel, firma de los 318) significa. Este documento NO se reescribe — sus decisiones firmadas y sus medidas (D-BG.3, los −64px de progress 0, el 0,062 del wheel tick) fueron correctas con el nombre de su día. Vale para las nueve menciones de aquí abajo.

  • 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

Sólo translate/opacity en las capas; scroll-driven por CSS 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. 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)
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. 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
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
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.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
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»

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
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)
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
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)
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.

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.

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.

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.

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.

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.

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.


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.

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 comodata-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.

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.

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).



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.