# imdUSD launch video — storyboard and edit plan

A 10.000 s launch cut for imdusd.com, in 16:9 and 1:1, assembled from the banknote plates
(plates step), the music sting (music step) and the finished end cards in
`marketing/kit/endcard/`. Per [the brief](../marketing/kit/BRIEF.md): 0–6.5 s a slow push
from the whole note into the engraved Pepe portrait with a shimmer along the engraving and a
single slow blink; a hard cut on the music hit at 6.5 s; 6.5–10 s hold the end card.

**Status of this document.** Every geometric constant below is derived from the plates step's
own source, and the full command sequence in §7 was executed end to end on this machine
against stand-in inputs built at the declared sizes and the derived geometry. What was *not*
available to me is the real artwork and the real audio — see §9 for exactly what that does and
does not leave open. Claims are tagged **[fact]** (measured or quoted from source),
**[inference]** (derived, not directly observed), and **[open]** (unresolved).

---

## 1. Inputs this plan expects

| Name | Size | Produced by | Used for |
|---|---|---|---|
| `note-front-16x9.png` | 1920×1080 | plates step | 16:9 push, frames 0–194 |
| `note-front-1x1.png` | 1080×1080 | plates step | 1:1 push, frames 0–194 |
| `portrait-detail.png` | 2048×2048 | plates step | high-resolution portrait inlay, both aspects |
| `note-angle-16x9.png` | 1920×1080 | plates step | not used in this cut (§8) |
| `note-angle-1x1.png` | 1080×1080 | plates step | not used in this cut (§8) |
| `endcard-1920x1080.png` | 1920×1080 | `marketing/kit/endcard/` | 16:9 closing shot |
| `endcard-1080x1080.png` | 1080×1080 | `marketing/kit/endcard/` | 1:1 closing shot |
| `sting-b.wav` *(or `sting-a.wav`)* | 10.000000 s | music step | full-length audio bed |

### Three corrections to the brief I was handed

1. **`portrait-detail.png` has no square version.** [fact] The plates step emits exactly six
   images — `marketing/the-note/build.html:12` names them `note-front-16x9`, `note-front-1x1`,
   `note-angle-16x9`, `note-angle-1x1`, `portrait-detail`, `sheet`. `portrait-detail` is a
   single 2048×2048 square master (`build.html:7`), and both the 16:9 and the 1:1 cut draw
   from that same file. The plan is written accordingly; nothing needs to change.

2. **The music step delivered two stings, not one.** [fact] `marketing/sting/README.md`
   documents `sting-a.wav` (110.77 BPM, D major, "bright and buoyant") and `sting-b.wav`
   (73.85 BPM, A♭ major, "warmer, slower, and more swung"), plus 320 kbps MP3 previews. Both
   are exactly 10.000000 s / 480,000 frames / 48 kHz stereo / 16-bit, and both land the stamp
   at **6.500 s ± 1 ms**, verified at 1 ms resolution in
   [`measurements.json`](../marketing/sting/measurements.json).
   **Recommendation: take B.** [inference] Its 73.85 BPM and swung eighths sit with a single
   slow 6.5 s camera move; take A's 110.77 BPM implies cuts the picture does not have. The
   script takes the file as a variable, so this is a one-word change.
   *Caveat on reading the measurements:* take B's `strongest_energy_rise_near_cut` is logged at
   **6.626 s**, not 6.500 s. That is not a second hit — the file's own `method` note explains
   that later RMS rises are amplitude beating in the held chord. The scheduled and verified
   stamp onset is 6.500 s for both takes. Cut to 6.500 s.

3. **Use the WAV master, not the MP3.** [fact] `marketing/sting/README.md` warns that players
   ignoring gapless metadata expose encoder padding, and names the WAV as the sync reference.

> **[open] The audio is not in a clean checkout.** `marketing/sting/README.md` states the
> delivered audio "remain[s] untracked for the artifact uploader", and `artifacts/` does not
> exist in this working tree. Before running §7, either fetch the uploaded artifacts or
> rebuild with `python3 marketing/sting/render.py` (stdlib + FFmpeg with libmp3lame, no
> network). The same applies to the plates PNGs, rebuilt with
> `marketing/the-note/run.sh build.html artifacts` (needs `google-chrome` and `python3`).

---

## 2. The one real engineering problem, and why `portrait-detail.png` is load-bearing

The brief asks the camera to end on the portrait. The portrait in the delivered 16:9 plate is
tiny. [fact] From `note.js:139-140`, the oval is drawn into the 3264×1400 flat master at
757.68 × 924 px; `flat(note,1920,1080,0.85)` (`build.html:9`, `note.js:237`) scales the master
by exactly 0.5, so in `note-front-16x9.png` the **oval occupies only 378.84 × 462 px** and its
inner portrait ellipse only **330.33 × 406.56 px**.

Filling a 1920-wide frame from that alone is a **5.62× upscale of the delivered plate** — a
mush of resampled line art, and unusable for an engraving. [fact, measured]

