# Mercury.com hero teardown — "zoom into the laptop"

*Analyzed 2026-07-08 against the live site (1440×900 desktop + 390w mobile). All evidence files in `mercury-hero/`.*

---

## TL;DR

- **The zoom is a video, not CSS.** A 6.4-second pre-rendered mp4 (`/videos/hero/hero-scrub-lg.mp4`, 1920×720, 30fps) contains the entire camera move — hilltop wide shot → dolly into the laptop → dashboard fills the frame. Nothing in the DOM scales or transforms.
- **Scroll scrubs the video timeline.** The hero wrapper is `250vh` tall with a sticky full-viewport inner layer; JS maps scroll position linearly to `video.currentTime` (`t = 6.4s × scrollY / 150vh`). The video is always *paused* — scrolling is the playhead.
- **The dashboard on the laptop screen is a static composited texture.** Same numbers, same chart in every frame ("Welcome, Jane", $5,216,471.18). All perceived motion is camera motion.
- **The handoff is baked into the footage.** In the last ~1.5s the laptop hardware and the landscape fade to near-black, leaving a flat dashboard panel on a dark field — which pixel-matches the dark DOM section that follows. A `hero_end_frame` JPG bridges the swap, then real DOM + full-bleed "flow" videos take over.
- **Seekability is engineered into the encode:** a keyframe every 3 frames (64 I-frames / 192) so `currentTime` seeks resolve instantly in both directions; a `hero_start_frame` JPG paints instantly while the video preloads in full.

---

## The asset

**File:** `https://mercury.com/videos/hero/hero-scrub-lg.mp4` (saved here as `hero-scrub-lg.mp4`)

| Property | Value |
|---|---|
| Codec | H.264 (yuv420p) |
| Resolution | 1920×720 (8:3 letterbox strip, `object-fit: cover` on a 100vw×100vh element) |
| Duration / fps | 6.4 s, 30 fps, 192 frames |
| Bitrate / size | ~6.0 Mbps, 4.8 MB |
| Keyframes | **64 I-frames = one every 3 frames (0.1 s)** — scrub-seek optimization |
| faststart | No (moov at end) — irrelevant because it's `preload="auto"` and fully fetched before scrubbing |
| Responsive variants | `hero-scrub-md.mp4` 1440×720 (~4 Mbps, 3.2 MB, ≥40rem), `hero-scrub-sm.mp4` 1080×720 (~3 Mbps, 2.4 MB, default) via `<source media>` |
| Companion stills | `hero_start_frame_{sm,xl}.jpg` and `hero_end_frame_{sm,xl}.jpg`, both 3840×1440, served through `next/image` |

**What the footage is.** A pre-rendered 3D / heavily composited scene, not a live plate with a CSS zoom: parallax is real (the chair rotates relative to the desk and exits frame left as the camera passes it), so there is a true 3D camera move, not a Ken Burns pan on a still. The scene: a mid-century desk, leather office chair, and an open MacBook on a grassy hilltop above a misty river valley at golden hour — dreamlike, obviously not literal.

**The dashboard within the footage is static.** Frame-diffing the screen region late in the clip (t=6.0 vs t=6.35, after the camera settles) shows only residual camera drift; every legible frame carries identical content — "Welcome, Jane", Mercury balance $5,216,471.18, the same balance chart, Invoicing / Credit Card / Bill Pay cards ("Skyscape Systems" demo org). The screen is a single UI texture tracked onto the laptop display. All animation the viewer perceives is the camera.

**Camera path** (see `frames/frame-00…11`):

| t | Frame | What you see |
|---|---|---|
| 0.0 | frame-00 | Wide establishing shot. Desk + chair tiny on the hilltop, river valley leading the eye to it. Heavy blur bands on left/right edges (baked-in DoF vignette). |
| 0.6–1.7 | 01–03 | Slow dolly-in, slightly low angle. The chair swings left out of frame — the camera "walks past" it toward the desk. Screen visible but unreadable. |
| 2.3 | 04 | Laptop centered above the desk edge; dashboard layout discernible, not legible. |
| 2.9–3.5 | 05–06 | Approach continues; "Welcome, Jane" becomes readable ≈ t 3.5. |
| 4.1 | 07 | **Full legibility.** Laptop nearly fills frame; every dashboard number readable. |
| 4.7 | 08 | Screen dominates; landscape is abstract blur behind the bezel. |
| 5.2–6.4 | 09–11 | The trick: scenery and laptop hardware **fade to near-black while the UI holds** — bezel and keyboard dissolve, and the final frame is a flat dashboard panel floating centered on a near-black field. |

That last beat is the whole handoff strategy: the footage ends *already looking like a web page*, so swapping video → DOM is invisible.

## The scroll binding

**DOM structure** (from `dom-dump.json`):

