# Cinematography

The Slate compiler turns authored timeline source into a frame-accurate plan. It never decides *where* a punch-in lands, *which* line deserves B-roll, or *how long* a moment should breathe. That taste is yours, exercised by what you write into VML source files when `.slate/editor/main.vml` exists.

This reference is the "when and how" for the editing loop in `edit-workspace.md` step 3. Use it after `slate edit hydrate`, while `owner` is `slate`, in the validate → probe → render loop. The goal is an edit that feels intentional — paced, dynamic, emotional — instead of a flat string of cuts.

Everything here is expressed in primitives that exist today. Confirm the live contract with `slate edit catalog --json`; the bounds below come from that catalog.

## Principles

- **The A-roll cut is the story.** Effects amplify a good cut; they never rescue a bad one. Fix the spine first.
- **Restraint.** Every non-A-roll item must earn its place. If you cannot say what a zoom, caption, or overlay is *for*, delete it. An edit where every line is zoomed and captioned reads as noise, not energy.
- **Motivate every move.** A punch-in lands on a line that matters. B-roll covers a word that wants a picture. A J-cut carries momentum into the next idea. Decoration without motivation cheapens the edit.
- **Vary the texture.** Sameness is what makes an edit feel mechanical — same cut length, same shot scale, same rhythm. Contrast (a held beat after fast cuts, a wide line before a punch-in) is what makes it feel alive.

## The spine first

Before any effect, shape the A-roll itself.

- **Trim dead air.** Tighten each `clip.video` with `sourceRange.start` / `sourceRange.duration` so lines start clean and end without trailing silence. Most "slow" edits are just untrimmed takes.
- **Vary cut length.** A run of identical-length cuts flatlines. Let a punchy line be short and the next breathe longer. Accelerating into a list or a build raises energy; a longer hold after a key line gives it weight.
- **Front-load the strongest line.** The opening seconds decide whether anyone stays. Lead with the most provocative or concrete sentence, not a warm-up.
- **Cut on motion or intent, not on silence gaps alone.** Use the trimmed spine to drive rhythm; silence detection from preprocessing is a guide, not the editor.

```json
{
  "id": "a_hook",
  "kind": "clip.video",
  "source": "clip-source-id",
  "sourceRange": { "start": "3.2", "duration": "2.6" },
  "at": "0",
  "lane": 1,
  "params": { "name": "Hook line" }
}
```

## Primitive → craft map

### Cold open / hook — `fx.title` + early `fx.zoom`

Open on the strongest line, then stamp the promise of the video and push in slightly to pull the viewer toward the speaker.

```json
{ "id": "title_hook", "kind": "fx.title", "at": "0", "lane": 5,
  "params": { "text": "I was wrong about productivity", "duration": "2.0", "style": "default" } }
```
```json
{ "id": "zoom_hook", "kind": "fx.zoom", "at": "0.4", "lane": 1,
  "params": { "scale": 1.12, "anchor": "face", "x": 0.5, "y": 0.4, "duration": "1.6" } }
```

Keep the hook title short and the push subtle (`scale` ~1.1). Save big moves for later beats so the open does not blow its energy budget.

### B-roll as punctuation — `fx.overlay`

B-roll is a visual sentence, not wallpaper. Reach for `cover` when a word wants a picture or to **conceal an A-roll jump cut** (lay the overlay across the cut so the splice disappears). Use `pip` for reaction, context, or a referenced artifact while the speaker stays present. Use the hydrated overlays that already exist before adding new ones.

```json
{ "id": "b_illustrate", "kind": "fx.overlay", "source": "broll-clip-id",
  "sourceRange": { "start": "0", "duration": "3.0" }, "at": "8.5", "lane": 2,
  "params": { "mode": "cover" } }
```
```json
{ "id": "b_context_pip", "kind": "fx.overlay", "source": "screenshot-clip-id",
  "sourceRange": { "start": "0", "duration": "4.0" }, "at": "20.0", "lane": 2,
  "params": { "mode": "pip", "x": 820, "y": 420, "w": 380, "h": 214, "opacity": 1 } }
```

`pip` requires `x`, `y`, `w`, `h` in pixels. Lower `opacity` (0–1) softens a layer when you want the A-roll to read through it. Match the overlay's length to the phrase it serves; cut back to the face when the idea returns to the person.

### Emphasis & emotional beats — `fx.zoom`

A punch-in directs the eye and raises intensity. Land it on the line that carries the point — the confession, the number, the turn.

```json
{ "id": "zoom_emphasis", "kind": "fx.zoom", "at": "31.5", "lane": 1,
  "params": { "scale": 1.3, "anchor": "face", "x": 0.48, "y": 0.36, "duration": "1.5" } }
```