`portrait-detail.png` carries the same oval at its native 1640 × 2000 px — **4.33× more linear
detail**. So the push cannot be a `zoompan` over `note-front-16x9.png`. It must run over a
**composite**: the delivered plate upscaled to carry the surround, with the native-resolution
oval inlaid back at exactly the position and scale the plates step drew it.

### Composite geometry (derived, then verified by rendering)

Scale factor `S` is chosen so the inlay lands on clean integers:

| | 16:9 | 1:1 |
|---|---|---|
| Composite canvas | **8320 × 4680** | **8028 × 8028** |
| `S` (plate → composite) | 13/3 = 4.333333 | 8028/1080 = 7.433333 |
| Oval paste origin | **(3339, 1478)** | **(3194, 3153)** |
| Oval paste size | **1642 × 2002** | **1640 × 2000** |
| Oval resample | 1.00122× (negligible) | **1.000000× (exact)** |
| Oval centre | (4160, 2478.67) | (4013.8, 4152.52) |
| Inner portrait ellipse | 715.7 × 880.9 | 714.97 × 879.96 |

[fact] The oval centre is at **x = 4160 = exactly half of 8320** — the plates step centres the
oval horizontally (`note.js:140`, `NW/2 - ow/2`). Vertically it sits 138.67 composite px
(32 output px) *below* frame centre. This is why the push has almost no pan: see §4.

### The inlay mask

`portrait-detail.png` is the oval drawn on a **solid ink rectangle** (`build.html:7`), so it
cannot simply be pasted — the ink corners would cover the note's ivory. The inlay is masked to
a feathered ellipse:

- Mask ellipse, in the 1642×2002 scaled crop: centre **(821, 1001)**, radii **(839, 993)**;
  for 1:1, centre (820, 1000), radii (838, 992).
- Feather: `alpha = clip((1 − r) / 0.012, 0, 1)`, `r = hypot((X−cx)/rx, (Y−cy)/ry)`.

[fact] Those radii are not arbitrary. `portrait.js:211` strokes the outermost frame ring at
`IRX+108` with `lineWidth 8`, so the ornament's outer edge is at `IRX+112 = 827`,
`IRY+112 = 992`. Horizontally that **exceeds the canvas half-width** `OCX = 820` by 7 px, so
the ring is clipped flat by the canvas edge and there is no ink margin at all at the left and
right extremes — the mask runs off-crop there and includes everything. Vertically the ring's
outer edge lands at `y = 8` in oval-canvas coordinates, leaving an **8 px ink sliver** above and
below. Setting `ry = 993` puts `r = 1` at `y = 8`, which excludes that sliver exactly.

[inference] The ≈1 px of ring top/bottom that falls inside the feather band is blended with the
upscaled plate rather than taken at native resolution. At the widest framing this is below one
output pixel.

**Verified:** rendered frame 0 shows the whole note with no ink corners, no seam ring and no
visible sharpness step at the oval boundary.

---

## 3. Shot table

30 fps. **195 + 2 + 103 = 300 frames = 10.000 s exactly.** [fact] 6.5 × 30 = 195 and
10 × 30 = 300 are both integers, so the cut lands on a frame boundary with no rounding. (24 fps
also works — 156 and 240. 25 fps does **not**: 6.5 × 25 = 162.5.)

### 16:9 — `imdusd-launch-16x9.mp4`, 1920×1080

| # | Start | End | Frames | Source | Framing | Motion | Transition out |
|---|---|---|---|---|---|---|---|
| 1 | 0.000 | 6.467 | 0–194 | `note-front-16x9.png` + `portrait-detail.png` (composite) | whole note, edge to edge → engraved portrait filling frame, eyes on the upper third | push-in, zoom 1.000 → 5.6216, look-at drifts 50.0 %/50.0 % → 50.0 %/52.96 %; shimmer sweeps ≈1.0–5.9 s; blink 5.80–6.12 s | hard cut |
| 2 | 6.500 | 6.567 | 195–196 | generated ivory field `#F7F5EF` | full frame | none (2-frame flash, 66.7 ms) | hard cut |
| 3 | 6.567 | 10.000 | 197–299 | `endcard-1920x1080.png` | full frame, 1:1 pixels | static hold | end of file |

### 1:1 — `imdusd-launch-1x1.mp4`, 1080×1080

| # | Start | End | Frames | Source | Framing | Motion | Transition out |
|---|---|---|---|---|---|---|---|
| 1 | 0.000 | 6.467 | 0–194 | `note-front-1x1.png` + `portrait-detail.png` (composite) | whole note centred in the square → portrait and oval ring filling frame | push-in, zoom 1.000 → 6.4224, look-at drifts 50.0 %/50.0 % → 50.0 %/51.72 %; shimmer and blink as above | hard cut |
| 2 | 6.500 | 6.567 | 195–196 | generated ivory field `#F7F5EF` | full frame | none | hard cut |
| 3 | 6.567 | 10.000 | 197–299 | `endcard-1080x1080.png` | full frame, 1:1 pixels | static hold | end of file |