```
<section>                                      ← hero
  <div style="height:250vh" class="dark-neutral-theme relative …">   ← scroll runway
    <div class="sticky top-0 h-screen overflow-hidden grid">          ← pinned viewport
      <picture class="absolute inset-0 z-1 data-[hidden=true]:hidden">  hero_start_frame (instant paint)
      <video preload="auto" muted playsinline class="absolute inset-0 object-cover">
        <source hero-scrub-lg.mp4 media="(min-width:64rem)"> …md… …sm…
      <div class="relative z-10 …">                                   ← headline overlay (DOM text)
        <div style="opacity:1">  ← JS-driven fade wrapper
          <h1>Radically different banking</h1> + email capture + FDIC bar
<section>                                      ← flows section ("Everything you do with money")
  <div data-phase="idle|transitioning|complete"
       class="… data-[phase=idle]:translate-y-[-500vh]">              ← parked off-screen until scrub completes
    <picture class="absolute z-20 …">  hero_end_frame (bridge overlay)
    <video flow_1.mp4 …flow_4>  full-bleed dashboard walkthroughs, CSS mask-image feathered
```

- **Runway / pin:** wrapper is `250vh` (2250 px at 900 px viewport); the inner layer is `position: sticky; top: 0; height: 100vh; overflow: hidden`. Scrub range = 250vh − 100vh = **150vh of scroll for the full move**.
- **Scrub math is linear, no easing curve in JS** (the easing is baked into the footage). Measured settled samples: scrollY 300 → t 1.39 s, 600 → 2.82, 960 → 4.52, 1260 → 5.97; the model `t = 6.4 × scrollY/1350` predicts 1.42 / 2.84 / 4.55 / 5.97. The `<video>` stays `paused` forever — JS assigns `currentTime` (mid-flight samples lag the target slightly, so there's a small lerp/rAF smoothing toward the scroll position rather than a raw assignment).
- **Headline fade:** the H1/email overlay lives *inside* the sticky layer (so it stays pinned, `getBoundingClientRect().top` constant at 144 px) and fades via a JS-driven inline `opacity` on its wrapper div — 1 → 0 early in the scrub (fully gone by ~40% progress). No transform, just opacity.
- **Start-frame swap:** the `hero_start_frame` `<picture>` sits over the video (`z-1`) and is toggled `data-hidden=true` once the video has buffered — that's how they afford `preload="auto"` on a 4.8 MB file without a blank hero.
- **The handoff is a phase state machine**, not more scrolling. The next section carries `data-phase`; while `idle` it is parked off-screen via `translate-y-[-500vh]`. When the scrub reaches the end, phase flips `idle → transitioning → complete`: an 800 ms `cubic-bezier(0.76, 0, 0.24, 1)` transition runs, **the page's scroll position is programmatically corrected** (we measured scrollY snap from ~1560 → 960 as the hero runway collapses out of the layout), and the `hero_end_frame` JPG (z-20, opacity transition, pixel-identical to the video's last frame) covers the seam. Then the flows section is live DOM: accordion text on the left, `flow_1…4.mp4` (3840×1440, 8–11 s each) playing as the dashboard walkthrough, edge-feathered with `mask-image: linear-gradient(...)` on all four sides and crossfaded (600 ms blur+opacity) when you switch accordion items.
- **Reduced motion:** `motion-reduce:` classes hide the video, force the wrapper to `100vh`, and show the still — the whole theater degrades to a static hero.

## The craft details

- **The story does the selling.** Wide shot = "your company out in the world"; the river literally leads the eye to the desk; landing on the dashboard says "and this is where you run it." The zoom is a narrative, not a decoration.
- **Baked-in cinematography you can't get from CSS:** true parallax (passing the chair), depth-of-field blur vignette on the left/right edges (which also hides the 8:3 crop and gives an anamorphic feel), golden-hour rim light, mist layers. Screen brightness is balanced against the sky so the laptop reads as the brightest *cool* element in a warm scene — your eye locks onto it from frame one.
- **Legibility is choreographed:** the dashboard resolves progressively — layout at t≈2.3, greeting at t≈3.5, every number at t≈4.1 — then the film *holds* on the legible screen for the final 2+ seconds while the world fades. Because scroll is the playhead, the user controls the pacing and can park on any beat; nothing plays past them.
- **The dark ending is the seam-hider.** Ending the footage as a flat UI on near-black means the video→DOM cut happens on an almost featureless dark frame, covered by a matching still, inside an 800 ms eased transition, *and* with a scroll-position correction — four layers of concealment on the one moment that could look cheap.
- **Scrub feel is an encoding decision:** keyframe every 3 frames makes reverse scrubbing as smooth as forward; 6 Mbps is spent on a fully-preloaded 4.8 MB file rather than streaming.
- **Mobile gets the same theater**, not a fallback: identical scroll-scrub mechanic with a narrower crop (`hero-scrub-sm.mp4`, 1080×720) framed so the desk sits below the headline in portrait.

