# Video2code

> Replicating a webpage shown in a screen-recording video — layout, visual style, interactions, and animations. Use whenever the task provides a recording (mp4, webm, mov, etc.) of a website and asks to recreate / reproduce / replicate / clone / build the page shown in it (复刻 / 复现 / 还原). Load this skill AND `web-replicate` together — two Skill calls in one turn — before implementing. If Phase-1 observation shows the page is a WebGL/3D scene (true 3D depth, particle fields, volumetric light/glow, camera flythrough, shader backgrounds), additionally load `video2code-3d`.

- Skill: `zai-org/video2code` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add zai-org/video2code`
- Raw SKILL.md: https://api.skillmd.com/api/skills/zai-org/video2code/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: zai-org (https://skillmd.com/u/zai-org)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/zai-org/video2code

---


# Video2Code

Faithfully replicate a webpage demonstrated in a reference video. Match **layout + visual style + interactions + animations**, not just a static lookalike. Every animation must exist, react to the **same driver** (time / scroll / hover / click / pointer position), and read the same way as the source.

## The visibility baseline — one rule, all four phases

Fidelity is bounded by what a viewer sees. The replica is judged on a same-scale composite at 1×, zoomable to ~3×; that, and nothing finer, is the resolution of every fact this task deals in. The floor cuts **both ways**, and the second direction is the one that gets missed:

- **Nothing sub-visible can fail.** A difference you cannot distinguish at ≤3× is within tolerance by definition — pass it, don't fix it, don't spend budget on it.
- **Nothing sub-visible may be produced.** Not observed, not written into the plan, not carried in `measured`. If establishing a value takes an instrument, that value is not part of this task — in **any** phase, on the **source** side exactly as much as the replica side.

The second clause is not a restatement of the first. A value read off a single source frame in Phase 1 is not yet a *difference*, so a floor written only about differences never reaches it — and by the time Phase 4 arrives, the whole run has been steered by numbers no viewer could see, with a plan full of targets no composite can settle.

**"Measure" in this document always means "read a value off the artifact the tool just showed you, at the granularity your eye resolves on it."** It never means "compute it from the pixel array." Reading a heading's width against the frame is measuring; `np.nonzero` on its ink is not.

Concretely, either side, any phase:

| Carry these | Never produce these |
|---|---|
| positions and sizes to the nearest few px, read off a 1×–3× view | any length claimed to ±1px; glyph ink bounding boxes; per-character advance |
| type size in px at ~2px granularity (14 / 16 / 20 / 32 / 48) | stroke or hairline thickness in px; weight inferred from stroke width |
| color as a nameable, reproducible token (one-shot sample, §4.4) | channel deltas, luminance levels, region mean/std, `Δ≈7`-style reads |
| durations to ~0.1s off timestamped grid cells | ink coverage %, energy ratios, per-pixel row/column profiles, sub-pixel amplitude |
| counts, gaps, fractions of the viewport | any quantity whose two sides differ by less than you can see |

A right-hand-column number appearing in `out/plan.md` or `out/verify.jsonl` is **a defect in its own right**, independent of how good the replica is: it sets a target no viewer asked for, and it makes that id's `pass`/`fail` unfalsifiable by eye — the only way to re-check it is to re-run the same script, which is not evidence. Each phase below restates this floor in its own terms (Phase 1–2 escalation cap, Phase 3 weight tags, §4.3 fix budgets, §4.4 Parity standard). They are all this one rule.

## Stack

Always uses `web-replicate` (React + TS + Vite + Tailwind + shadcn/ui). Pick the **lightest implementation that fits**:

| Use case | Tool |
|---|---|
| Hover/fade, simple slide | CSS transition / keyframes |
| Sticky / pinned / scroll-snap / position coupled to scroll | plain CSS (`position: sticky`, `scroll-snap`) — no library |
| Simple entrance | Tailwind `animate-*` / `tailwindcss-animate` |
| Orchestrated multi-element, scroll-triggered reveal | `framer-motion` |
| True 3D / WebGL — depth, particles, volumetric light/glow, shader bg, camera flythrough | **load `video2code-3d`, scaffold `default-3d`** (raw three.js) |

Reach for the lightest tool that fits — don't escalate to a library for a 200ms fade.

> **This table only decides how to BUILD an effect. It does NOT decide whether the effect is `[S]` or `[D]`** (Phase 3). Those are different questions: implementation is "CSS vs library", classification is "can one frozen frame prove it's correct". A pure-CSS effect (sticky, scroll-snap) can absolutely be `[D]`. Never reason "no library → static."

## File contract — the three artifacts that define "done"

All contract state lives in **files** under `out/` (relative to the project directory). Conversation text carries **zero contract status** — nothing counts as planned/verified/finished until it is in these files:

| File | What | When |
|---|---|---|
| `out/plan.md` | The design spec. Every observable concern is one line starting with `[S<#>]` or `[D<#>]`, carrying **measured numbers** and (for `[D]`) the **source clip time range** that proves it. | Written at end of Phase 3, **before any component code is written**. |
| `out/verify.jsonl` | Append-only verification log. One JSON object per check, status field named `result` — **exact schema in *Exact line formats* below**. `measured` values are read **off the evidence artifact** (composite scale, grid timestamps, tool receipts) at the tolerance's granularity — approximate (`~`) values are fine (see §4.4 *Where the numbers come from*). Never edit or delete earlier lines — a fix is recorded by **appending** a new line for the same id. The last line for an id is its current state. | Appended live during Phase 4 verification. |
| `out/report.md` | Close-out: what shipped, deployed URL, deferred list with reasons. | **Skeleton written in Phase 3** (carries a `V2C_REPORT_SKELETON` marker); **filled as the last act** of the task (marker removed). |

The task is complete only when **every `[S#]`/`[D#]` id in `out/plan.md` has a final `pass` or `defer` line in `out/verify.jsonl`** and `out/report.md` exists. A `defer` is only legitimate after the check was actually attempted to its cap (see §4.3) — never defer an effect you never tried to verify.

### Exact line formats — copy these literally

The close-out audit reads these two files by **literal field name and literal line shape**. A field renamed to a synonym, a value outside the allowed set, or an id moved off the start of its line all read as *"never planned / never verified"*: the work is done and scores zero, with no warning and no tolerance layer. Getting these literals right costs nothing; getting them wrong voids the run.

**`out/plan.md`** — every tagged concern begins its line with the bracketed id and ends with a `{core}`/`{detail}` weight; lines covering a moving region carry a `{render}`/`{footage}` source tag too:

```
[S4] Card grid: 3 columns at 1440px, 24px gutters, card 384×512 (still @2.1s) {core}
[D2] Card hover: lifts 6px, shadow blooms, ~180ms ease-out (source clip 5.2–6.0s; testid=card) {detail} {render}
[D5] Hero reel: full-bleed film, hard cuts ~1s apart (source clip 0.0–3.5s; <video autoplay muted loop>; testid=hero-video; asset=/assets/hero.mp4) {core} {footage}
```

The id is the **first thing on the line, in square brackets**. Not a table row (`| S4 | … |`), not a heading (`### [S4] …`), not `S4.` or `**S4**` — those forms are invisible to the audit, which then sees a plan with zero tagged concerns.

Source tags obey the same literalism: exactly `{render}` or `{footage}`, and a `{footage}` line's `asset=` path must match the file you actually ship. A shipped video the plan never declared, or an `asset=` on a `{render}` line, is a close-out gap — the audit reconciles the two lists.

**`out/verify.jsonl`** — one JSON object per line; the status field is named exactly **`result`**, and its value is exactly one of `pass` / `fail` / `defer`:

```
{"id": "S4", "result": "pass", "evidence": "out/cmp/S4_grid.png", "diffs": [{"what": "gutter reads 26px vs source 24px", "severity": "minor", "disposition": "negligible: within the 1.5× gap tolerance"}], "measured": {"columns": "3 both", "card_w": "src 384 / rep 386", "gutter": "src 24 / rep 26"}, "reason": ""}
{"id": "D2", "result": "fail", "evidence": "out/cmp/D2_hover.png", "measured": {"lift": "src ~6px / rep 0px", "duration": "src ~180ms / rep n/a"}, "reason": "hover handler never fires — lift measures 0px at every beat"}
```

- **The key is `result`.** Not `verdict`, not `status`, not `outcome`. *Verdict* is this document's English word for the judgment you reach; the JSON key that records it is `result`. Extra keys of your own (`kind`, `tier`, `method`, …) are harmless — a missing or renamed `result` is fatal.
- **The value is `pass`, `fail`, or `defer`.** Nothing else. Not `partial`, not `n/a`, not `ok`, not `skipped`. An id you could not settle is a `defer` carrying a `reason` (and only after §4.3's cap has actually been spent) — never a fourth state you invent for it.
- **`id` matches the plan's bracketed id exactly** — `"S4"`, not `"S4-anchorB"` or `"S4 grid"`. Need to record several judgments against one id? Append several lines with the same `id`; the last one is its state.

## Evidence tools

Two MCP tools make and SHOW you evidence images in one round (inline in the tool result — no follow-up `Read`); parameter details live in the tool schemas:

- `still_crops` — full-res frames at exact timestamps from the source video or your own recordings, optional crop/scale zoom. Batch every instant you currently want to see into one call.
- `composite_view` — same-scale SRC|REP composite, the standard Parity evidence form; crop+scale makes a **zoomed regional composite** — prefer it over any per-pixel measurement script.

Equivalent CLI scripts (`still.py`, `composite.py` under `<plugin_root>/skills/video2code/scripts/`, plugin root announced at session start; fallback `cat .v2c/plugin_root`) remain for Bash pipelines, but a script run costs an extra `Read` round — default to the tools.

## Workflow — strict order, nothing collapsible

Replication runs through four phases in this exact order. Complete each before the next — no merging, no reordering, no skipping.

`〈ingest the video〉 → 〈observation + ambiguities〉 → clip_video the ambiguities and [D] candidates → write out/plan.md → init webapp → build (assets → components) → npm run build → deploy_website → ZCode Browser Use IAB → verify sweep (append out/verify.jsonl) → batch-fix + re-verify → out/report.md`

---

### Phase 1 — Observe

**Ingest.** If the task input already provides pre-extracted, timestamped frames or contact sheets of the video, `Read` them directly. Otherwise call `ingest_video` on the video file — one call covers the whole duration at layout-level sampling and inlines timestamped overview sheets (do **not** substitute a few coarse `clip_video` segments: that path caps at 180s total and its detail-level sampling wastes budget on an overview). Either way, study the entire video end-to-end before anything else.

**Browser chrome in the recording.** Some captures (annotation-platform screen recordings) carry the browser's own chrome — a tab/address-bar strip along the top edge. It is not page content: read every layout fact, measurement and framing against the page region below it, and do not reproduce the chrome in the replica.

Then write a brief observation **in your response**:

```
## Video observation
<3–6 sentence free-form summary: what kind of page, visual style/mood, core interactions and notable animations, anything that will shape the implementation>

**Ambiguities to clip in Phase 2** (or "- (none)"):
  - <thing you couldn't tell from one watch>
  - ...
```

Keep the summary tight — Phase 3 will re-structure layout / tokens / animations in detail. The **Ambiguities list is load-bearing**: every item drives a `clip_video` call in Phase 2.

**Layout stills — one batched extraction, driven by the sheet.** The contact sheet is your index: every cell is timestamped. Pick each major section's **settled moment** (section fully in place, entrance motion finished) off the sheet, then pull those full-resolution frames in **ONE `still_crops(video, times=[...])` call** — all sections' timestamps in the same call. One settled frame per section is the complete layout evidence — pull an extra offset only for a section-to-section spacing measurement; do not `Read` frame files one by one or re-pull near-duplicate moments. **Measure, don't recall — read off these stills** at the visibility baseline's granularity (values your eye resolves on the frame the tool showed you; never a pixel-array computation)**:** the heading's width as a fraction of the viewport; column widths; margin/whitespace rhythm; type scale; each section's complete element inventory (side rails, badges, secondary copy — the pieces most easily lost). For **repeated elements** (card grids, logo walls, list rows): items fully visible per viewport, item width as a viewport fraction, gaps, stagger rhythm — and per item: aspect ratio, media-area width fraction, corner rounding, any curvature or tilt **with its sign** (bows toward or away from center; leans which way) — signs are binary facts, write them down explicitly; a remembered impression routinely flips them. For each **text role** (display heading, section heading, body, caption): serif vs sans, case, italic/outline, letter-spacing — and the display size **in absolute px off the frame** (to the nearest ~2px, read against the frame — not derived from a glyph bounding box), not by feel. These stills are the evidence the Phase 3 `[S]` lines must cite. **Animated-in content (cards, captions, overlays that fade/slide in): locate the settled frame before you crop** — a coarse sheet cell routinely catches these mid-entrance, and guessing tight-crop timestamps against a half-faded element turns one extraction call into three. Spend one call on a short dense series around the appearance (4–6 closely spaced times, full frames), pick the first complete-and-static frame, then take every tight crop at that exact timestamp.

**Site tours (multi-page).** When the video visits multiple distinct pages (full-viewport transitions / URL-bar changes), build a **multi-page site** (client-side routing, one route per page). List the pages in the observation; each page gets its own layout in the plan and its own `[S]`/`[D]` concerns.

**WebGL / 3D pages.** While observing, decide whether this is a WebGL page. Signals (any one is enough, judged from what's on screen — not from guessing the tech): true 3D perspective/depth (occlusion changes with viewpoint, perspective zoom) · a particle field (thousands of independently moving points) · volumetric light / glow / fog · a continuous free camera flythrough or orbit (not explainable by a CSS transform) · infinite-detail fractal zoom · a full-bleed animated shader background. If so, say so in the observation, **load `video2code-3d` now** (a third `Skill` call) and scaffold with the `default-3d` template in Phase 4. A card flip / slight tilt / parallax that a single CSS `perspective()` covers is **not** a WebGL page — stay 2D.

**Rendered or footage? — classify every moving region.** A page can move because it *renders* (WebGL, canvas, CSS/JS animation, a scroll-driven camera) or because it *plays a video file* (a hero reel, a project hover preview, a background film). The two demand opposite builds: rendered motion you must **write**; a `<video>` element you must **ship as a video asset**. Hand-drawing a film reel is as wrong as replaying a rendered scene. Decide this per moving region while observing; Phase 3 records the verdict as a `{render}`/`{footage}` tag, and the close-out audit checks the shipped assets against it.

| Reads as `{footage}` | Reads as `{render}` |
|---|---|
| live-action or pre-rendered film (people, cities, product shots) | particle fields, geometry, shader patterns |
| hard cuts, an editor's rhythm (tunnel → skyline → arch) | continuous camera move, no cut points |
| fixed-length loop with a visible seam | motion that never repeats |
| **no coupling at all** to scroll or pointer — it runs on its own clock | scroll drives the camera or the progress |
| play/mute chrome | frame rate wobbles under load; vector-crisp at the page's DPR |

Three rules settle it:
1. **`{render}` is the default, and doubt resolves to `{render}`** — the same shape as the `[S]`/`[D]` test below. `{footage}` is the cheap answer, so it needs positive evidence, not merely the absence of contrary evidence.
2. **Interaction that changes the *content* of the picture — not merely its playback position — forces `{render}`, no exceptions.** Drag a handle and watch the scene's glow spread and its background hue flip: that is a running program, whatever it looks like. A `<video>` cannot do it.
3. A page you just called WebGL is `{render}` across its whole scene layer. There is no footage exception for a rendered scene, however photoreal (see `video2code-3d`).

---

### Phase 2 — Clip ambiguous moments

Call `clip_video` with the exact ranges. **Batch ALL the windows you currently want into one call** — back-to-back calls with 1–2 segments each return the same information for extra rounds; split only when the caps force it. Limits: ≤8 segments, ≤60s each, ≤180s total. **Keep a window ledger:** track the ranges you have already clipped; before each new call, subtract covered ranges — new windows should target uncovered time or a *tighter micro-window inside* a covered range (the motion-ROI crop zooms in harder on a short window; that is the only reason to re-clip). The tool annotates overlaps with what a previous call already covered. Prioritize: **animation > motion-/scroll-coupled effects > hover states > one-off transitions**.

**Clip triggers — two independent obligations:**
1. **Ambiguity-driven:** clip at least one segment per Phase 1 ambiguity. An ambiguity list of `- (none)` skips *only* this obligation.
2. **`[D]`-driven (not skippable):** every effect you expect to tag `[D]` in Phase 3 **must** have a source clip. You need it twice — as the Phase 3 *evidence rule* (a clip showing the effect mid-progress forbids an `[S]` tag) and as the Phase 4 *side-by-side comparison* against your own recording. "I have no ambiguities" does **not** excuse skipping these: confidence is not evidence.

You may only reach Phase 3 with zero clips when the page has **no** motion, scroll-coupling, or hover/click animation at all.

**Evidence-escalation cap (Phases 1–2):** chasing one detail in the source video is capped at **three inspection attempts total** — crop/zoom stills, a `clip_video` segment, and any analysis script all count against the same cap. Still unresolved? Write it into the Ambiguities list with your best hypothesis and move on — Phase 4 verifies against your own deploy at full resolution, which usually settles it for free; blind-guessing crop coordinates over and over on a ~2px detail is the signature failure here. **The visibility baseline governs observation, not just verification: a value you can only establish with a measurement script is a value no viewer can see — it needs no number, no plan constant, and no further probe.** When eyeball reads keep disagreeing at ≤3× zoom, that instability IS the answer: write the mechanism with a confidence marker (Phase 3) and stop measuring. Rephrasing the question ("now I'm checking the cursor, not the color") does not reset the cap — attempts count against the underlying detail, whatever the probe is named. **Negative findings generalize:** once two representative elements of a family show no entrance animation / no fade-in, extend that conclusion to the whole family.

**Determine each effect's DRIVER by tracking the visible cursor** (the recording shows a visible cursor marker):
- deformation/response follows the cursor **path** (angle/offset varies with cursor position) → pointer-driven (`mousemove`), even if the effect also runs ambiently — some pages layer both; when ambiguous, implement **both** layers;
- changes **only during a drag** → drag-gated;
- motion with the cursor **parked or absent** → time-driven autoplay;
- motion locked to **scroll position** (advances and reverses with scroll) → scroll-driven.
The driver decides both the implementation and how Phase 4 must trigger the recording; getting it wrong burns the whole 3-attempt recording budget on a mechanism that can't fire.

**Read the frames, then write the motion spec yourself.** For every clipped effect, derive its spec **from the frames alone** before implementing: which element, which transform (translate / scale / rotate — and around which axis / 3D perspective flip / curvature bend / parallax), start and end states, approximate duration and easing. Frame timestamps give you the duration; the intermediate frames give you the transform type. Task-provided hints about motion are coarse pointers at best — the frames are the only ground truth.

**Phase 1+2 round budget:** for a 30–60s video, observation + clipping should land within **~15 rounds total**. Past that, stop investigating — write what's left into the Ambiguities list with your best hypothesis and move on: Phase 4 verifies against your own deploy at full resolution, where those questions settle for free.

---

### Phase 3 — Write `out/plan.md`

Spell out everything you intend to build, then `Write` it to `out/plan.md`. This file is the design baseline that Phase 4 verification will hold the replica against. Tag every **observable** concern with `[S<#>]` (static) or `[D<#>]` (dynamic):

```
# Plan — <site>
## Layout strategy
[S1] Three-column grid at 1440px; heading spans 0.42 of viewport width (still @12.0s) {core}
...
## Component breakdown (untagged — architectural)
- ...
## Design tokens
[S2] Colors: primary #FACC15, bg #1e293b, ... (still @3.5s) {core}
...
## Interactions & animations
[S3] Trash icon → red on hover (instant — the end state is the whole story) {detail}
[D1] Modal entrance: 250ms fade+scale (source clip 8.2–10.0s; framer-motion; testid=modal-panel) {detail} {render}
[D2] Gallery: left heading pins while images scroll under (source clip 18.3–20.0s; CSS sticky; testid=gallery-pin) {core} {render}
...
```

Tag rules:
- **The bracketed id opens the line, literally** (see *Exact line formats*). Prose lines, bullets and sub-sections around them are free-form — but a tagged concern that starts with anything else (a table cell, a `###` heading, `S4.`) is invisible to the close-out audit, which then reads the plan as having no tagged concerns at all.
- **Visibility is the only test for whether to tag.** If it shows up on screen, tag it — regardless of what drives it (`useState`, props, CSS, a library). Skip a line only when it produces *no* visible result. "It's just transient `useState`" is **not** grounds to skip a visible state-change.
- **Coverage — close the loop.** Every interaction/animation named in the Phase 1 observation, and every moment clipped in Phase 2, must map to at least one tagged line. Silently dropping an effect — no tag, no verify entry, no check — is the *same failure* as misclassifying it, and harder to catch.
- Number `[S<#>]` and `[D<#>]` as two independent sequences, continuous, no gaps, no duplicates.
- **Weight-tag every line at plan time: end each `[S#]`/`[D#]` line with `{core}` or `{detail}`.** `{core}` — structure-critical: the page-level layout skeleton and scroll→chapter mapping; any entity whose share of the frame reaches **~15–20%+ at ANY anchor state** (judge by its maximum across states); and **attention anchors regardless of area** — the brand mark, display-level headings, any element the video dwells on or zooms into. A core entity's structure, look, and signature motion are all core concerns. `{detail}` — every other observable concern (smaller subjects, visible details). The tag is fixed at plan time and decides the fix budget in §4.3. Deltas invisible at ≤3× zoom carry NO weight tag — they sit below the visibility baseline: zero fix rounds, no scripted probing, and no plan constant of their own. The plan's entity list is a build guide, **not** a bound on verification: the §4.2 difference scan judges whatever is on the composite, listed or not.
- Every `[D]` line also names the `data-testid` its element will carry (`testid=...`). Phase 4 builds the attribute straight from the plan and verification selects by it — naming it once here is what prevents guessed selectors, grep hunts for your own naming, and wasted recordings later.
- **Source-tag every line that covers a moving region: add `{render}` or `{footage}`** next to the weight tag (`... {core} {render}`). This is the Phase-1 rendered-or-footage call, written down where the audit can read it. `{render}` — the source generates these pixels at run time, so **your code must generate them too**: no mp4/webm/gif/APNG/animated-WebP asset, no numbered frame sequence, no blitting source frames into a canvas. All the same thing; all barred. `{footage}` — the source page is itself playing a video file, so shipping one is the faithful build. **A `{footage}` line must carry two things or it does not count: the source clip range that shows the footage signals, and `asset=/assets/<name>.mp4`** naming the file you will ship. Untagged lines are read as `{render}`; doubt resolves to `{render}`. Static lines over still regions need no source tag.
- Every `[S]` line carries the **measured numbers** read off the Phase 1 stills; every `[D]` line carries the **source-measured numbers** from the Phase 2 clip (duration and amplitude always; direction/axis/origin, stagger order and interval, easing character where the effect has them) plus the clip time range and the implementation approach. A line written from memory is a guess, not a design decision. **Every number here must be one you read, not one you computed** — a constant distilled by scripting the source frame (ink bbox, stroke thickness, per-character advance, channel delta) is barred by the visibility baseline even when it is arithmetically correct: it becomes a Phase-4 target no composite can settle, so the id can then only be closed by re-running the same script. **When the source measurement itself is low-confidence** (compression noise, sub-pixel amplitude, ambiguous readings), write the *mechanism* — what couples to what, in which direction — with a confidence marker, **not a derived constant**: a precise number distilled from weak data becomes a false verification target in Phase 4.

**Static vs Dynamic — a verification question, not an implementation question.**

> **The only test:** Can you name a single screenshot — one frozen frame, at a state you can navigate to and hold still — that on its own proves this line is correct?
> - **Yes → `[S]`.** The truth is a fixed configuration; one still frame is ground truth.
> - **No → `[D]`.** The truth lives in how two things change *together* as a continuous input advances — **elapsed time, scroll position, or pointer/drag position**. No single frame can prove a coupling.

Worked examples:
- Final layout, colours, typography, an instant modal pop, a hover state you can hold open → **`[S]`**.
- 300ms scale+fade modal entrance → **`[D]`** (the truth is the timing/easing *between* frames).
- Sticky header pinning; parallax; scroll-snap; scroll-progress reveal; drag-coupled carousel → **`[D]`, even though every one of these is plain CSS**.

**Tie-break — default toward rigor:** if you cannot name the one proving screenshot, it is `[D]`. "It's only CSS", "there's no easing", "it has a trigger so it must be simple" are **not** reasons to call something `[S]`.

**Evidence rule:** if a Phase 2 clip shows the effect *mid-progress* across two or more frames, it **cannot** be `[S]`. Reconcile the tag with what the clip actually shows — don't assert "static" against your own footage.

**`out/plan.md` must exist before any component code is written.** If you find yourself editing `src/` without the plan file on disk, stop and write it first.

**Write the `out/report.md` skeleton in the same turn as the plan** — issue both `Write` calls in one message, the plan first. The deliverable then exists from the very start; a run cut short later leaves a fileable report on disk instead of nothing. The skeleton is a placeholder carrying a marker you remove only when you finalize it at close-out:

```
<!-- V2C_REPORT_SKELETON -->
# Report
**Shipped:** (tbd)
**Deployed URL:** (tbd)
**Deferred (id → reason):** (tbd)
```

Grading treats a report still carrying the `V2C_REPORT_SKELETON` marker as **unfilled** (no credit). At close-out you replace the placeholders with the real content and delete the marker line.

---

### Phase 4 — Build, deploy, verify

#### 4.1 Build

1. Initialize the project per `web-replicate` (init script → project at `app/`). **WebGL/3D page** (per Phase 1): scaffold with the `default-3d` template (`init-webapp <title> default-3d`) and follow `video2code-3d` for the scene, recipes, software-render budget, and 3D verify rules.
2. **Assemble before polishing:** first put up **every section as a placeholder component** wired into a page that builds and deploys — coverage first — then flesh out one component/effect system at a time. Never leave a section unstubbed while polishing another. **Cap pre-verify polish:** before the first `out/verify.jsonl` line lands, do not iterate the *same* visual detail more than **twice** — get coverage and run the first sweep, then fix against evidence. Polishing a detail you have not yet judged against the source burns rounds blind; this is a separate, earlier failure from the post-sweep **tiered fix-round regime** (§4.3), which only starts once verification is underway. **Batch the writes:** independent new files (components, hooks, SVG assets) go as parallel `Write` calls, 3–4 per turn; collect every planned change to one file into a single `Edit` per turn — one hunk per round doubles the build's round count (the build is the sync point, not each file).
3. Ship image assets and real copy as you build each component (see **Image assets** below) — wire real `/assets/...` paths before writing the JSX that references them, and read the real **text** (headings, body copy, captions, button labels) off the frames; never lorem ipsum.
4. While implementing interactive/animated elements, add stable `data-testid` attributes — they are the selectors verification will need. Give every **text-less interactive control** (icon buttons, arrow buttons, dot navs) an `aria-label` too: the browser element list shows only tag+text, so unlabeled buttons all read as `button: ''` and cannot be told apart when you need to click them during your own verification.
5. `npm run build` → `deploy_website(local_dir="app/dist", type="static")` → **first deploy only：**按 ZCode 官方 `control-browser` skill 用 IAB 打开 URL，等待 `domcontentloaded`，再用 `domSnapshot()`/`tab.screenshot()` 确认可达与视觉状态。
6. **Any `src/` edit invalidates the current build:** edit(s) 和 `deploy_website` 可在同一轮完成；部署保留相同 URL。随后恢复同一个 IAB tab 并 `reload()`，等待具体页面状态或 `tab.playwright.waitForTimeout(settleMs)`，再截图/录像。部署工具不再持有浏览器，也不返回截图。Never capture, record, or judge a build older than your latest edit. 长加载动画可放进 `tab.recording.start({settleMs,...})`，其最终 WebM 再由 `still_crops(recording,[t_end])` 取稳定帧。

#### 4.2 Verify — sweep first, fix in batches

**Adversarial mindset:** you are trying to prove the replica **wrong**. Hunt specifically for blank/flat sections where the source has content or atmosphere, and for the signature effects most likely to have shipped broken (3D / perspective / parallax / scroll-pinning). Sweep `[D]` ids in that risk order; simple fades last.

**The verdict instrument is a difference scan on the image — not a checklist fill-in.** A composite is judged by *finding differences*, the way a viewer plays find-the-differences. "Does it match what I planned?" is the wrong question and produces blind verdicts. For every SRC|REP artifact, before any verdict on the ids it covers:

1. **Scan the whole composite, block by block** — every distinct visual block on either side (subjects, background layers, patterns/textures, shadows/reflections, cards/text, overlays). List every visible difference, ranked by prominence at 1×. Differences of **shape and structure** — how many of something, what geometry, what arrangement, regular vs irregular, crisp vs diffuse — weigh exactly as much as dimensions and colors.
2. **Full-frame anchor composites: name at least 3 candidate differences before any verdict.** You may argue a candidate negligible afterwards; you may not skip the naming. Genuinely fewer than 3? Then say per block why the two sides are indistinguishable — when they truly are, that justification is easy; the floor only bites when you are about to gloss. Regional/zoomed composites: the scan still comes first, but may return fewer (or zero, with a one-line why).
3. **Pixels are the only admissible source.** You know what you built, and the source may print its own labels/readouts on screen; neither is evidence. Matching config values, mode numbers, or intent does not make two sides look alike — and your builder's knowledge actively pulls you toward seeing them as alike; treat it as interference, not information. In one audited run all four chapters' particle figures passed as "(m,n) … both", read off on-screen mode readouts, while the composites showed a sparse irregular figure on the SRC side and a regular ring-and-spoke web on the REP side — four false passes from one shortcut.
4. **The scan lands in the ledger.** Rows judged from a composite carry a `diffs` array: `{"what": "<one line>", "severity": "prominent"|"minor", "disposition": "fix"|"defer"|"negligible: <why>"}`. `pass` is legal only while nothing is left at `"fix"`. A prominent difference needs no pre-existing plan row to be actionable — it rides the (anchor) id it surfaced under into the §4.3 fix loop or an honest defer; "the plan never listed it" is how it got missed, not a reason to skip it. The diff lists are themselves evidence: scans that come back empty near close-out, after rounds of non-empty scans, read as fatigue, not fidelity.

**The first capture pass IS the sweep.** Judge and ledger from the very first artifacts captured after deploy — including the `fail`s. Do not take an unledgered "reconnaissance" lap (capture every section, eyeball it, fix by impression, then re-capture everything for the formal sweep): that acquires the same evidence twice for one verdict. A capture round that produces no verify line for any id should make you stop and judge what you already hold.

**Sweep — evidence by artifact, judgment by id, fixes deferred:**
1. Acquire evidence efficiently up front — composites batched, parallel `Read`s, recordings of unrelated regions in one turn. The evidence unit is the **artifact**; one artifact usually covers several ids.
2. Judge: run the **difference scan** (above) over the artifact, then hold each covered id's plan numbers against it per the Parity standard (§4.4).
3. Record one line per id for `out/verify.jsonl` — `{"id": …, "result": "pass"|"fail", …}` in the literal schema above — carrying the scan's `diffs` (composite-judged ids) and `measured` source→replica pairs (for `fail`, the measured delta in `reason`). Do NOT edit source code yet. Never stamp several ids off evidence you haven't examined per-id.

**Append at the moment of judgment.** The bookkeeping unit is the evidence artifact: the moment you finish judging an artifact's ids, append those lines in one `cat >> out/verify.jsonl` call, and let that bookkeeping **ride in the same turn as the next acquisition** (append calls first, then the next capture call — pairing them halves the sweep's round count). A `fail` is written when it is discovered, never reconstructed later; do NOT hold judged lines for an end-of-run ledger dump — the file's append timeline is part of the evidence. One artifact usually settles several ids; write them together. Never write a line for a check you haven't actually run.

**Chaining the audit onto an append: use `--progress`.** Running the contract audit in the same `Bash` call as an append (`cat >> … <<EOF … EOF` then `python3 …/contract_audit.py`) is good round economy — keep doing it, but pass `--progress`. The shell returns the *last* command's exit code, so a bare audit that still has gaps (normally just "report.md is still the skeleton", which is the expected state until close-out) makes the whole call come back as a tool error even though your append landed fine. `--progress` prints the identical gap list and exits 0, so a successful append reads as successful. Drop the flag only for the final self-audit in §4.6, where a non-zero exit is the point.

Discipline (each a hard rule):
- **One id, one line, one examined verdict.** Every line cites the artifact it was actually judged from — ids covered by one artifact are judged one by one against it, never stamped in a batch.
- **No `pass` without a tool result that actually shows the effect.** Scrolling *past* the target, or two identical captures, proves nothing.
- **No `pass` without source-paired evidence.** For layout `[S]` ids: the same-scale SRC|REP composite (§4.4). For `[D]` ids: the recording proves the motion happened, but the closing line must cite a **matched-beat SRC|REP artifact** — source-clip frame vs recording frame at the same beats, one composite strip (recording path as supporting note). Build it in **ONE call**: `composite_view` accepts video paths on both sides — `composite_view(source=<Phase-2 clip>, replica=<your recording>, beats=[[t_src, t_rep], ...], out_path="out/cmp/<ID>_....png")` extracts and pairs the beats itself; no per-side `still_crops` needed. An artifact showing only the replica is a *working shot*: it can find bugs, it can never close an id — you would be certifying against your memory of the source, and memory certifies "a plate with patterns", not this plate. Save paired artifacts under `out/cmp/` **named after the id(s) they certify** (`out/cmp/D3_hover.png`; a shared artifact carries every id it covers, `out/cmp/D4_D12_reveal.png`) — the close-out audit keys on both the path and the name. `measured` carries **both sides as values read off the artifact**; a bare `"match"` is an impression, not a measurement — and a shared label or config value written once for both sides (`"(3,5) both"`) is a label, not a read; for visually-judged classes the verdict is "indistinguishable on the composite", citing the composite. The `evidence` field is a **bare artifact path** (a shared artifact: its one merged-name path) — viewport/beat annotations and `+`-joined path lists belong in `measured`/`reason`, never in `evidence`; the close-out audit parses that field as a path.
- **A `fail` → `pass` flip requires fresh evidence from THIS fix round** — the new `pass` line must cite a new capture, not the pre-fix one.
- **One capture per clean id.** Re-shooting a region you already judged adds zero evidence.

After the sweep: batch-edit files for **all** failed ids and append one `deploy_website(...)` **in the same turn** (auto-build, stable URL). Then recover the same IAB tab, reload once, and capture fresh evidence with Browser Use. Re-verify **only** the failed ids plus any passed id the fix plausibly touched (same file/section); give the fresh capture one glance for collateral regressions and stop — every other passed id stays passed, its evidence line stays valid. **Exception — global-system edits.** A fix that touches a shared visual system (layout skeleton / scroll mapping / camera / lighting / post-processing / a shared sim or animation core) invalidates evidence page-wide, not per-section. After that fix batch deploys, re-shoot the **anchor set** — the capture views your current evidence lines cite — in one batched capture round, build a fresh full-frame SRC|REP composite per anchor, and re-judge **every** anchor id through the difference scan (the 3-candidate floor applies), appending a fresh line per id. No anchor keeps its old line on a glance — "looks unchanged" is not a verdict.

#### 4.3 Caps

- **Fix budget follows the plan row's weight tag (Phase 3).** `{detail}`: **≤2 fix rounds**, then a `defer` line with the measured delta (`"reason": "duration 2.1× source after 2 fix rounds"`). `{core}`: **≤4 fix rounds** — a structure-critical miss is worth twic

…(truncated)
