Captures the design framing agreed this session, BEFORE implementation:
§D.12 — One universal `motion` prop on any component; the discriminant ("does the
animation realize a perceptual event?") decides the domain: event (firma, morfo+sema,
the prop is override/violation) / state (data-state preset) / content (prop is
primary). Supersedes §5's M5/M6 "motion vs animation separate" — its justification
(the coordinated `cascade-*` system) was retired in Plan A. Honest positioning:
content/state = parity with Framer/SwiftUI; event = above (motion coupled with
sound/haptic from one evaluated signal). Caveat: the JS engine (spring/handoff) is
built but dormant — the "above" on the engine axis needs a first spring preset.
§D.13 — The contracts the CSS model demands + failure modes, separating what the
mechanics already guard from what's deferred:
- structure: `:nth-child` ignores comments/`{#if}` (guarded); intermediate ELEMENT
wrapper breaks the count (the contract); a cascading wrapper becomes its own scope
(`inherits:false` isolates the index); `display:contents` = known dead-zone.
- exit: animated surface exits via Presence/`out:`, NEVER raw `{#if}`; unit-exit works
today (parent opacity carries the children); per-child staggered exit needs JS
lifecycle (PresenceGroup, deferred) → the container-driven rule is enter-only.
- debug: the index is `inherits:false` (no ancestor can step on it); a
`[data-debug-stagger]` mode to surface index/delay/name is pending.
Pointer added in eidos-motion.md. No code changed — design record only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| **evento** | `data-event-*` (durante el hold de sema) | morfo (la declara) + sema (firma: +sonido/háptico) | **override explícito** o **violación** (si esquiva la capa semántica) |
| **estado** | `data-state` (open/closed, checked…) | el state-preset (recipe) | **selecciona** qué preset reacciona al estado |
| **contenido** | nada — corre sola (loop / on-mount) | el prop, directo | **vía PRIMARIA** — no hay evento ni estado con qué componerse |
Canon del ejemplo: un **spinner** es evento (`sustain` — "proceso vivo"); un **logo que late** es contenido
(no comunica nada operativo). Mismo SVG, dominios distintos; la pregunta los separa.
**El guard (override vs violación).** Cuando un componente DECLARA un evento que debe portar la animación,
`motion="X"` es **override** si el diseñador cambia conscientemente la firma de ese evento (permitido,
explícito), o **violación** si usa el prop para animar algo que DEBERÍA declararse como evento (lo cazan
tipos/lint — el componente declara qué animaciones pertenecen a su evento). **Sin evento no hay nada que
violar: el prop es primario.** Es la regla "prop = override/violación cuando hay evento que la porte" del
rediseño, materializada.
**Por qué esto pone a UIX conceptualmente por delante (honesto, no hype).** En el eje "animación nombrada,
reutilizable, sobre cualquier superficie" hay **paridad** con Framer (`variants`) y SwiftUI (`.transition()`).
Lo distintivo NO es el selector — es que **el mismo prop uniforme sabe en qué dominio está**:
- contenido / estado → paridad limpia (prop directo + registro type-safe);
- **evento → por encima**: la animación sale coordinada con sonido + háptico de la misma señal evaluada
(lo que SwiftUI deja DESCONECTADO en `.sensoryFeedback`; lo que Framer no tiene capa para hacer);
- y un **discriminante principista** decide cuál, en vez de dejarlo al criterio del call-site.
Nadie más traza esta línea —evento-acoplado-a-percepción vs contenido-decorativo— **bajo un selector único**.
La universalidad de (a)+(b) NO diluye el acoplamiento metiendo los componentes-sin-evento en el mismo saco:
los pone en el dominio **contenido**, ya previsto y legítimo. El prop es uno; los dominios, tres; la frontera, nuestra.
**El asterisco (no omitido).** Esto es superioridad **conceptual** (el encuadre). En el eje **motor** (físicas
`spring`, reversa interrumpible con handoff de velocidad), Framer demuestra más HOY porque el motor JS de UIX
existe pero está **dormido** (todos los presets shipped son `driver: 'css'`). El "por delante" también en ese
eje exige **estrenar el driver `spring`** con un primer preset JS — trabajo aparte de (a)+(b).
**Plan de implementación (a)+(b) — acotado a eidos, NO el `PresenceGroup` pesado.**
- **(a) Unificar el nombre.** `<Cascade>``animation` → `motion`; confirmar que ningún otro wrapper use
`animation`. El catálogo ya es `MotionPresetName`.
- **(b) Universalizar.** Un mecanismo uniforme para que cualquier componente acepte `motion` y emita
`data-animation-style` (un helper/acción que los wrappers compongan, o un wrapper-primitivo `<Motion>`).
Decidir cómo dispara el dominio **contenido** (sin `data-state`): un attr/clase que corra el keyframe
on-mount o en loop, porque hoy los presets reaccionan SOLO a `data-state` (montaje).
- **(c) El guard.** Tipos/lint: un componente con evento-que-posee-la-animación marca el prop como override;
los demás, primario.
- Cierra §6 (routing) en su parte vigente: el enrutado a superficie sigue siendo la idea (el dev nombra, el
sistema sabe dónde), pero sin el eje coordinado separado que §6 contemplaba.
#### D.13 — Restricciones estructurales y modos de fallo (los contratos que el modelo CSS exige)
> **Encuadre de diseño (acordado 2026-06-20; D.13.1 ya vive en código, D.13.2/.3 con piezas DIFERIDAS).**
> El modelo declarativo (§D.11/§D.12) mueve a HTML/CSS lo que el motor paralelo retirado resolvía por
> fuerza bruta en JS. Eso exige **contratos estrictos**. Esta sección los fija y nombra los modos de fallo,
> separando lo que la mecánica **ya blinda** de lo **diferido** — para que las grietas se descubran en el
> contrato, no en producción.
**D.13.1 — Contrato de estructura (`:nth-child` ↔ hijos directos).**
- **Blindado por spec:**`:nth-child` cuenta SOLO elementos — ignora texto y comentarios. Los anclas `{#if}`
de Svelte (nodos-comentario) y el whitespace **no desincronizan** el índice. El renderizado condicional de
un *elemento* reindexa a los hermanos siguientes, lo cual es **correcto** (el escalonado refleja el set
realmente renderizado).
- **El contrato:** la cascada vincula un contenedor `[data-stagger]` con sus **hijos DIRECTOS** que llevan
`data-animation-style`. El componente **posee** esa estructura (como `<select>` posee sus `<option>`). Un
wrapper-elemento intermedio inyectado entre contenedor e ítems rompe el conteo — es **romper el contrato**,
no DOM arbitrario.
- **Wrapper-que-cascadea = su propio scope:** un agrupador que SÍ debe escalonar se marca a sí mismo
`[data-stagger]` → índice fresco de SU `:nth-child` (`--motion-stagger-index` es `@property … inherits: false`,
el índice del ancestro **no se filtra**) + su propio `--motion-stagger-each`. La regla container-driven es
`>` solo-hijo-directo → nunca alcanza nietos. El wrapper deja de ser bug y pasa a coreografía anidada.
- **Dead-zone conocida:** un wrapper `display:contents` que NO se re-scopea (ej. el `<Group>` del menú) — sus
ítems no son `:nth-child` del panel ni de un stagger propio → aparecen instantáneos. Documentado y aceptado
hasta `sibling-index()`.
**D.13.2 — Política de exit (la guillotina del desmontaje).**
Verdad-base del CSS: **no animas lo desmontado.** TODA salida exige un primitivo de retención; el modelo no lo
inventa — se apoya en los que ya existen.
- **Regla dura:** una superficie animada sale por **`Presence` (overlays) o `out:` (Svelte)**, **NUNCA por
`{#if}` crudo.** Un `{#if}` que desmonta directo = guillotina (cae incluso el unit-exit).
- **Unit-exit (barato, HOY):**`Presence` retiene el nodo, pone `data-state="closed"` y espera
`getAnimations().finished` antes de desmontar. El `opacity` del padre **agrupa el subárbol** → los hijos se
desvanecen CON el panel. Salida correcta de casi todos los overlays; cero coordinación. (Verificado en vivo:
`afterClose_retained: true`.)
- **Staggered-exit por-hijo (DIFERIDO):** un reverse-stagger donde el padre debe ESPERAR a los hijos es
inherentemente lifecycle (JS) — `Presence` espera su *propia* animación, no la del subárbol (sin
`{subtree:true}`). Es el `PresenceGroup` retirado (§8/§9). **Por eso la regla container-driven es
ENTER-ONLY:** no se envía un exit que se corte.
- **Puente pragmático (sin JS, interino):** dar al padre una salida con `duration ≥` la ventana de stagger de
los hijos → `Presence` retiene esa ventana → los hijos escalonan dentro. Hacky (depende del count) pero válido
para listas acotadas hasta que exista el `PresenceGroup`.
**D.13.3 — Afford de debug (`[data-debug-stagger]`).**
El peaje del declarativo: sin error en consola ni breakpoint JS. Matices + mitigación:
- **El miedo «una property heredada pisa el índice» está blindado:**`--motion-stagger-index` es
`inherits: false` → el índice de un ancestro **no puede** filtrarse. La única que hereda (a propósito, para
fijarse una vez en el contenedor) es el ritmo `--motion-stagger-each`.
- **Todo es inspeccionable + tipado** (`@property`) en Computed: `index`, `each`, `animation-delay`,
`animation-name`. Más que el closure de un motor JS; y un `@keyframes` no se breakpointea en ningún sitio
(compositor).
- **Mitigación SHIPPED (el análogo CSS de `UIX_DEBUG_MOTION`, PENDIENTE):** un modo `[data-debug-stagger]` que
materialice por hijo su `index/delay/name` (un `::after` contador y/o un dump a consola), para convertir el
spelunking en Computed en una herramienta.
---
> **Fuentes.** Canon semántico: [`docs/CANON.md`](../../../docs/CANON.md). Arquitectura de