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/decisions/design-text-effects.md

154 lines
8.9 KiB

feat(packs+text): incorporate the animation collection — Ambient pack + text-effects family + docs Two streams, split by what the animation touches: STREAM A — decorative backgrounds → the pack tier - arts/scene: a consolidated scene runtime ($scene) that owns, once, the citizenship every ad-hoc background reinvented or skipped (frame loop, off-view pause, DPR cap, mandatory reduced-motion, WebGL context loss/restore, scene budget, teardown). SceneDom port (adom satisfies it), webgl/webgl2/canvas2d drivers + a custom-pipeline extension (vertexShader + draw + glContext.depth/dprCap) for real geometry (beam, particles, dither, grid, eter, pixel-blast, hyperspeed). 32 effects as shared resources. - src/packs/ambient: the first pack — <Ambient effect="…"> mounts a registered effect; the P contract (P-1..P-6) guarded by scripts/packs-check.ts; colors are token-aware (P-4). One-way dependency, removable-by-construction. - resolveToken extended to semantic color slots (--color-{role}-{slot}) so consumers resolve theme tokens to concrete colors (the P-4 half). STREAM B — animations over real text → canon - Six components (count-up + text-{gradient,circular,blur,focus,scramble}): each a morfo + eidos recipe (where there's styling) + demo. CountUp is a service component (counts through uix.format.numbers). The five Text* are passive decoratives. Upgrades over the seeds: SR hardening (real text visually-hidden + aria-hidden decoration), a11y fix (no fake role=button), measurement discipline (cached rects via dom.measure, no reflow storm), reduced-motion, ecosystem citizenship (eidos.dom/timers, no raw platform). - MorfoElement gains 'p'. DOCS - docs/architecture/packs.md (pack tier, admission rule, P contract, Aura promotion path); docs/decisions/design-text-effects.md (the family design record) + indexed in decisions.md / README.md; glossary entries (scene/Ambient/Aura/text effects); scene README custom-pipeline + authoring bridge; motion-guide content-effects note; strata tables acknowledge packs. Gates: component:audit 141/0/0 · docs:check 0/0 · scene tests 9/9 · packs:check 0/36 · check 0 own errors. Verified in browser (32 effects mount+compile; 6 text components SSR+hydrate, CountUp re-formats by locale live, TextGradient resolves token stops to OKLCH via var()). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
title: Text effects — family design
type: reference
audience: human + agent
authority: E3 design record — why the text-effects family exists, where each member sits, and the doctrine every member obeys
status: current
related:
packs: docs/architecture/packs.md (the sibling stream — decorative backgrounds went to the pack tier)
format: src/uix/eidos/components/format-number/README.md (CountUp's service-component sibling)
---
# Text effects — family design
The text-effects family is one of the two outcomes of incorporating a large
animation collection into the ecosystem. The collection split along a single
axis — **what the animation is applied to**:
- **Backgrounds** paint behind content and touch nothing semantic → they are
decorative leaves → they went to the **pack tier**
([`architecture/packs.md`](../architecture/packs.md), the `Ambient` pack).
- **Text effects** treat REAL text content the reader consumes → the
accessibility surface *is* the contract surface → they are **canon**
components (this document).
Both streams share one provenance rule: the seed collection
(`web/routes/demos/animations`) is kept intact as comparative reference and is
NOT the shipped code; every component is a clean re-implementation against the
ecosystem's contracts.
## The members
Six components, in two roles (decision D-T1):
| Component | Role | What it does |
| --- | --- | --- |
| [`CountUp`](../../src/uix/eidos/components/count-up/README.md) | **service** | Spring-counts a number to a target on viewport entry, formatting every frame through `uix.format.numbers`. |
feat(text-gradient): `colors` discriminates canonical name vs stop list (folds `preset`) Per request: one `colors` prop that discriminates by shape instead of two rival props. A `string[]` is an explicit stop list (unchanged); a bare `string` names a canonical gradient from the theming's named set, resolved as `var(--gradient-{name})` — exactly what the removed `preset` prop did. The set is an open cage (`EidosConfig.gradients` / `applyGradients`), so the type stays `string | readonly string[]`, not a closed union. Removed `preset` outright (no back-compat shim, per project rule); the canonical S9 vocabulary and raw stops are now the SAME input. Docs shell: register a small canonical named set on `uix/+layout@` via `applyGradients` (`brand`, `spectrum`) — role-derived, so they re-tint with the seed and flip light/dark. Demo gains a live stops|named toggle so both branches are a real testbed. Finding worth recording (types + README): a named gradient painted through `background-clip: text` must be a single `<image>` (linear/radial/conic). A role-derived MESH serializes to radials + a base COLOR, and a bare color is not a valid `background-image` (only the `background` shorthand accepts it) — so a mesh name computes to `none` and never paints. The docs set is linear-only for this reason. Verified in real Chrome: stops branch paints the token gradient; `colors="spectrum"` paints `var(--gradient-spectrum)` through the text. svelte-check 0 own errors, component:audit 141/0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
| [`TextGradient`](../../src/uix/eidos/components/text-gradient/README.md) | decorative (CSS) | Animated gradient painted through the text (`background-clip`); `colors` takes a free stop list or a canonical gradient name. |
feat(packs+text): incorporate the animation collection — Ambient pack + text-effects family + docs Two streams, split by what the animation touches: STREAM A — decorative backgrounds → the pack tier - arts/scene: a consolidated scene runtime ($scene) that owns, once, the citizenship every ad-hoc background reinvented or skipped (frame loop, off-view pause, DPR cap, mandatory reduced-motion, WebGL context loss/restore, scene budget, teardown). SceneDom port (adom satisfies it), webgl/webgl2/canvas2d drivers + a custom-pipeline extension (vertexShader + draw + glContext.depth/dprCap) for real geometry (beam, particles, dither, grid, eter, pixel-blast, hyperspeed). 32 effects as shared resources. - src/packs/ambient: the first pack — <Ambient effect="…"> mounts a registered effect; the P contract (P-1..P-6) guarded by scripts/packs-check.ts; colors are token-aware (P-4). One-way dependency, removable-by-construction. - resolveToken extended to semantic color slots (--color-{role}-{slot}) so consumers resolve theme tokens to concrete colors (the P-4 half). STREAM B — animations over real text → canon - Six components (count-up + text-{gradient,circular,blur,focus,scramble}): each a morfo + eidos recipe (where there's styling) + demo. CountUp is a service component (counts through uix.format.numbers). The five Text* are passive decoratives. Upgrades over the seeds: SR hardening (real text visually-hidden + aria-hidden decoration), a11y fix (no fake role=button), measurement discipline (cached rects via dom.measure, no reflow storm), reduced-motion, ecosystem citizenship (eidos.dom/timers, no raw platform). - MorfoElement gains 'p'. DOCS - docs/architecture/packs.md (pack tier, admission rule, P contract, Aura promotion path); docs/decisions/design-text-effects.md (the family design record) + indexed in decisions.md / README.md; glossary entries (scene/Ambient/Aura/text effects); scene README custom-pipeline + authoring bridge; motion-guide content-effects note; strata tables acknowledge packs. Gates: component:audit 141/0/0 · docs:check 0/0 · scene tests 9/9 · packs:check 0/36 · check 0 own errors. Verified in browser (32 effects mount+compile; 6 text components SSR+hydrate, CountUp re-formats by locale live, TextGradient resolves token stops to OKLCH via var()). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
| [`TextCircular`](../../src/uix/eidos/components/text-circular/README.md) | decorative (JS) | Characters on a spinning circle; hover retunes the spin. |
| [`TextBlur`](../../src/uix/eidos/components/text-blur/README.md) | decorative (WAAPI) | Staggered blur→sharp entrance per word/letter on viewport entry. |
| [`TextFocus`](../../src/uix/eidos/components/text-focus/README.md) | decorative (CSS+JS) | Every word blurred except the active one; a corner frame travels to it. |
| [`TextScramble`](../../src/uix/eidos/components/text-scramble/README.md) | decorative (JS) | Characters near the pointer flicker through a charset and settle back. |
**`CountUp` is a service component**, not a text effect (D-T2): it is the
animated sibling of `<FormatNumber>` — formatting is FormatNumber's job,
counting is CountUp's, and both route through the same locale-aware runtime
formatter. It carries no CSS recipe (the E-2.2 exception), like the rest of
the service family (`Trans` / `FormatNumber` / `FormatDate` / `RelativeTime`).
The five `Text*` decoratives (D-T4 naming: the `Text*` prefix groups them in
the catalog beside the typographic primitives) are **eidos-native passive
components** — a morfo (parts + data-attrs, no events), an eidos recipe where
there is styling to own, and NO soma provider: none owns an accessible state
machine, so the 2-of-3 rule leaves them at eidos-only. Their interaction
(hover retune, pointer proximity) is decorative modulation of an ambient
loop, not a semantic event — hence a **0-event morfo** with a passive
justification, the same class as `banner` / `box`.
## Why canon and not the pack
A background is invisible to the a11y tree (`aria-hidden`, pointer-transparent)
— nothing in the system selects against it, so it earns the pack's exemption
from the acceptance matrix. A text effect wraps content a screen reader must
read and a keyboard user may reach. That is contract surface by definition:
the moment an artifact owns an accessibility obligation, it enters the canon
through the normal route and takes its row in `component:audit`. The
[admission rule](../architecture/packs.md#the-admission-rule-canon-vs-pack)
is the same rule read from the other side.
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los ejes que F3 añade, y separarlas dejaría un estado que nunca se probó. Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la deriva contra el puntero), `spotlight` y `attach='fixed'`. El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca `ScrollProgress` y escribe `--background-progress`, que la rama `@supports not` mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll del propio lector, que es justo la clase que provoca síntomas vestibulares. **La decisión que no estaba prevista.** El scroll y el puntero quieren mover la MISMA capa, 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` y nunca el shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`. El precio, dicho porque en la primera redacción escribí lo contrario tres veces: una custom property NO se puede compositar, así que el navegador recalcula estilo cada frame. Para una decoración es el intercambio correcto —una property por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era falso y ahora el código dice lo que ocurre. **Dos footguns cerrados por forma, no por disciplina:** - `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes había que escribirlo también en la pila, y olvidarlo dejaba la capa `position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae de vuelta). La prop de la pila desaparece: no hay nada que olvidar. - Un `speed` negativo —una capa que se mueve contra el scroll— invertía el bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora usa la magnitud. **Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones —`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio, `declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde el init, y seguir el prop sin escribir en la primera pasada. **La demo** (`/uix/components/background`, v2, nueve pestañas) monta un ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí solo y sin padre no se puede enseñar lo único que importa: que el padre se adopta y el layout no se mueve. Los chips son uniones completas verificadas por el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación. Y fue la demo la que destapó que, con A30, encender `animate` en caliente no hacía aparecer el control de pausa — el registro era un hecho de montaje. Invisible en una sonda, obvio con un interruptor. Registro documental (D-BG.11): `next-features.md` §11 · la frase en `design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) · `PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y `Aura` corregida (decía «Not built yet» y está construido) · y en `motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un preset nombra una transición discreta CON duración, y esto es modulación continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor. Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el `translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres de las cuatro clases de capa hasta la segunda auditoría). ⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60 fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con `animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome visible. Gates: audit `--only background` 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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Backgrounds later showed the rule cuts through the middle of a subject rather
than around it. The animated EFFECT stayed in the pack; the HOST of a surface's
layers — [`Background`](../../src/uix/eidos/components/background/README.md) —
entered the canon, because it owns a contract: named parts, `aria-hidden`
layers, the pause control WCAG 2.2.2 requires of anything that moves on its own,
and a token surface a theme swaps. One subject, two tiers, and the seam between
them is a layer the pack mounts its scene into.
feat(packs+text): incorporate the animation collection — Ambient pack + text-effects family + docs Two streams, split by what the animation touches: STREAM A — decorative backgrounds → the pack tier - arts/scene: a consolidated scene runtime ($scene) that owns, once, the citizenship every ad-hoc background reinvented or skipped (frame loop, off-view pause, DPR cap, mandatory reduced-motion, WebGL context loss/restore, scene budget, teardown). SceneDom port (adom satisfies it), webgl/webgl2/canvas2d drivers + a custom-pipeline extension (vertexShader + draw + glContext.depth/dprCap) for real geometry (beam, particles, dither, grid, eter, pixel-blast, hyperspeed). 32 effects as shared resources. - src/packs/ambient: the first pack — <Ambient effect="…"> mounts a registered effect; the P contract (P-1..P-6) guarded by scripts/packs-check.ts; colors are token-aware (P-4). One-way dependency, removable-by-construction. - resolveToken extended to semantic color slots (--color-{role}-{slot}) so consumers resolve theme tokens to concrete colors (the P-4 half). STREAM B — animations over real text → canon - Six components (count-up + text-{gradient,circular,blur,focus,scramble}): each a morfo + eidos recipe (where there's styling) + demo. CountUp is a service component (counts through uix.format.numbers). The five Text* are passive decoratives. Upgrades over the seeds: SR hardening (real text visually-hidden + aria-hidden decoration), a11y fix (no fake role=button), measurement discipline (cached rects via dom.measure, no reflow storm), reduced-motion, ecosystem citizenship (eidos.dom/timers, no raw platform). - MorfoElement gains 'p'. DOCS - docs/architecture/packs.md (pack tier, admission rule, P contract, Aura promotion path); docs/decisions/design-text-effects.md (the family design record) + indexed in decisions.md / README.md; glossary entries (scene/Ambient/Aura/text effects); scene README custom-pipeline + authoring bridge; motion-guide content-effects note; strata tables acknowledge packs. Gates: component:audit 141/0/0 · docs:check 0/0 · scene tests 9/9 · packs:check 0/36 · check 0 own errors. Verified in browser (32 effects mount+compile; 6 text components SSR+hydrate, CountUp re-formats by locale live, TextGradient resolves token stops to OKLCH via var()). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## The doctrine every member obeys
The seeds were self-contained demos; bringing them into the ecosystem meant
correcting the same citizenship gaps in each. These corrections ARE the design
— they are what makes a "text animation" a UIX component rather than a pasted
snippet:
1. **The content is the accessibility surface, always.** Effects that split
text into per-character/word spans (`TextBlur`, `TextScramble`) keep the
real string in a visually-hidden node and mark every animated fragment
`aria-hidden` — a screen reader reads one continuous string, never
letter-by-letter debris. `TextCircular` uses `role="img"` + `aria-label`.
`TextGradient` leaves the text untouched (paint-only). This is an **upgrade
over the seeds**, which shipped no SR surface.
2. **No fake interactivity.** `TextFocus`'s seed stamped `role="button"` +
`tabindex="0"` on every word with no activatable action — a keyboard trap
of phantom buttons. The component drops them: the words are presentational,
the sentence reads as text. An effect only claims an interactive role when
it owns a real interactive contract (which would move it up a class — see
each component's Gaps).
3. **Measurement discipline.** Layout reads run coalesced post-layout via
`dom.measure`, never sync-after-write. `TextScramble`'s seed measured every
character's rect on every `pointermove` (a reflow storm); the component
caches character centers once per layout (invalidated by `ResizeObserver`)
and does a single container read per move. `TextFocus` reads the active
word's rect the same way.
4. **Reduced motion is honored.** Where a seed ignored it (`TextCircular`
spun forever), the component renders a static, legible state under
`prefers-reduced-motion`; the others jump to the final frame or freeze.
5. **Platform access through the ecosystem.** `IntersectionObserver` and rAF
arrive via `eidos.dom` (`observeIntersection` / `requestFrame`); delay and
cadence timers via `eidos.timers`; the reduced-motion query via
`eidos.dom.prefersReducedMotion`. No raw `setTimeout` / `setInterval` /
`requestAnimationFrame` / `matchMedia`.
6. **Theme-aware color.** Color params accept eidos token names — CSS-native
`var()` interpolation for the gradient stops (`TextGradient` re-tints on a
mode switch with zero JS), or `var()`-wrapped custom properties for the
feat(text-gradient): `colors` discriminates canonical name vs stop list (folds `preset`) Per request: one `colors` prop that discriminates by shape instead of two rival props. A `string[]` is an explicit stop list (unchanged); a bare `string` names a canonical gradient from the theming's named set, resolved as `var(--gradient-{name})` — exactly what the removed `preset` prop did. The set is an open cage (`EidosConfig.gradients` / `applyGradients`), so the type stays `string | readonly string[]`, not a closed union. Removed `preset` outright (no back-compat shim, per project rule); the canonical S9 vocabulary and raw stops are now the SAME input. Docs shell: register a small canonical named set on `uix/+layout@` via `applyGradients` (`brand`, `spectrum`) — role-derived, so they re-tint with the seed and flip light/dark. Demo gains a live stops|named toggle so both branches are a real testbed. Finding worth recording (types + README): a named gradient painted through `background-clip: text` must be a single `<image>` (linear/radial/conic). A role-derived MESH serializes to radials + a base COLOR, and a bare color is not a valid `background-image` (only the `background` shorthand accepts it) — so a mesh name computes to `none` and never paints. The docs set is linear-only for this reason. Verified in real Chrome: stops branch paints the token gradient; `colors="spectrum"` paints `var(--gradient-spectrum)` through the text. svelte-check 0 own errors, component:audit 141/0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
focus frame. `TextGradient`'s `colors` prop is discriminated by shape: a
`string[]` is a free stop list, a bare `string` names a canonical gradient
(`--gradient-{name}`, decision D-T3) — so the S9 gradient vocabulary and raw
stops are the SAME input, discriminated by shape, not two rival props.
feat(packs+text): incorporate the animation collection — Ambient pack + text-effects family + docs Two streams, split by what the animation touches: STREAM A — decorative backgrounds → the pack tier - arts/scene: a consolidated scene runtime ($scene) that owns, once, the citizenship every ad-hoc background reinvented or skipped (frame loop, off-view pause, DPR cap, mandatory reduced-motion, WebGL context loss/restore, scene budget, teardown). SceneDom port (adom satisfies it), webgl/webgl2/canvas2d drivers + a custom-pipeline extension (vertexShader + draw + glContext.depth/dprCap) for real geometry (beam, particles, dither, grid, eter, pixel-blast, hyperspeed). 32 effects as shared resources. - src/packs/ambient: the first pack — <Ambient effect="…"> mounts a registered effect; the P contract (P-1..P-6) guarded by scripts/packs-check.ts; colors are token-aware (P-4). One-way dependency, removable-by-construction. - resolveToken extended to semantic color slots (--color-{role}-{slot}) so consumers resolve theme tokens to concrete colors (the P-4 half). STREAM B — animations over real text → canon - Six components (count-up + text-{gradient,circular,blur,focus,scramble}): each a morfo + eidos recipe (where there's styling) + demo. CountUp is a service component (counts through uix.format.numbers). The five Text* are passive decoratives. Upgrades over the seeds: SR hardening (real text visually-hidden + aria-hidden decoration), a11y fix (no fake role=button), measurement discipline (cached rects via dom.measure, no reflow storm), reduced-motion, ecosystem citizenship (eidos.dom/timers, no raw platform). - MorfoElement gains 'p'. DOCS - docs/architecture/packs.md (pack tier, admission rule, P contract, Aura promotion path); docs/decisions/design-text-effects.md (the family design record) + indexed in decisions.md / README.md; glossary entries (scene/Ambient/Aura/text effects); scene README custom-pipeline + authoring bridge; motion-guide content-effects note; strata tables acknowledge packs. Gates: component:audit 141/0/0 · docs:check 0/0 · scene tests 9/9 · packs:check 0/36 · check 0 own errors. Verified in browser (32 effects mount+compile; 6 text components SSR+hydrate, CountUp re-formats by locale live, TextGradient resolves token stops to OKLCH via var()). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## Where the animation lives (per member, D-T5)
Animation home follows the motion doctrine (perceptual anchoring; animation =
a channel), not a single mechanism:
- **CountUp** — an analytic damped-harmonic-oscillator spring evaluated in JS
per frame. `$motion`'s `spring` driver animates element CSS properties, not
numeric callbacks, so it does not apply; the spring is local.
- **TextGradient** — pure CSS keyframes in the recipe (an ambient
background-position pan), annotated `/* functional: */` per R-4.5.
- **TextBlur** — WAAPI stagger from the wrapper. This is a content ENTRANCE;
when the motion service's content domain lands
(`src/uix/eidos/MOTION_SERVICE_RFC.md`) the stagger migrates behind it.
- **TextCircular / TextScramble** — continuous JS state stepped by
`eidos.dom.requestFrame`.
- **TextFocus** — CSS transitions gated by `eidos.timers`.
## The Aura connection
`TextCircular` is orbital text around a round shape — a candidate piece of the
periphery of the future agent-presence component (reserved name `Aura`; the
promotion path is in [`architecture/packs.md`](../architecture/packs.md#the-promotion-path-the-agentic-direction)).
It stays presentation-only so `Aura` can compose it; the agent-lifecycle
events would be declared by `Aura`'s morfo (`delegate` / `sustain`), never by
`TextCircular`.
## Verification
Each member self-documents (per-component README with design · usage · API ·
Baseline · Comparativa · Gaps · Passive justification) and ships an
interactive demo at `web/routes/uix/components/{kebab}/`. The family passes the
acceptance matrix (`npm run component:audit`) as six passive components. The
per-effect user-facing decisions (D-T1…D-T5) were made in the session and are
summarized above; their working record is the (ephemeral) execution plan
`docs/process/PLAN-text-effects.md`.

Powered by TurnKey Linux.