diff --git a/skills/claude-design-hyperframes/SKILL.md b/skills/claude-design-hyperframes/SKILL.md
index 3ec47654c..35e45f1bf 100644
--- a/skills/claude-design-hyperframes/SKILL.md
+++ b/skills/claude-design-hyperframes/SKILL.md
@@ -1,677 +1,445 @@
---
name: claude-design-hyperframes
-description: Claude Design entry point for HyperFrames. Produce renderable HyperFrames videos in Claude Design with a working in-pane preview. Use for any request to create a video, animation, launch teaser, editorial explainer, product tour, social reel, or motion deliverable.
+description: Use when running inside Claude Design specifically. Produces a valid, brand-accurate HyperFrames video draft using pre-valid skeletons. For Claude Code / Cursor / Codex, use the `hyperframes` skill instead.
---
-# Claude Design + HyperFrames
+# Claude Design + HyperFrames (Template-First)
-For this project, your medium is **HyperFrames compositions**: plain HTML + CSS + a paused GSAP timeline. A separate CLI (`npx hyperframes render index.html`) turns the HTML into an MP4. You are authoring the HTML files — the user runs the CLI locally. You do NOT need a CLI environment to produce these files.
+Your medium is **HyperFrames compositions**: plain HTML + CSS + a paused GSAP timeline. The CLI (`npx hyperframes render index.html`) turns the HTML into an MP4. You author the HTML -- the user renders locally.
-**HyperFrames replaces your default video-artifact workflow for this project.** When the user asks for a video, animation, launch teaser, editorial explainer, product tour, social reel, or any motion deliverable:
-
-- Do **NOT** call `copy_starter_component` with `kind: "animations.jsx"`. The animations.jsx starter is the wrong format here — HyperFrames uses plain HTML + GSAP, not React Sprites.
-- Do **NOT** invoke the built-in "Animated video" skill. HyperFrames replaces it for this project.
-- Do **NOT** use React, Babel, or `
-
-```
-
-**Scene-count invariant — `scenes.length === transitions.length + 1`:** HyperShader enforces this at init. Pick one anchor scene BEFORE the first transition, and one anchor AFTER each transition. A video with three act-boundary transitions needs exactly four anchor scenes. Scenes between anchors (non-bracketing, runtime-managed) carry `style="visibility:hidden;"` instead of `style="opacity:0;"` — they're not HyperShader-managed so nothing animates their opacity back to 1.
-
-The simplest working pattern: list only the scene just before AND just after each shader cut. Do NOT list every scene in Act II just because they "span" a transition — that violates the invariant. If you genuinely need MORE listed anchors than real shader transitions (rare — e.g., tracking an additional fade beat that's not a visible shader bridge), insert `{ shader: "flash-through-white", duration: 0.01 }` as an invisible no-op bridge to satisfy the invariant. This is a workaround; the cleaner fix is almost always to drop the extra anchor.
-
-**Transition timing (critical — the scene boundary must fall INSIDE the transition window):**
-
-Scene windows are half-open (`[start, start+duration)`). At time `B` (the boundary), the runtime has already flipped the outgoing scene to `visibility:hidden`. If `transition.time === B`, `html2canvas` captures a blank outgoing texture → shader transitions from blank → incoming → visible blink.
-
-Rule: `transition.time < B` AND `transition.time + duration > B`. Simplest — center it: `transition.time = B - duration/2`. Example: scene-1 ends at 6, duration 0.5 → `time: 5.75`.
-
-**Scene visibility: HANDS OFF.** HyperShader owns scene `opacity` end-to-end. Do NOT add `tl.set(#scene-N, {autoAlpha: …}, …)` on scene containers. If you do, you create the same visibility race that produces the blink.
-
-### Sub-compositions — default NO for videos ≤ 3 minutes
-
-Default to a single `index.html` with scenes tiled inline. 30-second to 2-minute compositions fit cleanly in one file (~1500–2000 lines). Single file = single HyperShader instance = no canvas conflicts = everything works.
-
-Split into sub-compositions ONLY when one of these is true:
-
-- Video length > 3 minutes AND you need organizational structure.
-- You're extracting a REUSABLE sub-comp that appears in multiple places (chart block, logo outro).
-- A single scene is so complex it deserves its own file (full UI recreation, heavy data-vis).
-
-If you do split, **HyperShader lives at the ROOT `index.html` ONLY** — never inside a sub-composition. HyperShader hardcodes `#gl-canvas` as its canvas ID (see the canvas creation path in `packages/shader-transitions/src/hyper-shader.ts`); multiple HyperShader instances can't share one canvas. When a sub-comp's HyperShader fails silently on canvas conflict, its fallback code calls `document.querySelectorAll(".scene")` document-wide and sets every scene's opacity to 0 — corrupting visibility across the whole document. Symptom: only scene-1 of each act shows, scenes 2+ never appear.
-
-#### Sub-composition file shape
-
-Every sub-comp file in `compositions/` is wrapped in a ``. The template's contents are INERT in the browser by spec — the runtime extracts and nests them into the parent at render time. A standalone `index.html` (the main composition) does NOT use ``; the data-composition-id div goes directly in ``.
-
-```html
-
-
-
-
-
-
-
…
-
-
-
…
-
-
-
-
-
-```
-
-#### Parent `index.html` wiring
-
-The parent mounts each sub-comp via `data-composition-src` on an empty div that carries the clip contract:
-
-```html
-
-```
-
-Three rules when splitting:
-
-1. `` wrapper required on every sub-comp. Contents are inert; the runtime extracts them.
-2. The `data-composition-id` on the sub-comp's inner root div MUST match BOTH (a) the parent container's `data-composition-id` AND (b) the key in `window.__timelines[...]` inside the sub-comp's script.
-3. Tween positions in a sub-comp are LOCAL to that sub-comp (0 = its start). The parent auto-offsets by the container's `data-start`. Never manually add sub-timelines to the root timeline.
-
-Since the sub-comps in this pattern don't use HyperShader (by the rule above), their non-first scenes carry `style="visibility:hidden;"` only — see "Scene initial visibility" above for why.
-
-### Determinism ❌ / ✅
-
-The render engine seeks to exact frames and expects pixel-identical output on every repeat render. Violations produce broken output.
-
-| ❌ Never | ✅ Use instead |
-| ------------------------------------------ | ----------------------------------------------------- |
-| `Date.now()`, `performance.now()` | `tl.time()` inside `onUpdate`, or hard-coded timing |
-| `Math.random()` unseeded | seeded PRNG (e.g. mulberry32) with a known seed |
-| `setInterval`, `setTimeout` in timeline | timeline tweens + `onUpdate` callbacks |
-| `repeat: -1` on any tween or timeline | `repeat: Math.ceil(duration / cycleDuration) - 1` |
-| Timelines built in `async`/`await` wrapper | Construct synchronously at page load |
-| `video.play()`, `audio.play()` in code | Framework owns media playback |
-| Animating `visibility` or `display` | `autoAlpha` (animates opacity AND toggles visibility) |
-
-### Motion rules (HyperFrames-native, non-negotiable)
-
-Inherited from `skills/hyperframes/SKILL.md#Rules-Non-Negotiable`:
-
-- **GSAP visual properties only.** Animate `opacity`, `x`, `y`, `scale`, `rotation`, `color`, `transforms`. Do NOT animate `visibility` or `display` directly (use `autoAlpha`).
-- **One paused timeline per composition.** `{ paused: true }`. Register on `window.__timelines[""]`. Never call `.play()`.
-- **Vary eases** — at least 3 different eases per scene. Don't default to `power2.out` on everything.
-- **Offset first tween 0.1–0.3s.** Zero-delay entrances feel like jump cuts.
-- **Exit animations BANNED** except on the final scene. The transition IS the exit. See the code examples below — this is the single most frequently-violated rule in generated output.
-
-### Motion anti-patterns (observed in generated output, with fixes)
-
-These four patterns keep appearing in generated compositions despite the rules above. Each one is observed in real outputs; each has a known-clean replacement. Pattern-match these, not just the prose rules.
-
-#### Anti-pattern 1: Exit tween before a shader transition
-
-The shader's `captureScene(fromScene)` runs `html2canvas` on the outgoing scene at transition time. If you've animated content to `opacity: 0` (or `autoAlpha: 0`, or off-screen) before the transition fires, `html2canvas` captures an empty scene. The shader morphs from an empty outgoing texture → the incoming scene, which looks like "the content vanished, then the transition happened." This is independent of whether the shader itself works — it's a composition-level bug.
-
-This matches industry practice: in Remotion's ``, in the GSAP community's own guidance, and in HyperFrames' core `references/transitions.md` — the transition component owns the visual handoff. The scene's content does not animate its own exit.
+In the timeline block marked for this scene, add `tl.from()` tweens. Animate FROM offscreen/invisible TO the CSS position:
```js
-// ✖ WRONG — card fades to 0 before transition at t=17.80 fires.
-// Shader captures an empty phone. User sees the card disappear
-// 0.85s before the transition, then an empty-phone-to-next-scene morph.
-tl.to("#s6-card", { x: 180, rotation: 14, duration: 0.55, ease: "power3.in" }, 16.5);
-tl.to("#s6-card", { autoAlpha: 0, duration: 0.25 }, 16.95); // BANNED
-
-// HyperShader transition at 17.80 captures #s6 with card invisible
-```
-
-```js
-// ✓ RIGHT — mid-scene swipe gesture, then a different beat holds the final
-// frame. Card moves but stays visible. Transition handles the actual exit.
-tl.to("#s6-card", { x: 180, rotation: 14, duration: 0.55, ease: "power3.in" }, 15.3);
-tl.from("#s6-check", { scale: 0, duration: 0.3, ease: "back.out(2)" }, 15.6);
-tl.from("#s6-match-stamp", { scale: 1.5, autoAlpha: 0, duration: 0.4 }, 16.1);
-// scene 6 ends at 18.0 with the matched-stamp + pulsing check button visible.
-// HyperShader transition at 17.80 captures a FULL scene → clean morph.
-```
-
-Common trap: "I want to show a swipe gesture, so the card has to exit." No — the swipe gesture happens mid-scene, at 60–70% of scene duration. The last 30% of the scene shows the RESULT of the swipe (a match stamp, a confirmation, a badge). Keep something visible at transition time. If there's nothing logically left to show, the scene is too long — shorten it.
-
-#### Anti-pattern 2: Non-deterministic `stagger` origin
-
-```js
-// ✖ WRONG — `from: "random"` picks a random origin at timeline-construction
-// time using GSAP's internal unseeded random. Two renders of the same
-// composition produce different stagger orderings. Fails PSNR regression
-// tests and violates the deterministic-render rule.
+// === SCENE 3 ===
+tl.from("#s3-title", { y: 40, autoAlpha: 0, duration: 0.6, ease: "power3.out" }, 10.3);
+tl.from("#s3-sub", { y: 20, autoAlpha: 0, duration: 0.5, ease: "power2.out" }, 10.7);
tl.from(
- "#s12 .card",
- {
- scale: 0.7,
- autoAlpha: 0,
- y: 40,
- duration: 0.45,
- stagger: { each: 0.04, from: "random" }, // BANNED
- },
- 34.55,
+ "#s3-bar-chart",
+ { scaleY: 0, transformOrigin: "bottom", duration: 0.8, ease: "expo.out" },
+ 11.0,
);
```
-```js
-// ✓ RIGHT — deterministic stagger origins. All of these are safe.
-tl.from(
- "#s12 .card",
- { scale: 0.7, autoAlpha: 0, y: 40, duration: 0.45, stagger: { each: 0.04, from: "start" } },
- 34.55,
-); // natural order
+**Offset first tween 0.1-0.3s** into the scene. Zero-delay entrances feel like jump cuts.
-tl.from(
- "#s12 .card",
- { scale: 0.7, autoAlpha: 0, y: 40, duration: 0.45, stagger: { each: 0.04, from: "center" } },
- 34.55,
-); // ripple outward
+### 3c. Fill mid-scene activity (this is what separates video from slides)
-tl.from(
- "#s12 .card",
- {
- scale: 0.7,
- autoAlpha: 0,
- y: 40,
- duration: 0.45,
- stagger: { each: 0.04, grid: [3, 5], from: [0, 0] },
- },
- 34.55,
-); // grid-aware
-```
+Every visible element must keep moving AFTER its entrance. A still element on a still background is a JPEG with a progress bar. Use at least 2 patterns from Section 8 per scene.
-If you truly need pseudo-random ordering (rare), pre-shuffle the cards in the markup using a seeded PRNG like mulberry32 — the ordering is then committed to the DOM and deterministic forever.
+| Element | Mid-scene motion | Pattern from Section 8 |
+| ------------------ | --------------------------------- | -------------------------------------------------------------------------------------------- |
+| Stat / number | Counter animates from 0 to target | Counter animation |
+| SVG line / path | Draws itself in real-time | SVG stroke draw |
+| Title / wordmark | Characters enter one by one | Character stagger |
+| Logo / lockup | Subtle vertical drift | Breathing float |
+| Chart / bars | Bars fill sequentially | Bar chart fill |
+| Image / screenshot | Slow zoom: `scale: 1 -> 1.03` | Ken Burns (just `tl.to(el, { scale: 1.03, duration: sceneLength, ease: "none" })`) |
+| Accent / highlight | Sweep across text | Highlight sweep |
+| Background glow | Opacity pulse | `tl.to(".glow", { opacity: 0.6, duration: 1.5, ease: "sine.inOut", yoyo: true, repeat: 1 })` |
-#### Anti-pattern 3: Centering content with `position: absolute; top; left` on `.scene-content`
+**The minimum per scene:** entrance tweens + at least one continuous motion (float, counter, zoom, or glow). Scenes with stats or charts should always use the counter or bar-fill pattern — these are the most visually engaging and easiest to implement.
-```css
-/* ✖ WRONG — absolute-positioned content container with hardcoded pixels.
- Renders at 1920×1080 but overflows at any other aspect ratio. Also
- pushes you toward absolute-positioning every child, which is fragile. */
-.scene-content {
- position: absolute;
- top: 200px;
- left: 160px;
- width: 1920px;
- height: 1080px;
-}
-```
+### 3d. Adjust scene duration
-```css
-/* ✓ RIGHT — flex-filled container with padding for the positioning.
- Works at any aspect ratio. Children flow naturally. */
-.scene-content {
- width: 100%;
- height: 100%;
- padding: 120px 160px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- gap: 24px;
- box-sizing: border-box;
-}
-```
+The skeleton has placeholder durations. Adjust each scene's `data-duration` based on:
-See `skills/hyperframes/SKILL.md#Layout-Before-Animation` for the full rationale — in short: position every element at its final landing state first, then `gsap.from()` the entrance animating TO that position.
+- **Reading time:** count words of display text, use the budget below
+- **Last readable element** must finish entering by 50% of scene duration
-#### Anti-pattern 4: SVG filter data URLs used as `background-image` (grain, noise, turbulence)
+| Display text | Min duration |
+| ----------------------------------- | --------------------- |
+| No text (hero, icon) | 1.5-2s |
+| 1-3 words (kicker, number) | 2-3s |
+| 4-10 words (headline + subhead) | 3-4s |
+| 11-20 words (sentence or two lines) | 4-6s |
+| 21-35 words (paragraph) | 6-8s |
+| 35+ words | Split into two scenes |
-Safari's WebKit applies stricter canvas-taint rules than Chrome. When a scene has a `` SVG element referenced as a `background-image: url("data:image/svg+xml...")` — a common grain/noise pattern — `html2canvas` produces a tainted canvas. Safari's WebGL then throws `SecurityError: The operation is insecure` at `gl.texImage2D()`, which has no framework opt-out (WebGL spec requires the check). Every shader transition falls through to the CSS-crossfade fallback; in Claude Design's cross-origin iframe sandbox this compounds with iframe throttling, and users see the whole piece play as hard cuts.
+**Hard ceiling: 5s per scene** unless you name a specific reason (hero hold, cinematic push, long counter animation).
-Empirically observed: skill-test8 in Safari + Claude Design = transitions work. skill-test-9 (identical framework, different grain implementation) in the same environment = zero shader transitions, all catch-handler fallbacks. The only structural difference was this:
+When you change a scene's duration, update `data-start` on subsequent scenes to keep them tiled end-to-end. Also update the root's `data-duration` to match the total.
-```css
-/* ✖ WRONG — SVG filter as background-image.
- Taints html2canvas's output canvas in Safari → breaks every shader
- transition in Safari + cross-origin iframes. Also measurably slower in
- WebKit than CSS gradients even when it does work. */
-.grain {
- position: absolute;
- inset: 0;
- pointer-events: none;
- opacity: 0.08;
- background-image: url("data:image/svg+xml;utf8,");
- mix-blend-mode: overlay;
-}
-```
+### Vary eases
-```css
-/* ✓ RIGHT — layered CSS radial-gradient dots. Same grain effect visually,
- pure CSS rendering, zero canvas taint, fast everywhere. */
-.grain {
- position: absolute;
- inset: 0;
- pointer-events: none;
- opacity: 0.18;
- background-image:
- radial-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1.2px),
- radial-gradient(rgba(0, 0, 0, 0.18) 1px, transparent 1.2px);
- background-size:
- 3px 3px,
- 5px 5px;
- background-position:
- 0 0,
- 1px 2px;
- mix-blend-mode: overlay;
-}
-```
+Use at least 3 different eases per scene. Don't default to `power2.out` on everything.
-The same principle applies to other SVG-filter decoratives (paper fiber via `feTurbulence + feDisplacementMap`, CRT scanline overlays built from SVG patterns, etc.). In general, **avoid SVG filter data URLs in scene markup** — prefer layered CSS gradients, `backdrop-filter`, or solid-color overlays.
-
-**Escape hatch for unavoidable SVG effects.** If a scene genuinely needs an SVG filter (rare — usually a specific decorative that cannot be replicated in CSS), mark that element with `data-no-capture`. The shader's `captureScene()` already has logic to skip elements with this attribute — it won't enter the html2canvas clone pass, so it can't taint the output canvas. The element will still render live in the browser; it just won't appear in the shader transition textures (which for a grain overlay is usually invisible anyway, since the overlay is typically so subtle and repetitive that not seeing it mid-transition is imperceptible).
-
-```html
-
-
-```
-
-### Self-review — run this checklist before calling the build done
-
-Check every item with actual code, not assumptions.
-
-- [ ] Every scene has `class="scene clip"` + `data-start` + `data-duration` + `data-track-index`.
-- [ ] Non-first scenes have the correct inline style for the path in use. With HyperShader: `style="opacity:0;"` ONLY (no `visibility:hidden` — it breaks `captureIncomingScene` and produces content-fading-into-blank blinks during transitions). Without HyperShader: `style="visibility:hidden;"` ONLY (no `opacity:0` — nothing animates it back to 1).
-- [ ] Scene windows tile end-to-end with no gaps (scene-N's `data-duration` = next scene's `data-start` − this scene's `data-start`).
-- [ ] **Every scene has a `
` wrapper — not just scene-1.** Scan each scene's opening block and confirm the wrapper is present. Missing on any scene causes boxes/clipped elements during that scene's transition.
-- [ ] Animated content is INSIDE `.scene-content`; static decoratives are OUTSIDE.
-- [ ] **No scene is longer than 5 seconds** unless you can name the specific pacing reason (hero hold, cinematic push, silence beat, counter that needs ≥6s of runtime). Scenes of uniform length indicate you divided total duration by scene count instead of designing the rhythm.
-- [ ] **Every scene is long enough for its text to be read** — per the reading-time budget table in Step 3. 11–20 words needs ≥4s; 21–35 words needs ≥6s. The last readable text element in each scene finishes entering by the 50% mark of the scene so the viewer has the second half to actually read.
-- [ ] Shader transitions (if used) have the scene boundary strictly INSIDE the transition window — `transition.time < boundary < transition.time + duration`.
-- [ ] Zero `tl.set` / `tl.to` / `tl.from` / `tl.fromTo` on scene containers.
-- [ ] Every visible scene > 4s has a Breathe phase — at least one element in continuous motion, not just entrance + static.
-- [ ] Every element has a verb (from the verbs table) and an identifiable beat (build / breathe / resolve).
-- [ ] No banned fonts. No Inter, Roboto, Playfair, Syne. Check the full list.
-- [ ] No `Date.now()`, `Math.random()` unseeded, `repeat: -1`, `setInterval`, async timeline construction.
-- [ ] No `stagger: { from: "random" }` — GSAP's random is unseeded (Anti-pattern 2). Use `from: "start"`, `"center"`, `"end"`, or a grid origin instead.
-- [ ] No exit tweens except on the final scene. Grep every scene for `tl.to(..., { opacity: 0 })`, `tl.to(..., { autoAlpha: 0 })`, and `tl.to(..., { y: })` — these are Anti-pattern 1 and produce empty-scene captures.
-- [ ] No SVG filter data URLs as `background-image` (Anti-pattern 4). Grep for `data:image/svg+xml` in the CSS — if present, either replace with layered `radial-gradient`s (preferred) or add `data-no-capture` to the element. SVG filters taint html2canvas's canvas in Safari, killing every shader transition in Safari + cross-origin iframe environments.
-- [ ] Minimum font sizes: 60px+ headlines, 20px+ body, 16px+ labels. `font-variant-numeric: tabular-nums` on number columns.
-- [ ] No full-screen dark linear gradients (H.264 banding). Use radial or solid + localized glow.
-- [ ] `window.__timelines[""] = tl` is registered and the id matches `data-composition-id` on the root.
+| Feeling | Ease | Duration |
+| ---------- | --------------- | -------- |
+| Smooth | `power2.out` | 0.4-0.6s |
+| Snappy | `power4.out` | 0.2-0.3s |
+| Bouncy | `back.out(1.6)` | 0.3-0.5s |
+| Dramatic | `expo.out` | 0.3-0.5s |
+| Dreamy | `sine.inOut` | 0.5-0.8s |
+| Mechanical | `steps(5)` | 0.3-0.5s |
---
-## Step 5: Deliver
+## Step 4: Transitions
-**Gate:** `index.html`, `preview.html`, `README.md`, and (when identity was invented) `DESIGN.md` all exist in the project. `preview.html` loads in Claude Design's in-pane preview.
+### The professional rule: most cuts are hard cuts
-### `preview.html` template (copy verbatim)
+In professional video, ~95% of scene changes are hard cuts. Effect transitions (shaders, dissolves) are reserved for 2-3 key moments — a hero reveal, an energy shift, the CTA landing. Using a shader on every cut is the video equivalent of bolding every word in a paragraph.
-Claude Design's sandbox requires a `?t=` query on every internal URL. Without token forwarding, the iframe receives a `"preview token required"` placeholder and renders black.
+The skeleton pre-wires **2 shader transitions at key moments** and **hard cuts everywhere else**. This gives you varied rhythm: cut-cut-SHADER-cut-cut-SHADER-cut.
+
+### Three transition types
+
+**Hard cut (default -- most scenes use this):**
+No transition code needed. Scene N disappears, scene N+1 appears. The entrance animations on the new scene do all the visual work. This is the professional default.
+
+**Shader transition (2-3 per video -- hero/climax/CTA moments):**
+Pre-wired in the skeleton at key positions. HyperShader captures both scenes as textures and composites them pixel-by-pixel via WebGL.
+
+**When to use shaders vs hard cuts:**
+
+| Use shader for | Use hard cut for |
+| ------------------------------- | ------------------------------------ |
+| Hero reveal / product unveil | Connective scenes between features |
+| Major energy shift or act break | Rapid-fire lists or stats |
+| CTA / final brand moment | 3+ consecutive quick scene changes |
+| Any moment the music punctuates | Scenes where pacing should feel fast |
+
+Rule of thumb: a 6-8 scene video wants **2 shader transitions** and the rest hard cuts.
+
+### Adjusting shader transitions
+
+**Change shader names** -- pick from these 14:
+
+`domain-warp`, `ridged-burn`, `whip-pan`, `sdf-iris`, `ripple-waves`, `gravitational-lens`, `cinematic-zoom`, `chromatic-split`, `swirl-vortex`, `thermal-distortion`, `flash-through-white`, `cross-warp-morph`, `light-leak`, `glitch`
+
+**Match shaders to energy:**
+
+| Energy | Shaders |
+| -------------------- | ---------------------------------------------------- |
+| Calm, editorial | `cross-warp-morph`, `light-leak`, `domain-warp` |
+| Medium, professional | `cinematic-zoom`, `whip-pan`, `sdf-iris` |
+| High, aggressive | `glitch`, `chromatic-split`, `ridged-burn` |
+| Ethereal, mysterious | `gravitational-lens`, `ripple-waves`, `swirl-vortex` |
+
+**Adjust transition timing** -- when you change scene durations, recalculate each transition's `time`:
+
+```
+transition.time = scene_boundary - (transition.duration / 2)
+```
+
+Example: scene-3 ends at 8s, transition duration 0.5s -> `time: 7.75`.
+
+**Minimum transition duration: 0.3s.** Sweet spot is 0.5s.
+
+### How the skeleton handles this
+
+The skeleton only lists **anchor scenes** (the ones bracketing shader transitions) in `HyperShader.init()`. Anchor scenes use `style="opacity:0;"` because HyperShader manages their opacity. Non-anchor scenes use `style="visibility:hidden;"`.
+
+**CRITICAL — two bugs cause "invisible middle scenes" if you don't handle them:**
+
+1. **Non-anchor scenes need explicit `tl.set` visibility toggles.** Without them, the scene container stays at `visibility:hidden` and child animations play inside an invisible parent.
+
+2. **The first anchor scene in each shader group needs `tl.set("#sN", { opacity: 1 }, )`.** HyperShader browser mode does NOT auto-show the first anchor. It stays at `opacity:0` for its entire window. Every demov4 composition has this bug.
+
+The skeleton pre-wires these toggles for every non-anchor scene using **`autoAlpha`** (not `visibility`):
+
+```js
+// --- Non-anchor scene toggles (REQUIRED — must use autoAlpha, not visibility) ---
+tl.set("#s1", { autoAlpha: 0 }, 2.5); // hide s1 at its end time
+tl.set("#s2", { autoAlpha: 1 }, 2.5); // show s2 at its start
+tl.set("#s2", { autoAlpha: 0 }, 5.0); // hide s2 at its end
+tl.set("#s3", { autoAlpha: 1 }, 5.0); // show s3 at its start
+tl.set("#s3", { autoAlpha: 0 }, 7.5); // hide s3 at its end
+```
+
+**Why `autoAlpha` and NOT `visibility`:** When any shader transition fires, HyperShader blanks ALL `.scene` elements to `opacity:0`. If a non-anchor scene only toggles `visibility`, the blanket reset poisons its `opacity` — the scene becomes `visibility:visible` but `opacity:0` (invisible). `autoAlpha` sets BOTH `opacity` AND `visibility` in one call, overriding the blanket reset.
+
+**Rules:**
+
+- Every non-anchor scene gets `tl.set("#sN", { autoAlpha: 1 }, )` AND `tl.set("#sN", { autoAlpha: 0 }, )`
+- Scene 1 gets only a hide at its end time (it starts visible)
+- Anchor scenes do NOT get autoAlpha toggles — HyperShader owns their opacity
+- When you add or remove scenes, update these toggles to match
+
+Example for an 8-scene video with shaders at s4→s5 and s7→s8:
+
+- Anchor scenes: s4, s5, s7, s8 (listed in HyperShader `scenes` array, use `opacity:0`)
+- Non-anchor scenes: s1, s2, s3, s6 (NOT in HyperShader, use `visibility:hidden`, with explicit `tl.set` toggles)
+- Scene 1 has no inline style (visible from t=0)
+
+### Adding or removing shader transitions
+
+To add a shader transition between two scenes:
+
+1. Add both scene IDs to the `scenes` array in `HyperShader.init()`
+2. Add a transition object to the `transitions` array
+3. Change both scenes from `visibility:hidden` to `opacity:0`
+4. Invariant: `scenes.length === transitions.length + 1`
+
+To remove a shader transition (make it a hard cut instead):
+
+1. Remove the scene IDs from `scenes` (unless they're also anchors for another transition)
+2. Remove the transition from `transitions`
+3. Change affected scenes from `opacity:0` to `visibility:hidden`
+
+**BANNED: invisible bridge transitions.** Never pad with `flash-through-white` at 0.01s.
+
+---
+
+## Step 5: Verify the preview + deliver
+
+**Gate:** Preview plays start to finish. All scenes visible. No blinking. Text readable.
+
+### Verify in the preview pane
+
+Scrub through every scene and check:
+
+1. Does scene 1 appear immediately? (If black: runtime not loaded, or `__timelines` key mismatch)
+2. Do shader transitions fire cleanly? (If blinking: transition too short, or exit animation before transition)
+3. Is all text readable against its background?
+4. Does every scene have motion during its hold? (If static: missing mid-scene activity)
+5. Do animations play in the correct order?
+
+### Troubleshooting: preview is black
+
+| Symptom | Cause | Fix |
+| ----------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| All black | Runtime script missing or wrong order | Check GSAP loads before runtime in `` |
+| All black | `__timelines` key doesn't match `data-composition-id` | Both must be `"main"` |
+| All black | Token not forwarded in preview.html | Check `location.search` is appended to src |
+| Scene doesn't appear | Wrong `data-start` / `data-duration` | Check scene windows tile end-to-end |
+| Blink before transition | Exit animation before shader fires | Remove exit tweens -- shader IS the exit |
+| Blink before transition | Transition duration < 0.3s | Increase to 0.5s |
+| Seeking backwards shows blank | Async capture race condition | Known bug in HyperShader browser mode. Forward seek usually works. For reliable scrubbing, download and use `npx hyperframes preview` locally |
+| Middle scene invisible | First shader anchor not shown | Add `tl.set("#sN", { opacity: 1 }, startTime)` for first anchor in each shader group |
+| Middle scene invisible | Non-anchor uses `visibility` instead of `autoAlpha` | Change to `tl.set("#sN", { autoAlpha: 1 }, start)` and `tl.set("#sN", { autoAlpha: 0 }, end)`. Shader blanket reset poisons opacity; `visibility` alone can't fix it. |
+
+### Deliver
+
+Provide: `index.html`, `preview.html`, `README.md`, and `DESIGN.md`.
+
+The `preview.html` and `README.md` are already in the skeleton -- don't modify `preview.html`. Generate `DESIGN.md` from your `:root` custom properties as a reference document.
+
+In your final message, tell the user:
+
+1. **What you built** -- scene count, duration, visual identity summary, shader transitions used
+2. **What to do next** -- download the ZIP, run `npx hyperframes preview` locally to see the full composition with reliable playback
+3. **What to refine in Claude Code** -- be specific about which scenes need animation polish, where timing could be tighter, which mid-scene activities are basic and could be richer. Don't just say "refine in Claude Code" -- say "scene 4's counter animation could be smoother with a longer duration, and scene 6 would benefit from a breathing float on the logo."
+4. **Caveats** -- placeholder assets, unverified stats, elements inspired by a real brand
+
+---
+
+## Section 6: Rules you cannot break
+
+The skeleton handles most structural rules. These are the runtime rules the skeleton can't enforce:
+
+### Determinism (non-negotiable)
+
+| Never | Use instead |
+| --------------------------------- | ---------------------------------------------- |
+| `Math.random()` | Seeded PRNG (only if you need randomness) |
+| `Date.now()`, `performance.now()` | Hard-coded timing or `tl.time()` in `onUpdate` |
+| `setInterval`, `setTimeout` | Timeline tweens + `onUpdate` |
+| `repeat: -1` | `repeat: Math.ceil(duration / cycle) - 1` |
+| `stagger: { from: "random" }` | `from: "start"`, `"center"`, `"end"` |
+| Async timeline construction | Synchronous at page load |
+
+### Media rules
+
+| Never | Use instead |
+| ------------------------------- | --------------------------- |
+| `video.play()`, `audio.play()` | Framework owns playback |
+| `