**On the flash colour.** [inference] The brief's palette says "nothing else", so the flash is
the palette's own paper ivory `#F7F5EF`, not pure white. Measured in the encoded file it reads
RGB (246, 245, 238) — within 1/255 of target after yuv420p. Pure white is available by setting
`IVORY=0xFFFFFF` if the cut wants more violence on the hit.

**On the end card.** Held dead static. [inference] The card carries the lettering the brief
says is added in post; a drift or scale would soften it for no gain. A 1 %/3.4 s push is a
one-line change if the cut feels too inert.

**The two aspects differ in how tight they end**, and should. [inference] A 16:9 window cannot
contain a tall oval, so shot 1 ends inside the face with the inner rule kissing the corners;
the square window can hold the whole head with the woven ring visible. Both were rendered and
checked.

---

## 4. Exact zoom and pan values for the push-in

Driven by one normalised parameter. With `on` the 0-based output frame of shot 1:

```
u = on / 194                                  u ∈ [0, 1]
s = 0.5*u + 0.5*u²*(3 − 2u)                   easing
z = ZEND ^ s                                  zoom
look-at = (CX0 + (CX1−CX0)*s,  CY0 + (CY1−CY0)*s)
crop    = (CW/z) × (CH/z),  top-left = look-at − crop/2
```

| | 16:9 | 1:1 |
|---|---|---|
| `CW × CH` | 8320 × 4680 | 8028 × 8028 |
| `ZEND` | **5.621622** (= 8320/1480) | **6.422400** (= 8028/1250) |
| Start look-at `CX0, CY0` | **4160, 2340** | **4014, 4014** |
| End look-at `CX1, CY1` | **4160, 2478.67** | **4014, 4152.52** |
| Start crop | 8320 × 4680 (whole canvas) | 8028 × 8028 |
| End crop | **1480 × 832.5** | **1250 × 1250** |

Sampled schedule (both aspects, same `s`):

| t (s) | frame | `s` | 16:9 zoom | 16:9 crop | 1:1 zoom | 1:1 crop |
|---|---|---|---|---|---|---|
| 0.000 | 0 | 0.00000 | 1.0000 | 8320.0 × 4680.0 | 1.0000 | 8028.0 |
| 1.000 | 30 | 0.10949 | 1.2081 | 6886.8 × 3873.8 | 1.2258 | 6548.9 |
| 2.000 | 60 | 0.26854 | 1.5899 | 5233.1 × 2943.6 | 1.6478 | 4872.1 |
| 3.250 | 98 | 0.50644 | 2.3975 | 3470.3 × 1952.0 | 2.5648 | 3130.1 |
| 4.500 | 135 | 0.73733 | 3.5719 | 2329.3 × 1310.2 | 3.9404 | 2037.4 |
| 5.500 | 165 | 0.89508 | 4.6901 | 1773.9 × 997.8 | 5.2839 | 1519.3 |
| 5.900 | 177 | 0.94534 | 5.1153 | 1626.5 × 914.9 | 5.8016 | 1383.8 |
| 6.467 | 194 | 1.00000 | 5.6216 | 1480.0 × 832.5 | 6.4224 | 1250.0 |

### The same move expressed in the delivered plate's own pixels

Useful if a different tool renders the push: [fact]

- **16:9** ends on a **341.54 × 192.12 px** region of `note-front-16x9.png`, centred at
  **(960.0, 572.0)** — i.e. exactly the oval centre. Plate magnification **5.62×**.
- **1:1** ends on a **168.16 px square** of `note-front-1x1.png`, centred at
  **(540.0, 558.63)** — again the oval centre. Plate magnification **6.42×**.

Those magnifications are the whole argument for the composite in §2.

### Why the pan is almost nothing

[fact] Horizontal pan is **exactly zero** in both aspects: the plates step centres the oval on
the note and the note on the frame, so the look-at x never moves. Vertical pan is **138.67
composite px in 16:9** (2.96 % of canvas height) and 138.52 px in 1:1 (1.73 %) — the whole
"pan" is the 32 output px by which the oval sits below frame centre. Anyone expecting a
sweeping move should not add one: it would fight the symmetry the plate is built on.

### Why this easing and not smoothstep

`s(u)` is a 50/50 blend of linear and smoothstep. [inference] Pure smoothstep (`u²(3−2u)`) has
**zero velocity at u = 1**, and a camera that stops dead is exactly where `zoompan`'s
integer-truncated crop origin becomes visible as stair-stepping. The blend keeps terminal
velocity at 50 % of mean, so the move still settles into the cut without stalling. At `u = 0`
it opens at 50 % of mean rate — slow enough for the whole note to read before it starts moving.

### Measured quantisation [fact]

`zoompan` truncates its crop origin to whole input pixels. Measured against the per-frame
motion of this schedule:

| | worst case over the push | absolute, final frame |
|---|---|---|
| 16:9 | **30 %** of frame-to-frame motion | 1.30 output px |
| 1:1 | **29 %** | 0.86 output px |

