video-scroll-hero — a film the visitor scrubs
This skill turns one prepared video into a cinematic landing hero: scrolling
plays the film, rewinding plays it backward, an optional frozen-scene chapter
makes visitors actually read your key messages, and the film's final frames
dissolve into whatever real content the site needs. It was distilled from the
production hero at TakeonAI.com (by
@ideaLOCKIN) after weeks of on-device tuning; every
rule below earned its place by fixing a real, shipped failure. You (the AI
reading this) are the engineer — this skill hands you the laws, the recipes,
and the numbers that survived production. Adapt freely; verify with the probes.
Hard requirement: the user must already have a video. One continuous shot
with a clear journey works best. If they don't have a video, or they want a
3-D/WebGL environment, this is the wrong skill — say so and stop.
1 — Interview the user first (always)
Ask, in one batch, and don't build until answered:
- The video: file path, duration, fps, resolution. Any length works —
everything below scales from
DURATION × FPS = total frames.
- Mobile: a real vertical (9:16) companion video, or a center-crop of the
main one? (Center-crop is fine when the action is center-framed; never
auto-pan the crop — moving crops read as nauseating camera drift.)
- The message chapter (optional): do they want the film to pause at one
meaningful frame while scrolling walks through 3–7 short messages? If yes:
which moment of the film (describe it; you'll find the exact frame), and
what are the messages (offer to draft them — punchy line + one supporting
sentence each works well)?
- The end content: what should the film dissolve into? A form, a CTA
panel, a product embed — THEIR content. Never invent it; build a clean
placeholder if they haven't decided.
- Feel choices: free two-way scrubbing (recommended) — and whether the
opening should show a hook line + a "scroll" cue.
2 — The asset pipeline (video → frames)
Frames beat <video> seeking for scrubbing: deterministic both directions,
park-anywhere, no decoder quirks.
- Extract master PNGs with ffmpeg, then encode two WebP sets from the
master (never from intermediate JPEGs): desktop ≈ 1440-wide at the video's
native fps, quality ≈ 62; mobile ≈ 480-wide center-crop at ~⅔ the fps
(e.g. 24 → 16), quality ≈ 28–36. Budget guide: desktop ≈ 10–13 MB total,
mobile ≈ 2–4 MB.
- Bake the end-blur into the frames. For the final ~12% of both sets,
apply a gaussian blur that ramps from 0 to heavy (σ≈18 at ~810px tall) at
encode time. The runtime then NEVER runs a CSS blur filter — the dissolve
under the end content costs nothing but a cheap composited
scale() zoom.
This is the single biggest processing-power saver in the whole design.
- Serve frames from a versioned folder (
/frames/v1/…) with
public, max-age=31536000, immutable caching, and bump the version on EVERY
re-encode — changed bytes must live at a new URL or returning visitors get
mixed films.
- Export a small
poster.jpg for the no-JS/failure fallback.
- Choose the set once per mount by the smallest CSS viewport dimension
(≤ 520px → mobile set; landscape phones keep the vertical set — frames never
rotate; tablets get desktop and fill by zoom, letterboxing only at extreme
aspects, centered, on the page's night color). Preload the first ~12 frames
of each set with media queries that mirror the same breakpoint.
3 — The engine laws
A single <canvas> in a sticky, viewport-sized stage draws the current frame;
a tall scroll driver supplies the timeline. These laws are ordered by how much
pain they prevent:
- Scroll sets a TARGET; a rAF loop chases it under a sweep cap. Lerp
(
step = (target − playhead) × 0.12) capped at fps × ~2.5 (mobile) /
~2.8 (desktop) film-frames per second. Slowing the cap does NOT smooth
anything — perceived smoothness is distinct images per second (law 4).
- Never draw far from the playhead. Any stand-in frame (decoded or raw)
must be within ~6 frames of the requested one; otherwise keep the last
drawn frame. Violating this shows wrong-scene flashes.
- The playhead never enters undownloaded film. Fold a supply frontier
into the lerp bound so buffering EASES the film to a halt and back (never a
per-frame clamp stutter). Urgent-fetch what the playhead needs now
(fetchPriority high, small concurrency), give every frame a 2-retry budget
then a permanent failed set, and run a dead-network watchdog (~25s of held
playhead with ZERO deliveries → swap to poster + end content, so the page's
purpose is never stranded).
- The decode economy is budgeted and truly off-thread.
createImageBitmap(HTMLImageElement) decodes ON the main thread in
Chromium — fetch frames as Blobs and decode createImageBitmap(Blob)
so decodes ride the worker pool. All decode work draws from ONE per-tick
budget (4 issues/tick) with priority (imminent playhead first). Decode a
stride grid while sweeping fast (stride = clamp(floor(sweepSpeed/20), 1, 2 mobile / 4 desktop)) and backfill at rest. Keep a sliding bitmap window
(2s ahead, ~3s kept) and evict outside it — phones enforce real decoded-
image memory ceilings.
- The root document never scrolls. Put ALL scrolling inside a fixed,
full-viewport container: mobile browser bars then never collapse, expand,
or rubber-band, and the viewport height never changes mid-gesture. Make the
container 1 CSS pixel short of the viewport — Chrome Android promotes
an exactly-viewport-sized scroller to "implicit root scroller", which
re-enables URL-bar hiding; the 1px opt-out keeps the bars parked. Size the
stage with
100dvh (never lvh — the largest-viewport unit overhangs and
"slams" the header at scroll extremes). Ship theme-color matching the
film's darkest tone + viewport-fit=cover, and paint the page underlay
that same color so nothing can ever flash white behind the film.
- The message chapter is a flat segment in the scroll→film curve — not a
lock. Piecewise-map scroll: film act one → message zone (film holds at
the chosen frame, a zone-progress value runs 0→1) → film act two → a short
runway where the film holds its final frame. Fully two-way, no input
locking, every other law rides through untouched.
- Overlays gate on the right clock. Overlays that belong to a FILM MOMENT
(the message layer over the frozen scene) gate on the PLAYHEAD being there;
overlays that belong to a SCROLL RANGE gate on scroll. Mixing them makes
cards flash over mid-flight footage during fast flings.
- Messages are unblastable FORWARD — and never paced backward. The
message column follows a rate-limited progress value (~0.25–0.4 zone/s)
that only ADVANCES while the film is genuinely parked on the chapter
frame — and the FILM's forward bound waits at that frame until the
column completes. A mega-fling parks the film, plays the copy at reading
pace, then releases. But on ANY reversal the column snaps to the
scroll-derived target instantly: a paced rewind reads as an ignored
gesture (this shipped both-directions once, users hated the
direction-change delay, and it was rolled back). While the column is
still behind the scroll ("story debt"), exempt the forward leash of
law 9 — it otherwise trims the banked fling and freezes the story
partway through, waiting for a swipe the visitor never owed. Wheels are
deliberate; consider pacing touch devices only and leaving desktop's
column a pure function of scroll.
- Freshness — never let scrolling feel "committed", and never write the
scrollbar mid-gesture. Raw scroll can bank unbounded lead over the
capped film; reversing then feels ignored until the lead unwinds. Two
rules: cash-out — on any reversal against the lead, re-anchor to the
PRESENTED state (film + column), preserving that gesture's own delta;
forward leash — the scroll may never run more than
2.5 screens
ahead of the presented state. Do NOT leash backward: reversal freshness
is cash-out's job, and a backward leash fights the zone's state-coupled
inverse mapping. Deliver BOTH as a virtual mapping offset
(effective = physical + offset, consumed by the scroll→film mapping),
never as a programmatic scrollTo — every iOS browser is WebKit, and
any scrollTop write mid-momentum kills the fling dead. Fold the offset
into the physical scrollbar with ONE real write only at true idle
(250ms of scroll stillness, no finger or button down), and ZERO it
when the fold lands on a track edge — carried edge residue is a
permanent mapping shift. Finally, rail-drain the offset continuously,
in the mapping itself: the effective position may never sit beyond
the track's ends, and a physical rail must always mean the matching
film end — discard excess the moment a clamp would eat it (pure state
math, gesture-safe). Idle-only cleanup cannot rescue an ACTIVE scrubber:
chained corrections once banked an offset nearly the whole track long,
and the visitor scrolled ~100% of a dead scrollbar while the film sat
frozen. Between-gestures discipline applies to scroll WRITES; virtual
state may adjust any time.
- Designed stops land with a brake, and pre-decode their zone.
Approach at full sweep, then a constant-deceleration tail
(
v = vmax·√(d/D), D ≈ 0.45s of film) — never an exponential crawl,
never a dead wall. Pre-decode every frame of the stop's brake window,
TRICKLED a few frames per tick starting well out — a bulk decode burst is
its own jank (and the chapter frame is usually the film's most expensive
imagery).
- Floor every mid-sweep overlay at 0.002 opacity. At literal opacity 0,
Android Chrome drops composited layers despite
will-change, then
re-rasters big blurred text mid-scroll (a 200ms visible stutter on real
devices). An invisible epsilon keeps layers rastered from boot, where the
opening hold's idle absorbs the cost.
- Opening hold, honestly. Lock input ~1s (load-gated up to a few more
seconds until the opening ~2s of film has arrived), cap the loadbar at
92% while locked, and reveal the scroll cue at the ACTUAL release. Use a
ghost gesture cue that PERFORMS the input (a mouse glyph with a
rolling wheel-dot on desktop; a fingertip comet in the correct physical
direction on touch) — chevrons under-teach the gesture. A branded
"loading" screen is optional and often worse than nothing: the hook
line itself can BE the load-in — stagger its words visible one by one
(opacity-only keyframes, ~0.15s apart, ~0.55s fades,
fill: both)
over the page's night underlay while the opening frames arrive. The
words carry the wait; nothing says "loading".
- Measure, never eyeball. Sample the canvas (~30ms cadence, small
downscale, mean-abs pixel diff) to build motion profiles: smooth = a
clean speed ramp; broken = freeze-then-leap. Test at 4×–6× CPU throttle
AND on slow network (the two conditions that separate dev machines from
real phones). Ship a dev contract:
?jump=<scrollY> lands pre-scrolled
skipping the hold, a param to disable freshness reconciliation (tests
teleport with atomic jumps real gestures never produce), a build-version
global, and a tiny optional on-screen diagnostic overlay — on-device
screenshots of it will solve bugs emulation cannot reproduce. Harness
gotchas that masquerade as engine bugs: a background/unfocused tab
throttles rAF to ~2Hz (focus the tab before any speed-dependent probe);
per-mount decisions like frame-set choice need a RELOAD after the
harness resizes the viewport; pixel-luminance probes are blind to
dark-on-dark wrong content — a phone screen recording (provable frame
range + scrollbar position) out-diagnoses emulation for supply bugs.
4 — The message chapter, concretely
- Find the chapter frame by CONTENT (screenshot candidate frames and pick the
strongest composition — ideally an index that divides both frame sets
evenly). Zone length ≈ 55vh of scroll per message.
- Cards: alternate left/right inside a centered track (~max-w-3xl desktop;
~76% width on phones so the scene stays visible). Frost (backdrop blur
~22–28px, a faint dark tint, an
@supports solid fallback) works
beautifully over a frozen scene — the blur is nearly free because the
backdrop never changes while the zone is active. A dark veil under the
cards is optional taste; frost alone often carries readability.
- The backdrop-root trap: if ANY ancestor between a backdrop-filter
card and the film's canvas carries a
mask-image, clip-path,
filter, or animated opacity < 1, that ancestor forms a backdrop root
and the blur samples its own (empty) subtree instead of the footage —
the card renders as bare text. Keep every ancestor unmasked at opacity 1
and gate the CARDS individually. So no mask-gradient card viewports:
cards enter and exit by viewport CLIPPING, like physical panes.
- Never opacity-fade a frost card — fading the card dilutes its own
blur, so the frost only reads at full opacity and visibly "pops".
Clip, don't fade. Drop cards to true opacity 0 while the chapter is
closed (standing blur layers are real GPU cost on phones), and land
their first paint on the parked frame, never mid-sweep.
- The film freezing + cards sliding = the "pause" — visitors never feel a
lock because there isn't one.
- Offer the chapter eyes-open. It is the highest-maintenance feature
in this skill: scroll runway alone loses to phone momentum (one moderate
flick can blast the whole zone), pacing invites the reversal-delay trap
(law 8), and the overlay demands the frost laws above. The production
site this skill was distilled from iterated its chapter nine ways and
ultimately shipped film-only. It can be gorgeous — budget real
on-device time for it, or skip it.
5 — The ending
The baked-blur tail IS the transition: as the film's last frames play, the
image is already dissolving. Rise the end content over it (opacity +
translate), make it interactive ONLY when the film is truly done (p ≥ ~0.99;
inert + pointer-events none while hidden; reset its inner scroll on
deactivate; hide its inner scrollbar so activation doesn't shift the layout),
and wrap any dynamically-loaded end content in an error boundary so a failed
chunk can never take down the page the film worked so hard to deliver. Mount
heavy end content early (during the message chapter's idle if there is one —
never mid-sweep, where the mount hitch is visible on phones).
6 — What NOT to do
- No
<video>-element seeking for the scrub, no scroll-hijack libraries, no
auto-scroll, no fake scrollbars.
- No CSS blur/filter animation over the canvas — bake it into frames instead.
- No moving crops to chase off-center subjects on mobile.
- No timed text locks — if content must be read, make it a scroll segment.
- No scroll-position adjustments while a finger or mouse button is down.
- Don't relitigate these laws when a symptom moves after a fix — find which
law the new location violates; the classes recur, the locations don't.
Build it, verify it with the probes at throttle, and hand the user a hero
their visitors will scrub twice.
1---2name: video-scroll-hero3description: Build a scroll-scrubbed VIDEO hero section — a prepared video file is REQUIRED. The visitor's scrollbar becomes a film scrubber (forward and backward, butter-smooth on phones), with an optional mid-film "message chapter" where the film freezes and scrolling walks through glass-card messages, ending in a baked-blur dissolve into ANY static end content (a signup form, a product panel, whatever the user wants). Use when the user says "scroll video hero", "scrollytelling video landing", "video scrub hero like TakeonAI", or has a hero video and wants scrolling to play it. NOT for 3-D/WebGL scenes, model viewers, or pages without a real video.4---56# video-scroll-hero — a film the visitor scrubs78This skill turns one prepared video into a cinematic landing hero: scrolling9plays the film, rewinding plays it backward, an optional frozen-scene chapter10makes visitors actually read your key messages, and the film's final frames11dissolve into whatever real content the site needs. It was distilled from the12production hero at [TakeonAI.com](https://TakeonAI.com) (by13[@ideaLOCKIN](https://x.com/ideaLOCKIN)) after weeks of on-device tuning; every14rule below earned its place by fixing a real, shipped failure. You (the AI15reading this) are the engineer — this skill hands you the laws, the recipes,16and the numbers that survived production. Adapt freely; verify with the probes.1718**Hard requirement: the user must already have a video.** One continuous shot19with a clear journey works best. If they don't have a video, or they want a203-D/WebGL environment, this is the wrong skill — say so and stop.2122## 1 — Interview the user first (always)2324Ask, in one batch, and don't build until answered:25261. **The video**: file path, duration, fps, resolution. Any length works —27 everything below scales from `DURATION × FPS = total frames`.282. **Mobile**: a real vertical (9:16) companion video, or a center-crop of the29 main one? (Center-crop is fine when the action is center-framed; never30 auto-pan the crop — moving crops read as nauseating camera drift.)313. **The message chapter (optional)**: do they want the film to pause at one32 meaningful frame while scrolling walks through 3–7 short messages? If yes:33 which moment of the film (describe it; you'll find the exact frame), and34 what are the messages (offer to draft them — punchy line + one supporting35 sentence each works well)?364. **The end content**: what should the film dissolve into? A form, a CTA37 panel, a product embed — THEIR content. Never invent it; build a clean38 placeholder if they haven't decided.395. **Feel choices**: free two-way scrubbing (recommended) — and whether the40 opening should show a hook line + a "scroll" cue.4142## 2 — The asset pipeline (video → frames)4344Frames beat `<video>` seeking for scrubbing: deterministic both directions,45park-anywhere, no decoder quirks.4647- Extract master PNGs with ffmpeg, then encode **two WebP sets from the48 master** (never from intermediate JPEGs): desktop ≈ 1440-wide at the video's49 native fps, quality ≈ 62; mobile ≈ 480-wide center-crop at ~⅔ the fps50 (e.g. 24 → 16), quality ≈ 28–36. Budget guide: desktop ≈ 10–13 MB total,51 mobile ≈ 2–4 MB.52- **Bake the end-blur into the frames.** For the final ~12% of both sets,53 apply a gaussian blur that ramps from 0 to heavy (σ≈18 at ~810px tall) at54 encode time. The runtime then NEVER runs a CSS blur filter — the dissolve55 under the end content costs nothing but a cheap composited `scale()` zoom.56 This is the single biggest processing-power saver in the whole design.57- Serve frames from a **versioned folder** (`/frames/v1/…`) with58 `public, max-age=31536000, immutable` caching, and bump the version on EVERY59 re-encode — changed bytes must live at a new URL or returning visitors get60 mixed films.61- Export a small `poster.jpg` for the no-JS/failure fallback.62- Choose the set once per mount by the smallest CSS viewport dimension63 (≤ 520px → mobile set; landscape phones keep the vertical set — frames never64 rotate; tablets get desktop and fill by zoom, letterboxing only at extreme65 aspects, centered, on the page's night color). Preload the first ~12 frames66 of each set with media queries that mirror the same breakpoint.6768## 3 — The engine laws6970A single `<canvas>` in a sticky, viewport-sized stage draws the current frame;71a tall scroll driver supplies the timeline. These laws are ordered by how much72pain they prevent:73741. **Scroll sets a TARGET; a rAF loop chases it under a sweep cap.** Lerp75 (`step = (target − playhead) × 0.12`) capped at `fps × ~2.5` (mobile) /76 `~2.8` (desktop) film-frames per second. Slowing the cap does NOT smooth77 anything — perceived smoothness is distinct images per second (law 4).782. **Never draw far from the playhead.** Any stand-in frame (decoded or raw)79 must be within ~6 frames of the requested one; otherwise keep the last80 drawn frame. Violating this shows wrong-scene flashes.813. **The playhead never enters undownloaded film.** Fold a supply frontier82 into the lerp bound so buffering EASES the film to a halt and back (never a83 per-frame clamp stutter). Urgent-fetch what the playhead needs now84 (fetchPriority high, small concurrency), give every frame a 2-retry budget85 then a permanent failed set, and run a dead-network watchdog (~25s of held86 playhead with ZERO deliveries → swap to poster + end content, so the page's87 purpose is never stranded).884. **The decode economy is budgeted and truly off-thread.**89 `createImageBitmap(HTMLImageElement)` decodes ON the main thread in90 Chromium — fetch frames as **Blobs** and decode `createImageBitmap(Blob)`91 so decodes ride the worker pool. All decode work draws from ONE per-tick92 budget (~4 issues/tick) with priority (imminent playhead first). Decode a93 stride grid while sweeping fast (`stride = clamp(floor(sweepSpeed/20), 1,94 2 mobile / 4 desktop)`) and backfill at rest. Keep a sliding bitmap window95 (~2s ahead, ~3s kept) and evict outside it — phones enforce real decoded-96 image memory ceilings.975. **The root document never scrolls.** Put ALL scrolling inside a fixed,98 full-viewport container: mobile browser bars then never collapse, expand,99 or rubber-band, and the viewport height never changes mid-gesture. Make the100 container **1 CSS pixel short** of the viewport — Chrome Android promotes101 an exactly-viewport-sized scroller to "implicit root scroller", which102 re-enables URL-bar hiding; the 1px opt-out keeps the bars parked. Size the103 stage with `100dvh` (never lvh — the largest-viewport unit overhangs and104 "slams" the header at scroll extremes). Ship `theme-color` matching the105 film's darkest tone + `viewport-fit=cover`, and paint the page underlay106 that same color so nothing can ever flash white behind the film.1076. **The message chapter is a flat segment in the scroll→film curve — not a108 lock.** Piecewise-map scroll: film act one → message zone (film holds at109 the chosen frame, a zone-progress value runs 0→1) → film act two → a short110 runway where the film holds its final frame. Fully two-way, no input111 locking, every other law rides through untouched.1127. **Overlays gate on the right clock.** Overlays that belong to a FILM MOMENT113 (the message layer over the frozen scene) gate on the PLAYHEAD being there;114 overlays that belong to a SCROLL RANGE gate on scroll. Mixing them makes115 cards flash over mid-flight footage during fast flings.1168. **Messages are unblastable FORWARD — and never paced backward.** The117 message column follows a rate-limited progress value (~0.25–0.4 zone/s)118 that only ADVANCES while the film is genuinely parked on the chapter119 frame — and the FILM's forward bound waits at that frame until the120 column completes. A mega-fling parks the film, plays the copy at reading121 pace, then releases. But on ANY reversal the column snaps to the122 scroll-derived target instantly: a paced rewind reads as an ignored123 gesture (this shipped both-directions once, users hated the124 direction-change delay, and it was rolled back). While the column is125 still behind the scroll ("story debt"), exempt the forward leash of126 law 9 — it otherwise trims the banked fling and freezes the story127 partway through, waiting for a swipe the visitor never owed. Wheels are128 deliberate; consider pacing touch devices only and leaving desktop's129 column a pure function of scroll.1309. **Freshness — never let scrolling feel "committed", and never write the131 scrollbar mid-gesture.** Raw scroll can bank unbounded lead over the132 capped film; reversing then feels ignored until the lead unwinds. Two133 rules: **cash-out** — on any reversal against the lead, re-anchor to the134 PRESENTED state (film + column), preserving that gesture's own delta;135 **forward leash** — the scroll may never run more than ~2.5 screens136 ahead of the presented state. Do NOT leash backward: reversal freshness137 is cash-out's job, and a backward leash fights the zone's state-coupled138 inverse mapping. Deliver BOTH as a **virtual mapping offset**139 (`effective = physical + offset`, consumed by the scroll→film mapping),140 never as a programmatic `scrollTo` — every iOS browser is WebKit, and141 any scrollTop write mid-momentum kills the fling dead. Fold the offset142 into the physical scrollbar with ONE real write only at true idle143 (~250ms of scroll stillness, no finger or button down), and ZERO it144 when the fold lands on a track edge — carried edge residue is a145 permanent mapping shift. Finally, **rail-drain the offset continuously,146 in the mapping itself**: the effective position may never sit beyond147 the track's ends, and a physical rail must always mean the matching148 film end — discard excess the moment a clamp would eat it (pure state149 math, gesture-safe). Idle-only cleanup cannot rescue an ACTIVE scrubber:150 chained corrections once banked an offset nearly the whole track long,151 and the visitor scrolled ~100% of a dead scrollbar while the film sat152 frozen. Between-gestures discipline applies to scroll WRITES; virtual153 state may adjust any time.15410. **Designed stops land with a brake, and pre-decode their zone.**155 Approach at full sweep, then a constant-deceleration tail156 (`v = vmax·√(d/D)`, D ≈ 0.45s of film) — never an exponential crawl,157 never a dead wall. Pre-decode every frame of the stop's brake window,158 TRICKLED a few frames per tick starting well out — a bulk decode burst is159 its own jank (and the chapter frame is usually the film's most expensive160 imagery).16111. **Floor every mid-sweep overlay at 0.002 opacity.** At literal opacity 0,162 Android Chrome drops composited layers despite `will-change`, then163 re-rasters big blurred text mid-scroll (a 200ms visible stutter on real164 devices). An invisible epsilon keeps layers rastered from boot, where the165 opening hold's idle absorbs the cost.16612. **Opening hold, honestly.** Lock input ~1s (load-gated up to a few more167 seconds until the opening ~2s of film has arrived), cap the loadbar at168 92% while locked, and reveal the scroll cue at the ACTUAL release. Use a169 **ghost gesture** cue that PERFORMS the input (a mouse glyph with a170 rolling wheel-dot on desktop; a fingertip comet in the correct physical171 direction on touch) — chevrons under-teach the gesture. A branded172 "loading" screen is optional and often worse than nothing: the hook173 line itself can BE the load-in — stagger its words visible one by one174 (opacity-only keyframes, ~0.15s apart, ~0.55s fades, `fill: both`)175 over the page's night underlay while the opening frames arrive. The176 words carry the wait; nothing says "loading".17713. **Measure, never eyeball.** Sample the canvas (~30ms cadence, small178 downscale, mean-abs pixel diff) to build motion profiles: smooth = a179 clean speed ramp; broken = freeze-then-leap. Test at 4×–6× CPU throttle180 AND on slow network (the two conditions that separate dev machines from181 real phones). Ship a dev contract: `?jump=<scrollY>` lands pre-scrolled182 skipping the hold, a param to disable freshness reconciliation (tests183 teleport with atomic jumps real gestures never produce), a build-version184 global, and a tiny optional on-screen diagnostic overlay — on-device185 screenshots of it will solve bugs emulation cannot reproduce. Harness186 gotchas that masquerade as engine bugs: a background/unfocused tab187 throttles rAF to ~2Hz (focus the tab before any speed-dependent probe);188 per-mount decisions like frame-set choice need a RELOAD after the189 harness resizes the viewport; pixel-luminance probes are blind to190 dark-on-dark wrong content — a phone screen recording (provable frame191 range + scrollbar position) out-diagnoses emulation for supply bugs.192193## 4 — The message chapter, concretely194195- Find the chapter frame by CONTENT (screenshot candidate frames and pick the196 strongest composition — ideally an index that divides both frame sets197 evenly). Zone length ≈ 55vh of scroll per message.198- Cards: alternate left/right inside a centered track (~max-w-3xl desktop;199 ~76% width on phones so the scene stays visible). Frost (backdrop blur200 ~22–28px, a faint dark tint, an `@supports` solid fallback) works201 beautifully over a frozen scene — the blur is nearly free because the202 backdrop never changes while the zone is active. A dark veil under the203 cards is optional taste; frost alone often carries readability.204- **The backdrop-root trap:** if ANY ancestor between a backdrop-filter205 card and the film's canvas carries a `mask-image`, `clip-path`,206 `filter`, or animated `opacity < 1`, that ancestor forms a backdrop root207 and the blur samples its own (empty) subtree instead of the footage —208 the card renders as bare text. Keep every ancestor unmasked at opacity 1209 and gate the CARDS individually. So no mask-gradient card viewports:210 cards enter and exit by viewport CLIPPING, like physical panes.211- **Never opacity-fade a frost card** — fading the card dilutes its own212 blur, so the frost only reads at full opacity and visibly "pops".213 Clip, don't fade. Drop cards to true opacity 0 while the chapter is214 closed (standing blur layers are real GPU cost on phones), and land215 their first paint on the parked frame, never mid-sweep.216- The film freezing + cards sliding = the "pause" — visitors never feel a217 lock because there isn't one.218- **Offer the chapter eyes-open.** It is the highest-maintenance feature219 in this skill: scroll runway alone loses to phone momentum (one moderate220 flick can blast the whole zone), pacing invites the reversal-delay trap221 (law 8), and the overlay demands the frost laws above. The production222 site this skill was distilled from iterated its chapter nine ways and223 ultimately shipped film-only. It can be gorgeous — budget real224 on-device time for it, or skip it.225226## 5 — The ending227228The baked-blur tail IS the transition: as the film's last frames play, the229image is already dissolving. Rise the end content over it (opacity +230translate), make it interactive ONLY when the film is truly done (p ≥ ~0.99;231`inert` + pointer-events none while hidden; reset its inner scroll on232deactivate; hide its inner scrollbar so activation doesn't shift the layout),233and wrap any dynamically-loaded end content in an error boundary so a failed234chunk can never take down the page the film worked so hard to deliver. Mount235heavy end content early (during the message chapter's idle if there is one —236never mid-sweep, where the mount hitch is visible on phones).237238## 6 — What NOT to do239240- No `<video>`-element seeking for the scrub, no scroll-hijack libraries, no241 auto-scroll, no fake scrollbars.242- No CSS blur/filter animation over the canvas — bake it into frames instead.243- No moving crops to chase off-center subjects on mobile.244- No timed text locks — if content must be read, make it a scroll segment.245- No scroll-position adjustments while a finger or mouse button is down.246- Don't relitigate these laws when a symptom moves after a fix — find which247 law the new location violates; the classes recur, the locations don't.248249Build it, verify it with the probes at throttle, and hand the user a hero250their visitors will scrub twice.