feat: video films for social media, with the skill to make more

Adds apps/video, a standalone Remotion package (not part of the pnpm
workspace, so it never reaches the web build or the Docker image). It
renders six looping films, one per service, in German and English, as
4:5 for LinkedIn and 16:9 for YouTube and ads, together with the text
to post and an overview page.

Each film is one continuous shot: close on something abstract, the
camera pulls back to show what it really is, the form changes, and the
picture folds back into its opening. The slide deck code from the
earlier attempts is still in the package and no longer used.

Also adds the Google Ads concept, brings the relaunch concept in line
with the site as built, and a project skill that describes how to make
further films.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 21:23:01 +02:00
co-authored by Claude Opus 5.5
parent 9ed436330b
commit 4bfbcbcacd
53 changed files with 7649 additions and 13 deletions
+95
View File
@@ -0,0 +1,95 @@
---
name: video-film
description: Make or change a social media film in apps/video (Remotion) in the approved mintel.me style - one continuous shot, minimal, with a camera pull-back as the reveal, in German and English. Use when asked for a new video, a new topic, changes to an existing film, new formats, or to re-render the video library.
---
# Making a film for mintel.me
The videos in `apps/video` are short looping films for LinkedIn and ads. The owner approved this
style after rejecting several others. Read `apps/video/CLAUDE.md` first; this skill is the procedure.
## What the owner rejected, so do not go back to it
- Slides with a panel, a progress bar, labels and a frame around everything.
- Much text, small text, text sizes that change from line to line.
- Hectic motion: hops of a third of a second, six statements in 15 seconds.
- Busy drawings: icons, labels, counters, stacks, several effects at once.
- A tiny logo in a black box.
- German that reads translated.
## The style
One continuous shot on a dark stage. Only squares, dots, lines and bars. One statement at a time,
two short lines, large and centred under the drawing. Calm: nothing moves faster than it has to, every
statement stays for about three seconds.
Every film tells the same kind of story in six beats, on the shared timeline `T` in
`src/films/shot.ts` (690 frames at 30 fps, 23 seconds):
| Beat | Caption id | What happens |
| :--- | :--------- | :----------- |
| Hook | `hook` | Close on something small and abstract that looks like something else. "Looks like ping-pong." |
| Reveal | `reveal` | The camera pulls back and shows what it really is. "It's your office." This is the aha moment. |
| Problem | `problem` | The whole picture keeps going, and it is clearly too much. |
| Turn | `turn` | Everything slows to a halt, then the form changes: arcs become a line, a pile leaves by itself. |
| Calm | `calm` | The result at rest, with one quiet movement. |
| Call to action | `cta` | A question that picks up the hook ("Done with ping-pong?") and the website's button. |
Then the picture folds back into its opening, so the last frame equals the first and the film loops.
Frame 0 is the preview image: it must already look good and carry the hook line.
## Procedure
1. **Bring the worktree up to date with `main`** and read the service in
`apps/web/src/content/de.ts` and `en.ts`. Claims, prices and terms come from there and nowhere else.
2. **Find the picture before writing code.** Answer in one line each:
- What does the viewer see close up, and what harmless thing does it look like?
- What does the camera reveal when it pulls back?
- Which single change of form resolves it?
If the reveal does not make you think "oh", look for another picture. Existing ones:
ping-pong / office, skyscraper / pile of invoices, star / unanswered enquiries,
blinking cursor / empty website, Jenga / fragile app, loading bar / backlog.
3. **Write the six statements** in `src/content/videos.ts`, German and English, plus the post text.
Two lines each, as short as possible: one font size is used for the whole film, set by the longest
line. The hook is "Sieht aus wie ..." / "Looks like ...". A little sarcasm is welcome. German uses
"Sie" and has to sound like a native wrote it; write it first, do not translate it from English.
The post text opens with the hook. Copy rules of the root `CLAUDE.md` apply.
4. **Write the film** as `src/films/<Name>Film.tsx`. Copy the structure of `AppFilm.tsx` (simplest)
or `OrderFilm.tsx` (several moving things):
- `cameraAt(frame, zoomIn, zoomOut, focus)` gives `transform`, `zoom`, `hair` and `others`.
Everything that is not part of the opening picture gets `opacity={others}`.
- Draw in stage coordinates, (0, 0) in the centre, 1000 x 700. Use `hair` for stroke widths and
divide glow sizes by `zoom`, so lines look the same at every zoom.
- All motion is a pure function of the frame: `progress`, `inOut`, `out`, `mix` from
`src/illustrations/motion.ts`. No timers, no randomness, no CSS animations.
- For the loop: after `T.fold` the opening picture has to be rebuilt exactly. Anything periodic
needs a period that divides 690 (23, 30, 46). Compare frame 0 and frame 689.
- Logic worth testing goes into `src/lib` with a spec, written first (see `hop.ts`).
5. **Register it**: `src/compositions/<VideoId>.tsx` exporting a `VideoEntry` with `Film`, and the
list in `src/Root.tsx`. A new id also goes into `VIDEO_IDS` in `videos.ts`.
6. **Check**: `pnpm typecheck` and `pnpm test` in `apps/video`.
7. **Render and look**: `node scripts/export.ts --only <VideoId>`, then pull frames from the MP4
(start, middle of the pull-back, problem, turn, calm, call to action, last frame) in `feed` and
`wide`, and read them. Check: nothing overlaps the text, the drawing fades at the stage edges
during the camera move, frame 689 equals frame 0, German lines fit.
8. **Report** what was rendered, what you looked at, and what you did not. You cannot play video:
say so, and leave the judgement of pace and feel to the owner.
## Traps that have cost time here
- **A render can silently not happen.** Redirect logs with `>|`, not `>` (the shell refuses to
overwrite an existing file and the command never starts). After every export, compare the file times
with the clock before sending anything. Run one export at a time.
- **Glows**: `drop-shadow` on SVG shapes is fine. Blur filters under a CSS mask, blend modes and dashed
strokes came out with hard edges; build soft light from gradients that fade to transparent.
- **The stage is not clipped by itself.** While the camera is close, the drawing is larger than the
stage; `FilmStage` fades it at the edges. Do not rely on things being "off screen".
- **Do not name a helper script `copy.py`, `json.py` or similar**: it shadows the Python module.
- **Text from an old worktree is stale.** The website changed language and wording during this work.
## Formats and output
Compositions are `<VideoId>-<de|en>-<feed|wide|square|story>`. `node scripts/export.ts` renders
`feed` (4:5, LinkedIn) and `wide` (16:9, YouTube and ads) in both languages into
`out/library/<VideoId>/<lang>/`, with `post.txt` and an overview page `out/library/index.html`.
`out/` is not committed.