Both worst cases occur at the end, where motion is slowest. [inference] At ~30 % of a 3.4 px
move this should read as smooth; it is the number to check first if the push looks steppy on a
large screen. Mitigations, in order: raise the linear weight in `s(u)` from 0.5 toward 0.7;
lower `ZEND`; or render shot 1 as a PNG sequence from a renderer with sub-pixel sampling.
Note that supersampling does **not** help — the error is in input coordinates, so it is fixed
in output pixels regardless of intermediate resolution.

### Shimmer

A soft diagonal band sweeps the frame, multiplied by a Sobel edge map of the live frame so it
only brightens engraved linework, then screen-blended at 0.40.

```
band centre d(T) = −0.45 + 2.0·T/6.5      along d = (0.5·X + 0.866·Y)/W
gaussian half-width 0.13
```

[inference] For 16:9 the frame spans `d ∈ [0, 0.987]`, so the band is on screen roughly
**1.0–5.9 s**: it has cleared before the blink and well before the cut. The band travels in
screen space; what makes it read as light running *along* the engraving is the edge mask, not
band geometry. This is an approximation of a true per-line shimmer and is the element most
likely to want taste adjustment against the real plates — `all_opacity` and the `0.13`
half-width are the two knobs.

---

## 5. The blink

Both methods inject the blink **into the composite, before `zoompan`** — never into the
finished 1920×1080 frames. [inference] That is the key structural decision: the camera is still
moving during the blink, so lids authored in screen space would have to track the push. Authored
in oval-canvas coordinates they are simply carried along by the same zoom as everything else,
and the two methods become drop-in replacements for each other at the same overlay coordinates.

**Eye geometry** [fact], derived from `portrait.js:10-13, 169` through the art→oval transform
at `portrait.js:224` — `(x, y) → (1.25x − 205, 1.25y − 345)`:

| | left eye | right eye |
|---|---|---|
| pupil centre (oval coords) | (476.25, 842.50) | (1045.00, 830.00) |
| aperture x range | 332.50 – 707.50 | 895.00 – 1432.50 |
| aperture y range | 773.75 – 955.00 | 748.75 – 942.50 |
| mask ellipse used | centre (520, 865), radii (187.5, 90.6) | centre (1164, 846), radii (268.8, 96.9) |

Eye band crop in the composite: **1182 × 287 at (3631, 2186)** for 16:9
(1180 × 287 at (3486, 3861) for 1:1).

**Timing** — closed well before the hit so the blink and the stamp do not compete:

| phase | window |
|---|---|
| lids start down | 5.800 s |
| fully closed | 5.920 s |
| hold closed | 5.920 – 5.960 s |
| fully open again | 6.120 s |

```
k(T) = clip( min( (T−5.80)/0.12, 1, (0.32−(T−5.80))/0.16 ), 0, 1 )
```
The three-ramp `min` collapses close / hold / open into one expression and self-clamps outside
the window.

### 5a. With an image-to-video model

1. Export a locked, native-resolution still of the oval — **not** a frame of the push:
   ```
   ffmpeg -y -i portrait-detail.png -vf "crop=1640:2000:204:24" blink-src.png
   ```
   Locked framing matters: a model given a moving frame will try to animate the camera too.
2. Generate ~1.5–2 s at 24–30 fps, first-frame-conditioned on `blink-src.png`. Prompt for the
   constraint, not the content: *"engraved banknote portrait; the eyelids close once slowly and
   reopen; camera locked, no zoom, no drift; the paper, the oval frame and every engraved line
   stay perfectly still; no other movement."*
3. **Take only the eyes back.** [inference] This is not optional. Image-to-video models
   re-synthesise fine high-frequency linework and will visibly crawl the guilloché and the
   hatching across the whole frame. Resample the model clip back to 1640 × 2000, align it to
   `blink-src.png`, and keep only the two feathered mask ellipses from the table above.
4. Re-time so the closed frame lands at 5.92 s (`setpts`, then `fps=30`), pad with held open
   frames to 195, clamp to palette (`curves`/`colorbalance` against `#16202E` and `#F7F5EF`),
   and write `blink-band.mkv` at **1182 × 287**.
5. Drop it into §7 step 2 by replacing the `[eb]…geq…[eyes]` branch with a second input and
   `[base][blinkband]overlay=3631:2186[blinked]`. Everything downstream is unchanged.

> **[open] Not executed.** No image-to-video model was reachable from this task, so steps 2–4
> are a specified procedure, not a tested one. The parts that *were* tested are the overlay
> coordinates, the mask geometry and the timing — because the fallback uses all three.

### 5b. Fallback using only ffmpeg — tested, and good enough to ship

Rather than approximating a blink with a dim or a squash, `geq` **draws the lid**: inside each
eye ellipse a lid edge descends from the top of the aperture to the bottom, filled with the
palette's ivory, carrying an ink hatch every 9 px so it reads as engraved, with a 4 px ink lash
line at its leading edge.

