diff --git a/skills-manifest.json b/skills-manifest.json
index 184a7712c4..791e0fdf66 100644
--- a/skills-manifest.json
+++ b/skills-manifest.json
@@ -22,8 +22,8 @@
"files": 17
},
"hyperframes-animation": {
- "hash": "3947516ec450f3aa",
- "files": 121
+ "hash": "7e1a0bb8bc35353c",
+ "files": 122
},
"hyperframes-audio": {
"hash": "b39bac771e873eae",
diff --git a/skills/hyperframes-animation/rules-index.md b/skills/hyperframes-animation/rules-index.md
index 8568d22791..ed49654a8f 100644
--- a/skills/hyperframes-animation/rules-index.md
+++ b/skills/hyperframes-animation/rules-index.md
@@ -96,6 +96,7 @@ A rule's own **Critical Constraints** section lists only what is SPECIFIC to tha
Fake directional velocity blur on a fast entrance / camera push-through — blur peaks at max speed, resolves to 0 at the settle. Two paths: SVG `feGaussianBlur` stdDeviation on the motion axis (proxy-tweened), or a deterministic echo/ghost trail that collapses into the lead. Entrances / mid-shot only. Tags: motion-blur, streak, velocity, ghost, echo, fast
Staggered ARRIVAL cascade — words/elements whip in from below, each starting before the previous settles, an accelerating wave that resolves composed. Title cards, segment openers, list intros. Binary 0→1 opacity via `tl.set` — never fade an arrival. Tags: entrance, cascade, stagger, kinetic-text, title-card, arrival, waterfall
Deterministic particle / confetti events — confetti pop that bursts up and drifts down on gravity (optional instant-shrink), dot burst from behind text, glyph dissolve to particles. Fixed pool, index-seeded launch values, one `ease: "none"` driver whose onUpdate computes each particle as a pure ballistic function of time — scrub-safe mid-flight, ≤ ~40 particles. Tags: particles, confetti, burst, dissolve, ballistic, deterministic, punctuation
+Organic physics (rope, flag, cloth, debris, flock) never runs at render time — a build script steps the sim at `dt = 1/fps` with a seeded PRNG and writes per-frame data; playback is one `ease:"none"` driver doing `frames[round(t × fps)]`, an index lookup and nothing else. Synchronous script include (never fetched), ~60-frame pre-roll, frame 0 applied at setup. Tags: physics, simulation, baked, offline, verlet, rope, flag, seeded, replay, deterministic
Slow-fast-slow three-phase group slide (power3.in ramp → linear burst → power4.out tail, 10/65/25 distance, tail ≥3× ramp-in) to reposition a composed group and reveal content during the burst. Tags: slide, reposition, group-motion, nudge, slow-fast-slow
diff --git a/skills/hyperframes-animation/rules/baked-sim.md b/skills/hyperframes-animation/rules/baked-sim.md
new file mode 100644
index 0000000000..5729f22f17
--- /dev/null
+++ b/skills/hyperframes-animation/rules/baked-sim.md
@@ -0,0 +1,169 @@
+---
+name: baked-sim
+description: Organic physics (a rope, flag, cloth, debris, a flock) never runs at render time — a build script steps the simulation at dt = 1 / fps with a seeded PRNG and writes per-frame data; playback is one `ease:"none"` driver whose onUpdate does `frames[Math.round(t × fps)]`, an index lookup and nothing else. Seek-safe and deterministic by construction; shipped as a synchronous script include, never fetched.
+metadata:
+ tags: physics, simulation, baked, offline, verlet, rope, cloth, flag, pennant, debris, seeded, prng, replay, frame-index, deterministic
+---
+
+# Baked Sim
+
+A hanging pennant catching wind, a rope settling, a cloud of debris tumbling: motion that comes from a **simulation** — integration, constraints, forces — not from an ease. A simulation is stateful by nature (frame N depends on frame N−1), which is exactly what a seeked renderer cannot run. So the simulation runs **once, offline**, at build time, and the composition replays its output by frame index. The renderer sees a lookup, never an integrator.
+
+Boundaries: [particle-burst.md](particle-burst.md) needs no bake — each particle is a closed-form ballistic function of time and evaluates directly. Bake when the bodies **interact** (constraints, collisions, wind on a chain, flocking) or the step is iterative. A [sine-wave-loop.md](sine-wave-loop.md) idle is neither; keep it analytic.
+
+## How It Works
+
+1. **Bake** — a build script (`bake/bake-.mjs`) steps the sim at `dt = 1 / FPS` for `PRE_ROLL + FRAMES` steps with a seeded PRNG (`mulberry32(SEED)`, never `Math.random`), fixed iteration counts, and per-frame forces computed from the frame number. It writes one entry per composition frame: an SVG path `d`, an array of transforms, a list of points.
+2. **Ship synchronously** — the output is a script that assigns one global (`window.__PENNANT = {...}`), loaded with a plain `
+```
+
+```js
+const PEN = window.__PENNANT; // present before this script runs — no load event, no race
+const rope = document.getElementById("rope");
+const LAST = PEN.frames.length - 1;
+const apply = (i) => rope.setAttribute("d", PEN.frames[i]);
+apply(0); // structural frame 0: the first captured frame never depends on the driver having fired
+const proxy = { f: 0 };
+tl.to(
+ proxy,
+ {
+ f: LAST,
+ duration: LAST / PEN.fps, // f === k at every sampled frame k / fps — an off-by-one here shows the previous entry on half the frames
+ ease: "none",
+ onUpdate: () => apply(Math.max(0, Math.min(LAST, Math.round(proxy.f)))), // an index lookup, nothing else
+ },
+ SCENE_AT,
+);
+```
+
+## Variations
+
+- **Transform arrays** — bake `[x, y, rot]` per body per frame (debris, a flock) and write each element's `transform` in the same `onUpdate`; the pool is fixed at setup.
+- **Canvas draw** — bake point lists and draw them with `ctx` inside the driver: the canvas becomes a pure function of the frame index (paint frame 0 at setup).
+- **Two-rate bake** — simulate at a finer `dt` (`1 / (FPS × 2)`) for stiff constraints and write every second step; the composition still indexes at its own fps.
+- **Re-bake for direction** — a physically correct sim is not yet art-directed: the reference pennant hung limp until wind ≈ 3 × gravity, and streams at that ratio. Budget one bake iteration for the look, then freeze the seed.
+- **Designed motion on top** — the baked object still takes ordinary tweens (a fade-in, a settle, an exit); the bake owns only what the sim owns.
+
+## Values
+
+| token | range | notes |
+| --------- | ------------------------------ | ------------------------------------------------------------------------------------ |
+| FPS | = the composition's `data-fps` | one entry per composition frame; the driver's `round()` then never straddles two |
+| PRE_ROLL | 30–90 frames (default 60) | enough for the system to hang / settle before frame 0 |
+| ITER | 8–20 constraint passes | stiffness; more = stiffer rope, longer bake, same file size |
+| SEED | any fixed integer | change it to change the gusts; commit it with the data |
+| data size | ≲ 100–300 KB per baked object | a 210-frame, 16-point path is ~75 KB; past ~1 MB decimate the path or the body count |
+
+## Critical Constraints
+
+- **No integration at runtime** — the composition never steps, accumulates, or reads a previous frame; `frames[i]` is the whole runtime. A per-frame `+=` on baked state re-introduces the race the bake removed.
+- **Synchronous include** — the baked script is a `