# THEME mode — composed visual constitutions

Theme mode is the third compiler (`scripts/make-theme.cjs`), beside Standard and
Cinematic. It exists because "mode" was a bundle of orthogonal axes pretending
to be one switch. A theme DNA composes its identity from registries implemented
ONCE in the compiler — **paradigms are the unit of code; DNAs are the unit of
identity**. A new look is a JSON file; only a genuinely new paradigm/setpiece
(rare) touches the compiler.

```
theme DNA = body PARADIGM   how the transcript surface lives
          × body LAYER      fg-alpha (rail.html channel) | bg-embed
          × hero SETPIECE   the climax choreography
          × front FX        flash / rings / sparks / scanband / crowdflash / paflash (fg, over subject)
          × PLATE budget    charge-dim (in-page) + punch/shake/grain (_postfx.sh)
          × LINKAGES        declarative theme interactions
```

Standard and Cinematic were, in retrospect, two fixed points of this space.
Standard is now RETIRED (2026-06-12): its rail×embed-climax point is served by
the `anchor` theme (rail paradigm × settle setpiece — the quiet default).
Cinematic remains a separate compiler; do not re-implement it as a theme yet.

## Unification roadmap (strangler fig — interface first, engines later)

The user-facing model is already unified (SKILL.md Step 0): one catalog of
LOOKS; classic looks pick a DELIVERY (rail | column), themed looks bind their
own. "Standard/Cinematic" are delivery/compiler names, not modes. Remaining
phases, each gated on need — never rewrite for tidiness alone:

- **Phase 2 — one authoring schema.** `lines`/`minors`/`hero` are already
  ~90% shared between standard.json and theme.json; a router that translates a
  single `caption.json` into the engine-specific file removes the last
  user-visible seam. cinematic.json's blocks/planes are the odd one — map the
  common fields, pass engine-specific ones through.
- **Phase 3 — engine convergence.** Port a classic delivery into make-theme
  ONLY when something forces it (e.g. a classic DNA wants a plate budget or a
  setpiece). Acceptance bar: blind A/B on the cap_multi regression scenes vs
  the old compiler — swap engines only when indistinguishable or better. Until
  then the old compilers are the reference implementation of 8 rounds of
  validated typography (lockup/orbit, multi-climax, ratio-lock, per-plane
  legibility, occlusion adjudication) — that machinery is the moat, not debt.

## Body paradigms (registry)