```
lid_y(T) = (cy − ry) + k(T)·2·ry
inside aperture and Y < lid_y  →  lash line if |Y − lid_y| < 4, else ink if mod(Y,9) < 2, else ivory
otherwise                      →  source pixel
```

Rendered at native resolution in the composite, so the lid is resampled by the push exactly
like the artwork around it.

**Verified by rendering** — frames 172 / 176 / 178 / 181 / 185 read as: open → lids descending
with a visible lash line → fully closed → reopening → open. The hatch on the closed lid reads
as engraving rather than a flat shape.

[inference] Caveats against the real plates: the lid is a flat elliptical sweep, so it has no
brow follow-through and no change in the under-lid shadow that `portrait.js:139` draws. Against
the brief's "heavy-lidded, knowing" portrait this reads as a deliberate slow blink, but it is
plainly simpler than a drawn one.

### 5c. The option worth taking first [inference]

The portrait is **procedural, not generated** — `marketing/the-note/README.md` says so, and
`portrait.js` draws the lids as explicit paths (`lidL`, `lidR` at `portrait.js:12-13`, stroked
at `:158` and again at `:171`). A correct engraved blink is therefore a change to the plates
step — interpolate the `eyeL`/`eyeR` aperture toward the lid path over N frames and emit
`blink-####.png` — not a video-stage problem at all. That would beat both 5a and 5b: correct
hatching, correct shadow, no model drift, no mask.

I have not done this: it is a change to another step's deliverable and outside this
assignment's scope. Recommended as the first thing to try if the plates step can be re-run.

---

## 6. Audio

[fact] From `marketing/sting/README.md` and `measurements.json`: integrated loudness −14.6 LUFS
(take B) / −14.9 LUFS (take A), true peak −1.8 dBTP, zero saturated samples, last non-zero
frame at 9.676 s, final 100 ms digital silence.

**Do not re-normalise.** [inference] −14.6 LUFS / −1.8 dBTP already sits where YouTube,
Instagram and X normalise to; a `loudnorm` pass would only add a limiter's worth of damage to a
file that measured clean. Encode AAC-LC 256 kbps, 48 kHz, stereo, straight through.

**Measured on the finished file:** the largest 1 ms RMS rise in the decoded MP4 audio is a 4.6×
jump at **t = 6.500 s** — AAC did not shift the hit off the flash. [fact]

[fact] The decoded AAC is 480,256 frames (10.005333 s) against the WAV's 480,000 (10.000000 s):
256 frames of encoder padding. Container duration and the video track both report exactly
10.000000 s, and the padding lands inside the WAV's digital-silence tail, so it is inaudible and
does not move the edit. Worth knowing before someone reports a 5 ms discrepancy as a bug.

---

## 7. The command sequence

Run as `./build.sh 16x9` and `./build.sh 1x1`. Inputs in `$IN` under the names in §1.
Requires ffmpeg with `libx264`, `aac`, `ffv1`, `geq`, `zoompan`, `sobel`, `blend`, `alphamerge`.
**This script was executed end to end for both aspects** — see §9 for what that does and does
not establish.

