);
};
```
- [ ] **Step 2: Verify it renders**
Run: `cd marketing/product-film && npx remotion still LynkedUpProFilm out/p2.png --frame=2050`
Expected: Pillar 2 dispatch/kanban beat. No error.
- [ ] **Step 3: Commit**
```bash
git add marketing/product-film/src/sequences/Pillar2Dispatch.tsx
git commit -m "feat(film): pillar 2 — right person, automatically"
```
---
### Task 15: Pillar 3 — Every job runs on rails
**Files:**
- Modify: `marketing/product-film/src/sequences/Pillar3Rails.tsx`
- [ ] **Step 1: Implement `Pillar3Rails.tsx`** (3 beats: project lifecycle → estimates → sub portal)
```tsx
import { AbsoluteFill, Sequence } from "remotion";
import { PillarTitle } from "../components/PillarTitle";
import { ScreenFrame } from "../components/ScreenFrame";
import { KenBurns } from "../components/KenBurns";
import { Cursor } from "../components/Cursor";
import { Caption } from "../components/Caption";
import { DURATIONS } from "../timing";
const D = DURATIONS.pillar3;
const TITLE = 75;
export const Pillar3Rails: React.FC = () => {
const afterTitle = D - TITLE;
const beat = Math.floor(afterTitle / 3);
const last = afterTitle - beat * 2;
return (
{/* Sub portal — sees only their task */}
);
};
```
- [ ] **Step 2: Verify it renders**
Run: `cd marketing/product-film && npx remotion still LynkedUpProFilm out/p3.png --frame=3400`
Expected: Pillar 3 beat renders. No error.
- [ ] **Step 3: Commit**
```bash
git add marketing/product-film/src/sequences/Pillar3Rails.tsx
git commit -m "feat(film): pillar 3 — every job runs on rails"
```
---
### Task 16: Pillar 4 — Scale with control (access boundaries)
**Files:**
- Modify: `marketing/product-film/src/sequences/Pillar4Control.tsx`
- [ ] **Step 1: Implement `Pillar4Control.tsx`** (owner sets permission → switch to rep view → boundary visible; then sub portal)
```tsx
import { AbsoluteFill, Sequence } from "remotion";
import { PillarTitle } from "../components/PillarTitle";
import { ScreenFrame } from "../components/ScreenFrame";
import { KenBurns } from "../components/KenBurns";
import { Cursor } from "../components/Cursor";
import { Caption } from "../components/Caption";
import { DURATIONS } from "../timing";
const D = DURATIONS.pillar4;
const TITLE = 75;
export const Pillar4Control: React.FC = () => {
const afterTitle = D - TITLE;
const beat = Math.floor(afterTitle / 3);
const last = afterTitle - beat * 2;
return (
{/* Owner toggles a module permission */}
{/* Switch to the rep's view — Financials gone, only their leads */}
{/* Sub sees only their jobs */}
);
};
```
- [ ] **Step 2: Verify it renders**
Run: `cd marketing/product-film && npx remotion still LynkedUpProFilm out/p4.png --frame=4900`
Expected: Pillar 4 beat renders. No error.
- [ ] **Step 3: Commit**
```bash
git add marketing/product-film/src/sequences/Pillar4Control.tsx
git commit -m "feat(film): pillar 4 — scale with control (access boundaries)"
```
---
### Task 17: Pillar 5 — Know your true profit
**Files:**
- Modify: `marketing/product-film/src/sequences/Pillar5Profit.tsx`
- [ ] **Step 1: Implement `Pillar5Profit.tsx`** (financials → leaderboard; storm-attribution bookend in caption)
```tsx
import { AbsoluteFill, Sequence } from "remotion";
import { PillarTitle } from "../components/PillarTitle";
import { ScreenFrame } from "../components/ScreenFrame";
import { KenBurns } from "../components/KenBurns";
import { Cursor } from "../components/Cursor";
import { Caption } from "../components/Caption";
import { DURATIONS } from "../timing";
const D = DURATIONS.pillar5;
const TITLE = 75;
export const Pillar5Profit: React.FC = () => {
const afterTitle = D - TITLE;
const half = Math.floor(afterTitle / 2);
return (
{/* Financial overview — drill into a job */}
{/* Leaderboard — storm attribution bookend */}
);
};
```
- [ ] **Step 2: Verify it renders**
Run: `cd marketing/product-film && npx remotion still LynkedUpProFilm out/p5.png --frame=6300`
Expected: Pillar 5 financial beat renders. No error.
- [ ] **Step 3: Commit**
```bash
git add marketing/product-film/src/sequences/Pillar5Profit.tsx
git commit -m "feat(film): pillar 5 — know your true profit"
```
---
### Task 18: CraneClose sequence
**Files:**
- Modify: `marketing/product-film/src/sequences/CraneClose.tsx`
- [ ] **Step 1: Implement `CraneClose.tsx`** (crane lowers the swinging CTA card; end plate)
```tsx
import { AbsoluteFill } from "remotion";
import { BlueprintBg } from "../components/BlueprintBg";
import { Crane } from "../components/Crane";
import { CraneCard } from "../components/CraneCard";
import { theme } from "../theme";
export const CraneClose: React.FC = () => {
return (
{/* crane anchored top; card hangs from its hoist cable */}
{/* end plate */}
LynkedUpPro.com · 866-259-6533
);
};
```
- [ ] **Step 2: Verify it renders (card descended + swinging)**
Run: `cd marketing/product-film && npx remotion still LynkedUpProFilm out/crane-mid.png --frame=7900`
Expected: the crane with the "Ready to Build?" card suspended from the cable, rotated slightly (mid-swing). No error.
- [ ] **Step 3: Commit**
```bash
git add marketing/product-film/src/sequences/CraneClose.tsx
git commit -m "feat(film): crane CTA close (swinging suspended card)"
```
---
## Phase 5 — Audio + final render
### Task 19: Add music + enable audio
**Files:**
- Add (by hand): `marketing/product-film/public/audio/music.mp3`
- Modify: `marketing/product-film/src/components/AudioTrack.tsx`
- [ ] **Step 1: Place a royalty-free track**
Drop a licensed/royalty-free cinematic-corporate track (≥ 4:30 long) at `marketing/product-film/public/audio/music.mp3`.
> If no track is available at execution time, **skip this task** — the film renders silently (captions carry the message) and music can be added later. Note the skip in the task report.
- [ ] **Step 2: Enable audio**
In `src/components/AudioTrack.tsx`, change:
```tsx
export const HAS_MUSIC = false;
```
to:
```tsx
export const HAS_MUSIC = true;
```
- [ ] **Step 3: Verify audio is referenced without render error**
Run: `cd marketing/product-film && npx remotion render LynkedUpProFilm out/audio-check.mp4 --frames=0-60`
Expected: short clip renders; the output has an audio track. No error.
- [ ] **Step 4: Commit**
```bash
git add marketing/product-film/src/components/AudioTrack.tsx
git commit -m "feat(film): enable music bed"
```
---
### Task 20: Full render + README
**Files:**
- Create: `marketing/product-film/README.md`
- [ ] **Step 1: Run the capture pipeline (if not already current)**
Start the CRM (`npm run dev` from repo root), then:
Run: `cd marketing/product-film && npm run capture`
Expected: 17 fresh captures in `public/captures/`.
- [ ] **Step 2: Run the unit tests**
Run: `cd marketing/product-film && npm test`
Expected: all Vitest suites pass (timing, captures, craneSwing).
- [ ] **Step 3: Render the full film**
Run: `cd marketing/product-film && npm run render`
Expected: `out/lynkedup-pro-film.mp4` (~4:25–5:00, 1920×1080, 30fps) renders with no error. Spot-check: cold-open hero, a captured pillar screen with cursor, the rep/sub boundary beats in Pillar 4, and the swinging crane CTA at the end.
- [ ] **Step 4: Write `README.md`**
````markdown
# LynkedUp Pro — Product Film (Remotion)
Flagship ~5-min marketing film, composed from real app screenshots + animated brand beats.
See `docs/superpowers/specs/2026-05-30-product-film-design.md` for the design.
## Workflow
1. **Capture** the app (dark mode). From the repo root, start the CRM:
```
npm run dev # serves http://localhost:5173
```
Then in this folder:
```
npm install
npx playwright install chromium # first time only
npm run capture # writes public/captures/*.png
```
2. **Preview** in Remotion Studio:
```
npm run dev
```
3. **Render**:
```
npm run render # -> out/lynkedup-pro-film.mp4
```
## Notes
- `public/captures/` and `out/` are gitignored (local artifacts). Re-run `npm run capture` to refresh.
- Music: drop a royalty-free track at `public/audio/music.mp3` and set `HAS_MUSIC = true` in `src/components/AudioTrack.tsx`.
- Voiceover: captions ship now; VO script lives in the design spec §5 (record later, drop onto the same timeline).
- Does **not** modify any CRM code — it only screenshots the running app.
````
- [ ] **Step 5: Commit**
```bash
git add marketing/product-film/README.md
git commit -m "docs(film): capture/render workflow readme"
```
---
## Self-Review
**1. Spec coverage** (against `2026-05-30-product-film-design.md`):
- §4 structure (Cold Open → Promise → 5 pillars → Crane close) → Tasks 12–18. ✓
- §5 beat sheet captions/VO → caption text in each sequence task mirrors the §5 lines. ✓
- §6 feature→pillar coverage → manifest (Task 3) captures every screen; pillars reference them. Storm Intel, Territory Map (`territory-map`), Pro Canvas (`pro-canvas`), Lead Verification ✓①; Dispatch, People/skills, Kanban ✓②; Projects, Estimates, Sub tasks/portal ✓③; Settings/access, rep & sub boundaries, people ✓④; Snapshot, Leaderboard ✓⑤. *(Note: `territory-map`, `pro-canvas`, `people-skills`, `sub-tasks-owner`, `rep-dashboard` are captured and available; the sequence tasks above feature a representative subset per pillar to hold ~5 min. Additional captured screens can be slotted into a pillar's KenBurns beats without new capture work.)*
- §7.1 capture pipeline → Task 4 (dark mode, role logins, manifest-driven). ✓
- §7.2 Remotion structure/components → Tasks 1–11. ✓
- §7.3 audio (captions+music, VO later) → Tasks 10, 19; captions in all sequences. ✓
- §7.4 render 1080p/30fps → Root composition (Task 2) + Task 20. ✓
- Crane close reproduces `craneSwing` (±3°, 4s, top-center, damped entry) → Task 9 (TDD) + Task 18. ✓
- No CRM code edits → all paths under `marketing/product-film/`. ✓
**2. Placeholder scan:** No "TBD/TODO". Every code step has complete code. The only conditional is Task 19 (skippable if no music asset) — that's an explicit, real fallback (`HAS_MUSIC` flag), not a placeholder.
**3. Type/name consistency:** `swingRotation(frame, fps, settleSeconds)` consistent (Task 9 → CraneCard). `validateManifest`/`Manifest`/`Capture` consistent (Task 3). `DURATIONS`/`SEQUENCE_ORDER`/`TOTAL_FRAMES` consistent (Task 2 → Root, Film, sequences). `captureId` prop on `ScreenFrame` matches manifest `id`s. `Promise` sequence exported as `Promise`, imported `as PromiseSeq` (Task 11) — consistent. Cursor prop names (`from`,`to`,`moveStartFrame`,`clickAtFrame`) consistent across sequences.
No gaps found.