**Contrast — demo.mercury.com:** entirely different animal. It's a real, interactive sandboxed instance of the product (live DOM dashboard, "Viewing as Admin", clickable nav — no videos at all, `scrollHeight` = viewport). The homepage hero is cinema that *ends* on the dashboard; the demo subdomain is the dashboard. The marketing site even links to it ("Launch demo") right where the scrub hands off — the two are a funnel pair. (`demo-mercury.jpg`)

## How to replicate without a 3D render

The live-DOM version of this trick, for Entity.ID:

1. **Plate:** one high-res photo/render of a laptop in a scene, screen blank/dark. Even a good stock or AI-generated plate works — Mercury's scene is itself a stylized render.
2. **Screen composite:** render the real dashboard (actual product screenshot, or better, live DOM in an `<iframe>`/div) and pin it to the laptop screen with `transform: matrix3d(...)` computed from the screen's four corner points (a 4-point homography — small JS utility, or precompute in devtools). Wrap in `overflow:hidden` + a subtle inset gradient for glass/glare.
3. **Scroll runway + pin:** same skeleton as Mercury — wrapper `height: 250vh`, child `position: sticky; top: 0; height: 100vh; overflow: hidden`. Progress = `scrollY / 150vh`.
4. **Zoom:** scale the *whole plate* (photo + composited screen together, one layer) with `transform: scale()` where `transform-origin` sits at the laptop-screen center. Drive it from progress with an eased curve and a lerp (`current += (target-current)*0.12` per rAF) for the weighted, smoothed feel. `will-change: transform`, and keep it to transform/opacity only.
5. **Handoff:** at max zoom the composited screen should land pixel-aligned with a full-bleed DOM dashboard section stacked underneath; crossfade the plate out over ~200 ms and unpin. Fade the headline overlay's opacity out over the first ~40% of progress, exactly as they do.

**Tradeoffs vs Mercury's baked footage:**

| | Live-DOM zoom | Baked scrub video (Mercury) |
|---|---|---|
| Perspective | Flat scale — no parallax, no camera path around objects | True 3D dolly, passes the chair, perspective on the desk changes |
| Light/DoF | Static; fake with pre-blurred plate layers | Focus pull, edge blur, light response all free |
| Dashboard | **Real product, always current, crisp text at every zoom level** | Frozen texture; every UI change means re-rendering the film |
| Weight | ~200–400 KB (one plate + DOM) | 2.4–4.8 MB per breakpoint |
| Effort | 1–2 days of front-end | 3D scene, camera animation, screen tracking, three encodes |

A 2.5D cheat closes half the gap: cut the plate into 3–4 layers (foreground grass / desk+laptop / background), translate them at different rates while scaling — fake parallax that reads as camera movement at small zoom ranges.

**When a baked video is worth it:** when the move must travel *around* things (reveal the scene, pass objects, rack focus) rather than just *toward* the screen — that's unfakeable in DOM. For a fundraise-stage site, ship the live-DOM version first (it also demos the real product, which a frozen texture can't); commission the film when the brand scene itself is the message. If you do bake one: 30 fps, GOP ≤ 3, ~6 s, per-breakpoint crops from one 3840×1440 master, export first/last frames as JPGs, end the footage already looking like your next DOM section.

## Evidence index

All under `brand/research/mercury-hero/`:

| File | What it shows |
|---|---|
| `hero-scrub-lg.mp4` / `hero-scrub-sm.mp4` | The desktop and mobile scrub films themselves |
| `hero_start_frame_xl.jpg` | Instant-paint poster (video frame 0 at 3840×1440) |
| `hero_end_frame_xl.jpg` | Handoff bridge still (video final frame — flat UI on black) |
| `frames/frame-00…11-t*.jpg` | 12 evenly-spaced frames, the full camera path |
| `wheel-000.jpg` | Live page at rest: headline + email overlay on video frame 0 |
| `live-scrub-300.jpg` | ~22% scroll (t≈1.4 s): headline still up, camera passing the chair |
| `live-scrub-620.jpg` | ~46% (t≈2.9 s): headline gone, laptop centering |
| `live-scrub-940.jpg` | ~70% (t≈4.5 s): dashboard fully legible in-scene |
| `live-scrub-1260.jpg` | ~93% (t≈6.0 s): flat UI on black — the pre-handoff frame |
| `live-handoff.jpg` / `wheel-final.jpg` | After phase flip: flows section, accordion + masked flow video (animated cursor) |
| `mobile-top.jpg` | 390w variant — same scene, portrait crop, same mechanic |
| `demo-mercury.jpg` | demo.mercury.com — the real interactive product demo, for contrast |
| `dom-dump.json` | Full media/style/ancestor dump of the hero DOM |
| `wheel-samples.json` | rAF samples: scrollY ↔ currentTime ↔ data-phase through a full scrub |
| `headline-states.json` | Headline ancestor opacity/transform states across the scrub |
| `scrub-probe.json`, `mobile-dump.json` | Earlier probe passes (scroll snap-back behavior, mobile sources) |