```bash
#!/usr/bin/env bash
# Assemble the 10 s imdUSD launch video. See artifacts/storyboard.md for the derivation.
set -euo pipefail

FF=${FF:-ffmpeg}; FP=${FP:-ffprobe}
IN=${IN:-in}; WORK=${WORK:-work}; OUT=${OUT:-out}
STING=${STING:-$IN/sting-b.wav}
ASPECT=${1:-16x9}
mkdir -p "$WORK" "$OUT"

FPS=30; HIT=6.5; TOTAL=10
PUSH_FRAMES=195          # 0 .. 6.4667 s   (HIT*FPS)
FLASH_FRAMES=2           # 6.5000 .. 6.5667 s
CARD_FRAMES=103          # 6.5667 .. 10.000 s
IVORY=0xF7F5EF

if [ "$ASPECT" = "16x9" ]; then
  W=1920; H=1080; CW=8320; CH=4680
  NOTE=$IN/note-front-16x9.png; CARD=$IN/endcard-1920x1080.png
  PX=3339; PY=1478; OVW=1642; OVH=2002       # oval inlay paste + size
  MCX=821;  MCY=1001; MRX=839;  MRY=993      # inlay mask ellipse
  CX0=4160; CY0=2340; CX1=4160; CY1=2478.67  # crop centre, start -> end
  ZEND=5.621622                              # 8320 / 1480
else
  W=1080; H=1080; CW=8028; CH=8028
  NOTE=$IN/note-front-1x1.png; CARD=$IN/endcard-1080x1080.png
  PX=3194; PY=3153; OVW=1640; OVH=2000
  MCX=820;  MCY=1000; MRX=838;  MRY=992
  CX0=4014; CY0=4014; CX1=4014; CY1=4152.52
  ZEND=6.422400                              # 8028 / 1250
fi
SS_W=$((W*2)); SS_H=$((H*2))
SWS="-sws_flags lanczos+accurate_rnd+full_chroma_int"

# easing: s(u) = 0.5*u + 0.5*u^2*(3-2u)   (gentle start, never stalls into the cut)
U="(on/$((PUSH_FRAMES-1)))"
S="(0.5*$U+0.5*$U*$U*(3-2*$U))"
Z="pow($ZEND,$S)"
CX="($CX0+($CX1-$CX0)*$S)"
CY="($CY0+($CY1-$CY0)*$S)"

# ---------------------------------------------------------------- 1. composite
# Upscale the delivered flat plate, then inlay the native-resolution oval from
# portrait-detail.png behind a feathered elliptical mask that excludes its ink corners.
echo "== 1. composite $ASPECT ${CW}x${CH}"
$FF -y -v error $SWS \
  -i "$NOTE" -i "$IN/portrait-detail.png" \
  -filter_complex "
    [0:v]scale=${CW}:${CH},format=rgba[bg];
    [1:v]crop=1640:2000:204:24,scale=${OVW}:${OVH},format=rgba[ov];
    color=c=black:s=${OVW}x${OVH},format=gray,
      geq=lum='255*clip((1-hypot((X-${MCX})/${MRX},(Y-${MCY})/${MRY}))/0.012,0,1)'[m];
    [ov][m]alphamerge[ovm];
    [bg][ovm]overlay=${PX}:${PY},format=rgb24[c]" \
  -map "[c]" -frames:v 1 -update 1 "$WORK/composite-$ASPECT.png"

# ------------------------------------------------- 2. push-in + shimmer + blink
# Blink (ffmpeg-only fallback): a lid drawn in-palette sweeps the eye apertures.
# Eye ellipses are given in oval-canvas coords and mapped into the inlay.
# To use an image-to-video blink instead, replace the [eb]...geq...[eyes] branch
# with a second input and [base][blinkband]overlay=${EBX}:${EBY}[blinked].
OSX=$(python3 -c "print($OVW/1640)"); OSY=$(python3 -c "print($OVH/2000)")
EBX=$(python3 -c "print(int($PX+332*$OSX)-40)")   # eye band origin in composite px
EBY=$(python3 -c "print(int($PY+748*$OSY)-40)")
EBW=$(python3 -c "print(int(1101*$OSX)+80)")
EBH=$(python3 -c "print(int(207*$OSY)+80)")
eye() { python3 -c "print(round(($2*$3)-$4,2))"; }
L_CX=$(eye c 520  "$OSX" "$((EBX-PX))"); L_CY=$(eye c 865 "$OSY" "$((EBY-PY))")
R_CX=$(eye c 1164 "$OSX" "$((EBX-PX))"); R_CY=$(eye c 846 "$OSY" "$((EBY-PY))")
L_RX=$(python3 -c "print(round(187.5*$OSX,2))"); L_RY=$(python3 -c "print(round(90.6*$OSY,2))")
R_RX=$(python3 -c "print(round(268.8*$OSX,2))"); R_RY=$(python3 -c "print(round(96.9*$OSY,2))")

BLINK_T0=5.80; BLINK_CLOSE=0.12; BLINK_HOLD=0.16; BLINK_END=0.32
K="clip(min(min((T-$BLINK_T0)/$BLINK_CLOSE,1),($BLINK_END-(T-$BLINK_T0))/($BLINK_END-$BLINK_HOLD)),0,1)"
# lid edge descends from the top of each eye ellipse to its bottom
LID_L="(($L_CY-$L_RY)+$K*2*$L_RY)"
LID_R="(($R_CY-$R_RY)+$K*2*$R_RY)"
INL="lt(hypot((X-$L_CX)/$L_RX,(Y-$L_CY)/$L_RY),1)"
INR="lt(hypot((X-$R_CX)/$R_RX,(Y-$R_CY)/$R_RY),1)"
# lid skin = ivory with ink hatch every 9 px; lash line = ink
GEQ_CH() { # $1 = channel fn, $2 = ink level, $3 = ivory level
  echo "if($INL*gt($LID_L-Y,0)+$INR*gt($LID_R-Y,0),if(lt(abs(Y-$LID_L),4)+lt(abs(Y-$LID_R),4),$2,if(lt(mod(Y,9),2),$2,$3)),$1(X,Y))"
}

echo "== 2. push $PUSH_FRAMES frames"
$FF -y -v error $SWS \
  -loop 1 -framerate $FPS -i "$WORK/composite-$ASPECT.png" \
  -filter_complex "
    [0:v]format=rgb24,split[base][eb];
    [eb]crop=${EBW}:${EBH}:${EBX}:${EBY},
      geq=r='$(GEQ_CH r 22 247)':g='$(GEQ_CH g 32 245)':b='$(GEQ_CH b 46 239)'[eyes];
    [base][eyes]overlay=${EBX}:${EBY}[blinked];
    [blinked]zoompan=z='$Z':x='$CX-(iw/zoom/2)':y='$CY-(ih/zoom/2)':d=1:s=${SS_W}x${SS_H}:fps=$FPS,
      scale=${W}:${H},format=rgb24[pushed];
    [pushed]split[p][pe];
    [pe]format=gray,sobel,gblur=sigma=2,format=gray[edges];
    color=c=black:s=${W}x${H}:r=$FPS,format=gray,
      geq=lum='255*exp(-pow(((X*0.5+Y*0.866)/${W}-(-0.45+2.0*T/$HIT))/0.13,2))'[band];
    [edges][band]blend=all_mode=multiply,format=gbrp[shim];
    [p]format=gbrp[pc];
    [pc][shim]blend=all_mode=screen:all_opacity=0.40,format=gbrp[v]" \
  -map "[v]" -frames:v $PUSH_FRAMES -c:v ffv1 "$WORK/push-$ASPECT.mkv"

# ----------------------------------------------------- 3. flash + end card hold
echo "== 3. flash + end card"
$FF -y -v error $SWS -f lavfi -i "color=c=${IVORY}:s=${W}x${H}:r=${FPS}" \
  -vf "format=gbrp" -frames:v $FLASH_FRAMES -c:v ffv1 "$WORK/flash-$ASPECT.mkv"
$FF -y -v error $SWS -loop 1 -framerate $FPS -i "$CARD" \
  -vf "scale=${W}:${H},format=gbrp" -frames:v $CARD_FRAMES -c:v ffv1 "$WORK/card-$ASPECT.mkv"

# ------------------------------------------------------------ 4. concat and mux
echo "== 4. concat + mux"
printf "file '%s'\n" push-$ASPECT.mkv flash-$ASPECT.mkv card-$ASPECT.mkv > "$WORK/list-$ASPECT.txt"
$FF -y -v error $SWS -f concat -safe 0 -i "$WORK/list-$ASPECT.txt" -i "$STING" \
  -filter_complex "[0:v]fps=$FPS,format=yuv420p[v]" \
  -map "[v]" -map 1:a \
  -c:v libx264 -preset slow -crf 17 -profile:v high -level 4.0 \
  -x264-params "keyint=${FPS}:min-keyint=1:scenecut=0" \
  -color_primaries bt709 -color_trc bt709 -colorspace bt709 \
  -c:a aac -b:a 256k -ar 48000 -ac 2 \
  -t $TOTAL -movflags +faststart "$OUT/imdusd-launch-$ASPECT.mp4"

echo "== built $OUT/imdusd-launch-$ASPECT.mp4"
```