| paradigm      | surface                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | layer | entrance verbs                                                                                                                                                                                                                                                                                           | exit verbs                                                                                                                                                                                                                         |
| ------------- |   | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rail`        | lower-third lines, replace                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | fg    | `stamp` (1f hot appear→crush→recoil→cool), `flick` (tube double-flick), `bootflick` (corrupted-cyan boot 1–2f → settles to UI color; minors keep a cyan/red registration shadow). `stamp` minors cool to `palette.minorCool` (default amber)                                                             | `drop` (mass falls), `powercut` (bright→ghost→off)                                                                                                                                                                                 |
| `panel`       | docked glass console, accumulate + typed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | fg    | `type` (width steps per word, $ prompt, caret blinks)                                                                                                                                                                                                                                                    | hold                                                                                                                                                                                                                               |
| `poem`        | open-space stanzas, accumulate; letters CONDENSE from seeded dust                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | fg    | `condense`                                                                                                                                                                                                                                                                                               | `drift` (stanza hand-off) + optional dispersal linkage                                                                                                                                                                             |
| `takeover`    | full-frame hard-cut cards, plate dimmed throughout                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | bg    | `cut` (+ tension `creep` before silences, auto-detected)                                                                                                                                                                                                                                                 | hard cut / blur dissolve                                                                                                                                                                                                           |
| `lastpage`    | manuscript rail (typed serif) + a seeded field of BLURRED apex-word instances haunting the room from t=0                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | bg    | typed rail; field breathes imperceptibly                                                                                                                                                                                                                                                                 | rail fades; field resolves at apex (see `rackfocus`)                                                                                                                                                                               |
| `flaprail`    | split-flap word tiles on a chyron bar (tag + ROW flip-counter; optional odometer chip docks above the bar while the first emphasis line reads)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | fg    | `splitflap` (3 flips: wrong word → wrong word → real word clacks on hot, squash → elastic settle → cools to bone; emphasis tiles keep amber text + underline)                                                                                                                                            | flip-to-blank cascade left→right (completes before the next line)                                                                                                                                                                  |
| `ledboard`    | amber LED departure strip docked lower-third (`body.board` geometry); lines page by vertical scroll-snap (steps(6)) inside the board window; furniture: green platform tag, ticking seconds clock (stacked digit reel), ⚠ DELAYED blinks then pages to ● ON TIME ~0.7s after the apex, idle LED shimmer in dead air ≥1.2s                                                                                                                                                                                                                                                                                                                        | fg    | `ledwipe-mini` (per-word L→R LED column wipe, clipPath steps(len), + 1-frame arrival flicker; emphasis words get the LED-refresh double-blink; last word gets one late refresh)                                                                                                                          | scroll-snap page-up (next line pages in at the same instant)                                                                                                                                                                       |
| `vhsrail`     | camcorder-playback lower third: every word carries a PERMANENT 1px red+cyan misregistration fringe (split clip-path halves); furniture: ▶ PLAY / ◀◀ REW top-left (REW gag in dead air ≥0.7s + the final rewind), ● REC top-right through the apex window, green timestamp bottom-right with 1 Hz blinking colon, idle tape-drift line in long silences, plus apex-coupled artifacts (1-frame white tear band + head-switching noise bar at REC FREEZE, scrub lines at the apex band on the hero rewind)                                                                                                                                          | fg    | `trackglitch` (instant on + 2-frame y-shake + 1-frame band-slice shift of the word's bottom half; emphasis = PAUSE jitter ±1px y oscillation + white noise-band flicker)                                                                                                                                 | REWIND streak left + flat-white echo copy + 2 scrub-line flickers                                                                                                                                                                  |
| `hudrail`     | arcade-cabinet HUD: whole frame plays through CRT overlays (scanlines `body.scan` + corner vignette); docked HUD bar (`body.bar` geometry, 2px accent border, glow) with `body.tags` 1UP / CREDIT furniture — the 1UP tag blinks on the classic 0.45s attract cadence all clip; HUD yields (`body.yield`) while the boss lands                                                                                                                                                                                                                                                                                                                   | fg    | `coinpop` (instant-on + steps(2) scale pop from the baseline + 1-frame white coin-blip tick over the word; emphasis = 3-frame palette swap body→accent→magenta→body)                                                                                                                                     | checkerboard dissolve (words vanish in 2 alternating 2-frame passes, line off ≤ DUR−0.02)                                                                                                                                          |
| `carbonstrip` | cold-war case-file transcript strip docked on the clearer side (manila gradient, punch holes, faint red `body.watermark`, typed `body.header` that types on by width steps); lines live on up to `body.rows` fixed slots; PAGES (broken at full rows, dead-air gaps > 0.7s, and after the hero hand-off line — it is filed away while the stamp owns the frame); the strip top-shadow breathes during the stamp's dead-still hold and the strip yields (`body.yield`) while it lands; the final page is pulled with the whole strip at clip end                                                                                                  | fg    | `hammer` (per-char sets 30ms apart, scale 1.35→1 in one frame = the stamp motif at low amplitude; seeded ±1.2px baselines + seeded ribbon-ink opacity; every strike kicks the strip 1px — y-channel locked while the strip itself tweens; emphasis words get a red ink ellipse drawn around them, 0.25s) | X-row over-strike (3 chunks per line, 0.05s apart, lines 0.16s apart) → carriage yank-down feed → return; roomy cadence in dead air, compressed when the next page is close — always completes before the next page's first strike |
| `laserrail`   | concert lower third: lines centered on a bottom rail (`body.bottomPx`), uppercase laser-green words with a neon text-glow; emphasis words render at `body.minorScale`× with a hotter glow; word centers are BAKED at compile time from the measured char-width table (`assets/fonts/char-widths.json`, ≤3px vs browser ground truth) so the beam aim can't be skewed by font-load timing; per-word furniture = two thin beams (accent from top-left, magenta from top-right) that sweep to CONVERGE on the word 2 frames early + a convergence flare; beams dim ×`yield.beamDim` inside the apex window and the visible line dims to `yield.dim` | fg    | `igniteconverge` (beams converge at st−0.083 → flare pop → word snaps on at opacity 1 with brightness 1.85, crush 0.09s power3.in → 2-frame contact squash → elastic settle → brightness cools; emphasis = single 1-frame strobe off/on + lingering beams)                                               | sweeper beam crosses L→R in 0.13s and each word streaks away (x+44, blur 5) as it passes; line off ≤ DUR−0.02                                                                                                                      |

| `stormrail` | rain-washed lower third: ambient rain = `body.rain.count` seeded thin streaks falling on repeating linear cycle tweens (pure f(t), one slanted wind vector, mid-fall at t=0); a second seeded set DOUBLES the rain for ~1s after the strike; lines sit on a bottom gradient scrim (stable reading surface); the visible line dims to `yield.dim` while the bolt lands and its text-shadow flips toward the strike (`hero.params.side`) for the flash frames | fg | `lightningpop` (the apex motif at low amplitude: instant on at brightness 2.2, y+4, scale 1.06 → 0.12s micro settle; emphasis = sheet-lightning double flash, 2-frame brightness 1.9/1.0/1.8 chain — the bg sky double-kisses at the same beats) | wash downward with the rain (y+22, blur 3, 0.18s power1.in); a line facing dead air washes early at lastWord+1.08s; line off ≤ DUR−0.02 |
| `holorail` | volumetric projection plate: a scanlined glass slab (`body.plateW`×`plateH`, centered at `H − bottomPx`) with corner brackets + feed tag (`body.tag` / `theme.plateTag` override) boots like a projector turning ON (scaleY 0.03→1 expo + 1-frame flicker) and breathes a finite yoyo luminance cycle sized to the clip length; lines sit centered IN the plate (`lineDy` below plate center) with a constant chromatic-fringe text-shadow (−1.5px accent / +1.5px magenta) and drift slowly in rotationY (it IS a projection); the whole railwrap — plate included — dims to `yield.dim` while the apex lands | fg | `sweepboot` (the apex motif at low amplitude: instant opacity + rising clip-wipe reveal 0.15s + 1-frame x interference glitch at st+0.05/0.09/0.13; emphasis = 2-frame projection DROPOUT at `body.emphDelay`, restores brighter in `palette.em` with a hotter fringe, then settles back — the delay auto-shrinks near the line's exit so dropout+restore always complete) | projector cut — brightness 2.2 pop → collapse to a horizontal slit (scaleY 0.045, 0.07s power3.in) → to a dot (scaleX 0.015, 0.06s) → off at exit+0.15; the plate itself gets the same cut at DUR−0.14 |
| `planktonrail` | deep-sea lower third on TWO coexisting row slots: even lines ride the upper row (`H − bottomPx − rowDy`), odd lines the lower (`H − bottomPx`) — couplets share the water like drifting strata. A line clears when its row is next needed (line i+2 in − 0.15) or ~0.3s after its couplet finishes speaking (staggered pair exits, +0.12s on odd rows); the pre-apex leftover clears at heroIn−0.21 while the FEEDING line (last to start before the apex) yields to `yield.dim` instead — no restore unless ≥1.2s of hold remains (it exits dimmed, by design); a line entering during the hold arrives at `yield.enter` and restores at I+`yield.post`; every line floats on a gentle y keyframe drift sized to its window. Furniture: `body.motes` = {count, near, seed} plankton motes on seeded drift walks f(t); the first `near` seed a ring around the apex heart (ring radius scales with hero fontPx/120) and get ATTRACTED to the bloom at I−0.19 (pulled 0.72× toward it, brightening to 0.42 at scale 1.7), wiggle through the hold, and sink at clip end | fg | `glowon` (the apex bloom at low amplitude: 2-frame opacity ramp + radial bloom from word center — scale 0.92→1, blur 5→0, brightness 0.45→1, 0.4s sine.out — while a cyan glow text-shadow grows in; emphasis = slow DOUBLE-BEAT propulsion pulse at st+0.52, 5-key brightness/scale 0.55s, guarded ≤ DUR−0.09) | light sinks — y+15 while dimming, 0.3s power1.in; the last line floats to the end of the clip; line off ≤ DUR−0.02 |
| `sheenrail` | iridescent silk lower third on TWO coexisting row slots: even lines ride the upper row (`H − bottomPx − rowDy`), odd lines the lower (`H − bottomPx`) — couplets read together like a woven pair. Words are aurora-gradient ink (background-clip:text over a rose→teal→violet 300% wash whose backgroundPosition drifts f(t) 0%→300% across the whole clip — the body's hold life) above a blurred dark understroke (top+3px, blur 3); a soft radial band scrim sits under the rows (stable reading surface). Line scheduling mirrors planktonrail: a row clears when next needed (line i+2 in − 0.15) or shortly after its couplet finishes speaking (pairEnd + 0.23 + 0.17 on odd rows); the pre-apex leftover clears at heroIn−0.32 while the FEEDING line stays and dims with the railwrap yield (`yield.dim` at I−`pre`, restore at I+`post` — band included); the last line holds to the end | fg | `flowon` (the ribbon's flow at low amplitude: 2-frame opacity + y8 settle 0.38s sine.out; emphasis = ONE bright sheen sweep — a white highlight marches through the word once at st+0.16 (backgroundPosition −80%→180%, 0.42s power2.inOut) + a brightness 1.55 pulse) | dissolve UPWARD — words to y−12, blur 3, 0.32s power1.in staggered 0.035; each line releases `body.streaks` seeded light streaks (rose/teal/violet verticals) that float up from its row (y 14→−78, 0.62s, start clamped ≤ DUR−0.66) |
| `scoperail` | oscilloscope lower third: a dark phosphor scope band (`body.bandH`, top border + glass shadow) hosts HUD readouts (`body.hudL` left, sample-rate `· TRIG` + amber trig LED right), a graticule, and an ALWAYS-ALIVE waveform — an SVG polyline rebuilt from baked point-set states (seeded pseudo-RMS envelope at ~12 states/s + 3-frame triplets on word arrivals: speech grows the amplitude, silence flattens it, every word kicks a local gaussian burst at its slot center — burst x BAKED at compile time from the measured char-width table; the hero words kick WIDE bursts spread from `HG.x` toward `HG.x + 0.6·halfW`, where the trace surges to write). Body lines sit centered on the band above the trace with reserved-width slots (sizer + clip — no re-center drift as words arrive); the whole band (lines + wave + HUD) dims to `yield.dim` while the apex writes. At the FINAL transcript word the scope DIES: band x-shake, trig LED panics red then dies, the trace explodes to max-amp chaos for ~0.3s, flatlines, collapses to a single dot (scaleX 0.015) and `body.siglost` stamps with a 2-frame flicker (all clamped ≤ DUR−0.02) | fg | `tracewipe` (the apex pen-write at low amplitude: clip wipes the word in L→R over 0.10s + a 2-frame just-traced flash — flash-white instantly, settle to phosphor green; the death word settles to `palette.em` amber; the trig LED blinks on every arrival; emphasis = a ticking frequency-readout chip above the word, 3 seeded ascending values stepping every 2 frames) | the line collapses INTO the waveform (y+30, 0.13s power2.in) — early into dead air (lastWordStart+0.67), else just before the next line needs the slot; the last line collapses as the trace flatlines (min(NZ+0.33, DUR−0.2)) |
| `paperrail` | stop-motion craft table: a kraft strip (torn top edge via seeded polygon clip-path, washi-taped corners, handwritten `body.tag` in the tag font) docked lower-third (`body.strip` geometry, centered by default); body words are cream paper chips (`palette.chip` on `palette.chipEdge` border, hard dy3 shadow) on centered replace-lines; the whole strip jitters ±0.7px every 4 frames (handmade stop-motion weave); the hero slot keeps a red paper star (`palette.accent`, rail↔climax hand-off) that pulses 1.18→1.06→1 as the apex chips land; the FINAL transcript word is an accent-red sign-off chip; emphasis words get a washi-tape slap (steps(2) scale 1.7→1 at `word.start + body.tapeDelay`, chip pressed 1 frame); strip + lines yield (`body.yield`) while the apex lands | fg | `place` (the papermat drop at low amplitude: opacity full in 0 frames + seeded ±2.5° rotation, y −16 + scale 1.06→1 steps(2) 0.12s; hold = rotation-only ~5fps wobble) | `peel` (rotation −8°, y −40, fade, steps(3), staggered 0.03s; last line clamped to finish on frame) |
| `popuprail` | pop-up book page: a cream page strip (`body.strip` geometry, center foldline, top border + paper glow inset) unfolds up from the bottom edge (scaleY 0.02→1 back.out at 0.14 — the stage is built) and hosts TWO reading rows (`body.rowA`/`rowB` px above H) that alternate per line; the whole page breathes a gentle y drift sized to the clip; the hero slot keeps a blue paper star chip (`palette.accent` clip-path star, rail↔climax hand-off — spliced into the line whose window holds heroIn); emphasis words land at `body.emPx` weight 800 with a contact squash (1.06/0.93 set → elastic settle) the strip FEELS (1px dip), then flick-wobble (5-key rotationX, squeezed to fit before the fold) with a blue paper strut leaning in behind (scaleY back.out(2)); the FINAL transcript word is a blue sign-off with a strut; page yields (`body.yield`) while the centerfold lands; on the final word the whole page TILTS 2° +x6 (paper physics, `body.tilt:false` opts out) and the strip folds shut on the last frame (scaleY power3.in ≤ DUR−0.017) | fg | `popup` (the centerfold motif at low amplitude: opacity 1 in 0 frames + rotationX 85→0 baseline hinge, 0.25s back.out(1.4), back-shadow grows as it rises) | `fold` (the page closing: rotationX 88 power2.in 0.12s, shadow off, L→R wave staggered 0.045s, slot hidden at +0.13; fold at next line in / lastWord+`body.maxHold` in silence, wave clamped ≤ DUR−0.017) |
| `chalkrail` | night-lecture chalk band: a deep-green board strip (`body.strip` geometry, worn top hairline, eraser smudges, handwritten `body.tag` header in the tag font, two chalk sticks bottom-right) docked at the bottom edge hosts `body.rows` reading rows that ACCUMULATE — lines fill the rows top→bottom in phases of rows.length; when the next phase needs the rows an ERASER SWIPE clears the board (blurred light band sweeps across 0.42s at `prevEnd + body.eraseDelay` clamped clear of the next phase, the old words SMEAR out scaleX 1.3 + x+24 + blur(8) staggered 0.06s, faint ghost smudges fade in and STAY — chalk never fully erases); the FINAL transcript word lands in yellow chalk (`palette.em` sign-off); the band yields (`body.yield`) while the apex writes | fg | `scribble` (the chalkwrite verb at low amplitude: 2-frame opacity pop + scribble clip-reveal steps(4) 0.12s + y4 drop-settle + 6 seeded dust specks puffing at the word base, seeded ±2° tilt; emphasis = double underline dashes drawn beneath at st+0.22/+0.38) | eraser-swipe smear between phases (scaleX 1.3, x+24, blur 8, power2.in 0.18s); the final phase holds to clip end |
| `markerrail` | street handstyle rail: centered marker lines on a bottom rail (`body.bottomPx`), `palette.body` cream paint with a heavy drop text-shadow; emphasis words take ALTERNATING paint colors (`palette.em`/`em2`, yellow/magenta) + a spray-underline SWOOSH (dash-revealed squiggle viewBox-stretched to the word) with 8 seeded overspray dots that pop and fade; the FINAL transcript word lands BIG (1.5×) in `palette.em` with mini apex-impact physics (crush 0.09s power3.in → 2-frame contact squash → elastic settle — it rides the plate punch); the line visible at the tag's onset yields (`body.yield`, restore only with runway before its buff — else it exits dimmed) | fg | `flick` (the spray-write verb at low amplitude: opacity full in 1 frame at scale 1.18, seeded ±6° rotation settling to ±2°, 1-frame squeak skewX 7, back.out(2.2) settle 0.18s) | ROLLER BUFF (gray paint roller scaleX-sweeps the line 0.15s power2.in, words vanish under it, the wet smear lingers at 0.15 + blur(2) before fading); the last line is never buffed — the sign-off stays painted |
| `brushrail` | washi paper band: a deckle-edged band (`body.strip` geometry, two faintly rotated `paperHi`→`paperLo` under-sheets at 0.42 + a translucent `sheetHi`→`sheetLo` main sheet with warm inset shadow) swipes in L→R behind a traveling brush-head shadow (clipPath wipe 0.2s power2.out + blurred `palette.bh` gradient head — the ink gesture at low amplitude), then the under-sheet breathes faintly (0.32 Hz baked f(t)); body lines are centered replace-lines on the band (`fonts.body` serif at `body.fontPx`/`body.weight`, seeded ±1° tilt); emphasis words get a DOUBLE curved ink underline (`palette.ulInk`, fat 7/0.3 wash + tight 2.6/0.95 core, dash-drawn at st+0.12/+0.15); band + the line under the gesture yield (`body.yield`) while the apex lands; the band exits with the last line on one final R→L wipe | fg | `brush-wipe` (the gesture at low amplitude: clipPath inset reveal L→R 0.16s power2.out with a blurred per-word brush-head shadow riding the reveal edge, then vanishing) | wet-cloth WIPE (the reveal in reverse: the line clipPath wipes R→L over 0.10–0.14s power2.in with an erase brush head riding it, at min(next line − 0.136, lastWord + `body.maxHold`); the LAST line and the band wipe together at DUR−0.142) |
| `inkrail` | sumi ink on still water: up to THREE fixed reading rows at the bottom center (`body.bottomPx` = last-row center, `body.rowGap` apart; rows cycle in pairs, the closing line takes the third row — the final thought ACCUMULATES instead of replacing); `fonts.body` Mincho serif at `body.fontPx`/`body.weight`/`body.letterSpacing`, `palette.body` ink over a paper-white text-shadow halo (`palette.paper`/`paper2` glow stack — readable on bright skies) with a duplicate blurred dark halo per word; an ink-settling RULE (86%-width gradient hairline, scaleX 0→1 over 0.9s) draws beneath each line at firstWord+0.12; emphasis words shed a faint ink wisp curling off the word top (st+0.55, ±dir by line parity, skipped near clip end); the rows visible under the bloom yield (`body.yield` — dim at heroIn+`delay`, restore at heroIn+`post` only with runway before the exit) | fg | `bleed` (the apex bloom at low amplitude: opacity snaps in 2 frames; blur 12→0 + scale 1.25→1 power2.out carry the bleed — late words bleed faster so they resolve before clip end; the dark halo overshoots scale 1.5→1.04 while its opacity decays 0.5→0.25) | RE-DISSOLVE (blur 9 + scale 1.13 + fade + y−8 power1.in — ink dispersing back into the water) as the line-after-next approaches (= the next occupant of its row, outDur 0.5−0.05·i, never mid-read); the closing TWO lines persist to clip end |
| `ransomrail` | kidnapper's collage rail: every transcript word is a cutout chip in one of 4 cycled recipes (cream/`fonts.body` Anton, black/`fonts.dark` Inter-800, newsprint/`fonts.news` Special Elite, accent/`fonts.accent` Permanent Marker — emphasis words snap to the accent chip, never two identical chips in a row) PASTED along the bottom rail line (`body.bottomPx`) with seeded crookedness (rot sign alternates with line+word parity, baseline dy leans the other way), seeded ±px sizes (`body.fontPx`−2..+1, emphasis +3) and torn edges (seeded 12-vertex tear polygon, same paper stock as the apex); chips carry a hard dy5 offset drop-shadow; lines breathe (5-key scale keyframes) through their hold; the whole railwrap yields (`body.yield`, container opacity — never contests per-chip channels) while the apex letters slam | fg | `paste` (the apex slam motif at low amplitude: opacity full in 1 frame at scale 1.12 / rot×1.5 / y−8 → power3.in crush 0.08s → 2-frame contact squash 1.05/0.95 → elastic settle; emphasis = 1-frame RE-PASTE lift at `body.emphDelay` — shadow grows to a flight blur, rotation flips sign for good, then crush + squash + elastic again, rip-runway-guarded) | RIPPED off one by one (rotation ±16/17° throw + y−30 + fade, power2.in, staggered 0.045s) with a 1-frame paper-tear flash under each chip — timed so the rip tail overlaps the next line's pastes a beat (collage chaos, by design); the last line tears off clamped to finish on frame |

fg body uses the **rail.html alpha-webm channel** (true alpha — dark panels and
scrims work; never the screen-blend index_fg path, which can only add light).

## Hero setpieces (registry)

| setpiece      | what happens                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | notes                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------- |   | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `detonation`  | charge-dim → sheared stencil slices crush in hot → SNAP into register → squash → elastic settle → paint cools to bone → bars/ticks/designation-tag deploy → shear-apart exit                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | pairs with fx flash/rings/sparks + plate punch/shake                                                                                                                                                                                                                                                                                                                                                                          |
| `decode`      | slot-machine glyph reels (steps() ease, seek-safe) lock left→right with RGB jitter → lock snap → CRT power-off exit                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | pairs with `redact-until-hero`                                                                                                                                                                                                                                                                                                                                                                                                |
| `drawon`      | the word is WRITTEN stroke-by-stroke from a single-line font (Hershey) — per-stroke paths revealed sequentially at constant pen speed, nib rides `getPointAtLength`, hops at pen lifts; then hum + buzz dip                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | any word, zero tuning: `gen-stroke-path.py` lays out glyph pen-paths at compile time                                                                                                                                                                                                                                                                                                                                          |
| `cpslam`      | acid-yellow stencil word slams in BEHIND the subject with REAL diagonal notch cuts (SVG mask = true transparency over footage), chromatic split that settles to a PERMANENT cyan/red registration error, seeded glitch ticks, katakana tag, glitch-out exit                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | bounded hold (~2.6s default, `params.hold` / `hero.exitAt` override) — a climax is an event, not wallpaper                                                                                                                                                                                                                                                                                                                    |
| `assembly`    | (inline, poem) apex word condenses BIG while star particles fly into it                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `colorflip`   | (inline, takeover) accent-color crush card + dim kick + squash/settle + loom                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | pairs with fx flash + plate punch/shake                                                                                                                                                                                                                                                                                                                                                                                       |
| `flapboard`   | split-flap departures board takeover: floodlight dim/scrim swing on, the housing clacks down (rotationX, squash, elastic) behind the subject, one giant flap tile per char flip-cycles 3–5 seeded wrong glyphs then LOCKS left→right across `params.lockWindow` (hot flash → cools to bone) under baked clack y-jitter; lock-complete = big clack + amber glow flicker + rule wipe + steps() status-ticker; hold = loom + one tile re-flutter; exit = tiles flip to blank cascade, housing flips away                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | pairs with `fx.crowdflash` (pops at lock start/end) and `plate.punchOffset = lockWindow` so the plate punch lands on the lock-complete clack, not the hero onset                                                                                                                                                                                                                                                              |
| `ledwipe`     | transit LED board takeover: cool scrim + dim charge, an amber dot-matrix panel EXPANDS upward (scaleY steps(6)) behind the subject, header tags + unlit LED ghost boot-flicker alive, then the word ILLUMINATES column-by-column (clipPath L→R, steps ≈ 1.4×chars over `params.wipeDur`) hot → cools to amber; contact = cell squash + elastic settle + PA "ding" double brightness pulse; an 8-phase chasing LED-marquee border runs to the exit; hold = glow breathe + seeded-look LED dropouts; exit = word pages up out of the window, panel collapses back down                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | pairs with `fx.paflash` (amber PA flash at wipe-complete contact). Panel geometry derives from fontPx (`panelH = 44 + 1.42×fontPx + 18`); `params.panelW` fixed, width-fit shrinks the type only. Plate: grain-only (no punch) is the demo register                                                                                                                                                                           |
| `vhsosd`      | REC FREEZE camcorder slam: permanent lens vignette (`params.vignette`) + backlight-comp scrim (peak = `plate.charge`, cools to 0.69×) kill the bright sky; the word crushes in as a huge OSD readout — 3 horizontal clip-path tracking bands (`params.bandClips`) sheared (`params.bandShear`) + blurred (power4.in over `params.crush`) SNAP into register on the contact frame, squash → elastic settle; red/cyan misregistration copies cool from ±`misregIn` to ±`misreg` px and keep BREATHING through the hold under tape-weave y jitter (`params.weave`) + slow loom; auto-iris pump (white wash 0.20, dark dip = `plate.dim`); exit at `I + params.hold` = REWIND streak left + flat-white echo copy                                                                                                                                                                                                                                                                                                                                                                   | pairs with the `vhsrail` paradigm, which emits the fg REC-FREEZE artifacts (tear band, head-switching bar, apex scrub lines, ● REC) at the same I/X times. Plate: grain-only (no punch) is the demo register; hold-life keyframes auto-clamp inside [contact, exit]                                                                                                                                                           |
| `bossintro`   | arcade BOSS INTRO: the room dims (gradient `plate.dim`) behind a CRT edge glow (cyan base wash that pulses on the last emphasis word before the boss and spills magenta on the strobe + the final word); `params.tag` (WARNING) blinks 2× above the word rect; ~COLS×ROWS (`params.rows`, cells 0.7×/0.5× fontPx) seeded pixel blocks fall steps(3) from above into a tight matrix grid covering the word rect, each landing with a 1-frame white tick; at `I + params.flashDelay` the word flashes ON (Press Start 2P, instant at opacity 1, scale 1.15 → steps(3) crush → 2-frame squash → elastic settle) with ±4px chromatic ghost layers + a 3-frame palette strobe (white→cyan→magenta→white); hold = quantized 1.6% breathe + CRT glow cycle + baked block shimmer blinks; exit at `I + params.hold` = per-letter checkerboard dissolve, blocks drop away steps(3)                                                                                                                                                                                                      | pairs with the `hudrail` paradigm; emits fg furniture via setpiece fg parts: letterbox bars (`params.letterbox` px) slam in steps(3) with a 1-frame overshoot and retract after the exit, HI-SCORE odometer (accent label, yellow digits) rolls every 0.066s through the hold, 1-frame white tick out. `fx.coinflash` adds the 1-frame radial impact flash at the strike; plate punch anchors at `punchOffset` = `flashDelay` |
| `rubberstamp` | the footage becomes an archive page (vignette charge = `plate.charge`, manila sepia wash `params.sepia`, dim kick = `plate.dim` that cools to 0.385×) and the verdict slams in BEHIND the subject as a bordered rubber stamp at `params.rotation`: a manila paper wash lights an instant before contact, the stamp crushes 2.2→1 power4.in over 0.115s, 2-frame contact squash, double-strike ghost print (`opacity 0.35`, offset +5/−4 rot 1.3), two thin-ink patches (`params.patches` clip-path circles in paper color), wet-ink saturate/brightness settle, paper indent shadow bloom; stamps don't move — the hold is DEAD STILL, sold by the breathing indent + a slow paper loom; exit at `C + params.hold` = mechanical 3-frame yank-down cut (the page is filed away), the archive plate releases 0.25s before                                                                                                                                                                                                                                                        | contact `C = heroIn + params.crush`; pairs with `fx.inkflecks` (seeded red spatter at C, ballistic with gravity) and `plate.punchOffset = params.crush` so the plate punch lands on the stamp contact; pairs with the `carbonstrip` paradigm (strip yield + shade breathe anchor at the same C); face chrome (border, radius, padding, ghost offset) scales with fontPx from the 120px demo cut                               |
| `lasercage`   | concert-blackout laser show: the plate dims to `plate.dim` at t=0 and deepens to `plate.charge` before the drop while haze cones breathe in the top corners; a 6-beam array (alternating accent/magenta, emitters along the top edge) snaps on at I−0.26 and SWEEPS to converge on the word center (staggered 0.022s), a hot core pops at convergence; the word IGNITES behind the subject as an additive glow stack (wide accent blur 0.22×fontPx + magenta fringe offset + tight soft blur + near-white core): scale 1.16 crush power4.in → 2-frame contact squash → elastic settle + 2 controlled strobe pops (brightness 2.05, 1s apart, runway-guarded); a criss-cross beam CAGE (6 lines derived from the word rect) draws itself dash-by-dash around the word and flickers; through the hold the array FANS OUT slowly to floor targets (the iconic concert fan) while the word looms; exit at `heroOut − 0.38` = cage un-draws, beams sweep away L→R (+0.74W) carrying the letters (per-letter streak + blur, stagger auto-clamped ≤ DUR), the room lifts back to 0.12 | word geometry is metric-exact via `assets/fonts/char-widths.json` (cage/spill/flash all derive from `halfW` + `fontPx`); pairs with `fx.laserflash` (accent contact flash at I+0.03, sized 2·halfW+80 × 4.8·fontPx) and the `laserrail` paradigm (same converge-ignite motif at low amplitude); plate punch anchors at `punchOffset` ≈ 0.07 (the ignite contact)                                                              |

| `boltstrike` | THE STRIKE: a slate storm mood (`params.mood`) sits on the room from t=0 and the sky KISSES (1-frame 6% gradient brighten) on every body word — double-kiss on emphasis words, re-flashes at I+0.84/I+1.44 during the hold; the room charge-dims to `plate.charge` under a radial word scrim from I−0.39; a hand-authored branched bolt (5-seg zigzag + 2 branches, haze/glow/core SVG stack in `palette.accent`/`glow`/`core`) flashes 2 frames BEFORE the word — leader 1 → 0.35 → return stroke 1 at contact — with a sky flash; the word slams in (Anton scale 3.1 crush power4.in 0.105s, blur 14→0, brightness 3.2→2.5) → 2-frame contact squash → elastic settle, lit HARD for 2 frames then cools to 1; SIGNATURE: the word's text-shadow flips toward the bolt side for the flash frames; ozone under-glow (accent, blur 26×fontPx/145) blooms at contact and breathes (6-key opacity cycle) through the hold + slow loom + 2-frame lit blips on the sheet re-flashes; the dim releases at the flash, settles to a `plate.dim` storm hold and lifts before the exit; exit at `min(heroOut−0.2, I+params.hold)` = washes downward with the rain (y+38, blur 6) | bolt geometry is baked at compile time as offsets from the word center (halfW/360 × fontPx/145 units, `params.side` flips it); word rect is metric-exact via `assets/fonts/char-widths.json`; word scrim = 2·halfW+380 × 2.28·fontPx; pairs with `fx.strikeflash` (full-frame fg flash with the same leader/return flicker grammar) and the `stormrail` paradigm (rain doubles + shadow flip at the same I); plate punch anchors at `punchOffset` ≈ 0.02 (the return stroke) |
| `holoboot` | full volumetric boot behind the subject: a desk emitter dot pops (back.out scale), the projection cone SNAPS wide (scaleX 0.07→1 expo, 0.11s, then dips 0.55→0.95), a seed beam fires up from the emitter (scaleY power3.in, fades after contact), and the word boots with a RISING clip sweep (opacity instant at I−0.055, the reveal is the wipe) into a 7-layer projection stack (dark backing shadow / wide `palette.glow` blur / magenta + cyan chromatic fringes / `palette.core` word / scanline mask / climbing interference bands, band pitch 0.39×fontPx); contact at I+0.07 = brightness 2.3 flash + scaleX 1.07 / scaleY 0.91 squash held 2 frames + elastic settle + 1-frame x interference jumps; fringes blow ±8px then settle to the permanent ±2px registration offset; hold = bands climb −6 pitches, rotationY oscillates [0,6,−7,5,0], glow/cone breathe, emitter dot pulses, slow 1.028 loom; at `I + params.passAt` (≥ I+1.25, runway-guarded) a hand "passes through" the projection (skew 6/−4/0 + blur + fringe blowout + scaleY ripple — pure projection logic); exit at `min(heroOut−0.22, DUR−0.3)` = projector cut: brightness 2.4 → collapse to a horizontal line (scaleY 0.018) → to the base dot (scaleX 0.015), cone fades, dot pops 2.2× and dies. A second smaller emitter cone (`params.coneW`) rises from the desk under the holorail plate ALL video with baked flicker cycles + dot pulses, and gets its own projector cut at DUR−0.2 | word rect is metric-exact via `assets/fonts/char-widths.json` (cone width 2·halfW+80, emitter at cy+2.4·fontPx, all rig geometry derives from `halfW` + `fontPx`; scene scrim is a radial centered on the hero's frame fractions); pairs with `fx.holoflash` (cyan fg lock flash at I+0.075, radial at the same hero fractions) and the `holorail` paradigm (same sweep-boot + interference-glitch motif at low amplitude; the body cone is aimed at ITS plate); plate: grain-only (no punch) is the demo register, scene reaction = `plate.scrim`/`charge`/`dim` |
| `biobloom` | bioluminescent BLOOM behind the subject: the abyss grade (`params.mood`) + a breathing vignette (6-key opacity cycle sized to the clip) sit on the room from t=0 — the world is underwater before anything speaks; a creature pre-glow (1.083×/0.717× fontPx ellipse) gathers with a double-beat anticipation at I−0.31 and crushes to a point at I−0.115; a dark pocket scrim (`plate.scrim`, 2·halfW+280 × 2.5·fontPx) opens behind the word zone at I−0.03; the word CONDENSES in (scale 2.35 crush power4.in over `params.crush`, blur 15→2, brightness 2.6→1.7) → 2-frame contact squash (1.09/0.93) → elastic settle while cyan light SPILLS onto the scene (radial at the hero's frame fractions: snap 0.55 at contact → 0.2 → shimmer keyframes); 6 luminous tendrils (paired blurred SVG strokes — glow w9/blur5 + core w3/blur1.1, accent/violet/ice alternating) draw outward from the word rect staggered 0.05s (stagger auto-clamps so the last filament completes before the sink); hold = glow breathes 8–14% (4-key scale/opacity), glow-layer flicker, two counter-drifting caustic gradients shimmer over the text zone, the bloom drifts slowly with the current (`params.drift` on the x channel); exit at `min(heroOut−0.2, DUR−0.3)` = the light SINKS — tendrils (via their HTML wrapper, never gsap-y on inner SVG), caustics, pocket, spill and the word all drift down (+14/+26) while dimming, word blurs out | tendril geometry is hand-authored from the demo and baked at compile time as offsets from the word center (x scaled by halfW/demoHalf, y by fontPx/120, endpoints frame-clamped); word rect is metric-exact via `assets/fonts/char-widths.json` (0.01em tracking); pairs with `fx.sporeburst` (seeded spore puffs burst PAST the bloom at contact in the fg layer, ballistic with slight sink) and the `planktonrail` paradigm (same glow-on motif at low amplitude + mote attraction anchored at the same I); plate: pushIn+grain (no punch) is the demo register — the demo's postfx profile was `push` |
| `silkribbon` | aurora silk WRITTEN into the night sky behind the subject: the hero word is a sequential Hershey-stroke reveal (one `<path>` per pen stroke at constant pen speed, `gen-stroke-path.py` baked at compile time — any word, zero tuning) in a blurred halo (`params.haloWidth`, feGaussianBlur 7) + tight core (`params.coreWidth`, white drop-shadow), BOTH stroked by ONE userSpaceOnUse rose→teal→violet linearGradient with reflect spread; SIGNATURE: the gradient's x1/x2 translate f(t) from I−0.07 to the clip end (shift 1.5× the gradient span) so the aurora colors FLOW THROUGH the drawn ribbon during the hold; a soft light mote (blurred halo + core dot) rides the pen, hops at lifts, and blooms out 2.4× at the final stroke; scene reaction = two huge aurora blobs (teal top-left / rose top-right, frame-fraction geometry) drift the sky f(t) from t=0 at `params.wash` and DEEPEN to `params.deepen` as the ribbon writes, a top scrim (`plate.scrim`, 0.653H) charges in at I−0.34 (sky washout guard), a light spill (`params.spill`) tints the sky at I+0.1 once the ribbon completes and flickers gently, and the halo breathes (5-key opacity cycle) through the hold; gentle — no shake, no contact squash: the apex verb is the writing itself; writing window = `params.window` clamped so the ribbon finishes + holds before ENDT = DUR−0.04 | canvas/gradient/spill all derive from `params.targetWidth` (demo: 880 ink on a 1010×280 canvas, baseline 200; svgW = tw+130, spill = (tw+60)×0.51, gradient span = 0.6364·tw), centered at the scene-aware HG (pref top 18, the sky band — same as drawon); pairs with `fx.silkmotes` (seeded aurora motes rise near the ribbon during the hold, fg layer, demo spread fractions keep the face gap) and the `sheenrail` paradigm (same flow motif at low amplitude); plate: pushIn+grain (no punch) is the demo register — the demo's postfx profile was `push` |
| `scopetrace` | the scope's own trace LEAPS off the band and WRITES the hero word behind the subject: a surge column (3px, bottom-anchored scaleY pop, landing at the pen-start point parsed from the baked path) jumps from the scope band at I−0.12; the word is a sequential Hershey-stroke reveal (one `<path>` per pen lift at constant pen speed, `gen-stroke-path.py` baked at compile time — any word, zero tuning; punctuation stripped for the pen) drawn CORE-ONLY in a thin phosphor stroke (`params.coreWidth`, bright `palette.bright` + accent drop-shadows — it is a trace, not a tube), nib rides `getPointAtLength` and pops 2.4× at DRAWN; the phosphor FILL (blurred halo strokes, `params.haloWidth`, blur 7) flicker-arrives AFTER the write at FLICK = DRAWN+`params.flickGap` (pre-arm 2 frames early, 5-step 1/0.25/1/0.55/1 blink, core dips 0.55 for a frame); hold = glow hum (7 Hz + 11.3 Hz sine keyframes, f(t)) + 1.6% breathe on an HTML wrapper with transformOrigin at the hero's frame fractions, ending exactly at the collapse; exit = the word COLLAPSES to a flatline (wrapper scaleY 0.02, 0.14s power3.in, same origin) + a horizontal flash bar (2·halfW wide at the word center) that shrinks out — scheduled at nextSpokenWord − `params.clearGap` (clamped ≥ FLICK+0.45, ≤ heroOut) so the trace clears BEFORE the next line speaks; scene reaction = phosphor radial scrim (`plate.scrim` at the hero fractions, I−0.18 → EXIT+0.05) + green spill sized from targetWidth (`params.spill`); second beat: a 1-frame phosphor pulse answers the FINAL transcript word — the same death beat the scoperail finale owns | trace geometry derives from `params.targetWidth` (svgW = tw+140, local baseline 215, ink center ≈ baseline−28 — the demo registration; surge x parsed from the first `M` of the path); centered at the scene-aware HG (pref top 25, ink height 115); pairs with the `scoperail` paradigm (tracewipe = the same pen-write at low amplitude; the paradigm's hero waveform kicks fire at the same word onsets) and needs no front fx; plate: grain-only (no punch) is the demo register |
| `papermat` | STOP-MOTION PAPERCUT behind the subject: a torn-edge kraft mat (two seeded torn polygon layers) DROPS from above with steps(2) gravity and slaps down (squash → rest, soft blob shadow); then one torn cream chip per char (`palette.paper`/`paper2`, hero-font ink `palette.ink`) drops with steps(3) gravity + 1-frame overshoot at `I + i·params.step`, each announced by an anticipation shadow 3 frames early, landing crooked (seeded ±3° ±4px), settling through three 12fps poses, then holding an 8fps wobble (baked set chain — the stop-motion heartbeat) while the whole tableau breathes 1.6% (camera-push loom); scene reaction = a warm vignette (`params.vig`) charging with the landing; on the FINAL word the table BUMPS (y4, 1 frame) and the chips SCATTER off down-right steps(4) staggered 0.045s, the mat swept off last (disperse-on-last-word, `hero.exitAt` overrides) | chip geometry derives from `hero.fontPx` (demo ratios: tile 0.91×/1.31× fontPx, pitch tile+14, mat N·pitch+80 × 1.846× fontPx), width-fit shrinks type and chips together; pairs with `fx.paperscraps` (paper scraps puff at each chip contact, fg) and the `paperrail` paradigm (same place verb at low amplitude); plate: grain only is the demo register |
| `centerfold` | POP-UP BOOK CENTERFOLD behind the subject: blue + cream 12-point paper starbursts (deterministic starPts polygon, `palette.accent`/`cream` fills with kraft-edge strokes) unfold on a baseline hinge (scaleY 0.03→1 back.out(1.6), cream staggered +0.06) starting I−0.272 while a blob shadow grows beneath; the hero word pops up at I in `params.segs` hinged clip-path segments (rotationX 85→0 back.out(1.4) staggered `params.segStagger` — the body's popup verb at full amplitude); two kraft struts lean in at I+0.33; backdrop + word wobble-settle TOGETHER (one `#cfWob` wrapper owns rotationX, 6-key) then breathe through the hold (word 5-key rotationX + shadow scaleX, both auto-squeezed into the window); scene reaction = a dusk radial scrim at the hero's frame fractions (`params.scrim`, bright-sky insurance) charging with the unfold; exit = the whole spread folds FLAT (segments rotationX 88 staggered, struts/bursts scaleY collapse cream-then-blue, scrim out) at `max(I+segStagger·(segs−1)+0.45, min(heroOut−0.33, DUR−0.39))` — the book closes before the next sentence | burst geometry derives from the word rect (metric-exact via char-widths: burstR = clamp(1.83·fontPx, 0.59·wordW, 0.42·H), box 2R+180 × 2R+fontPx, demo props scale at burstR/220), width-fit shrinks the type first; pairs with `fx.confetti` (seeded ballistic paper confetti launched from the hinge line at I+0.128, fg) and the `popuprail` paradigm (same baseline-hinge verb at low amplitude); plate: grain only is the demo register |
| `chalkwrite` | NIGHT LECTURE behind the subject: the lower chalk band grows into a FULL deep-green board (scaleY 0→1 power3.out from the bottom edge at I−0.3, the room dims `params.dim` + vignette `params.vig` with it); the hero word is HAND-WRITTEN in chalk — sequential Hershey stroke reveal (drawon pattern, `gen-stroke-path.py` baked at compile time), dry/rough via seeded ±1px jitter on every pen point (`params.jitterSeed`), a blurred halo (`params.haloWidth`, feGaussianBlur 3.2, group opacity `params.halo`) under a tight core (`params.coreWidth`), a chalk-stick nib (rotated rect) riding the pen — while `params.dustN` dust specks fall from the nib along the path; at write-complete a hand-slap CLAP shakes the board 1 frame (set-chain y 3/−2/1/0; the dust burst lives in the rail, in FRONT: `fx.chalkclap`), then TWO violent accelerating underlines slash beneath (seeded waviness, power3.in, the nib rides them, dust kicks at the ends), the chalk flicks away, and the halo breathes (baked 1.6/3.7 Hz f(t)) through the hold with two slow motes drifting off the letters | geometry registers on the MEASURED ink box (the stroke path is parsed at compile time; ink CENTER lands on scene-aware HG.y, pref top 44): the board rect (`params.boardGap`×inkH above the ink, bottom flush with the frame, width = the body strip), the underlines (inkL+14→inkR−4 at inkBot+30/+50) and the fg clap point (the pen's LAST contact point → HG.clapX/Y/T) all derive from it; x defaults to the board's LEFT margin + tw/2 — a hand writes from the left, never centered on the subject (`theme.hero.x` overrides); pairs with `fx.chalkclap` (12 seeded radial dust specks + a blurred puff at DRAWN, fg) and the `chalkrail` paradigm (same scribble verb at low amplitude); plate: grain only is the demo register; needs `python3` |
| `spraytag` | STREET TAG behind the subject: the dusk wall dims (`params.dim`) + a radial scrim charges (`params.scrim`) at I−0.42 as the writer steps up; the can RATTLES at the pen-start point (6 seeded set-chain nib jitters, I−0.30→I−0.02), then the hero word is spray-written stroke by stroke — sequential Hershey dash reveal at constant pen speed (`gen-stroke-path.py` baked at compile time) — as a TRIPLE stack per stroke: fat dark under-stroke `palette.under` (`params.underWidth`, sky-contrast armor) + blurred overspray halo (`params.haloWidth`, feGaussianBlur 7, group opacity `params.haloOpacity`) + wet paint core (`params.coreWidth`, drop-shadow), the glowing nib (r9, `palette.nib`) riding each stroke; `params.osprayN` seeded overspray dots pop near the nib as it passes; nib throws off 2.6× at DRAWN; through the hold `params.drips` seeded DRIPS (paint→`dripMid`→`dripLo` gradient bars) grow from painted low points (x-separated >110px) and KEEP growing, the halo hums (7/11 Hz baked f(t)) and the whole tag looms 1.014; writing window = `params.window` (demo 0.58s) clamped so write + splat + drip fit before ENDT = DUR−0.04 | geometry registers on the MEASURED ink box (ink CENTER lands on scene-aware HG.y, pref top 26 — the sky band, crossing the subject silhouette is the point; tag centers on the hero anchor, `targetWidth` capped at W−140); the pen-finish point is baked onto HG.splatX/Y/T for the fg splat; pairs with `fx.paintsplat` (hot `em2` ring + paint ring + seeded ballistic droplets at DRAWN, fg, in FRONT of the subject) and the `markerrail` paradigm (same flick verb at low amplitude); plate: punch with `punchOffset` ≈ window+0.05 so the punch lands on the splat, not the hero onset; needs `python3` |
| `brushwrite` | SUMI-E GESTURE behind the subject: a warm paper wash (`palette.wash`/`wash2` radial, `targetWidth`+380 × `params.washH`) charges at I−0.18 to `params.wash`, settling to `params.washSettle` through the hold; the brush SLAPS down at the first pen point — a start-SPLAT (irregular ink ellipse rotated −24° + 5 seeded satellite dots, scale 0.35→1 power3.out 0.12s; the fg fleck pass rides `fx.brushflecks`) — then the hero word is written in ONE sequential gesture (Hershey dash reveal at constant pen speed, `gen-stroke-path.py` baked at compile time) as a FOUR-layer stack per stroke: blurred bleed (`params.bleedWidth`, feGaussianBlur 6, group hidden until the hold) under fat/mid/core widths (`fatWidth` 0.25 / `midWidth` 0.6 / `coreWidth` 1.0 stroke-opacity = pressure-tapered wet brush), the round ink nib (r7.5, `palette.nib`) riding the pen and flicking off (+42,−30, scale 1.9) at DRAWN; ≤`params.spatterN` spatter droplets eject PERPENDICULAR at the sharpest-curvature points of the gesture (angle-delta > 0.55 rad, te-separated > 0.045s); through the hold the ink BLEEDS into the paper (bleed group fades to `params.bleed` over ≤1.7s) and the whole work breathes 0.6% (`params.breatheHz` baked f(t), svgOrigin = ink center); then the vermilion SEAL chop (`params.sealGlyph`, `palette.seal`/`sealMid`/`sealLo` radial face, `fonts.seal` glyph) stamps at DRAWN+`params.sealDelay` — power4.in crush 0.09s → 2-frame squash → elastic settle + 3 seeded red flecks — skipped entirely if the clip ends too soon; writing window = `params.window` (demo 0.45s) clamped so write + seal + bleed fit before ENDT = DUR−0.04 | geometry registers on the MEASURED ink box (ink CENTER lands on scene-aware HG.y, pref top 19 — the sky band, crossing the subject silhouette is the point; gesture centers on the hero anchor, `targetWidth` capped at W−240 — on scenes with a dark object near the pen start, set `theme.hero.x` so the slap-down lands clear, like the demo's 864); the pen-START point is baked onto HG.fleckX/Y/T for the fg flecks and the seal sits at (inkR−103, inkBot+72) clamped on frame (`params.sealDx/sealDy` override); pairs with `fx.brushflecks` (seeded upward ballistic ink flecks at the slap-down, fg, in FRONT of the subject) and the `brushrail` paradigm (same brush-wipe verb at low amplitude); plate: punch with `punchOffset` ≈ window+`sealDelay` so the punch lands on the seal CONTACT, not the hero onset; needs `python3` |
| `ransomnote` | RANSOM NOTE slam behind the subject: one cutout chip per letter (4 recipes cycled, seeded ±13% sizes around `hero.fontPx`, alternating-sign rotations ±2–5°, misaligned baselines ±2–6px, seeded tear-polygon edges) slams in oversized (scale 1.5, rot×1.8, flight drop-shadow) at S_k = I + 0.01 + `params.step`·k, crushes power3.in over 0.085s, and on contact the shadow POPS tight (1-frame filter swap) → 2-frame squash 1.07/0.93 → elastic settle; when the LAST letter lands the whole word takes a collective 1-frame JOLT (y6 → power3.out) + a scene pulse (`params.pulse` full-frame dark blink); hold = slow 5-key loom (the crooked grid never straightens); exit = letters RIPPED off one by one (rot+±15–22° throw, y−34, fade, 1-frame tear flash under each) at ripT = min(lastWord − `params.ripLead`, DUR-clamped), ≥ jolt+0.45; scene reaction = collage scrim (`params.scrim` top-dark gradient) charges at I−0.07, out with the rip | width-fit measures each glyph IN ITS OWN recipe font (`assets/fonts/char-widths.json`) + 0.20em chip padding + 6px margins so the collage never overflows; chip chrome (shadows, space width) scales with fontPx from the 130px demo cut; the setpiece RECOMPUTES `plate.punchOffset` = 0.01 + step·(N−1) + crush from the real letter count so the plate punch always lands on the collective jolt, whatever the hero word length; pairs with the `ransomrail` paradigm (same paste verb + tear flashes at low amplitude, same recipe classes/seeded tear polygon emitted into both layers); no front fx — the flashes live inside the chips |
| `inkbloom` | INK DROP IN STILL WATER behind the subject: paper-white radial washes charge the two text zones (`palette.wash` rail wash from t≈0.15, PERSISTS to clip end so the closing body lines keep their ground — it registers on the body row geometry `body.bottomPx`/`rowGap`; `palette.washApex` apex wash at I−0.077, out with the bloom) and a gentle vignette (`params.vig`) STILLS the world while the ink blooms; the drop (ink teardrop, `params.fall` 0.15s) appears above the word point at I−0.08, swells, falls power2.in with a scaleY stretch, squashes 1.7/0.35 on contact (2 frames) and merges into the BLOOM — 3 stacked blurred irregular ink ellipses (`palette.ink`, opacity snaps 0.82/0.62/0.5 then decays) scaling at different rates with counter-rotations (expo.out 1.15/1.35/1.5s) + 2 thin concentric ripple rings expanding 7.2×/9.6× on the water surface; the hero words RESOLVE out of the blob (opacity snaps, blur 16→0 + scale 1.12→1 power2.out) — the FIRST at contact+0.02, the following at their own spoken times (multi-word hero, per-word hand-off); through the hold the glyph edges BREATHE (baked 5-key blur keyframes per word, alternating 0.66/0.5s), the group looms 1.018 and two faint ink wisps curl off the word top (C+0.573/C+0.753, fit-guarded); exit: RE-DISSOLVE (blur 11 + scale 1.15 + fade, 0.52s power1.in) at XOUT = min(I+`params.maxHold`, next body line − `params.clearGap`, DUR−0.6) — the ink clears before the next thought needs the reader | geometry derives from the measured word rect (`assets/fonts/char-widths.json`; kx = halfW/demoHalf, ky = fontPx/132 scale the demo's blob/ring/wisp anchors; pref top 25 — the sky band, crossing the subject silhouette is the point); dark `palette.ink` + its paper washes carry the contrast on bright skies; pairs with the `inkrail` paradigm (same bleed verb at low amplitude); plate: quiet `pushIn`, no punch — the bloom is liquid, not percussive |

Inline setpieces (`hero.inline: true`) live inside the body paradigm — the hero
word stays in `lines`. Embed setpieces own the word: leave it OUT of `lines`
(rail↔climax hand-off), except `panel` + `redact-until-hero`, where the words
appear in the log as `█████` until the decode locks.

A setpiece may additionally return `fgCss/fgHtml/fgJs` — those parts are merged
into the fg alpha page (rail.html), for furniture that must ride ABOVE the matte
(e.g. a verbatim chip that may never be occluded) while the setpiece scenery
stays embedded behind the subject.

## Linkages

- `redact-until-hero` — panel log shows hero words as blocks; un-redacts at lock
- `corrupt-on-last-word` — panel jitters + hue-shifts on the final word
- `disperse-on-last-word` — every visible poem letter scatters back to dust
- `field-is-the-apex` — (lastpage) the blurred future-field rack-focuses at the
  apex: every faint line was the apex word all along; the main instance holds,
  the rest re-blur and die

## Hard rules inherited (do not relax)

- determinism: paused GSAP on `window.__timelines["main"]`, seeded PRNG only,
  set-chains / keyframes, no Math.random/Date.now/CSS-keyframes
- 80 ms word sync (timings come from transcript.json via the strict matcher;
  any uncovered transcript word is a compile ERROR — verbatim completeness)
- apex owns the frame: fg furniture must avoid the setpiece rect or yield
  (`body.yield`) during the hero window
- plate punch/shake ALWAYS post-composite (`_postfx.sh`) so subject + text +
  plate move as one photographed frame (matte never desyncs)

## SVG gotchas (paid for, do not rediscover)

- dash patterns restart on EVERY subpath → sequential per-stroke paths, never
  one dashoffset across a multi-stroke word
- round linecap renders a DOT at dashoffset=len → hide each stroke until its turn
- inline-block trailing spaces collapse → word gaps via margin, not text spaces
- SVG-font glyph y is UP → flip when converting (gen-stroke-path.py does)
- NEVER gsap x/y/scale on raw SVG children: it runs `_parseTransform`/getBBox, and
  zero-area shapes (fan-kit slivers) trip GSAP's bbox hack → `appendChild` on a
  null docElement in injected-gsap contexts (preview-frames, renderer). Jitter
  SVG pieces via `attr:{transform:"translate(...)"}` only; scale/loom belongs on
  an HTML wrapper div. Nuance: a ROOT `<svg>` inside a div has null
  ownerSVGElement → GSAP classifies it as HTML (CSS transforms, no getBBox), so
  scale/skew on the svg root is safe — but copying that onto an inner `<g>`
  becomes the fatal class. attr transforms can't TWEEN — use frame-quantized
  step chains (identical on screen at 24fps)

## Timeline ownership rules (audit-derived, do not relax)

- ONE owner per (target, property) at any instant. A duration tween re-renders
  every tick until it ends — any set/tween on the same property inside that
  window gets visually overwritten a frame later (1-frame ghosts, flash-backs).
  Known shapes: yield dim vs line exit; yield restore vs powercut set-chain;
  per-word line-y bounces with word gaps < 0.22s. The rail emitter guards these
  (bounce ownership tracker, dim clamped ≥ line-in, restore only with ≥0.3s
  runway before exit — else the line exits dimmed, by design).
- Inverted clamps: `Math.max(lo, Math.min(hi, x))` with lo > hi silently pins to
  lo. When a lockup is wider than the frame, CENTER it (x = W/2) instead of
  clamping — decorative bleed must be symmetric.
- The overflow gate only measures elements with direct text nodes — SVG-painted
  setpieces (mask text + rects) are invisible to it. Do not read its "ok" as
  proof for svg lockups; judge those on the preview frames.

## theme.json — full schema

```json
{ "dna": "<themes/*.json name>",            // required
  "lines": [["word","word",...], ...],       // required — verbatim transcript tokens, in order,
                                             //   INCLUDING punctuation ("size.", "noise.")
  "minors": ["word", ...],                   // optional — theme's inline emphasis treatment
  "hero": { "match": "word or phrase",       // required — exact transcript tokens (case-insensitive,
                                             //   punctuation-stripped match); first occurrence wins
            "text": "DISPLAY FORM",          // optional display override
            "out": 5.9, "exitAt": 3.3 },     // optional window overrides (seconds)
  "width": 1280, "height": 720,              // optional, default 1280x720 (theme DNAs are tuned
                                             //   for 16:9; vertical needs a DNA variant)
  "fps": 24,                                 // optional — matte.fps is AUTHORITATIVE when present
  "panelHeader": "..." }                     // optional (panel paradigm)
```

Scene awareness (inherited from the existing typography system): when
`safe-zones.json` is present the compiler re-anchors the hero setpiece to the
measured occlusion bands (`heroBands.profile`, 18–55% occlusion, closest to the
setpiece's preferred height) and `heroAnchor.centerXPct`, width-fits the hero
font to the actual word, and docks panel/poem furniture on
`subject.clearerSide`. Explicit `theme.json` values > scene auto > DNA fallback.

Expected (benign) log noise: `[Browser:REQUESTFAILED] GET source.mp4
net::ERR_ABORTED` during the fg-layer page load, reel-glyph overflow lint
lines, and `media_missing_data_start` — all cosmetic; the render is good if
render-theme.sh exits 0 and prints the gates summary.

## Authoring a project (this is ALL a user writes)

`<project>/theme.json`:

```json
{
  "dna": "ordnance",
  "lines": [
    ["The", "stars", "were", "only", "limited"],
    ["by", "the", "pixel", "size."],
    ["It's"],
    ["what", "you", "can", "actually"],
    ["recover", "from", "the", "noise."]
  ],
  "minors": ["stars"],
  "hero": { "match": "remarkable" }
}
```

- `lines` follow the transcript verbatim, in order (compile error otherwise);
  every transcript word must be covered by `lines` ∪ `hero.match` — the example
  above omits "remarkable" from `lines` precisely because `hero.match` covers it
  (it slots between `["It's"]` and `["what",...]`)
- leave the hero word(s) out of `lines` for embed setpieces; keep them for
  inline setpieces and for panel+redact
- `takeover`: each line is one CARD — group by beat (1–2 words), not subtitle
  length; `panel` scrolls automatically past ~5 lines (terminal scroll)
- the ordnance "charge" is an edge vignette + rail yield (the center stays
  clean by design) — judge it at the frame corners, not the center
- `minors`: words that get the theme's inline emphasis treatment
- preview first: `node scripts/make-theme.cjs <project>` then
  `node scripts/preview-frames.cjs <project> <t1> <t2> ...` (Visual QA)
- then: `bash scripts/render-theme.sh <project>` → **`final_fx.mp4`** (the
  deliverable; `final.mp4` is pre-plate-reaction)

Prereqs identical to other modes — `prepare.sh` outputs: transcript.json,
frames_fg/, matte.fps, safe-zones.json (scene awareness), source.mp4.
frames_bg/ is only needed by preview-frames.cjs, not by the render.

## Adding a theme DNA

Copy a `themes/*.json`, change identity fields. You may ONLY combine existing
paradigm/setpiece/linkage names — if the look you want needs a new mechanism,
that is a registry contribution (this file + make-theme.cjs), reviewed like an
engine change. A DNA that needs per-word visual tuning to ship is rejected:
**any input, zero hand-fixing** is the acceptance bar (the draw-on lesson).
