diff --git a/skills/transitions/SKILL.md b/skills/transitions/SKILL.md
new file mode 100644
index 000000000..5ae95734d
--- /dev/null
+++ b/skills/transitions/SKILL.md
@@ -0,0 +1,83 @@
+---
+name: transitions
+description: Use when a HyperFrames composition has multiple scenes that need transitions between them. Also use when the user mentions crossfades, wipes, reveals, scene changes, or when a composition feels like scenes just pop in and out without visual handoffs.
+---
+
+# Scene Transitions
+
+A transition tells the viewer how two scenes relate. A crossfade says "this continues." A push slide says "next point." A flash cut says "wake up." A blur crossfade says "drift with me." Choose transitions that match what the content is doing emotionally, not just technically.
+
+## Energy → Primary Transition
+
+| Energy | Primary | Accent for key moments | Duration | Easing |
+| ---------------------------------------- | ---------------------------- | ------------------------------ | --------- | ---------------------- |
+| **Calm** (wellness, brand story, luxury) | Blur crossfade, focus pull | Light leak, circle iris | 0.5-0.8s | `sine.inOut`, `power1` |
+| **Medium** (corporate, SaaS, explainer) | Push slide, staggered blocks | Squeeze, vertical push | 0.3-0.5s | `power2`, `power3` |
+| **High** (promos, sports, music, launch) | Flash cut, zoom through | Staggered blocks, gravity drop | 0.15-0.3s | `power4`, `expo` |
+
+Pick ONE primary (60-70% of scene changes) + 1-2 accents. Never use a different transition for every scene.
+
+## Mood → Transition Type
+
+Think about what the transition _communicates_, not just what it looks like.
+
+| Mood | Transitions | Why it works |
+| ------------------------ | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
+| **Warm / inviting** | Light leak, blur crossfade, focus pull, film burn | Soft edges, warm color washes. Nothing sharp or mechanical. The transition feels like sunlight. |
+| **Cold / clinical** | Squeeze, zoom out, blinds, shutter, grid dissolve | Content transforms mechanically — compressed, shrunk, sliced, gridded. Zoom out creates clinical distance. No softness. |
+| **Editorial / magazine** | Push slide, vertical push, diagonal split, shutter | Like turning a page or slicing a layout. Clean directional movement. Diagonal split is a bold editorial cut. |
+| **Tech / futuristic** | Grid dissolve, staggered blocks, blinds, chromatic aberration | Grid dissolve is the core "data" transition. Pattern-based, coordinated. Data-grid aesthetic. |
+| **Tense / edgy** | Glitch, VHS, chromatic aberration, flash cut, ripple | Instability, distortion, digital breakdown. The medium itself is corrupted. |
+| **Playful / fun** | Elastic push, 3D flip, circle iris, morph circle, clock wipe | Overshoot, bounce, rotation, expansion. Clock wipe has a whimsical mechanical quality. |
+| **Dramatic / cinematic** | Zoom through, zoom out, gravity drop, overexposure, diagonal split, color dip to black | Scale, weight, light extremes. Big spatial movements. Diagonal split is a bold geometric cut. |
+| **Premium / luxury** | Focus pull, blur crossfade, color dip to black, slow crossfade | Restraint. Long durations, soft easing, minimal movement. The transition barely exists — that IS the luxury. |
+| **Retro / analog** | Film burn, light leak, VHS, clock wipe | Organic imperfection. Warm color bleeds, scan line displacement. Clock wipe evokes broadcast TV. |
+
+## Narrative Position
+
+| Position | Use | Why |
+| -------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------- |
+| **Opening** | Your most distinctive transition. Match the mood. 0.4-0.6s | Sets the visual language for the entire piece. |
+| **Between related points** | Your primary transition. Consistent. 0.3s | Don't distract — the content is continuing. |
+| **Topic change** | Something different from your primary. Staggered blocks, shutter, squeeze. | Signals "new section" — the viewer's brain resets. |
+| **Climax / hero reveal** | Your boldest accent. Fastest or most dramatic. | This is the payoff — spend your best transition here. |
+| **Wind-down** | Return to gentle. Blur crossfade, crossfade. 0.5-0.7s | Let the viewer exhale after the climax. |
+| **Outro** | Slowest, simplest. Crossfade, color dip to black. 0.6-1.0s | Closure. Don't introduce new energy at the end. |
+
+## Blur Intensity by Energy
+
+| Energy | Blur | Duration | Hold at peak |
+| ---------- | ------- | -------- | ------------ |
+| **Calm** | 20-30px | 0.8-1.2s | 0.3-0.5s |
+| **Medium** | 8-15px | 0.4-0.6s | 0.1-0.2s |
+| **High** | 3-6px | 0.2-0.3s | 0s |
+
+## Presets
+
+| Preset | Duration | Easing |
+| ---------- | -------- | ----------------- |
+| `snappy` | 0.2s | `power4.inOut` |
+| `smooth` | 0.4s | `power2.inOut` |
+| `gentle` | 0.6s | `sine.inOut` |
+| `dramatic` | 0.5s | `power3.in` → out |
+| `instant` | 0.15s | `expo.inOut` |
+| `luxe` | 0.7s | `power1.inOut` |
+
+## Implementation
+
+Read [catalog.md](./catalog.md) for GSAP code and hard rules for every transition type.
+
+| Category | Transitions |
+| -------------------- | ----------------------------------------------------------------------------------------------- |
+| Content-transforming | Push slide, vertical push, elastic push, squeeze, zoom through, zoom out, gravity drop, 3D flip |
+| Reveal/mask | Circle iris, diamond iris, diagonal split, clock wipe, shutter |
+| Dissolve | Crossfade, blur crossfade, focus pull, color dip |
+| Cover | Staggered blocks, horizontal blinds, vertical blinds |
+| Light | Light leak, overexposure burn, film burn |
+| Distortion | Glitch, chromatic aberration, ripple, VHS tape |
+| Pattern | Grid dissolve |
+| Instant | Flash cut, morph circle |
+
+## Transitions That Don't Work in CSS
+
+Avoid: star iris, tilt-shift, lens flare, hinge/door. See catalog.md for why.
diff --git a/skills/transitions/catalog.md b/skills/transitions/catalog.md
new file mode 100644
index 000000000..6ec134ab0
--- /dev/null
+++ b/skills/transitions/catalog.md
@@ -0,0 +1,661 @@
+# Transition Catalog
+
+GSAP implementation code + hard rules for every transition type. All code examples use `old` for the outgoing scene-inner selector and `new` for the incoming, with `T` as the transition start time.
+
+## Table of Contents
+
+- [Hard Rules](#hard-rules)
+- [Scene Template](#scene-template)
+- [Linear / Push](#linear--push)
+- [Radial / Shape](#radial--shape)
+- [3D](#3d)
+- [Scale / Zoom](#scale--zoom)
+- [Dissolve](#dissolve)
+- [Cover](#cover)
+- [Light](#light)
+- [Distortion](#distortion)
+- [Mechanical](#mechanical)
+- [Grid](#grid)
+- [Other](#other)
+- [Blur](#blur)
+- [Destruction](#destruction)
+
+## Hard Rules
+
+These cause real bugs if violated.
+
+**Scene visibility:** Scene 1 visible by default (no `opacity: 0`). Scenes 2+ have `opacity: 0` on the CONTAINER div. GSAP reveals them. No visibility shim (`timedEls`).
+
+**Iframe compatibility:** No external font links (`` to Google Fonts, `@import`). They block sandboxed iframes. Use system fonts.
+
+**Element structure:** No `class="clip"` on scene divs in standalone compositions. Only the root div gets `data-composition-id`/`data-start`/`data-duration`.
+
+**Overlay elements:** Staggered blocks = full-screen 1920x1080, NOT thin strips. Glitch RGB overlays = normal blending at 35% opacity, NOT `mix-blend-mode: multiply` (invisible on dark backgrounds). Light leak overlays = larger than the frame (2400px+), never a visible shape. Overexposure = use `filter: brightness()` on the scene, not just a white overlay.
+
+**VHS tape:** Clone actual scene content with `cloneNode(true)`, NOT colored bars. Each strip: wider than frame (2020px at left:-50px). Red+blue chromatic copies at z-index above main strip. Seeded PRNG for deterministic random offsets.
+
+**Z-index:** Gravity drop, zoom out, diagonal split need outgoing scene ON TOP (`zIndex: 10`) so it exits while revealing the new scene behind (`zIndex: 1`).
+
+**Page burn:** Content burns with the page — no falling debris. Hide scene1 via `tl.set` at burn end, NEVER `onComplete` (not reversible). `onUpdate` must restore `clipPath: "none"` when `wp <= 0` for rewind support. Incoming scene fades from black at 90% through burn.
+
+**Clock wipe:** 9-point polygon with intermediate edge positions. Step through 4 quadrants with separate tweens.
+
+**Grid dissolve:** Cycle 5 palette colors per cell, not monochrome.
+
+**Blinds count by energy:** Calm: 4h/6v. Medium: 6-8h/8v. High: 12-16h/16v.
+
+**Don't use:** Star iris (polygon interpolation broken), tilt-shift (no selective CSS blur), lens flare (visible shape, not optical), hinge/door (distorts too fast).
+
+## Scene Template
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Every transition follows: position new scene → animate outgoing → swap → animate incoming → clean up overlays.
+
+## Linear / Push
+
+### Push Slide
+
+Both scenes move together — new pushes old out.
+
+```js
+tl.to(old, { x: -1920, duration: 0.5, ease: "power3.inOut" }, T);
+tl.fromTo(new, { x: 1920, opacity: 1 }, { x: 0, duration: 0.5, ease: "power3.inOut" }, T);
+```
+
+### Vertical Push
+
+Same as push slide but vertical.
+
+```js
+tl.to(old, { y: -1080, duration: 0.5, ease: "power3.inOut" }, T);
+tl.fromTo(new, { y: 1080, opacity: 1 }, { y: 0, duration: 0.5, ease: "power3.inOut" }, T);
+```
+
+### Elastic Push
+
+Push with overshoot bounce on the incoming scene.
+
+```js
+tl.to(old, { x: -1920, duration: 0.5, ease: "power3.in" }, T);
+tl.fromTo(new, { x: 1920, opacity: 1 }, { x: 30, duration: 0.4, ease: "power4.out" }, T + 0.1);
+tl.to(new, { x: -15, duration: 0.15, ease: "sine.inOut" }, T + 0.5);
+tl.to(new, { x: 0, duration: 0.1, ease: "sine.out" }, T + 0.65);
+```
+
+### Squeeze
+
+Old compresses, new expands from opposite side.
+
+```js
+tl.to(old, { scaleX: 0, transformOrigin: "left center", duration: 0.4, ease: "power3.inOut" }, T);
+tl.fromTo(new, { scaleX: 0, transformOrigin: "right center", opacity: 1 },
+ { scaleX: 1, duration: 0.4, ease: "power3.inOut" }, T + 0.1);
+tl.set(old, { opacity: 0 }, T + 0.5);
+```
+
+## Radial / Shape
+
+### Circle Iris
+
+Expanding circle from center reveals new scene.
+
+```js
+tl.set(new, { opacity: 1 }, T);
+tl.fromTo(new,
+ { clipPath: "circle(0% at 50% 50%)" },
+ { clipPath: "circle(75% at 50% 50%)", duration: 0.5, ease: "power2.out" }, T);
+tl.set(old, { opacity: 0 }, T + 0.5);
+```
+
+### Diamond Iris
+
+Expanding diamond shape from center.
+
+```js
+tl.set(new, { opacity: 1 }, T);
+tl.fromTo(new,
+ { clipPath: "polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%)" },
+ { clipPath: "polygon(50% -20%, 120% 50%, 50% 120%, -20% 50%)", duration: 0.5, ease: "power2.out" }, T);
+tl.set(old, { opacity: 0 }, T + 0.5);
+```
+
+### Diagonal Split
+
+Old scene shrinks to a triangle in one corner.
+
+```js
+tl.set(new, { opacity: 1, zIndex: 1 }, T);
+tl.set(old, { zIndex: 10, clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)" }, T);
+tl.to(old, { clipPath: "polygon(60% 0%, 100% 0%, 100% 40%, 60% 0%)", duration: 0.5, ease: "power3.inOut" }, T);
+tl.set(old, { opacity: 0, zIndex: "auto", clipPath: "none" }, T + 0.5);
+tl.set(new, { zIndex: "auto" }, T + 0.5);
+```
+
+## 3D
+
+### 3D Card Flip
+
+180° Y-axis rotation. Requires CSS: `backface-visibility: hidden; transform-style: preserve-3d;` on both scene-inners. Parent needs `perspective: 1200px`.
+
+```js
+tl.set(new, { rotationY: -180, opacity: 1 }, T);
+tl.to(old, { rotationY: 180, duration: 0.6, ease: "power2.inOut" }, T);
+tl.to(new, { rotationY: 0, duration: 0.6, ease: "power2.inOut" }, T);
+tl.set(old, { opacity: 0 }, T + 0.6);
+```
+
+## Scale / Zoom
+
+### Zoom Through
+
+Old zooms past camera + blurs, new zooms in from behind.
+
+```js
+tl.to(old, { scale: 2.5, opacity: 0, filter: "blur(8px)", duration: 0.4, ease: "power3.in" }, T);
+tl.fromTo(new,
+ { scale: 0.5, opacity: 0, filter: "blur(8px)" },
+ { scale: 1, opacity: 1, filter: "blur(0px)", duration: 0.4, ease: "power3.out" }, T + 0.15);
+```
+
+### Zoom Out
+
+Old shrinks away, new was behind it. Needs z-index management.
+
+```js
+tl.set(new, { opacity: 1, zIndex: 1 }, T);
+tl.set(old, { zIndex: 10, transformOrigin: "50% 50%" }, T);
+tl.to(old, { scale: 0.3, opacity: 0, duration: 0.4, ease: "power3.in" }, T);
+tl.set(old, { zIndex: "auto" }, T + 0.4);
+tl.set(new, { zIndex: "auto" }, T + 0.4);
+```
+
+## Dissolve
+
+### Crossfade
+
+Simple opacity swap. The baseline.
+
+```js
+tl.to(old, { opacity: 0, duration: 0.5, ease: "power2.inOut" }, T);
+tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "power2.inOut" }, T);
+```
+
+### Blur Crossfade
+
+Dissolve with blur + scale shift. **Scale blur amount by energy** — see SKILL.md "Blur Intensity by Energy" section. The examples below show the medium (default) version. For calm compositions, increase to 20-30px with a 0.3-0.5s hold at peak blur. For high-energy, decrease to 3-6px with no hold.
+
+**Medium (default):**
+
+```js
+tl.to(old, { filter: "blur(10px)", scale: 1.03, opacity: 0, duration: 0.5, ease: "power2.inOut" }, T);
+tl.fromTo(new,
+ { filter: "blur(10px)", scale: 0.97, opacity: 0 },
+ { filter: "blur(0px)", scale: 1, opacity: 1, duration: 0.5, ease: "power2.inOut" }, T + 0.1);
+```
+
+**Calm (wellness, luxury) — heavy blur, holds at abstract color:**
+
+```js
+tl.to(old, { filter: "blur(25px)", scale: 1.05, duration: 0.6, ease: "power1.in" }, T);
+tl.to(old, { opacity: 0, duration: 0.4, ease: "power1.in" }, T + 0.4);
+tl.fromTo(new,
+ { filter: "blur(25px)", scale: 0.95, opacity: 0 },
+ { filter: "blur(25px)", scale: 0.95, opacity: 1, duration: 0.3, ease: "power1.inOut" }, T + 0.5);
+tl.to(new, { filter: "blur(0px)", scale: 1, duration: 0.6, ease: "power1.out" }, T + 0.8);
+```
+
+### Focus Pull
+
+Outgoing slowly blurs while incoming fades in sharp. Depth-of-field feel. **Scale blur amount and hold duration by energy.**
+
+**Medium:**
+
+```js
+tl.to(old, { filter: "blur(15px)", duration: 0.5, ease: "power1.in" }, T);
+tl.to(old, { opacity: 0, duration: 0.3, ease: "power2.in" }, T + 0.25);
+tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.3, ease: "power2.out" }, T + 0.25);
+```
+
+**Calm — slow rack focus with long hold at peak defocus:**
+
+```js
+tl.to(old, { filter: "blur(30px)", duration: 0.8, ease: "power1.in" }, T);
+tl.to(old, { opacity: 0, duration: 0.5, ease: "power1.in" }, T + 0.6);
+tl.fromTo(new, { opacity: 0, filter: "blur(20px)" },
+ { opacity: 1, filter: "blur(20px)", duration: 0.3, ease: "power1.inOut" }, T + 0.7);
+tl.to(new, { filter: "blur(0px)", duration: 0.6, ease: "power1.out" }, T + 1.0);
+```
+
+### Color Dip
+
+Fade to solid color, hold, fade up new scene.
+
+```js
+tl.to(old, { opacity: 0, duration: 0.2, ease: "power2.in" }, T);
+// Background color shows through
+tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.2, ease: "power2.out" }, T + 0.25);
+```
+
+## Cover
+
+### Staggered Color Blocks
+
+Full-screen (1920x1080) colored divs slide across staggered. Scene swaps while covered.
+
+**2-block** (standard):
+
+```js
+tl.set("#wipe-a", { x: -1920 }, T - 0.01);
+tl.set("#wipe-b", { x: -1920 }, T - 0.01);
+tl.to("#wipe-a", { x: 0, duration: 0.25, ease: "power3.inOut" }, T);
+tl.to("#wipe-b", { x: 0, duration: 0.25, ease: "power3.inOut" }, T + 0.06);
+tl.set(old, { opacity: 0 }, T + 0.2);
+tl.set(new, { opacity: 1 }, T + 0.2);
+tl.to("#wipe-a", { x: 1920, duration: 0.25, ease: "power3.inOut" }, T + 0.28);
+tl.to("#wipe-b", { x: 1920, duration: 0.25, ease: "power3.inOut" }, T + 0.34);
+```
+
+**5-block** (dense variant): same pattern with 5 blocks at 0.04s stagger. Use composition palette colors.
+
+### Horizontal Blinds
+
+Full-width strips slide across staggered. Each strip: `width: 1920px; height: Xpx`.
+
+**6 strips** (180px each): `0.03s` stagger
+**12 strips** (90px each): `0.018s` stagger
+
+```js
+for (var i = 0; i < N; i++) {
+ tl.set("#blind-h-" + i, { x: -1920 }, T - 0.01);
+ tl.fromTo("#blind-h-" + i, { x: -1920 }, { x: 0, duration: 0.2, ease: "power3.inOut" }, T + i * stagger);
+}
+tl.set(old, { opacity: 0 }, T + coverTime);
+tl.set(new, { opacity: 1 }, T + coverTime);
+for (var i = 0; i < N; i++) {
+ tl.to("#blind-h-" + i, { x: 1920, duration: 0.2, ease: "power3.inOut" }, T + exitStart + i * stagger);
+}
+```
+
+### Vertical Blinds
+
+Same as horizontal but strips are tall and narrow, moving on Y axis.
+
+## Light
+
+### Light Leak
+
+Multiple warm-colored overlays wash across frame. Needs: a flat warm tint layer + 2-3 bright radial gradient divs, all larger than the frame so edges are never visible.
+
+```js
+// Warm tint washes over entire frame
+tl.to("#leak-warm", { opacity: 0.4, duration: 0.3, ease: "power1.in" }, T);
+// Bright leak elements drift in
+tl.to("#leak-1", { opacity: 0.9, x: 300, duration: 0.5, ease: "sine.inOut" }, T + 0.05);
+tl.to("#leak-2", { opacity: 0.8, x: 200, duration: 0.6, ease: "sine.inOut" }, T + 0.1);
+// Peak warmth then swap
+tl.to("#leak-warm", { opacity: 0.6, duration: 0.15, ease: "power2.in" }, T + 0.35);
+tl.set(old, { opacity: 0 }, T + 0.45);
+tl.set(new, { opacity: 1 }, T + 0.45);
+// Leak fades
+tl.to("#leak-warm", { opacity: 0, duration: 0.4, ease: "power2.out" }, T + 0.5);
+tl.to("#leak-1", { opacity: 0, x: 600, duration: 0.35, ease: "power1.out" }, T + 0.5);
+```
+
+### Overexposure Burn
+
+Scene progressively blows out to white using CSS `filter: brightness()`, then white overlay fades in. Swap at peak white. White recedes to reveal new scene.
+
+```js
+tl.to(old, { filter: "brightness(1.5)", scale: 1.03, duration: 0.2, ease: "power1.in" }, T);
+tl.to(old, { filter: "brightness(3)", scale: 1.06, duration: 0.2, ease: "power2.in" }, T + 0.2);
+tl.to("#flash-overlay", { opacity: 0.5, duration: 0.25, ease: "power1.in" }, T + 0.15);
+tl.to("#flash-overlay", { opacity: 1, duration: 0.15, ease: "power2.in" }, T + 0.4);
+tl.set(old, { opacity: 0, filter: "brightness(1)", scale: 1 }, T + 0.55);
+tl.set(new, { opacity: 1 }, T + 0.55);
+tl.to("#flash-overlay", { opacity: 0, duration: 0.35, ease: "power2.out" }, T + 0.55);
+```
+
+### Film Burn
+
+Staggered warm overlays (amber, orange, red) bleed from one edge. Each overlay is a large radial gradient div at high z-index.
+
+```js
+tl.to("#burn-a", { opacity: 1, x: -300, duration: 0.4, ease: "power1.in" }, T);
+tl.to("#burn-b", { opacity: 1, x: -500, duration: 0.5, ease: "power1.in" }, T + 0.05);
+tl.to("#burn-c", { opacity: 1, x: -200, duration: 0.45, ease: "power1.in" }, T + 0.1);
+tl.set(old, { opacity: 0 }, T + 0.35);
+tl.set(new, { opacity: 1 }, T + 0.35);
+tl.to("#burn-a", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.45);
+tl.to("#burn-b", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.5);
+tl.to("#burn-c", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.55);
+```
+
+## Distortion
+
+### Glitch
+
+RGB-tinted overlays (NOT multiply blend — use normal blending at 35% opacity) jitter with large offsets. Scene itself also jitters.
+
+```js
+tl.set("#glitch-r", { opacity: 1, x: 40, y: -8 }, T);
+tl.set("#glitch-g", { opacity: 1, x: -30, y: 12 }, T);
+tl.set("#glitch-b", { opacity: 1, x: 15, y: -20 }, T);
+tl.set(old, { x: -15 }, T);
+// 6 jitter frames at 0.03s intervals with big offsets (±30-60px)
+// ... swap and clear at T + 0.2
+```
+
+### Chromatic Aberration
+
+RGB overlays start aligned then spread apart (±80px), scene fades, converge on new scene.
+
+```js
+tl.set("#glitch-r", { opacity: 0.6, x: 0 }, T);
+tl.set("#glitch-g", { opacity: 0.6, x: 0 }, T);
+tl.set("#glitch-b", { opacity: 0.6, x: 0 }, T);
+tl.to("#glitch-r", { x: -80, opacity: 0.8, duration: 0.3, ease: "power2.in" }, T);
+tl.to("#glitch-b", { x: 80, opacity: 0.8, duration: 0.3, ease: "power2.in" }, T);
+tl.to("#glitch-g", { y: 30, duration: 0.3, ease: "power2.in" }, T);
+// Swap at T + 0.3, converge back at T + 0.3
+```
+
+### Ripple
+
+Rapid oscillation (±30px) + scale distortion (0.97-1.03) + increasing blur. Swap at peak distortion.
+
+```js
+tl.to(old, { x: 30, scale: 1.02, duration: 0.04, ease: "none" }, T);
+tl.to(old, { x: -25, scale: 0.98, filter: "blur(4px)", duration: 0.04, ease: "none" }, T + 0.04);
+// ... more oscillations with increasing blur
+// Swap at peak, incoming stabilizes with decreasing wobble
+```
+
+### VHS Tape
+
+Clone scene into 20 horizontal strips (each 54px, clip-path'd). Each strip shifts x independently with seeded pseudo-random offsets at per-bar random intervals. Add red+blue chromatic offset copies on each strip (z-index above main, 35% opacity). Make strips wider than frame (2020px at left:-50px) so edges never show.
+
+See SKILL.md for clone-based implementation pattern.
+
+## Mechanical
+
+### Shutter
+
+Two full-screen halves close from top and bottom, meet in the middle. Swap while closed. Open again.
+
+```js
+tl.to("#shutter-top", { y: 0, duration: 0.25, ease: "power3.in" }, T);
+tl.to("#shutter-bot", { y: 0, duration: 0.25, ease: "power3.in" }, T);
+tl.set(old, { opacity: 0 }, T + 0.25);
+tl.set(new, { opacity: 1 }, T + 0.25);
+tl.to("#shutter-top", { y: -540, duration: 0.25, ease: "power3.out" }, T + 0.3);
+tl.to("#shutter-bot", { y: 540, duration: 0.25, ease: "power3.out" }, T + 0.3);
+```
+
+### Clock Wipe
+
+Radial polygon sweep stepping through quadrants. Use 9-point polygon with intermediate edge positions for smooth sweep.
+
+```js
+tl.set(new, { opacity: 1, zIndex: 10 }, T);
+var d = 0.1; // duration per quadrant
+tl.set(new, { clipPath: "polygon(50% 50%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%)" }, T);
+tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 50%, 100% 50%, 100% 50%, 100% 50%, 100% 50%)", duration: d, ease: "none" }, T);
+tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 100%)", duration: d, ease: "none" }, T + d);
+tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 0% 100%, 0% 50%, 0% 50%)", duration: d, ease: "none" }, T + d*2);
+tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 0% 100%, 0% 50%, 0% 0%)", duration: d, ease: "none" }, T + d*3);
+tl.set(new, { clipPath: "none", zIndex: "auto" }, T + d*4 + 0.02);
+tl.set(old, { opacity: 0, zIndex: "auto" }, T + d*4 + 0.02);
+```
+
+## Grid
+
+### Grid Dissolve
+
+Grid of colored cells covers the frame in a ripple from center. Scene swaps at 50% coverage. Cells fade out in ripple.
+
+**12-cell** (4x3, each 480x270): standard
+**120-cell** (12x10, each 160x108): dense variant — lower opacity (0.75), tighter ripple
+
+Cells are created dynamically in JS, sorted by distance from center for ripple stagger.
+
+## Other
+
+### Flash Cut
+
+White full-screen overlay flashes at swap point. 0.03s on, 0.1s off.
+
+```js
+tl.to("#flash-overlay", { opacity: 1, duration: 0.03, ease: "power4.out" }, T);
+tl.set(old, { opacity: 0 }, T + 0.03);
+tl.set(new, { opacity: 1 }, T + 0.03);
+tl.to("#flash-overlay", { opacity: 0, duration: 0.1, ease: "power2.out" }, T + 0.05);
+```
+
+### Gravity Drop
+
+Old scene falls down with slight rotation. New scene was behind it. Needs z-index.
+
+```js
+tl.set(new, { opacity: 1, zIndex: 1 }, T);
+tl.set(old, { zIndex: 10 }, T);
+tl.to(old, { y: 1200, rotation: 4, duration: 0.5, ease: "power3.in" }, T);
+tl.set(old, { opacity: 0, zIndex: "auto" }, T + 0.5);
+tl.set(new, { zIndex: "auto" }, T + 0.5);
+```
+
+### Morph Circle
+
+A circle scales up from center to fill frame (becoming the new scene's background color). New scene content fades in on top.
+
+```js
+tl.set("#morph-circle", { background: newBgColor, opacity: 1, scale: 0 }, T);
+tl.to("#morph-circle", { scale: 30, duration: 0.5, ease: "power3.in" }, T);
+tl.set(old, { opacity: 0 }, T + 0.4);
+tl.set(new, { opacity: 1 }, T + 0.4);
+tl.to("#morph-circle", { opacity: 0, duration: 0.15, ease: "power2.out" }, T + 0.5);
+```
+
+## Blur
+
+All blur transitions scale with energy. See SKILL.md "Blur Intensity by Energy" for the full table.
+
+### Blur Through
+
+Content becomes fully abstract before resolving. The heaviest blur transition.
+
+**Calm (default for this type — it's inherently heavy):**
+
+```js
+tl.to(old, { filter: "blur(30px)", scale: 1.08, duration: 0.5, ease: "power1.in" }, T);
+tl.to(old, { opacity: 0, duration: 0.3, ease: "power1.in" }, T + 0.3);
+// Hold: both scenes in abstract blur state
+tl.fromTo(new,
+ { filter: "blur(30px)", scale: 0.92, opacity: 0 },
+ { filter: "blur(30px)", scale: 0.92, opacity: 1, duration: 0.2, ease: "none" }, T + 0.5);
+// Slow resolve
+tl.to(new, { filter: "blur(0px)", scale: 1, duration: 0.7, ease: "power1.out" }, T + 0.7);
+```
+
+**Medium:**
+
+```js
+tl.to(old, { filter: "blur(15px)", scale: 1.05, opacity: 0, duration: 0.4, ease: "power2.in" }, T);
+tl.fromTo(new,
+ { filter: "blur(15px)", scale: 0.95, opacity: 0 },
+ { filter: "blur(0px)", scale: 1, opacity: 1, duration: 0.4, ease: "power2.out" }, T + 0.2);
+```
+
+### Directional Blur
+
+Blur + skew simulating motion in one direction. Scale blur and skew with energy.
+
+**Medium (default):**
+
+```js
+tl.to(old, { filter: "blur(12px)", skewX: -8, x: -200, opacity: 0, duration: 0.4, ease: "power3.in" }, T);
+tl.fromTo(new,
+ { filter: "blur(12px)", skewX: 8, x: 200, opacity: 0 },
+ { filter: "blur(0px)", skewX: 0, x: 0, opacity: 1, duration: 0.4, ease: "power3.out" }, T + 0.15);
+```
+
+**Calm (heavier blur, gentler motion):**
+
+```js
+tl.to(old, { filter: "blur(20px)", skewX: -4, x: -100, opacity: 0, duration: 0.6, ease: "power1.in" }, T);
+tl.fromTo(new,
+ { filter: "blur(20px)", skewX: 4, x: 100, opacity: 0 },
+ { filter: "blur(0px)", skewX: 0, x: 0, opacity: 1, duration: 0.6, ease: "power1.out" }, T + 0.3);
+```
+
+## Destruction
+
+### Page Burn
+
+The outgoing scene literally burns away from a corner. A fire front expands with noise-based irregular edges, a canvas draws the scorched char line at the burn boundary, and individual text characters/elements chip off and fall with gravity as the fire reaches them. The incoming scene reveals behind the burn.
+
+This transition has three systems working together:
+
+1. **Fire geometry** — a radial front expanding from a corner (e.g., bottom-right) with noise-based irregularity for organic edges
+2. **Scene clipping** — the outgoing scene uses an SVG clip-path (with `fill-rule: evenodd`) that cuts a hole matching the fire front. As the fire expands, more of the scene is clipped away. All content (text, images, lines) burns with the page — no separate debris.
+3. **Scorched edge** — a `