Dos hallazgos del contraste doctrinal del tier, ambos de CANON, ambos medidos
antes y despues. El tercero (A-67) queda REGISTRADO y sin ejecutar: cae en el
eje de sema.
── A-99 · Group no aplicaba NINGUNO de sus dos defaults ──────────────────────
Su README los promete en tres sitios («defaults pensados para action rows —
`align: center`, `wrap: wrap`»). No pasaba ninguno a `Flex`, que cae a
`nowrap` + `stretch`. De ahi que las acciones de `hero` no apilaran a 375px
(A-98): 315 de 327px en una linea, con la segunda etiqueta truncandose.
⚠️ CORRECCION A MI PROPIA FICHA. Habia escrito que «el `align` SI se cumple, lo
que descarta que el recipe no cargue». Era FALSO: el `center` que medi salia de
la demo, que lo pasa explicitamente en su control. `group.css` no declara ni
`align-items` ni `flex-wrap` — no habia nada que cargar. Medir un default en
una demo que lo pisa no es medir un default.
Y de las tres salidas que la ficha proponia, «que lo declare el recipe» es
IMPOSIBLE: `flex.svelte` tiene `wrap = 'nowrap'` como default de JS y siempre
escribe `--flex-wrap` inline, asi que el fallback de `var(--flex-wrap, nowrap)`
no se evalua nunca. Solo ganaria una declaracion dura, que congela el eje.
El arreglo usa el mecanismo que el propio fichero ya tenia para `direction` y
`effectiveGap`: `align = 'center'` destructurado (sobreescribible — `align` no
esta omitido de `FlexProps`) y un `effectiveWrap` interno, `wrap` salvo
`attached`, que lo invierte. Espejo exacto de «`attached` implica `gap=0`», y
por la misma razon fisica: el recipe cuadra los radios interiores y tira de un
margen negativo asumiendo UNA linea, asi que un segmentado envuelto abriria su
segunda linea con una esquina plana y el borde recortado.
EL CENSO DECIDIO LA FORMA. 77 de 91 usos de `<Group` ya pasan `align`; de los
que no, tres blocks lo reenvian con default propio y **`ButtonGroup` es el
unico que voltea `Group` a columna en todo el ecosistema** — donde `center`
habria dejado los botones verticales en dientes de sierra. Un solo caso no
justifica un default condicional, asi que la excepcion vive en su call site.
Medido: `hero` a 375x800 pasa a DOS lineas (tops 416,3 / 472,3) y la accion
secundaria de 124px truncados a 156,9px, su etiqueta entera. ButtonGroup
horizontal `attached` sigue `nowrap`; el vertical, `stretch` con los tres hijos
a 301px. El README se corrige entero: negaba `attached` como prop canonico y
listaba `grow` como «gap conocido» cuando ambos llevan tiempo implementados —
describia la era del baseline `air`.
── A-68 + A-93 · dos observadores que no se hablaban ─────────────────────────
`CountUp` trae su propio IntersectionObserver (threshold 0) y `Motion` usa otro
(threshold 0.1, rootMargin −10%). Un IO no mira la opacidad, asi que las cifras
contaban su recorrido entero detras de `opacity: 0`: por scroll lento la
primera aparecia ya al 97,8% de su valor.
⚠️ EL DILEMA QUE PLANTEE ERA FALSO. Dije que escalonar el arranque y aterrizar
juntas eran incompatibles — cierto — y de ahi que exponer el `seen` de `Motion`
rompiera A-69. No se sigue: el retardo del stagger vive en `animation-delay`
(CSS) y los observadores de los cuatro `Motion` disparan en el MISMO instante,
porque los stats estan en una fila, a la misma altura. Gatear en el REVELADO da
arranque conjunto, duraciones iguales y aterrizaje conjunto intacto. Presente
como decision entre dos males algo que tenia una tercera opcion mejor.
Tambien descarto la «alternativa minima» que llegue a recomendar (alinear el
observador de `CountUp` con el de `Motion`): es acoplamiento por copia —
duplica threshold + rootMargin en otro fichero, la deriva que el framework
elimina con builders tipados — y cambia los defaults de `CountUp` para todos
sus usos sueltos.
`Motion` publica ahora su momento visto por contexto (`motion/context.ts`,
calcado de `cascade/context.ts`, que ya exponia `open`), y `stats-band-value`
lo pasa a `startWhen` ANTES del spread de `countOptions`, para que el app pueda
desactivarlo. Cero cambios en `CountUp`, cero observadores propios en el block,
B-6 intacto — la costura era justamente lo que faltaba en canon (A-93).
Medido con Playwright headless, 1280x800. ⚠️ La primera corrida fue INVALIDA y
se descarta: inserto el espaciador DESPUES de cargar, y como `Motion` es
`once: true` su observador ya habia disparado con la banda en pantalla. El
espaciador tiene que existir en el primer pintado (`addInitScript`). Segunda
corrida, con la banda nacida bajo el pliegue:
- fuera de pantalla 2,6 s: pending 4, opacidades [0,0,0,0], contadores 0/0/0
- ventana que discrimina (banda a 6px dentro, donde el IO de CountUp dispara y
el de Motion no): 0/0/0 en la llegada Y tras 2,6 s. Antes, ahi mismo, corrian
(1144/31/4) y acababan practicamente terminados (12.121/330/47)
- al revelarse, la primera cifra al 0% — antes 97,8%
- A-69 INTACTA: las tres aterrizan a 1781 ms, dispersion 0 ms
RESIDUO, y es peor que mi estimacion: las cifras que el stagger revela despues
llegan al 16,8% (+70 ms) y 35,4% (+210 ms). Estime «≤10%» y la medicion dice
35,4% — el muelle es sobreamortiguado y cubre mucho recorrido al principio. Es
el precio del aterrizaje conjunto que A-69 firmo y no baja sin reabrirla.
Queda documentado, no escondido.
── A-67 · registrada, no ejecutada ──────────────────────────────────────────
Contrastadas contra `engine.ts` y el canon, las tres puertas de su ficha fallan:
el pack no puede distinguir el `collapse` de un intercambio del de un cierre a
secas (mismo evento, mismo tipo de nodo; pediria un `:has()` a mano sobre el
hermano); el arbitro ya implementa la mitad del «on a tie, the most recent» que
un filtro de LLEGADA permite, y la otra mitad exigiria revocar una nota ya
agendada, que `EngineSound` no sabe hacer; y un evento unico rompe los
`commits`, porque cada uno estampa `data-state` sobre SU item.
La puerta correcta no estaba en la lista: un nuance declarado,
`emerge-collapse-swap`. La forma es canonica y el provider lo sabe de forma
determinista en `setValue`. Cae en el eje de sema, asi que NO se ejecuta aqui —
se deja elegida para su sesion.
Puertas: svelte-check 72 errores / 59 avisos = linea base exacta · blocks:check
0 sobre 15 blocks · docs:check 0/0 sobre 629 docs. La suite completa deja 8
rojos en 3 ficheros, todos nombrando waveform / skin-media-player / radio-group
mas un timeout de orca — ninguno en lo tocado aqui. Prettier solo se queja de
ficheros CRLF preexistentes (el diff seria el fichero entero), asi que no se
reformatean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ -111,7 +111,7 @@ Columna **or.** = de qué sección del documento viejo salió la fila (`C` confi
| A-65 | `pricing` | percepcion | MEDIA | SV | Un solo gesto sobre el CTA de un plan produce DOS eventos sema sobre el MISMO elemento a 1 ms: el `commit-select` de la app pisa el… | `web/routes/blocks/pricing/PricingSite.svelte` | CONFIRMADO |
| A-66 | `pricing` | percepcion | MEDIA | SV | El plan destacado se grada `fulfill` en lo visual pero su consecuencia suena `affirm`, exactamente igual que los otros dos planes: la lectura… | `web/routes/blocks/pricing/PricingSite.svelte` | REFUTADO |
| A-67 | `faq` | percepcion | MEDIA | SV | Un solo clic que cambia de pregunta dispara DOS earcons emerge simultaneos y sin atenuar (collapse descendente + expand ascendente): un gesto, dos… | `src/uix/blocks/faq/faq-list.svelte:14` | CONFIRMADO |
| A-68 | `stats-band` | percepcion | MEDIA | SV | La coreografia de la banda esta desincronizada: la ENTRADA escalona, pero los contadores arrancan todos en el mismo frame, asi que una cifra ya va… | `src/uix/blocks/stats-band/stats-band-value.svelte:20` | CONFIRMADO |
| A-68 | `stats-band` | percepcion | MEDIA | SV | Los contadores corren detras de `opacity: 0` — dos observadores distintos (CountUp threshold 0 · Motion 0.1/-10%) y un IO no mira la opacidad… | `src/uix/blocks/stats-band/stats-band-value.svelte:20` | ARREGLADO |
| A-69 | `stats-band` | percepcion | MEDIA | SV | Las cuatro cifras no aterrizan nunca juntas y `duration` no significa segundos: con el valor por defecto (2), la cifra grande tarda 7,9 s en… | `src/uix/eidos/components/count-up/count-up.svelte:169` | ARREGLADO |
| A-70 | `feature-grid` | percepcion | BAJA | SV | DATO, NO DEFECTO: `feature-grid` no tiene ningun elemento interactivo — nada que emitir y nada que auditar perceptivamente en el. | `src/uix/blocks/feature-grid/feature-grid.svelte` | REFUTADO |
| A-71 | `site-footer` | doctrina | BAJA | SV | La página de demo pinta el block ANTES de su propio `h1`, así que el documento arranca en h2/h3 y el h1 llega el último — jerarquía de encabezados… | `web/routes/blocks/_lib/BlockDemo.svelte` | CONFIRMADO |
@ -136,13 +136,13 @@ Columna **or.** = de qué sección del documento viejo salió la fila (`C` confi
| A-90 | `contact` | — | — | R | Vocabulario paralelo: `ContactVerification` re-declara palabra por palabra el `ProofOfHumanStatus` del canon, y lo hace señalando a ese mismo… | — | ARREGLADO |
| A-91 | `banner` | doctrina | BAJA | F7 | El block re-expone el eje `intent` de `Banner` —que el canon declara fuera del sistema de color abierto, en su propio typedoc— sin registrarlo en sus Gaps… | `src/uix/blocks/banner/README.md` | ARREGLADO |
| A-92 | `pricing` | doctrina | BAJA | F7 | Una restricción estructural del modelo de cascada (sólo los presets CSS participan del stagger) vive en un comentario del block y falta en §D.13 de la doctrina… | `docs/theming/motion.md` | CONFIRMADO |
| A-93 | `stats-band` | doctrina | MEDIA | F7 | `Motion` no expone su momento «visto» (privado + `data-animation-pending`), así que un block no puede sincronizarse con el revelado sin violar B-6… | `src/uix/eidos/components/motion/motion.svelte` | CONFIRMADO |
| A-93 | `stats-band` | doctrina | MEDIA | F7 | `Motion` no expone su momento «visto» (privado + `data-animation-pending`), así que un block no puede sincronizarse con el revelado sin violar B-6… | `src/uix/eidos/components/motion/motion.svelte` | ARREGLADO |
| A-94 | `feature-grid · team · testimonials` | doctrina | MEDIA | F7 | Los tres declaran `= AutoGridProps` y ponen `{...rest}` antes de props fijadas: el consumidor tipa `align`/`width` y se descartan en silencio (A-28 ×3)… | `src/uix/blocks/testimonials/types.ts` | ARREGLADO |
| A-95 | `banner` | doctrina | MEDIA | F7 | Con `affix="top"` la tira fijada tapa la cabecera pegada y la navegación queda inalcanzable; la pieza que falta es la que posee las alturas de página (`app-shell`)… | `web/routes/blocks/banner/BannerSite.svelte` | CONFIRMADO |
| A-96 | `banner` | doctrina | BAJA | F7 | `affixOffset` es prop público sin control vivo en la demo, contra la regla «cada prop público, un control»… | `web/routes/blocks/banner/+page.svelte` | CONFIRMADO |
| A-97 | `feature-grid + 6 blocks` | percepcion | MEDIA | F7 | El texto `color="muted"` mide 3,70:1 en claro — bajo AA de cuerpo — en 8 sitios de 7 blocks; el hallazgo estaba enterrado en la ficha de A-70, que es REFUTADO… | `src/uix/blocks/feature-grid/feature-grid-item-text.svelte` | REFUTADO |
| A-98 | `hero` | percepcion | MEDIA | F7 | Las acciones no apilan en móvil: `Group` es row/nowrap y a 375px los dos botones ocupan 315 de 327px; `cta` ya lo resolvió con `Flex` responsive… | `src/uix/blocks/hero/hero.svelte` | CONFIRMADO |
| A-99 | `Group` (canon) | doctrina | MEDIA | F7 | `Group` no aplica el `wrap` que su README promete en tres sitios: no lo pasa a `Flex`, no lo declara el recipe, y `Omit<FlexProps,'wrap'>` impide compensarlo… | `src/uix/eidos/components/group/group.svelte` | CONFIRMADO |
| A-98 | `hero` | percepcion | MEDIA | F7 | Las acciones no apilan en móvil: `Group` es row/nowrap y a 375px los dos botones ocupan 315 de 327px. Síntoma de [A-99]; cae sin tocar el block… | `src/uix/blocks/hero/hero.svelte` | ARREGLADO |
| A-99 | `Group` (canon) | doctrina | MEDIA | F7 | `Group` no aplica NINGUNO de los dos defaults de action row que su README promete (`wrap: wrap` y `align: center`): nunca se los pasa a `Flex`, y el recipe no puede suplirlos… | `src/uix/eidos/components/group/group.svelte` | ARREGLADO |
## Reparto
@ -417,6 +417,18 @@ Lo que su README afirma: «`wrap="wrap"` + `align="center"` por defecto», «**D
**Disposición** — CANON. O `group.svelte` pasa el `wrap` con su default documentado, o el recipe lo declara, o la documentación deja de prometerlo. Nota de tipo: `Omit<FlexProps,'wrap'>` fue deliberado (el wrap era una decisión del componente, no del consumidor) — coherente con un default fijo, incoherente con que ese default no exista.
**CORREGIDA (2026-08-17) — dos errores de esta ficha, y una de sus tres salidas es imposible.**
1. ⚠️ **«El `align` SÍ se cumple, lo que descarta que el recipe no cargue» era FALSO.** El `align-items: center` que medí salía de la demo, que lo pasa explícitamente (`web/routes/uix/components/group/+page.svelte:15` inicializa su control a `'center'`); su propio generador de snippet trata `stretch` como el default real (línea 84). `group.css` no declara **ni**`align-items`**ni**`flex-wrap`: no había nada que cargar. La lección se repite — **medir un default en una demo que lo pisa no mide un default.**
2. **El hallazgo infracontaba.** Faltaban los DOS defaults que el README promete, no uno. Y el README estaba rancio en las dos direcciones: negaba `attached` como prop canónico y listaba `grow` como «No — gap conocido», cuando ambos llevan implementados desde hace tiempo. Describía la era del baseline `air`.
3. **La salida «que el recipe lo declare» es arquitectónicamente imposible.**`flex.svelte:21` tiene `wrap = 'nowrap'` como default de JS y SIEMPRE escribe `--flex-wrap` inline vía `pushStyleVar`, así que el fallback de `var(--flex-wrap, nowrap)` no se evalúa nunca. Un recipe sólo ganaría con una declaración dura, que congelaría el eje. (Para `align` sí sería viable — `pushStyleVar` ignora `undefined` — pero mezclar las dos vías por el mismo par de defaults sería incoherente.)
**ARREGLADA (2026-08-17)** — en `group.svelte`, con el mecanismo que el propio fichero ya usaba para `direction` y `effectiveGap`: `align = 'center'` como default destructurado (sobreescribible: `FlexProps.align` no está omitido) y `effectiveWrap` interno, `'wrap'` salvo `attached`, que lo invierte a `'nowrap'` — el recipe cuadra radios interiores y tira de un margen negativo asumiendo UNA línea, así que un segmentado envuelto abriría su segunda línea con esquina plana. Espejo exacto de «`attached` implica `gap=0`».
**El censo que decidió la forma**: 77 de 91 usos de `<Group` ya pasan `align` explícito; de los que no, los tres blocks lo reenvían con default propio y sólo **`ButtonGroup` voltea `Group` a columna** en todo el ecosistema — donde `align: center` habría dejado los botones verticales en dientes de sierra. Por eso la excepción vive en su call site (`align="stretch"` cuando `orientation="vertical"`) y no como default condicional en `Group`: un caso no justifica una abstracción.
**Medido tras el arreglo** (Chromium, `/uix/components/button-group`): grupo horizontal `attached` → `flex-wrap: nowrap` (el segmentado no se parte); grupo vertical → `align-items: stretch`, los tres hijos a 301px = ancho del grupo, sin dientes de sierra.
### A-98 — CONFIRMADO · `hero` · percepcion · MEDIA
@ -430,6 +442,8 @@ Lo que su README afirma: «`wrap="wrap"` + `align="center"` por defecto», «**D
Alcance del síntoma, medido: `hero` (dos acciones, 315 de 327px a 375px, la segunda etiqueta truncándose — contenido REAL de la demo) y `feature-split` (`.Actions` también compone `Group`; su escenario pone una sola acción, así que el caso no se ejercita — al forzar una segunda, sigue en una línea. ⚠️ el «desborda el viewport» que se afirmó primero era artefacto: el ancho lo puso el texto inventado para el clon).
**ARREGLADA (2026-08-17) — sin tocar `hero`**, como predecía la reescritura: cayó con [A-99]. Medido en `/blocks/hero/preview` a 375×800 tras la puerta de hidratación, seleccionando el `[data-group]` con ≥2 hijos interactivos (el selector bueno, no el que invalidó la primera medición): `flex-wrap: wrap` · `align-items: center` · **2 líneas** (tops 416,3 y 472,3) · sin desbordamiento. Y el dato que cierra el defecto: la acción secundaria pasa de **124px truncados a 156,9px**, su etiqueta entera.
`cta` se salva por accidente: usa `Flex` a mano, no porque `Group` fuera inadecuado.
**Disposición corregida** — sin arreglo block-side. En cuanto `Group` envuelva, los dos blocks se curan solos. Si se quisiera además APILAR en móvil (que no es lo mismo que envolver), eso sí sería block-side y por props: `direction={{ base: 'column', sm: 'row' }}` sobre el propio `Group`.
@ -462,6 +476,8 @@ Alcance del síntoma, medido: `hero` (dos acciones, 315 de 327px a 375px, la seg
**Disposición** — CANON: dar a `Motion` una costura para su momento visto (contexto o callback), espejando `Cascade`. Desbloquea A-68 sin que el block viole B-6.
**ARREGLADA (2026-08-17)** — `src/uix/eidos/components/motion/context.ts`, calcado de `cascade/context.ts`: `Motion` publica `{ readonly seen }` con un getter sobre su `$state`, y `getMotionContext()` sale por el barrel. Devuelve `undefined` fuera de un `Motion`, así que un consumidor suelto no paga nada. La pieza que faltaba en el canon existe ya en las dos formas de animador — el contenedor (`Cascade.open`) y el de un solo elemento.
## Veredictos re-medidos
@ -1427,7 +1443,19 @@ Emisión sema con LAS DOS sondas. (a) MutationObserver ligado a CADA nodo botón
**Evidencia** — Playwright headless sobre http://127.0.0.1:5201/blocks/faq/preview (scripts probe-A-67.mjs y probe-A-67b.mjs en el scratchpad). Puerta de hidratación: espera a `document.getElementById('uix-blocks-display')`, luego a 0 nodos `[data-animation-pending]`, y un ida y vuelta reactivo real (clic en el trigger 0 → `aria-expanded` pasa a `true`). Sonda (a) contadores WebAudio parcheando `AudioContext.prototype.createOscillator/createBufferSource` y `OscillatorNode.prototype.start` ANTES de interactuar; sonda (b) MutationObserver atado a los NODOS `[data-accordion-item]` concretos. Gesto medido = un solo clic en el trigger 1 con el ítem 0 abierto. Números: 4 osciladores arrancados, 0 bufferSources — voz A fundamental 480 Hz + quinta 720 Hz, voz B 600 Hz + 900 Hz; los CUATRO con el mismo `when` de AudioContext (1.4628571428571429 en la corrida b; 1.401904761904762 en la a) y el mismo fin (1.6128571428571428) ⇒ solape del 100 % durante 150 ms. Envolventes: pico 0.06 (voz 480, la de collapse) y 0.08 (voz 600, expand), ambas con rampa de caída al mismo instante. Contornos opuestos confirmados en el detune: owner de 480 Hz `setValueAtTime(+400)` → `linearRampToValueAtTime(-400)` (DESCENDENTE); owner de 600 Hz `setValueAtTime(-400)` → `linearRampToValueAtTime(+400)` (ASCENDENTE). Estampas visuales del MutationObserver en el mismo milisegundo: ítem 0 `data-event=collapse` / `data-event-family=emerge` / `sig-1`, ítem 1 `data-event=expand` / `emerge` / `sig-2`. Control (segundo clic sobre el mismo ítem abierto, un solo cambio de estado): solo 2 osciladores (480 + 720), una rampa 0.06, una estampa `collapse` ⇒ el doblete es específico del cambio de pregunta. Sonido activo por configuración real de la página: `events: { sound: true, haptic: true, components: semaPacks }` en web/routes/blocks/\_lib/BootUix.svelte:62 (y en web/routes/blocks/+layout@.svelte:61), con `dominance` en su default `true` (src/uix/sema/engine.ts:257).
**Disposición** — Canon, no block: en src/uix/blocks/faq no hay nada que tocar (solo elige `type='single'`, que es el default del Accordion canónico). Tres sitios posibles, por orden de preferencia: (1) pack sema del accordion — src/uix/sema/components/accordion.ts, dar al par swap una tuña propia (p. ej. que el `collapse` coincidente baje a `emerge.exit.soft`/gain ~0.02 o se calle, dejando el `expand` como único signo del gesto); (2) árbitro de dominancia — src/uix/sema/engine.ts:559-593, resolver el empate del mismo tick según docs/CANON.md:253-254 («on a tie, the most recent») atenuando la ocurrencia estructural anterior en vez de exigir `>` estricto; (3) soma — src/uix/soma/components/accordion/accordion-provider.svelte.ts:119-120, emitir un único evento para el intercambio en modo `single` en lugar de collapse+expand (esta opción cambia el contrato del morfo, así que es la más cara). Severidad: la declarada MEDIA es razonable como problema de legibilidad, no de volumen (la suma 0.06+0.08 = 0.14 queda por debajo de un solo `emerge.strong` = 0.15).
**DISPOSICIÓN CORREGIDA (2026-08-17) — APLAZADA al eje de sema, con las tres puertas de abajo descartadas y una cuarta que ninguna nombraba.**
Contrastadas contra `engine.ts`, `CANON.md` §8 y `sema.md`, las tres opciones originales fallan:
1. **Tuñar el par en el pack** — inimplementable como está escrito. Una regla de cascada matchea los `data-event-*` del ítem estampado y **no puede distinguir** el `collapse` de un intercambio del `collapse` de un cierre a secas: son el mismo evento sobre el mismo tipo de nodo. Distinguirlos pediría un `:has()` sobre el hermano, manuscrito y dependiente del orden de emisión — dos violaciones (selector a mano + acoplamiento al orden).
2. **Resolver el empate en `applyDominance`** — malinterpreta al árbitro. El `>` estricto YA implementa la mitad del «on a tie, the most recent» que un filtro de LLEGADA puede implementar: el recién llegado no se mutea, o sea el más reciente gana. La otra mitad — atenuar al anterior — exige revocar una nota ya agendada, y `EngineSound` no tiene ducking ni handle de revocación. Tocaría la librería de sonido y cambiaría la conducta de TODO empate del ecosistema por el caso de un componente.
3. **Un solo evento para el intercambio** — rompe el modelo de commits: cada evento lleva su `commits` (`data-state` open/closed) sobre SU ítem, y un evento único no puede estampar dos estados en dos nodos.
**La puerta correcta: un evento con NUANCE declarado — `emerge-collapse-swap`.** La forma es canónica (`{family}-{verb}[-{nuance}]`, ejemplo vivo `emerge-dismiss-outside`) y el provider lo sabe de forma determinista en `setValue`: `closing` y `opening` no vacíos a la vez sólo ocurre en el intercambio de `type='single'`. Con eso el pack lo NOMBRA (`SILENT`, o algo sub-audible) → un gesto, un sonido, que es justo lo que la dominancia del libro pediría; los dos estampados VISUALES se quedan (dos superficies distintas, sin conflicto — el aire es lo único que es uno); eidos no se entera, porque sus selectores por prefijo (`[data-event^='emerge-collapse']`) siguen matcheando el nuance; y el engine y `$sound` no se tocan.
Coste: morfo + provider + pack. Cae en el eje de sema, así que **no se ejecuta aquí** — se ejecuta en una sesión suya, con esta puerta ya elegida.
**Disposición original (superada)** — Canon, no block: en src/uix/blocks/faq no hay nada que tocar (solo elige `type='single'`, que es el default del Accordion canónico). Tres sitios posibles, por orden de preferencia: (1) pack sema del accordion — src/uix/sema/components/accordion.ts, dar al par swap una tuña propia (p. ej. que el `collapse` coincidente baje a `emerge.exit.soft`/gain ~0.02 o se calle, dejando el `expand` como único signo del gesto); (2) árbitro de dominancia — src/uix/sema/engine.ts:559-593, resolver el empate del mismo tick según docs/CANON.md:253-254 («on a tie, the most recent») atenuando la ocurrencia estructural anterior en vez de exigir `>` estricto; (3) soma — src/uix/soma/components/accordion/accordion-provider.svelte.ts:119-120, emitir un único evento para el intercambio en modo `single` en lugar de collapse+expand (esta opción cambia el contrato del morfo, así que es la más cara). Severidad: la declarada MEDIA es razonable como problema de legibilidad, no de volumen (la suma 0.06+0.08 = 0.14 queda por debajo de un solo `emerge.strong` = 0.15).
### A-68 — CONFIRMADO · `stats-band` · percepcion · MEDIA
@ -1438,6 +1466,21 @@ Emisión sema con LAS DOS sondas. (a) MutationObserver ligado a CADA nodo botón
**Disposición** — La costura es del block (`src/uix/blocks/stats-band/stats-band-value.svelte:22` compone `CountUp` sin gate; `stats-band-stat.svelte:18` pone el `Motion`), pero el arreglo necesita canon: que `src/uix/eidos/components/motion/motion.svelte` exponga su momento de revelado (un `onEnter` o el `seen` como parámetro del snippet, hoy privado en la línea 42) y, si se quiere sincronía exacta, el retardo de stagger efectivo — que hoy sólo vive en CSS (`motion.css:41-43` + `render-css.ts:1118`). Con eso, `StatsBand.Value` alimenta el `startWhen`/`delay` que `CountUp` ya acepta (`count-up.svelte:31,34`) y la cifra empieza a contar cuando aterriza, no antes. Alternativa mínima en canon sin tocar el block: alinear el observador de `CountUp` con el de `Motion` (mismo threshold/rootMargin) — corrige el caso del scroll lento pero NO el desfase de 210 ms del stagger.
**⚠️ EL DILEMA ERA FALSO (2026-08-17), y lo planteé yo.** «Escalonar el arranque y aterrizar juntas son incompatibles» es cierto, pero **exponer el `seen` de `Motion` no implica escalonar nada**: el retardo del stagger vive en `animation-delay` (CSS), mientras que los observadores de los cuatro `Motion` de la banda disparan en el MISMO instante — los stats están en una fila, a la misma altura. Gatear los contadores en el REVELADO da arranque conjunto → duraciones iguales (λ derivada, A-69) → aterrizaje conjunto intacto. Se presentó como una decisión entre dos males y no lo era: la opción buena estaba fuera de la lista.
Corolario sobre la alternativa «mínima» que llegué a recomendar (alinear el observador de `CountUp` con el de `Motion`): es **acoplamiento por copia** — duplica `threshold` + `rootMargin` en otro fichero, la clase exacta de deriva que el framework elimina con builders tipados, y cambia los defaults de `CountUp` para todos sus usos sueltos. Descartada.
**ARREGLADA (2026-08-17)** — `stats-band-value.svelte` lee `getMotionContext()` (la costura de [A-93]) y pasa `startWhen={motion?.seen ?? true}` a `CountUp`, ANTES del spread de `countOptions`, para que el app pueda desactivar el gate explícitamente. Cero cambios en `CountUp`, cero observadores propios en el block, B-6 intacto.
**Medido tras el arreglo** — Playwright headless, 1280×800, sonda `probe-A68-after.mjs`. ⚠️ **La primera corrida fue INVÁLIDA y se descarta**: insertó el espaciador DESPUÉS de cargar, y como `Motion` es `once: true` su observador ya había disparado con la banda en pantalla (`pending: 0`, opacidades a 1). El espaciador tiene que existir en el primer pintado — se instala por `addInitScript` con una hoja de estilo. Segunda corrida, con la banda nacida bajo el pliegue:
- **Fuera de pantalla, 2,6 s**: `pending: 4`, opacidades `[0,0,0,0]`, contadores `0 / 0 / 0`, sin moverse.
- **La ventana que discrimina** — borde superior de la banda a **6 px dentro** del viewport, donde el IO de `CountUp` (threshold 0) dispara y el de `Motion` (0.1 / −10 %) no: contadores a `0 / 0 / 0` en la llegada **y** tras 2,6 s. Antes, en esa misma ventana, corrían (1144 / 31 / 4) y acababan prácticamente terminados (12.121 / 330 / 47).
- **Al revelarse**: la primera cifra aparece al **0 %** de su valor (antes: **97,8 %**).
- **A-69 intacta**: las tres aterrizan en t = 1781 ms, **dispersión 0 ms**.
**Residuo, honesto y peor que mi estimación** — las cifras que el stagger revela más tarde ya llevan camino andado cuando se hacen visibles: 16,8 % la segunda (+70 ms) y **35,4 %** la tercera (+210 ms). Estimé «≤10 %» y la medición dice 35,4 %: el muelle es sobreamortiguado y cubre mucho recorrido al principio, así que 210 ms pesan bastante más que su fracción de los 2 s. Es el precio del aterrizaje conjunto que A-69 firmó, y no se puede bajar sin reabrirla — retrasar cada arranque por su índice devolvería el escalonado. Queda documentado, no escondido.
**Disposición corregida (2026-08-10, contraste doctrinal)** — **No es arreglable desde el block sin romper el contrato B.**`theming/motion.md` §D.11 dice que «the children's timing is per-component realization», así que sincronizar contador y revelado SÍ es trabajo del block; pero `Motion` guarda su `seen` en `$state` privado y lo único observable es el `data-animation-pending` del DOM, y leerlo exigiría que el block monte un observador propio — **exactamente lo que B-6 prohíbe** («needing to observe something is the admission rule firing»). La pieza que falta tiene nombre: `Motion` necesita exponer el momento visto, como `Cascade` ya lo expone por contexto. ⚠️ Y hay un conflicto que hay que FIRMAR antes de tocar nada: A-69 acaba de fijar en el canon que la banda **aterrice junta** («by construction», medido: 2005 ms × 3, desfase 0), y arrancar cada cifra con su retardo de entrada rompería ese aterrizaje. Escalonar el arranque y aterrizar juntas son incompatibles.