### Two traps this script already works around

Both were real failures during testing, not hypotheticals. [fact]

1. **The push came out greyscale.** `split` does no format conversion, so the `format=gray` on
   the Sobel branch negotiated backwards through `split`, `scale` and `zoompan` and turned the
   *entire* push monochrome — the ivory, the engraved green and the oxblood seal all gone, while
   the end card (a separate command) stayed in colour, which makes it easy to misread as an
   artwork problem. Fixed by pinning `format=rgb24` immediately before the `split` and
   `format=gbrp` on the picture branch.
2. **The flash frames were wrong after concat.** The push was being encoded as greyscale FFV1
   while the flash and card were yuv420p; the concat demuxer does not reconcile mismatched pixel
   formats, and the two ivory frames decoded as mid-grey. Fixed by forcing `format=gbrp` on all
   three intermediates. **Check this first if the flash ever looks wrong** — it presents as a
   colour bug but the cause is the concat.

### Verification

```bash
# container, streams, exact frame count
ffprobe -v error -show_entries format=format_name,duration,size,bit_rate \
  -show_entries stream=codec_name,codec_type,width,height,pix_fmt,r_frame_rate,\
sample_rate,channels,duration,nb_frames,profile,level \
  -of default=nw=1 out/imdusd-launch-16x9.mp4
ffprobe -v error -count_frames -select_streams v:0 \
  -show_entries stream=nb_read_frames -of csv=p=0 out/imdusd-launch-16x9.mp4   # must be 300

# the cut lands on frame 195 = 6.500000 s
ffprobe -v error -select_streams v:0 -show_entries frame=pts_time -of csv=p=0 \
  out/imdusd-launch-16x9.mp4 | sed -n '195,198p'

# the flash frames really are ivory (expect ~246,245,238)
for n in 194 195 196 197; do
  ffmpeg -v error -i out/imdusd-launch-16x9.mp4 \
    -vf "select=eq(n\,$n),scale=1:1,format=rgb24" -vsync 0 -frames:v 1 -f rawvideo - | od -An -tu1
done

# faststart: moov must precede mdat
ffprobe -v trace -i out/imdusd-launch-16x9.mp4 2>&1 | grep -m4 "type:'\(moov\|mdat\)'"
```

**Results on the tested build (16:9):** `h264 / High / level 4.0 / 1920×1080 / yuv420p / 30 fps
/ 300 frames / 10.000000 s`; `aac / LC / 48000 Hz / stereo / 10.000000 s`; top-level box order
`ftyp moov free mdat` → **faststart confirmed**; frame 195 at exactly `6.500000`; frames 195–196
RGB (246, 245, 238). 1:1 identical at 1080×1080. [fact]

