diff --git a/.gitignore b/.gitignore
index 12dadf7..cc2d5e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,3 +49,6 @@ content/screen_markers/
# video masters
content/*.mov
+
+# previz source renders (web versions in public/previz)
+content/crowd/
diff --git a/app/previz/crowd/page.tsx b/app/previz/crowd/page.tsx
new file mode 100644
index 0000000..1d0d377
--- /dev/null
+++ b/app/previz/crowd/page.tsx
@@ -0,0 +1,412 @@
+import type { Metadata, Viewport } from "next";
+import Fox from "@/components/Fox";
+import SectionLabel from "@/components/SectionLabel";
+import ShotList, { type Shot } from "@/components/previz/ShotList";
+import { site } from "@/content/site";
+
+// Pre-shoot explainer for production: how the drone crowd shot gets built.
+// Shared by link only. Not linked from the site, kept out of search and the sitemap.
+export const metadata: Metadata = {
+ title: "Drone crowd shot: previz",
+ description: "How the drone crowd shot is planned, shot and composited.",
+ robots: { index: false, follow: false },
+};
+
+export const viewport: Viewport = {
+ themeColor: "#0a0a0b",
+};
+
+const dir = "/previz/crowd";
+
+// Drop the renders in public/previz/crowd/ and fill these in when they're done.
+const videos: { hero?: string; heroPoster?: string; final?: string; finalPoster?: string } = {
+ hero: `${dir}/hero-previz.mp4`,
+ heroPoster: `${dir}/hero-previz.jpg`,
+ final: `${dir}/final-comp.mp4`,
+ finalPoster: `${dir}/final-comp.jpg`,
+};
+
+// Plate A sits nearest the stage, D furthest back (order in the 3D layout).
+const plates = [
+ { id: "A", src: `${dir}/plate-a.jpg`, where: "Nearest the stage, just behind the real crowd" },
+ { id: "B", src: `${dir}/plate-b.jpg`, where: "Second row back" },
+ { id: "C", src: `${dir}/plate-c.jpg`, where: "Third row back" },
+ { id: "D", src: `${dir}/plate-d.jpg`, where: "Back of the crowd, where the drone starts" },
+];
+
+// Labels pinned to the front edge of each block in layout-3d.jpg (percent of the image).
+const layoutLabels = [
+ { id: "Hero", x: 68, y: 52, hero: true },
+ { id: "A", x: 64, y: 61 },
+ { id: "B", x: 54, y: 75 },
+ { id: "C", x: 45.5, y: 80 },
+ { id: "D", x: 34, y: 79 },
+];
+
+// In shooting order: hero first, then the plate drone setup.
+const shots: Shot[] = [
+ {
+ id: "hero",
+ name: "Hero drone move",
+ setup: "Hero drone",
+ notes: "Behind the crowd to the truck stage. Constant height and tilt. Real crowd placed close to the stage.",
+ },
+ {
+ id: "clean",
+ name: "Clean plate",
+ setup: "Plate position",
+ notes: "Empty block area, no people, from the locked plate position. Roll a few seconds before the crowd walks in.",
+ },
+ ...plates.map((p) => ({
+ id: `plate-${p.id.toLowerCase()}`,
+ name: `Crowd plate ${p.id}`,
+ setup: "Plate position",
+ notes: `Locked off, crowd fills the marked block. ${p.id === "A" ? "Mix people up from their hero shot spots" : "Everyone swaps places from the last plate"}. Roll longer than the hero shot.`,
+ })),
+];
+
+type Tip = { title: string; body: string };
+
+// For the 1st AD to brief the extras before each plate
+const crowdTips: Tip[] = [
+ {
+ title: "Front two rows: arms below the shoulders",
+ body: "The front of each block (the stage side) overlaps the block in front of it in the comp. Arms up there cut across other people and are very hard to cut out. Clap, fist pump, bounce, but keep hands low. Everyone behind can go big.",
+ },
+ {
+ title: "Face the stage, not the drone",
+ body: "Same direction as the real crowd in the hero shot. Nobody looks up at the drone or points at it.",
+ },
+ {
+ title: "Stay inside the cones",
+ body: "Fill the marked block and don't drift out of it. Don't line up neatly on the edges either; a slightly ragged edge blends better.",
+ },
+ {
+ title: "Keep it going until cut",
+ body: "Same energy as the hero crowd from action to cut. No easing off when it feels long; we need the full plate.",
+ },
+ {
+ title: "New spot every plate",
+ body: "Nobody keeps their place or their neighbours. Mix front to back and side to side between plates.",
+ },
+ {
+ title: "Swap the memorable stuff",
+ body: "Bright hats, jackets, scarves and bags get swapped or taken off between plates, so the same person doesn't show up four times.",
+ },
+ {
+ title: "No flags, signs or poles",
+ body: "Anything held up high crosses into the next block. Keep them for the real crowd in the hero shot only.",
+ },
+ {
+ title: "Leave nothing on the grass",
+ body: "No bags, bottles or jackets on the ground inside or near the block, and clear it fully for the clean plate.",
+ },
+];
+
+// For the DOP and drone operator
+const cameraTips: Tip[] = [
+ {
+ title: "Plates: fast shutter, minimal motion blur",
+ body: "Around 1/100 to 1/200 (90° or narrower) on the crowd plates so waving arms stay sharp and can be cut out. We add matching motion blur in comp. Use ND to hold exposure.",
+ },
+ {
+ title: "Hero: normal shutter, slow and steady",
+ body: "180° is fine for the hero move. Keep the move slow, straight and constant speed with no yaw, so the track is clean and blur stays low.",
+ },
+ {
+ title: "Everything manual and locked",
+ body: "Same lens, resolution, frame rate, ISO, white balance and colour profile for the hero and every plate. No auto exposure, auto ISO or auto white balance.",
+ },
+ {
+ title: "Same image settings throughout",
+ body: "Keep lens distortion correction, digital zoom and stabilisation crop in the same mode for every take. Changing them changes the framing.",
+ },
+ {
+ title: "Best quality the drone can record",
+ body: "Highest resolution and bitrate, 10-bit log. The plates get scaled down into the shot, so extra resolution keeps people crisp.",
+ },
+ {
+ title: "Lock the plate position",
+ body: "One spot, one height, one gimbal tilt for every plate and the clean plate. Tilt matches the hero shot. Let the drone settle before calling action.",
+ },
+ {
+ title: "Roll long",
+ body: "Each plate runs longer than the hero shot so we can offset timing and stop blocks moving in sync. Pre-roll and post-roll on the hero too.",
+ },
+ {
+ title: "Log the numbers",
+ body: "Height, gimbal tilt, lens and position for the hero start, hero end and the plate spot. A screenshot of the drone screen is perfect.",
+ },
+];
+
+// Production and VFX on the day
+const setTips: Tip[] = [
+ {
+ title: "Mark the block on the ground",
+ body: "Cones or tape at the four corners of the crowd rectangle so every plate fills the same area.",
+ },
+ {
+ title: "Grab a clean plate",
+ body: "A few seconds of the empty block area from the same spot, so we can blend the ground under the crowd.",
+ },
+ {
+ title: "Watch the sun",
+ body: "Shoot the plates straight after the hero shot. Shadows must fall the same way, so no big gap in time and ideally no passing cloud.",
+ },
+ {
+ title: "Measure the field",
+ body: "Ten minutes with a tape before we wrap: stage, real crowd and block corners, plus the hero drone height. It lets us line the plates up in the tracked shot.",
+ },
+];
+
+function PreviewVideo({ src, poster, label }: { src?: string; poster?: string; label: string }) {
+ if (src) {
+ return (
+
+ );
+ }
+ return (
+
+ One continuous drone move from behind the crowd toward the truck stage. We film one real crowd near the stage,
+ then build the rest of the audience from four locked-off plates of the same people, rearranged each time.
+
+
+ The full layout from above. The block at the stage is the real crowd, filmed in the hero shot. The four
+ blocks behind it are added in post, each one a separate plate. The orange frames are those plates sitting in
+ the scene.
+
+
+
+
+
+
+
+
+ The drone starts behind the audience and flies forward toward the truck stage. Height and camera tilt stay
+ constant for the whole move, which keeps the angle on the crowd consistent and makes the added blocks hold
+ up.
+
+
+ The real crowd stands close to the stage, where the camera ends up closest and sees the most detail. The
+ empty ground behind them is where the plates go.
+
+
+
+
+
+
+
+
+ After the hero shot, the drone flies to a single marked position and holds still. No camera movement. The
+ same group of people fills a rectangle on the ground and we roll.
+
+
+ Between takes, everyone swaps places. Each take becomes one block of crowd, so four takes make four
+ different-looking blocks from the same people.
+
+ Colours are only there to tell the blocks apart in the previz. On the day it's the same extras every
+ time; only their positions change.
+
+
+
+
+
+
+ We track the hero drone move in 3D, then place each plate as a flat layer on the ground behind the real
+ crowd, back to front. The drone moves, the layers stay put in the scene, and the crowd fills the field.
+
+
+ The proof of concept below was rendered exactly this way: the four plates projected onto cards inside the
+ tracked shot.
+
+
+
+
+
+
+ Shot list
+
Six shots, two setups
+
+ Tick them off as they're in the can.
+
+
+
+
+
+
+
+ Crowd direction
+
Briefing the extras
+
+ For the 1st AD to run through before the first plate, and again between plates.
+
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+ {/* Front two rows of the block in plate-a.jpg (percent of the image) */}
+
+
+ Front 2 rows · hands low
+
+
+ ↑ Stage this way
+
+
+
+ The crowd faces away from the drone, toward the stage. The rows nearest the stage overlap the next block in
+ the comp, so that's where arms need to stay down.
+
+
+
+
+
+
+ Camera
+
Notes for DOP and drone op
+
+
+
+
+ On set
+
Production and VFX
+
+
+
+
+
+
+ );
+}
diff --git a/components/previz/ShotList.tsx b/components/previz/ShotList.tsx
new file mode 100644
index 0000000..8c91f9b
--- /dev/null
+++ b/components/previz/ShotList.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+export type Shot = { id: string; name: string; setup: string; notes: string };
+
+// Ticks live in this device's localStorage only (static site, no server).
+export default function ShotList({ shots, storeKey }: { shots: Shot[]; storeKey: string }) {
+ const [done, setDone] = useState(null);
+
+ // Storage is read after mount so the static HTML matches on first paint
+ useEffect(() => {
+ let saved: string[] = [];
+ try {
+ saved = JSON.parse(localStorage.getItem(storeKey) ?? "[]");
+ } catch {}
+ setDone(Array.isArray(saved) ? saved : []); // eslint-disable-line react-hooks/set-state-in-effect
+ }, [storeKey]);
+ useEffect(() => {
+ if (!done) return;
+ try {
+ localStorage.setItem(storeKey, JSON.stringify(done));
+ } catch {}
+ }, [done, storeKey]);
+
+ const ticked = done ?? [];
+ const count = shots.filter((s) => ticked.includes(s.id)).length;
+ const toggle = (id: string) =>
+ setDone((d) => ((d ?? []).includes(id) ? (d ?? []).filter((x) => x !== id) : [...(d ?? []), id]));
+
+ return (
+