- Typical range is `scale` ~1.05–1.4. The catalog allows up to 8, but anything past ~1.5 on a talking head reads as a gimmick — reserve extreme values for deliberate, rare shock.
- `anchor: "face"` with normalized `x,y` (0–1 across the frame) keeps the subject centered; `anchor: "center"` is fine for symmetric framing.
- Match `duration` to the spoken line so the move resolves as the thought lands. Probe the framing before committing: `slate edit probe --at 31.8 --out .slate/renders/probe.jpg --json`.
- Do not punch in on consecutive lines. The contrast between a flat line and a pushed line is the effect; back-to-back zooms erase it.

### Momentum via J/L-cuts — `fx.jcut_audio`

Hard sync cuts feel clunky. Letting audio **lead** the next picture (J-cut) or **overhang** past the cut (L-cut) makes ideas flow into each other and keeps energy across a splice.

```json
{ "id": "jcut_next", "kind": "fx.jcut_audio", "source": "next-clip-id",
  "sourceRange": { "start": "0", "duration": "2.4" }, "at": "40.0", "lane": 3,
  "params": { "lead": "0.5", "overhang": "0", "volume": 1 } }
```

`lead` pulls the audio in *before* the picture cut; `overhang` keeps it *after*. Both are non-negative time-strings. Use small values (0.2–0.8s) for conversational flow; larger leads can preview a reveal.

### Music & audio bed — `clip.audio`

A bed sets emotional tone and lifts energy, but it must sit *under* speech. Duck it with `volume` well below 1 wherever narration plays; let it come up in title cards, montages, or held beats with no dialogue.

```json
{ "id": "music_bed", "kind": "clip.audio", "source": "music-clip-id",
  "sourceRange": { "start": "0", "duration": "45.0" }, "at": "0", "lane": 4,
  "params": { "volume": 0.18, "name": "Underscore" } }
```

`volume` is linear gain 0–4. Under speech, ~0.1–0.25 is usually enough to feel without masking words.

### Captions & titles — `fx.caption`, `fx.title`

Caption the one phrase that must land, not the whole transcript. Use titles to mark chapters or stamp a claim.

```json
{ "id": "cap_keyline", "kind": "fx.caption", "at": "33.0", "lane": 5,
  "params": { "text": "The real mistake was invisible.", "duration": "2.0", "style": "default" } }
```

Both require `text` and `duration`. Keep caption text to a short, quotable line and its `duration` to roughly the time it takes to read. Blanket captioning is a separate accessibility decision, not an emphasis tool — do not confuse the two.

## Storytelling devices

Primitives are the words; **devices are the sentences** — recognizable compositions with a story job. Each is built only from the primitives above; none introduce anything the catalog cannot render. Reach for a device when the *narrative* asks for it, never to decorate.

These device names are stable vocabulary. The timeline reverse-engineering that breaks a published video back into a Slate-shaped edit recognizes the same names, so keep them identical here and there.

| Device | Story job | Built from | Shown |
|---|---|---|---|
| `cold_open_hook` | Open on the strongest line, stamp the promise, pull the viewer in | `fx.title` + subtle `fx.zoom` | Cold open / hook · Energetic explainer open |
| `punch_in_on_turn` | Land emphasis on the confession, number, or turn | `fx.zoom` on the key line | Emphasis & emotional beats |
| `build_to_reveal` | Accelerate, preview the payoff in audio, caption the line | `fx.jcut_audio` lead + `fx.caption` | Build to a reveal |
| `hide_the_cut` | Make a jump cut disappear under a picture | `fx.overlay` cover across the splice | Hide a jump cut with cover B-roll |
| `phenomenon_montage` | Show "this is everywhere" — headlines, clips, screenshots in fast succession | `fx.overlay` cover run + `clip.audio` bed | below |
| `establishing_silence` | Let an establishing or moody beat breathe; silence *is* the device | held shot, no audio bed, no caption | below |
| `enumeration_stack` | March through a list, accelerating, marking each item | accelerating `clip.video` cuts + `fx.title` | below |
| `reaction_pip` | Show a reaction or referenced artifact while the speaker stays present | `fx.overlay` pip | B-roll as punctuation |
| `receipt_overlay` | Put a tweet, article, or quote on screen as evidence | `fx.overlay` pip (screenshot) or `fx.caption` | below |

The first four already appear under the primitive map and the recipes below. The remainder are the higher-order compositions worth spelling out.

### Phenomenon montage — `fx.overlay` cover run + `clip.audio`