---

## 8. What this cut does not use, and why

**`note-angle-16x9.png` / `note-angle-1x1.png` have no slot.** [inference] The cut the brief
specifies is a single continuous 6.5 s move plus an end card; there is no second angle to cut
to without breaking the one unbroken push that is the whole idea. Keeping them unused is a
choice, not an oversight. If a variant is wanted later, the obvious one is a 0.0–1.2 s hold on
`note-angle-16x9.png` dissolving into the push at matched scale — but that costs 1.2 s of the
6.5 s build and the angle plate is a perspective projection (`note.js:178-236`), so the
dissolve would need a homography to match, not a scale. Out of scope here; flagging it as the
known-hard part.

`sheet` (the contact sheet from `build.html:12`) is a review aid and is not a video input.

---

## 9. Limits — what was and was not established

**Established by execution** [fact]:
- The full §7 sequence runs clean for both aspects (≈2 min 35 s each on this machine) and
  produces 300-frame, 10.000000 s H.264 High/yuv420p + AAC-LC files with faststart confirmed.
- The cut lands on frame 195 = 6.500000 s; the flash frames are ivory; the audio hit measures
  at 6.500 s in the decoded output.
- The composite's inlay mask produces no ink corners, no seam ring and no visible sharpness
  step.
- The push lands where the geometry predicts: the rendered end frame is the portrait filling
  the frame with the eyes on the upper third, matching the computed pupil positions.
- The ffmpeg-only blink opens, closes and reopens on schedule.
- `zoompan` quantisation is ≤30 % of per-frame motion, ≤1.30 output px.

**Not established** [open]:
- **The real artwork was never in frame.** The plates PNGs were not available to me, so I built
  stand-ins at the declared sizes reproducing only the geometry the maths depends on — canvas
  size, note rectangle, oval placement and scale, frame ring, eye and pupil positions, all taken
  from `note.js` and `portrait.js`. Geometry, timing, format and container results therefore
  transfer. **Aesthetic results do not.** Specifically untested against real engraving:
  shimmer opacity and width; whether the guilloché moirés during the downscale at the wide end
  (the 2× supersample in step 2 is there to suppress this, and is the first thing to raise if it
  appears); and whether the drawn lid sits convincingly among real hatching.
- **The real audio was never played.** A stand-in WAV matching the declared container spec and
  the 6.500 s onset was used. Sync and format transfer; the musical fit of take B over this
  particular move is a recommendation from the music step's own description, not something I
  heard. Audition both takes against the real picture before locking.
- **No image-to-video model was reachable**, so §5a is specified, not tested.
- Output file sizes from the test build (6.2 MB / 4.7 MB) reflect the stand-in art and say
  nothing about the real files; real engraving is high-entropy and will encode larger at
  CRF 17.

**Open questions for whoever runs this** [open]:
1. Take A or take B? Needs a human ear against the real picture.
2. Should the push end as tight as 16:9 does, or back off to keep more of the woven ring?
   `ZEND` is the single knob; 5.0 keeps noticeably more frame.
3. Ivory flash or white flash? Palette discipline says ivory; punch says white.
4. Is the plates step re-runnable? If so, §5c beats both blink methods and should be done
   instead.
5. Does a 9:16 cut matter for Stories/Reels? Nothing in the kit targets it — there is no 9:16
   end card — and the same composite method would work, but it needs an end card first.

---

## 10. Provenance

| Claim | Source |
|---|---|
| Note master 3264×1400; oval drawn at 757.68 × 924 at (1253.16, 302) | `marketing/the-note/note.js:2,139-140` |
| `flat()` scaling and centring; 0.85 / 0.88 width fractions | `marketing/the-note/note.js:237-243`, `build.html:9` |
| Oval canvas 1640×2000, `OCX/OCY 820/1000`, `IRX/IRY 715/880` | `marketing/the-note/portrait.js:3` |
| Frame ring at `IRX+108` lw 8 → outer edge `IRX+112` | `marketing/the-note/portrait.js:211` |
| art→oval transform `1.25x−205, 1.25y−345` | `marketing/the-note/portrait.js:224` |
| Eye and pupil paths | `marketing/the-note/portrait.js:10-13,169` |
| `portrait-detail` = 2048×2048 ink, oval at (204, 24); six outputs, no square detail | `marketing/the-note/build.html:7,12` |
| Two stings, 10.000000 s, hit at 6.500 s ±1 ms, −1.8 dBTP, −14.6/−14.9 LUFS | `marketing/sting/README.md`, `marketing/sting/measurements.json` |
| Take B's 6.626 s rise is chord beating, not a second hit | `marketing/sting/measurements.json` → `verified_stamp_onset.method` |
| Palette, no-lettering rule, end cards are the closing shot | `marketing/kit/BRIEF.md` |
| End cards 1920×1080 and 1080×1080 | `marketing/kit/endcard/` |
