The cascade is no longer a separate system — it's the existing state-presets + the
Material stagger + one foundation rule. Retires the --cascade-* reinvention.
- render-css: foundation structural-index writer — `[data-stagger] > *:nth-child` →
`--motion-stagger-index` (forward) / `:nth-last-child` → `--motion-stagger-index-rev`
(reverse). Nobody writes the index; soma never writes a visual var. Every preset's
enter/exit rule consumes it (parallel = `--motion-stagger-each` 0, cascade = N).
- Cascade rewritten as a thin EXPLICIT orchestrator: marks `[data-stagger]`, reflects
open→data-state, passes the preset to items via context; the `out:` retention flips
to data-state=closed (reusing the exit preset), reading the duration via dom.getWindow.
Drops the whole `--cascade-*` namespace + bespoke keyframes.
Docs: MOTION_SERVICE_RFC §D.11 — the final model (three orthogonal axes sema/motion/
eidos, the full lifecycle, the realization ladder snap->transition->animation->JS, the
firma-vs-realization seam, the cascade = existing system + one rule, the motion service
as the engine). eidos-motion aligned.
Verified: browser (enter scale-in/fade-in + nth-child index, exit reverse, zero inline
writes), npm run check 0 mine, motion.test 22/22, eidos-lint 0 invalid.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ -1279,6 +1279,136 @@ reparto verificado y **cero `--var` visual escrita por JS** (confirmado por valo
OK en este caso, pero la versión glitch-free pide que el **contenedor orqueste** la retirada como unidad
(lifecycle de soma, lo que hacía el `pending` borrado — sin tocar lo visual). Pendiente para producción.
### D.11 — El modelo final: tres ejes ortogonales + el lifecycle completo (2026-06-19)
Tras un re-análisis de fondo (workflow sobre código + canon) y correcciones duras del usuario, **este es el
modelo cerrado**. Supersede los matices de D.2/D.9 donde difieran; D.8 (retirada) y D.10 (prototipo) siguen
vigentes como historia.
#### D.11.0 — Tres ejes ORTOGONALES (el error de raíz era confundirlos)
El sistema NO es «una forma de animar». Son **tres cosas separables** que no hay que mezclar:
| Eje | Qué es | Qué hace EXACTAMENTE |
| --- | --- | --- |
| **sema** (semántico) | el «qué ocurrió» | **SOLO emite `data-event-*`** en el DOM (+ corre sus 2 canales runtime sound/haptic). **NO es animación.** |
| **motion** (`arts/motion`, el SERVICIO) | el **MOTOR** de animación | corre los drivers JS (`spring`/`waapi`/`rect`/`svelte`) + el path CSS declarativo + handoff de velocidad. Motor full, par/superior a Framer. |
| **eidos** (visual) | la materialización | **LEE `data-event-*` (+ `data-state`) y reacciona**: genera el CSS, decide estilo/animación, consume el servicio motion para lo JS. Único dueño visual. |
Y los dos que orquestan alrededor: **morfo** DECLARA (semántico + estructura, cero visual); **soma** DISPARA
(`runtime.trigger`) + ESTADO (`data-state`) + LIFECYCLE (presence/retención), cero var visual.
**La regla mental:** sema = el QUÉ (emite el token) · motion = el MOTOR (corre lo JS) · eidos = el CÓMO-SE-VE
(lee el token y materializa). Mezclar «sema» con «la capacidad de animar», o comparar el suelo-CSS contra el
motor entero de otro framework, es el error a no repetir.
#### D.11.1 — El lifecycle completo de un evento
```
1. DECLARACIÓN (estática, en código)
morfo: el evento (family/intent/verb/target/sequence/hold) + estructura (parts, data-state, data-stagger)
eidos: la realización — presets/signatures/keyframes en EidosConfig.motion
(derivados a generated/base.css + registrados en uix.motion)
2. TRIGGER (runtime, ORIGEN)
soma: runtime.trigger('open') ← origina desde interacción/lifecycle (sin engine sigue: sema es ornamental)
+ escribe data-state (en el contenedor y, propagado, en los items)
3. EMISIÓN (sema PROCESA)
sema: engine.emit → resuelve la firma (cascada 5 capas → SOLO sound/haptic)
→ el META-canal visual ESTAMPA data-event-* en signal.target (UN elemento)
→ abre el HOLD; en paralelo sound/haptic se realizan
4. REACCIÓN (eidos LEE, durante el hold)
eidos: el CSS del componente reacciona a data-event-* (+ data-state) → tres salidas posibles (D.11.2)
5. CLEANUP
sema: al cerrar el hold, des-estampa data-event-* (transient). Persistente → soma lo limpia (clearTarget)
6. PRESENCE (lifecycle de salida)
soma: Presence retiene el nodo durante la salida (await getAnimations / out:), luego desmonta.
La retención es soma; el visual, eidos.
```
#### D.11.2 — La reacción de eidos: una escalera de realización
Leído el `data-event-*` (+ `data-state`), el CSS del componente decide CÓMO se realiza. **No es siempre una
animación** — hay una escalera de cuatro escalones, de menos a más:
`--motion-stagger-each: 0` (the default) → **parallel**; `N` → **cascade**. The keyframe,
duration, easing and reduced-motion all come from the `animation` preset.
## How it works — it reuses everything
Nothing here is a new system. The cascade is the *existing declared motion* plus one
foundation rule:
| Piece | Where it's declared / derived |
| --- | --- |
| the per-item **animation** (keyframe, duration, ease, reduced-motion) | a **state-preset** declared in `EidosConfig.motion` (`fade` / `scale-fade` / …), generated by eidos to `generated/base.css` and registered with `uix.motion` |
| the **stagger delay** (`index × --motion-stagger-each`, default 0 = parallel) | already in **every preset's** enter/exit rule (`lib/render-css.ts`) |
| the **structural index** (`--motion-stagger-index` / `-rev`) | the one new foundation rule: `[data-stagger] > *:nth-child / :nth-last-child` (generated by `lib/render-css.ts`) — **nobody writes it**, soma never writes a visual var |
`<Cascade>` just **wires** those: it marks the container `[data-stagger]`, reflects `open` as
`data-state`, and passes `open` + the chosen preset to its items (context), so each item
carries `data-animation-style` + `data-state`. Enter counts up (`:nth-child`); exit counts
down (`:nth-last-child`) so the last item leaves first.
## Props
### `<Cascade>`
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `open` | `boolean` | `true` | Drives the staggered enter (forward) / exit (reverse) via `data-state`. |
| `animation` | `MotionPresetName` | `'fade'` | The state-preset every item plays (`fade` / `scale-fade` / `slide-fade` / …). |
Plus any `HTMLAttributes<HTMLDivElement>`. Set the layout + the rhythm (`--motion-stagger-each`)
here.
### `<Cascade.Item>`
A participant. On removal it flips to `data-state='closed'` (the existing **exit** preset
plays, with the reverse structural index), is retained (`out:`) for the eidos-declared exit
duration, then unmounts. Accepts any `HTMLAttributes<HTMLDivElement>`.
## Ownership
| | Owns | Never touches |
| --- | --- | --- |
| **soma** (the wrappers) | **state** (`open` → `data-state`, propagated to items) + **lifecycle** (`out:` retention) | any `--motion-*` / visual variable — it reads the eidos duration via the ActiveDom `getComputedStyle`, writes nothing |
| **eidos** | **all the visual** — the preset (keyframe/duration/ease/reduced-motion) + the foundation stagger index | the semantics / state |
## Two ways to stagger children
1. **Explicit** — `<Cascade>` (this component). You want these elements to stagger; there is no
semantic event. The content/intrinsic animation domain.
2. **Event-driven** — a real component (a menu, a list) declares its own `emerge` firma (the
container flourish + sound) **and** marks its item container `[data-stagger]`; its items
carry a preset + the container's `data-state`. Same foundation stagger, no `<Cascade>`
wrapper — the appearance flows from the declared event. (The firma is *generic by event*:
"children of an emerging container present together, by structural order"; the concrete
timing — parallel/cascade, ms, keyframe — is the per-component realization.)
## Known limitation — bulk removal
When **several** items are removed at once (e.g. a slider jumping 10 → 3), the leaving nodes
unmount one-by-one as each finishes, so the grid reflows during the cascade and the remaining
items' `:nth-last-child` index re-evaluates. In practice it reads fine, but the glitch-free
version wants the **container to retain the leaving set as a unit** (one reflow at the end) —
a soma **lifecycle** concern (what the retired `pending` did, without touching the visual).
Dragging the slider down step-by-step (a sequence of lone removals) is always clean.
## Decisions
- **No recipe, no `--cascade-*` namespace, no bespoke keyframe.** An earlier prototype forked
all of that — it reinvented the existing `--motion-stagger-*` + state-preset system. This
replaces it by reusing them; the only new code is the foundation structural-index writer.
- **`Cascade` is the EXPLICIT domain.** Event-driven children appearance rides the firma +
the same foundation stagger, declared in the component's own morfo — not this wrapper.
- **Index from `:nth-child`, capped 1–24** (the foundation writer). Beyond the cap items share
the last index until `sibling-index()` is broadly supported.