A fast run of cover overlays — news clips, headlines, newspaper crops, article screenshots — over a music bed, to land the feeling that something is happening *everywhere at once*. The speaker's line sets it up; the montage is the evidence. Cut each piece short and let the bed carry the energy across the run.

```json
[
  { "id": "mon_bed", "kind": "clip.audio", "source": "music-id", "sourceRange": { "start": "0", "duration": "4.0" }, "at": "12.0", "lane": 4, "params": { "volume": 0.3, "name": "Montage bed" } },
  { "id": "mon_1", "kind": "fx.overlay", "source": "headline-1", "sourceRange": { "start": "0", "duration": "0.5" }, "at": "12.0", "lane": 2, "params": { "mode": "cover" } },
  { "id": "mon_2", "kind": "fx.overlay", "source": "newsclip-2", "sourceRange": { "start": "0", "duration": "0.6" }, "at": "12.5", "lane": 2, "params": { "mode": "cover" } },
  { "id": "mon_3", "kind": "fx.overlay", "source": "screenshot-3", "sourceRange": { "start": "0", "duration": "0.5" }, "at": "13.1", "lane": 2, "params": { "mode": "cover" } },
  { "id": "mon_4", "kind": "fx.overlay", "source": "headline-4", "sourceRange": { "start": "0", "duration": "0.7" }, "at": "13.6", "lane": 2, "params": { "mode": "cover" } }
]
```

Slate has no speed ramps, dissolves, or beat-sync — the montage is **hard cuts only**, and the drive comes from short cut length plus the bed coming up (`volume` ~0.3, higher than under speech because dialogue drops out under the run). Bring the bed back down and cut to the face when the run resolves. Four to ten pieces is a montage; two is just B-roll.

### Establishing silence — held shot, no bed

The opposite move. Hold a single wide or atmospheric shot and let it sit with **no music and no caption** so an establishing or reflective beat can breathe. The silence is the device; do not fill it.

```json
{ "id": "establish_hold", "kind": "fx.overlay", "source": "wide-establishing-id", "sourceRange": { "start": "0", "duration": "4.0" }, "at": "60.0", "lane": 2, "params": { "mode": "cover" } }
```

Silence is the *absence* of a `clip.audio` bed under the beat, not a fade (there are no fades). Make the hold long enough (~3–6s) and the framing deliberate enough that it reads as an intentional beat, not dead air. A held `clip.video` on the speaker works too when the moment belongs to them.

### Enumeration stack — accelerating cuts + `fx.title`

March through a list. Tighten each A-roll cut as the list builds and stamp each item with a short title so the structure is legible.

```json
[
  { "id": "enum_1", "kind": "clip.video", "source": "clip-id", "sourceRange": { "start": "20.0", "duration": "2.2" }, "at": "30.0", "lane": 1 },
  { "id": "enum_t1", "kind": "fx.title", "at": "30.1", "lane": 5, "params": { "text": "1 — Stop multitasking", "duration": "1.6", "style": "default" } },
  { "id": "enum_2", "kind": "clip.video", "source": "clip-id", "sourceRange": { "start": "24.0", "duration": "1.8" }, "at": "32.2", "lane": 1 },
  { "id": "enum_t2", "kind": "fx.title", "at": "32.3", "lane": 5, "params": { "text": "2 — Batch your inputs", "duration": "1.4", "style": "default" } }
]
```

Keep titles short and consistent in form. Let the cuts shorten toward the end of the list to lift energy into whatever follows.

### Receipt overlay — `fx.overlay` pip or `fx.caption`

Put the source on screen as evidence while the speaker keeps talking: a tweet, an article headline, a quote. A screenshot rides as a `pip`; a short pulled quote can be a `fx.caption`.

```json
{ "id": "receipt_pip", "kind": "fx.overlay", "source": "tweet-screenshot-id", "sourceRange": { "start": "0", "duration": "3.5" }, "at": "45.0", "lane": 2, "params": { "mode": "pip", "x": 760, "y": 120, "w": 440, "h": 248, "opacity": 1 } }
```

Hold it long enough to be read (~3–4s for a short screenshot) and cut it before it overstays. Do not stack multiple receipts on screen at once — one piece of evidence at a time.

## Recipes

Concrete combinations. Adjust `source`, `at`, and timing to your footage; lanes follow the hydrated convention (1 A-roll, 2 B-roll overlay, 3 J-cut audio, 4 music, 5 graphics).

**Energetic explainer open** — title + subtle push + tight first cuts.
```json
[
  { "id": "a_l1", "kind": "clip.video", "source": "clip-id", "sourceRange": { "start": "3.2", "duration": "2.6" }, "at": "0", "lane": 1 },
  { "id": "title_open", "kind": "fx.title", "at": "0.2", "lane": 5, "params": { "text": "Why this changed everything", "duration": "1.8", "style": "default" } },
  { "id": "zoom_open", "kind": "fx.zoom", "at": "0.4", "lane": 1, "params": { "scale": 1.12, "anchor": "face", "x": 0.5, "y": 0.4, "duration": "1.4" } }
]
```

**Emotional reflective beat** — hold the cut longer, push in slowly, music up.
```json
[
  { "id": "a_reflect", "kind": "clip.video", "source": "clip-id", "sourceRange": { "start": "12.0", "duration": "5.5" }, "at": "60.0", "lane": 1 },
  { "id": "zoom_reflect", "kind": "fx.zoom", "at": "60.0", "lane": 1, "params": { "scale": 1.22, "anchor": "face", "x": 0.5, "y": 0.38, "duration": "5.0" } },
  { "id": "music_swell", "kind": "clip.audio", "source": "music-id", "sourceRange": { "start": "0", "duration": "6.0" }, "at": "60.0", "lane": 4, "params": { "volume": 0.32 } }
]
```

**Hide a jump cut with cover B-roll** — lay an overlay across the splice.
```json
[
  { "id": "a_pre", "kind": "clip.video", "source": "clip-id", "sourceRange": { "start": "0", "duration": "4.0" }, "at": "0", "lane": 1 },
  { "id": "a_post", "kind": "clip.video", "source": "clip-id", "sourceRange": { "start": "9.0", "duration": "4.0" }, "at": "4.0", "lane": 1 },
  { "id": "b_cover_cut", "kind": "fx.overlay", "source": "broll-id", "sourceRange": { "start": "0", "duration": "2.5" }, "at": "3.0", "lane": 2, "params": { "mode": "cover" } }
]
```

**Build to a reveal** — accelerating short cuts, J-cut audio previewing the payoff, caption on the line.
```json
[
  { "id": "jcut_reveal", "kind": "fx.jcut_audio", "source": "reveal-clip-id", "sourceRange": { "start": "0", "duration": "2.0" }, "at": "48.0", "lane": 3, "params": { "lead": "0.6", "volume": 1 } },
  { "id": "cap_reveal", "kind": "fx.caption", "at": "48.6", "lane": 5, "params": { "text": "Here's the part nobody mentions.", "duration": "2.0", "style": "default" } }
]
```

## Pacing heuristics

Starting points, not rules — vary deliberately.

- A-roll cut length: roughly 2–5s; shorter to build energy, longer to let a beat land.
- Zoom push: `scale` ~1.05–1.4, `duration` matched to the line (often 1–2s for a punch, longer for a slow reflective push).
- Caption on screen: about as long as it takes to read the line, ~1.5–2.5s for a short phrase.
- B-roll cover: match the phrase it illustrates; cut back to the speaker when the idea returns to them.
- J/L-cut lead/overhang: 0.2–0.8s for conversational flow.
- Music under speech: `volume` ~0.1–0.25; bring up only where there is no narration.
- Phenomenon montage: ~0.3–0.8s per piece, 4–10 pieces, bed `volume` ~0.3 (dialogue drops out under the run).
- Establishing silence: hold ~3–6s with no bed; long enough to read as a beat, not dead air.

## Anti-patterns

- Zooming or captioning every line. Emphasis only works against contrast.
- Covering all A-roll with B-roll. The viewer needs the person; B-roll is punctuation, not the sentence.
- Music at full volume under speech. Always duck.
- Stacking many items at the same `at` on overlapping lanes without checking the composite — probe it.
- A phenomenon montage with no narrative reason — fast cuts do not manufacture meaning the line did not set up.
- A silence that reads as a mistake. Give the held beat a deliberate frame and enough length that the quiet is obviously intentional.
- Stacking multiple receipts on screen at once — show one piece of evidence at a time.
- Skipping validation. Run `slate edit validate --json` after each batch and `slate edit probe --at <t>` on every zoom, overlay, and reveal before `slate edit render`.

## What Slate cannot do

Do not author items that imply these — they will not render as imagined:

- No dissolves, fades, or wipes. Transitions are hard cuts and overlaps only. Softness comes solely from `opacity` and `cover` overlays.
- No speed ramps or slow motion.
- No automatic beat-sync or music-driven cutting; align to music by hand via `at`. To get exact beat and onset times to align to, run `slate media snap --path <track> --json` and place cuts/effects on the reported `snapPoints` times (see the Media section of `cli-reference.md`).
- Time is rational or decimal seconds (e.g. `"1.5"`, `"24000/1001"`); there is no frame-number syntax in authored items.
- A-roll stays the primary spine unless the user explicitly asks for a different structure.
