
Talking Head Recut
- 95.1k installs
- 38.1k repo stars
- Updated July 28, 2026
- heygen-com/hyperframes
talking-head-recut is a HyperFrames skill that packages existing talking-head, interview, or podcast videos by layering transcript-synced graphic overlay cards.
About
talking-head-recut is a HeyGen HyperFrames skill for dressing up existing talking-head, interview, or podcast videos without re-editing the source footage.
- Automation
- Integration
- Productivity
Talking Head Recut by the numbers
- 95,101 all-time installs (skills.sh)
- +17,075 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #9 of 826 Skill Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/heygen-com/hyperframes --skill talking-head-recutAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 95.1k |
|---|---|
| repo stars | ★ 38.1k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | heygen-com/hyperframes ↗ |
How do you add lower-thirds to an interview video?
Extends skill capabilities for developers building video applications.
Who is it for?
Developers automating broadcast-style packaging of existing talking-head or podcast footage inside HyperFrames.
Skip if: Plain subtitle burns, building videos from scratch, or beat-synced music promos without a talking-head source.
When should I use this skill?
The user wants graphic overlays, lower-thirds, data callouts, kinetic titles, or to package or dress up an existing video.
What you get
Packaged MP4 with timed graphic overlay cards, designed HTML per card, and transcript-synced titles and callouts.
- packaged MP4
- HTML overlay card designs
By the numbers
- Supports 6+ overlay card types: titles, lower-thirds, data callouts, quotes, side panels, and picture-in-picture
Files
Talking Head Recut
Talking Head Recut takes a local video that plays in full and layers a sequence of timed, designed graphic cards onto it — titles, lower-thirds, data callouts, quotes, side panels, picture-in-picture — synced to what's being said. The agent designs the cards (timing + content) and writes each card's HTML directly in the conversation, then assembles a single composition HTML and renders it to MP4 via hyperframes. There is no fixed archetype list and no prescribed card structure — the overlays emerge from what the transcript actually says.
Confirm the route before you build. This skill packages an existing talking-head clip with designed graphic cards (titles, lower-thirds, data callouts, quotes, side panels, PiP). If the user wants plain captions / subtitles (the spoken words as text) →/embedded-captions; a single short unnarrated element (one logo sting / lower-third) →/motion-graphics. The clip plays untouched — re-timing, recoloring, reframing, reordering, or audio is NLE editing and out of scope. Building from a URL / topic / PR → the creation workflows. Unsure overlays-vs-captions? Read `/hyperframes` first.
Graphic-packaging sibling of `embedded-captions`. Captions add the _spoken words_
as a readable subtitle; this adds _designed graphics_ on top of the playing video.
Plain subtitles → embedded-captions. Build a video from scratch → the creationworkflows (product-launch-video/faceless-explainer/ …).
Inspectable intermediate files in the work directory:
metadata.json— duration / width / height / fpsaudio.mp3— extracted audiotranscript.json— a flat word array[{ text, start, end }, …](Whisper; nosegments, nowordswrapper)storyboard.json— lightweight card outline (the agent's plan)public/cards/card-XX.html— one HTML fragment per cardpublic/index.html— final assembled compositionoutput.mp4— rendered video
CLI Resolution
# hyperframes — transcription (local Whisper) + rendering the assembled HTML to MP4
npx hyperframes --helpThis skill runs entirely on the hyperframes CLI plus system ffmpeg / ffprobe. Transcription is local Whisper via hyperframes transcribe — no third-party service, API key, or rate-limited proxy.
Workflow
1. Check Environment
npx hyperframes doctor # ffmpeg, headless browser, render deps
# confirm bundled assets:
ls "<SKILL_DIR>/assets/fonts" "<SKILL_DIR>/assets/vendor/gsap.min.js"Required:
ffmpeg/ffprobe(system)<SKILL_DIR>/assets/fonts/*.woff2,<SKILL_DIR>/assets/vendor/gsap.min.js(bundled inside this skill, staged to work dir in Step 9)
Transcription needs no key — hyperframes transcribe runs Whisper locally (Step 4).
Strongly recommended on macOS for hyperframes render:
export PRODUCER_BROWSER_GPU_MODE=hardware2. Create a Work Directory
All artifacts live under videos/<project-name>/ — the same convention as the other video workflows (product-launch-video / faceless-explainer / pr-to-video). Keep the cwd at the workspace root; everything below writes under this one subdirectory.
VIDEO_PATH="/absolute/path/input.mp4"
WORK_DIR="videos/$(basename "$VIDEO_PATH" | sed 's/\.[^.]*$//')"
mkdir -p "$WORK_DIR"3. Extract Audio and Metadata
# metadata — duration / width / height / fps
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,r_frame_rate \
-show_entries format=duration -of json "$VIDEO_PATH" > "$WORK_DIR/metadata.json"
# audio
ffmpeg -y -i "$VIDEO_PATH" -vn -acodec libmp3lame -q:a 2 "$WORK_DIR/audio.mp3"Outputs: metadata.json (read width/height/duration; fps = the r_frame_rate fraction evaluated, e.g. 30000/1001 → 29.97) + audio.mp3.
4. Transcribe
npx hyperframes transcribe "$WORK_DIR/audio.mp3" -d "$WORK_DIR" --json --model small.enLocal Whisper — no API key, no proxy, no rate limit. Writes a word-level transcript.json into the work dir (word text + start / end timestamps). Read it for the word / sentence timings that drive card timing in Step 6; group words into sentences yourself at punctuation / pauses if you need segment-level chunks.
Clamp to media duration. Whisper can return the final word's end a hair past the actual clip length — clamp every card endSec and composition.durationSeconds to the metadata.json duration, or the render will show a black tail past the video.
5. Correct Transcript
transcript.json is a flat array of word objects — [{ "text": "...", "start": s, "end": s }, …] (no segments array, no words wrapper; the per-word key is `text`). Read it and fix obvious ASR errors:
- Homophones, product names, technical terms, punctuation
- Edit a word's
textin place; preserve its `start` / `end` timestamps - There is no pre-grouped
segmentsarray — group words into sentences yourself (split at terminal punctuation / pauses) when you need segment-level chunks for card timing
6. Draft a Lightweight Storyboard (in chat)
No CLI involved. Read transcript.json + metadata.json and design cards directly. storyboard.json is an agent-internal planning artifact — no CLI command consumes it; it exists so you can think clearly about timing and content before writing each card's HTML. Keep the shape consistent with the example below so the same outline can drive the composition you author in Step 9:
{
"schemaVersion": 3,
"composition": {
"fps": 30,
"width": 1080,
"height": 1920,
"durationSeconds": 121.2,
"layout": "portrait",
"themeId": "noir",
"seed": 42
},
"videoTrack": {
"sourcePath": "input-video.mp4",
"startSec": 0,
"endSec": 121.2,
"bounds": { "x": 0, "y": 0, "width": 1080, "height": 1920 }
},
"subtitles": { "enabled": false },
"cards": [
{
"id": "card-01",
"intent": "Hook with the speaker's anxious midnight question",
"startSec": 0.5,
"endSec": 13.0,
"accentIndex": 0,
"zone": "fullscreen",
"contentHints": {
"kicker": "AN HONEST QUESTION",
"title": "The soul-searching question at 11 PM",
"detail": "Client's 60-second voice message: 'If the RMB appreciates, does that mean my USD policy is a terrible loss?'"
}
}
]
}Required Card fields:
| field | type | purpose |
|---|---|---|
id | string | stable id used in card HTML & GSAP selectors |
intent | string | natural-language description; fed to card synthesis |
startSec / endSec | number | times in seconds (endSec > startSec) |
accentIndex | 0 \ | 1 \ |
zone | enum (see below) | where on the canvas the card lives |
contentHints | object | free-form bag; agent puts kicker/title/detail/data/quote here |
archetype (optional) | string | free-form label you may attach to remember a card's pattern; absent = free-form, which is the default |
transition (optional) | enum: cut \ | fade \ |
Five `zone` values:
| zone | resolved bounds | when to use |
|---|---|---|
fullscreen | covers whole canvas | hero moments, big numbers, mantras |
whiteboard-area | inset 40px margin (or 45% of portrait height) | dense data / annotated content |
lower-third | bottom 30% band | annotation over visible video |
side-panel | right 42% (landscape) or bottom 40% (portrait) | data side, video other side |
video-overlay | full canvas, expects mostly-transparent card | annotation overlays on full-bleed video |
When you assemble the composition in Step 9, resolve each card's zone into pixel bounds on the card-host wrapper following the table above. Video bounds are set once at composition level (videoTrack.bounds); to make video appear to "move between cards", author GSAP tweens against #video-wrap in the composition's <script> (see Step 9).
No prescribed card roles, no prescribed narrative arc. Cards emerge from what the video actually says — could be all quotes or all data, could open with a number or with a story. Let the transcript drive the rhythm.
How many takeaways? — auto-infer from duration + density. No fixed upper limit. Pick a base pace from the video duration, then adjust by information density. Only floor is fixed: minimum 5 cards so even short videos have rhythm.
Step 1 — base pace by duration (the natural sec/card for medium density):
| video duration | base pace (sec per card) | rationale |
|---|---|---|
| < 60s (short reel) | 6–8s | viewers expect fast cuts in short-form |
| 60s – 3 min | 8–12s | normal social pace |
| 3 – 10 min | 12–20s | give breathing room; each card carries more |
| 10 – 30 min | 20–35s | long-form lecture / interview rhythm |
| > 30 min | 30–60s | episodic, near-chapter feel |
Step 2 — density multiplier (multiplies the base pace):
| signal in the transcript | multiplier | effect |
|---|---|---|
| High density — many numbers, distinct claims, staccato pacing, list-like enumeration, every 1–2 sentences is a new idea | × 0.7 | cuts faster, more cards |
| Medium density — mixed flow with both data and narrative | × 1.0 | base pace |
| Low density — one extended story, repeated reframing, slow reflective pacing, single argument unfolding | × 1.5 | cuts slower, fewer cards |
Step 3 — compute:
secPerCard = basePace × densityMultiplier
cardCount = max(5, round(videoDurationSec / secPerCard))Examples (notice — no upper clamp; long videos naturally produce more cards):
- 30s reel, single punchline (low density) → 7 × 1.5 = 10.5s/card → round(30/10.5)=3 → floor to 5 cards
- 60s reflective monologue (low density) → 10 × 1.5 = 15s/card → 4 → floor to 5 cards
- 121s talking-head with rich data (high density) → 10 × 0.7 = 7s/card → 17 cards
- 5 min interview, mixed density → 16 × 1.0 = 16s/card → 19 cards
- 10 min deep-dive, high density → 16 × 0.7 = 11s/card → 55 cards
- 30 min lecture, medium density → 28 × 1.0 = 28s/card → 64 cards
- 1 hr podcast, low density → 45 × 1.5 = 67.5s/card → 53 cards
When a card holds longer than ~15s, plan for a richer card (data block, multi-step reveal, several sub-points unfolding with staggered animations) — a static one-liner gets boring past 8s. For long pieces where many cards exceed 30s, consider chunking the timeline into sub-compositions (one .html per chapter, mounted with data-composition-src) so the GSAP timeline per file stays manageable — see the timeline_track_too_dense HyperFrames lint warning.
content can be a plain string ("Title: annualized 5.69%\nNotes: ...") or any JSON shape that captures the data. The agent decides the shape per card.
Optional outro. This skill ships no fixed brand outro. If the user wants a closing card, design a neutral one yourself (wordmark + one-line tagline, ~1.5-2s, fade in -> short hold -> fade out), append it to cards[], and extend composition.durationSeconds to its endSec. Otherwise end on the last content card.
7. Decide Render Strategy
Confirm Visual Direction with User (DO THIS FIRST)
Before you start designing cards or deciding bounds, ask the user to pick the output ratio, the layout, the style, and the card-density preset. Frames are auto-selected from the chosen layout × style combination (see "Auto-pick frame" table below). Before sending the question, precompute two things:
1. `recommendedRatio` from the source video's aspect ratio (metadata.json width / height):
sourceAspect = width / heightsourceAspect ≥ 1.5(≥ ~3:2 wide) → recommend `16:9`sourceAspect ≤ 0.7(≤ ~9:13 tall) → recommend `9:16`0.7 < sourceAspect < 1.5(near-square) → recommend `4:5`
Mark the recommended option's label with " (recommended · matches source video X:Y)" so the user sees why it's recommended.
2. `autoCount` from Step 6 (max(5, round(videoSec / (basePace × densityMultiplier)))) so the "auto" option's label can show the concrete number.
Environment compatibility — pick the best available question channel. Not every runtime exposes the same structured-question tool. Apply this order:
1. `AskUserQuestion` (Claude Code, Anthropic Console) — use the structured 4-question call below. 2. Other native clarification tool (e.g. ask_question, request_user_input, IDE-specific prompt) — use that tool with the same 4 question texts and option lists. Preserve the recommendation markers and the precomputed values. 3. No native tool (Codex CLI, plain text-only runtimes) — ask directly in normal conversation. Use the plain-text template at the end of this section. Keep it to one message, 4 numbered questions (the global cap is 2–5 questions per round; we stay inside it).
Rules that apply to every channel:
- Ask at most 2–5 questions per round. Our 4 here fits.
- Even if missing info doesn't block rendering, **ask once to confirm
the parameters that materially affect the final output** (ratio, layout, style, cardCount).
- If the user has already pre-approved defaults ("just use defaults",
"no need to ask", "auto-pick everything") or asked you not to ask — skip the question entirely and use: recommendedRatio, layout="stack" (safest cross-ratio default), style chosen from transcript tone in the most neutral group (editorial/data), autoCount. Tell the user what you picked in one sentence and continue.
Channel A — native `AskUserQuestion`:
// Precompute before the call:
// recommendedRatio = "16:9" | "9:16" | "4:5"
// autoCount = integer (from Step 6)
AskUserQuestion({
questions: [
{
question: "Output video aspect ratio (canvas):",
header: "Aspect ratio",
multiSelect: false,
// Reorder so the recommended option appears FIRST (per AskUserQuestion convention).
// Append " (recommended · matches source video W×H)" to the recommended option's label.
options: [
{ label: "16:9 (1920×1080) landscape", description: "TV / YouTube / desktop playback. Most natural when the source video is already landscape; widest canvas." },
{ label: "9:16 (1080×1920) portrait", description: "TikTok / Reels / short-form mobile. Most natural for portrait source; native mobile experience." },
{ label: "4:5 (1080×1350) near-portrait", description: "Instagram feed / WeChat Moments. Best when source is near-square or you want to cover both platforms." }
]
},
{
question: "Choose the overall layout: how should the video and cards coexist on the canvas?",
header: "Layout",
multiSelect: false,
options: [
{ label: "side-by-side (split)", description: "Video and card each take half the canvas. Most stable for interview / data side-by-side; clear visual separation." },
{ label: "top-bottom (stack)", description: "Video on top (~52%), card below. Classic combo of speaker face + summary card; works well in portrait too." },
{ label: "picture-in-picture (pip)", description: "Card fills the canvas, video shrinks to a rounded corner window. Use when content is primary and speaker is secondary." },
{ label: "full-screen overlay (overlay)", description: "Video plays full-bleed, card floats as a glass layer on top. Strong cinematic / emotional feel." }
]
},
{
question: "Choose the card visual style (style):",
header: "Style group",
multiSelect: false,
// NOTE: these 3 groups intentionally match the frame auto-pick matrix
// rows below, so picking a group resolves both `style` group AND the
// frame matrix column in one step. Memberships are mutually exclusive.
options: [
{ label: "warm paper (warm-paper)", description: "academic notebook · editorial big-type · whiteboard hand-drawn · xhs social. Best for interview reflections, product launches, lifestyle, emotional stories." },
{ label: "clinical / cold (clinical)", description: "audit magazine · swiss grid · terminal CLI · minimal modern. Best for financial analysis, investigative reports, technical tutorials, serious presentations." },
{ label: "experimental / avant-garde (experimental)", description: "geom color-clash geometry · spotlight dark-background. Best for short-form highlights, product launches, strong emotion, cinematic feel." }
]
},
{
question: "Card count (takeaway pacing): how many cards to cut?",
header: "Card count",
multiSelect: false,
options: [
{ label: "Auto (recommended) · approx N cards", description: "Inferred automatically from video duration and information density (see Step 6 rules). This run estimates approx N cards. Substitute the real N (your autoCount) into the label." },
{ label: "Fewer · approx round(N × 0.6) cards", description: "Sparser cuts, each card holds longer — suits reflective / slow-paced content." },
{ label: "More · approx round(N × 1.5) cards", description: "Tighter cuts, faster rhythm — suits staccato / data-dense / short-form highlight content." }
]
}
]
})About "Other" — AskUserQuestion automatically adds an "Other" option to the card count question. The user can type a number directly (e.g. "8", "20") as the cardCount target. Parse the input as an integer: if parsing succeeds → use that value (minimum 5 as a floor); if parsing fails → fall back to "auto".
Channel B — plain-text fallback (Codex CLI, runtimes without a native question tool). Post this as one normal message, then wait for the reply. Bullet-style 1/2/3/4 keeps the reply parseable:
I need to confirm four visual decisions with you before I start cutting cards:
1) Output aspect ratio (canvas):
A. 16:9 landscape (1920×1080) — TV / YouTube / desktop playback
B. 9:16 portrait (1080×1920) — TikTok / Reels / short-form mobile
C. 4:5 near-portrait (1080×1350) — Instagram feed / works for both platforms
▸ My recommendation: <recommendedRatio> (matches source video W×H = <sourceW>×<sourceH>)
2) Overall layout (how video & card coexist):
A. split side-by-side (50/50)
B. stack top-bottom (video top, card bottom)
C. pip picture-in-picture (card full canvas, video rounded corner window)
D. overlay full-screen glass overlay (video full-bleed, card glass layer)
3) Card style group (maps to frame auto-pick matrix, pick 1 of 3):
A. warm paper (warm-paper) (academic / editorial / whiteboard / xhs)
B. clinical / cold (clinical) (audit / swiss / terminal / minimal)
C. experimental (experimental) (geom / spotlight)
4) Card count (takeaway pacing):
A. Auto (recommended) — approx <autoCount> cards
B. Fewer — approx round(<autoCount> × 0.6) cards
C. More — approx round(<autoCount> × 1.5) cards
D. Give me a specific number (e.g. "8", "20")
Reply format: "1A 2C 3B 4A" or natural language is fine.
If you want all recommended defaults, reply "default" / "auto" / "use all recommendations".Parsing the plain-text reply:
- Accept loose formats:
"1A 2C 3B 4A","A C B A", `"16:9 / pip /
data / auto", full sentences, or default`.
- If any answer is ambiguous → re-ask only the ambiguous ones (still
inside the 2–5 cap).
- If the user says "default / auto / use all recommendations" → skip without re-asking.
After the user answers (any channel):
1. Resolve the output canvas from the ratio answer — these are the exact storyboard.composition.width / height values to write:
| user choice | composition.width × height | storyboard.layout field |
|---|---|---|
16:9 | 1920 × 1080 | "landscape" |
9:16 | 1080 × 1920 | "portrait" |
4:5 | 1080 × 1350 | "portrait" (schema treats 4:5 as portrait — height > width) |
For *4:5 bounds inside `references/layouts/.html** — those files only document landscape (1920×1080) and portrait (1080×1920). For 4:5 (1080×1350) derive bounds by **proportional scaling from portrait**: keep horizontal values, scale vertical values by 1350/1920 ≈ 0.703. Example: overlay portrait card = { x: 24, y: 1280, w: 1032, h: 564 } → 4:5 card = { x: 24, y: round(1280 × 0.703), w: 1032, h: round(564 × 0.703) } = { x: 24, y: 900, w: 1032, h: 397 }`.
2. Map the style group to a specific style by looking at the transcript tone — pick the one that best fits, but stay inside the user's chosen group. If you're unsure between two specific styles inside the group, send a second AskUserQuestion with those 2–4 specific style options.
3. Resolve final cardCount from the density answer:
| user choice | final cardCount |
|---|---|
| Auto (recommended) | the autoCount you already computed |
| Fewer | max(5, round(autoCount × 0.6)) |
| More | round(autoCount × 1.5) (no upper clamp) |
| Other = "<n>" (integer) | max(5, parseInt(n)) |
| Other = anything else | fall back to autoCount |
4. Auto-pick the video frame from this table (frames don't ask the user — they follow from layout × style):
| layout | warm-paper styles (academic / whiteboard / editorial / xhs) | clinical styles (audit / swiss / terminal / minimal) | experimental styles (geom / spotlight) |
|---|---|---|---|
split | polaroid | hairline | clean |
stack | polaroid | hairline | clean |
pip | clean (pip pill already has chrome) | clean | clean |
overlay | clean (full-bleed forbids deco frames) | clean | clean |
5. Tell the user what you chose in one sentence — ratio (+ canvas size), layout, specific style, frame, and final cardCount — then proceed with the rest of Step 7 (per-card layouts, motion patterns). 6. Record the five values (ratio / layout / style / frame / cardCount) in working memory (no schema field needed); you'll reference them while writing each card's HTML in Step 8 and while reading the matching references/<dim>/<key>.html for tokens and structure.
If the user picks an answer via "Other" with a free-text style name not in the 10-style library, treat it as a hint to design a fresh card visual yourself, but still anchor on the chosen layout's bounds.
Render Strategy Inputs
With ratio / layout / style / cardCount / frame locked from Step 7.0, the remaining per-card decisions are:
- Source-video fit inside the GSAP target: video element has
object-fit: cover and is clipped to #video-wrap's tween bounds. If you want NO cropping (e.g. portrait source on landscape canvas shouldn't get its top/bottom chopped), aim the tween at a rect that matches the source's aspect ratio and let surrounding canvas show through (or fill with the card / a backdrop).
- `card.zone` per card: derive from your chosen composition layout
(split → side-panel, stack → lower-third, pip → fullscreen, overlay → video-overlay), OR pick a different zone for one-off variants (fullscreen for hero / quote, whiteboard-area for dense data).
- `accentIndex` per card: each card pulls one of the 5 theme accent
colors. Vary across cards for rhythm; reuse the same index when two cards belong to the same narrative beat.
- Motion vocabulary: pick 2–3 repeatable patterns from
data-anim kinds (see the table later) and stick to them so the composition feels coherent.
Pick from these themeId palettes (use them as --accent-N / --bg / --text CSS variables in your composition <style> block):
| themeId | accent palette (5 colors) | board bg | text |
|---|---|---|---|
| classic | #1971c2 #e03131 #2f9e44 #e8590c #9c36b5 | #FFF9E3 (paper) | #1e1e1e |
| noir | #4cc9f0 #f72585 #4ade80 #fb923c #a78bfa | #1a1a1a | #f1f1f1 |
| mint | #0077b6 #d62828 #2d6a4f #e76f51 #7209b7 | #e8faf0 | #1b4332 |
| craft | #bf5700 #d62728 #6c757d #e9b54a #3d5a80 | #f6efe1 | #2d2d2d |
| slate | #0ea5e9 #ef4444 #22c55e #f97316 #a855f7 | #1e293b | #f1f5f9 |
| mono | #000 #555 #888 #aaa #ccc | #fff | #000 |
Available fonts (woff2 in <SKILL_DIR>/assets/fonts/, staged to work dir in Step 9): Caveat (handwriting), LXGW WenKai TC (Chinese hand-script), Inter (modern sans), Virgil (geometric hand). Reference via @font-face or font-family directly.
For inspiration on visual patterns, <SKILL_DIR>/references/styles/ ships 10 self-contained reference cards (academic / editorial / minimal / spotlight / geom / whiteboard / audit / terminal / swiss / xhs) that you can copy as starting points — but do not feel constrained to match any of these. Each card is your own design.
Visual Design Library (<SKILL_DIR>/references/)
Beyond the composition-level themeId, the skill ships a richer reference library at <SKILL_DIR>/references/ covering three orthogonal visual dimensions you can freely mix:
Style × Layout × VideoFrame
(10) (4) (3)| dimension | keys | what it decides |
|---|---|---|
| style | academic editorial minimal spotlight geom whiteboard audit terminal swiss xhs | the card's visual language — fonts, colors, ornament, layout-within-card |
| layout | split stack pip overlay | how the source video and the card share the canvas |
| frame | clean hairline polaroid | the decorative chrome around the video element |
Read <SKILL_DIR>/references/DESIGN_INDEX.md for the full matrix and a loose decision guide (interview / product launch / data analysis / social clip / technical tutorial / emotional story …). When you decide to use a specific style / layout / frame, Read the corresponding file:
references/styles/<key>.html— self-contained card fragment with that
style's CSS tokens (colors, fonts, padding, ornament) and a placeholder takeaway. Copy the .card[data-card-id="ref-<key>"] style block, rename the data-card-id to your card's id, swap the placeholder content for the real takeaway, and you're done.
references/layouts/<key>.html— exactvideoBounds+cardBoundsfor
both landscape and portrait, with a copy-paste JSON snippet for storyboard.json's per-card layout field.
references/frames/<key>.html— decorative HTML to add as a sibling of
#video-wrap, plus placement instructions for the composition CSS.
Pick style × layout × frame per card — you can change all three between cards as long as the transitions read smoothly. A common rhythm: open editorial × overlay × clean, switch to audit × split × hairline for the data card, close on whiteboard × pip × polaroid.
The 10 styles are skill-side design tokens, not composition-level themes — they don't need to be declared in storyboard.composition; they live inside each card's HTML. The themeId field can still pick a composition-level palette (table above) that controls page-body background and video border chrome.
Layout Compositions (Card + Video)
Two coordinated decisions per card define how it shares the canvas with the source video:
- `card.zone` (declared in
storyboard.json) — one of the 5 schema
values; resolve it into pixel bounds (per the table in Step 6) when you write the card-host wrapper's inline style in Step 9.
- `#video-wrap` bounds at this card's time window (declared
imperatively in the composition's GSAP timeline) — the agent tweens #video-wrap to a target rect for each layout transition.
Schema does NOT store per-card video bounds. videoTrack.bounds is one-time at composition level (defaults to full canvas). Video "moving" between cards is purely a GSAP animation authored in index.html. There is no card.layout field — earlier versions of this doc invented one; the real schema only has card.zone.
4 composition layouts (from references/layouts/) — each is a recipe pairing a zone with a #video-wrap tween target:
| composition layout | recommended card.zone | GSAP target for #video-wrap (landscape 1920×1080) | GSAP target for #video-wrap (portrait 1080×1920) | when to use |
|---|---|---|---|---|
split | side-panel | { left: 960, top: 0, width: 960, height: 1080 } | { left: 0, top: 960, width: 1080, height: 960 } (bottom half) | speaker + data side-by-side / 50:50 weight |
stack | lower-third | { left: 14, top: 14, width: 1892, height: 548 } (top 52%) | { left: 0, top: 0, width: 1080, height: 844 } (top 44%) | speaker on top + summary card below |
pip | fullscreen | { left: 1480, top: 760, width: 400, height: 300 } + add .framed class | { left: 690, top: 28, width: 360, height: 203 } + add .framed | content-heavy card + corner pip |
overlay | video-overlay | { left: 0, top: 0, width: 1920, height: 1080 } (full-bleed) | { left: 0, top: 0, width: 1080, height: 1920 } | cinematic / dramatic / glass card on full video |
For 4:5 (1080×1350), scale portrait y/h values by 1350/1920 ≈ 0.703 (see Step 7.0 Channel A / Channel B recommendedRatio resolution table).
Other zone values for one-off variants (still uses card.zone; no fake "layout" field):
zone | resolved bounds | common use |
|---|---|---|
fullscreen | covers whole canvas | hero card, video tweens to hidden/pip |
whiteboard-area | inset 40px margin (landscape) or bottom 45% (portrait) | dense data card, free margins |
lower-third | bottom 30% band | talking-head annotation |
side-panel | right 42% (landscape) or bottom 40% (portrait) | sidebar / "split" recipe |
video-overlay | full canvas; expect transparent card root | glass overlay on full-bleed video |
You can mix recipes per card — choose card.zone based on what suits the moment, then write the GSAP tween for #video-wrap between cards.
Storyboard Render Contract
storyboard.json is an agent-internal planning artifact — no CLI command parses it. It exists to keep your timing and content decisions explicit before you write each card's HTML. Stick to the v3-style shape below so the same outline drives the composition you assemble in Step 9.
Required structure (see Step 6 for the full example):
schemaVersion: 3composition: { fps, width, height, durationSeconds, layout, themeId, seed }— notedurationSeconds/fps/themeId/layoutlive insidecomposition, NOT at top levelvideoTrack: { sourcePath, startSec, endSec, bounds? }— video bounds default to full canvassubtitles: { enabled, ... }cards[]— each card has the 6 required fields:id,intent,startSec,endSec,accentIndex,zone,contentHints
Rules:
- Card times stay inside
composition.durationSecondsand should not overlap unless intentional (usedata-track-indexto control z-order when they do). - Visual details live in card HTML fragments (Step 8), NOT in
contentHints.contentHintsis your own structured prompt for designing the card; the rendered look is the HTML. - Keep the storyboard shape stable — even though nothing parses it, you read it back while authoring Step 8/9, and consistency keeps card IDs and timing in sync.
- Agent-side decisions like "I picked overlay × geom × clean" do NOT belong in
storyboard.json— keep them in working memory and use them when authoring card HTML + GSAP tweens.
Transparent card backgrounds for cards that share canvas with video. When the GSAP tween leaves video visible behind/beside the card (overlay recipe, pip recipe, or any card.zone = 'lower-third' | 'video-overlay' moment), the card's .root MUST NOT paint a full opaque background — otherwise it occludes the video. Two patterns:
/* Pattern A: transparent root, page body provides the cream backdrop */
html,
body {
background: var(--bg);
}
.card[data-card-id="card-X"] .root {
background: transparent;
}
/* Pattern B: explicit per-card background ONLY for fullscreen cards */
.card[data-card-id="card-hero"] .root {
background: var(--bg);
}
.card[data-card-id="card-overlay"] .root {
background: transparent;
}For side-panel-zone cards (split recipe), the card-host is already only half the canvas, so an opaque card bg is fine — it only covers its half.
8. Write Each Card's HTML
Create $WORK_DIR/public/cards/{card-id}.html for each card. Each file contains a single rooted HTML fragment that follows this contract:
Card HTML Contract
<div class="card" data-card-id="{cardId}">
<style>
/* MUST: every rule starts with .card[data-card-id="{cardId}"] */
.card[data-card-id="card-01"] .root {
width: 100%; height: 100%;
display: flex; ...;
font-family: 'Caveat', 'LXGW WenKai TC', serif;
color: var(--text);
background: var(--bg);
}
.card[data-card-id="card-01"] .title { font-size: 84px; ... }
</style>
<div class="root">
<h1
id="card-01-title"
data-anim="kinetic-chars"
data-anim-at="0.3"
data-anim-duration="0.5"
data-anim-stagger="0.04"
data-anim-pattern="pop"
>
<span class="char">S</span>
<span class="char">u</span>
</h1>
<div
id="card-01-line"
data-anim="grow-x"
data-anim-at="0.65"
data-anim-duration="0.5"
data-anim-target-w="420"
style="width:0;height:8px;background:var(--accent-0);border-radius:4px;"
></div>
</div>
</div>Hard rules (hyperframes lint will reject violations):
- Single root
<div class="card" data-card-id="{cardId}"> - Inline
<style>rules MUST be prefixed with the scope selector above - No `<script>` tags
- No external URLs in
src=/href=(no CDN, no remote fonts) - No inline event handlers (
onclick=etc.) - All assets via relative paths into the same
public/directory - Colors via
var(--accent-N)etc. for portability across themes
Animations are declared, not coded. Use data-anim-* attributes only; never write <script> to animate. You compile every data-anim-* declaration into the single master GSAP timeline in Step 9.
Card Sizing — Mobile-First in Portrait
The 10 references/styles/*.html are sized for a 1920×1080 landscape preview. When storyboard.layout = "portrait" (1080×1920, the dominant case for social / mobile), scale every visual size up — phones hold the screen close, and the same pixel count reads smaller than on a landscape TV-style canvas.
| token | landscape baseline | portrait target | scale |
|---|---|---|---|
| title (h1/h2 hero) | 64–96px | 88–132px | ×1.35 |
| detail / body | 24–30px | 30–40px | ×1.30 |
| kicker / chip label | 14–16px | 18–22px | ×1.30 |
| timecode / meta | 12–14px | 16–18px | ×1.30 |
| data block primary number | 48–60px | 64–88px | ×1.40 |
| line-height multiplier | 1.05–1.5 | same | (don't scale) |
Rule of thumb: portraitPx = round(landscapePx × 1.3), then floor to a nearby 4px multiple for visual rhythm. Hero headlines may go up to ×1.4; small meta text stays at ×1.2 to avoid crowding.
Padding shrinks slightly in portrait — the card is narrower so big landscape padding (40–64px) eats too much width. Use 24–36px horizontal padding in portrait.
If you're producing a single card that must work in both layouts, prefer a @container query on the card root over hard-coding sizes:
.card[data-card-id="X"] .root {
container-type: inline-size;
}
.card[data-card-id="X"] .title {
font-size: clamp(64px, 8.5cqi, 132px);
}
.card[data-card-id="X"] .detail {
font-size: clamp(24px, 3.2cqi, 40px);
}But for most cards, a single layout choice is fine — just pick the size table column that matches the storyboard's layout field.
Available data-anim Kinds
| kind | use for | key params |
|---|---|---|
fade-in | enter | at, duration, ease? |
fade-out | exit | at, duration, ease? |
slide-in | slide enter | at, duration, `from=left\ |
kinetic-chars | per-char pop | at, duration, stagger, `pattern=pop\ |
typewriter | per-char fade | same as kinetic-chars but slower default stagger |
count-up | animate number | at, duration, from, to, `format=.0f\ |
draw-path | SVG path reveal | at, duration — element should be a <path> |
grow-y | bar height | at, duration, target-h (px) — element starts height:0 |
grow-x | bar width | at, duration, target-w (px) — element starts width:0 |
scale-pop | pop entrance | at, duration |
blur-in | unfocused → focused | at, duration |
mask-reveal | clip reveal | at, duration, `direction=left\ |
morph-to | tween any CSS | at, duration, props='{...JSON...}' |
data-anim-at is seconds relative to the card's startSec — when you compile each declaration into the GSAP timeline in Step 9, add the card's startSec to get the absolute time and quantize to 1/fps.
9. Assemble the Composition HTML
Stage the assets and write $WORK_DIR/public/index.html:
# SKILL_DIR is injected by the host ("Base directory for this skill: …")
SKILL_DIR="<SKILL_DIR>"
mkdir -p "$WORK_DIR/public/fonts" "$WORK_DIR/public/vendor" "$WORK_DIR/public/cards"
cp -n "$SKILL_DIR/assets/fonts/"* "$WORK_DIR/public/fonts/"
cp -n "$SKILL_DIR/assets/vendor/gsap.min.js" "$WORK_DIR/public/vendor/"
# stage the input video — RE-ENCODE with dense keyframes. Sources with a sparse GOP
# (keyframe interval > ~1s) freeze on seek in the renderer (a frozen frame under the
# overlays); -g / -keyint_min set to your composition fps make every frame seekable.
# (Set both to your fps — 30 shown; use 24/25/60 to match.)
ffmpeg -y -i "$VIDEO_PATH" -c:v libx264 -crf 18 -g 30 -keyint_min 30 \
-pix_fmt yuv420p -movflags +faststart -c:a aac "$WORK_DIR/public/input-video.mp4"Composition Template
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
@font-face {
font-family: "Caveat";
src: url("fonts/Caveat-400-latin.woff2") format("woff2");
font-weight: 400;
font-display: block;
}
@font-face {
font-family: "Caveat";
src: url("fonts/Caveat-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
@font-face {
font-family: "LXGW WenKai TC";
src: url("fonts/LXGWWenKaiTC-400-latin.woff2") format("woff2");
font-weight: 400;
font-display: block;
}
@font-face {
font-family: "Inter";
src: url("fonts/Inter-400-latin.woff2") format("woff2");
font-weight: 400;
font-display: block;
}
@font-face {
font-family: "Inter";
src: url("fonts/Inter-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
@font-face {
font-family: "Virgil";
src: url("fonts/Virgil.woff2") format("woff2");
font-display: block;
}
:root {
/* Pick from the themeId palette table in Step 7 — example: classic */
--bg: #fff9e3;
--text: #1e1e1e;
--accent-0: #1971c2;
--accent-1: #e03131;
--accent-2: #2f9e44;
--accent-3: #e8590c;
--accent-4: #9c36b5;
--font-family: "Caveat", "LXGW WenKai TC", serif;
}
* {
box-sizing: border-box;
}
/* Body font-family MUST list concrete font names (not just var(--font-family)) —
the HyperFrames renderer's static analyzer doesn't expand CSS variables when
resolving fonts, so a var-only chain triggers `font_family_without_font_face`
lint and falls back to a generic. Use the concrete chain here; cards that
want the theme font can still reference var(--font-family) internally. */
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
font-family: "Inter", "Caveat", "LXGW WenKai TC", ui-sans-serif, system-ui, sans-serif;
}
#stage {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
/* video-wrapper holds the source video. Its position / size are animated
over time by the master timeline (one tween per layout transition). */
.video-wrapper {
position: absolute;
left: 0;
top: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
border-radius: 0;
box-shadow: none;
}
.video-wrapper video {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-host {
position: absolute;
pointer-events: none;
overflow: hidden;
}
.card-host .card {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.card-host .char {
display: inline-block;
visibility: visible;
}
/* Subtle drop shadow + rounded corners for non-fullscreen video framings */
.video-wrapper.framed {
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
}
</style>
</head>
<body>
<div
id="stage"
data-composition-id="talking-head-recut"
data-start="0"
data-duration="121.2"
data-fps="30"
data-width="1920"
data-height="1080"
>
<!-- Layer 1: source video — initial position matches card-01's layout -->
<div class="video-wrapper" id="video-wrap">
<video
id="bg-video"
src="input-video.mp4"
muted
playsinline
data-start="0"
data-duration="121.2"
data-track-index="1"
></video>
</div>
<!-- Layer 2: each card-host sits at the bounds dictated by its layout. -->
<!-- IMPORTANT: every card-host MUST carry BOTH "card-host" and "clip" classes. -->
<!-- - "card-host" → our positioning + pointer-events styles -->
<!-- - "clip" → HyperFrames runtime uses this to enforce visibility -->
<!-- only during data-start … data-start+data-duration. -->
<!-- Without "clip" the host stays visible the whole video -->
<!-- (lint: timed_element_missing_clip_class). -->
<!-- Example: card-01 with zone="fullscreen" → card-host covers (0,0,1920,1080) -->
<div
class="card-host clip"
data-card-id="card-01"
data-start="1.0000"
data-duration="6.5000"
data-track-index="2"
style="left:0;top:0;width:1920px;height:1080px;visibility:hidden;opacity:0;"
>
<!-- paste the contents of public/cards/card-01.html here -->
</div>
<!-- Example: card-02 with zone="side-panel" (split composition layout) → card on left half -->
<div
class="card-host clip"
data-card-id="card-02"
data-start="8.0000"
data-duration="12.0000"
data-track-index="2"
style="left:0;top:0;width:960px;height:1080px;visibility:hidden;opacity:0;"
>
<!-- card-02 HTML -->
</div>
<!-- ...one "card-host clip" per card with inline bounds matching resolveZoneBounds(card.zone)... -->
<script src="vendor/gsap.min.js"></script>
<script>
(function () {
// count-up formatter helper
window.__fmt = function (v, fmt) {
if (typeof fmt === "string" && /^\.[0-9]+f$/.test(fmt)) {
return Number(v).toFixed(Number(fmt.slice(1, -1)));
}
if (fmt === ",d") return Math.round(v).toLocaleString();
return String(Math.round(v));
};
const tl = window.gsap.timeline({ paused: true });
// ── Card lifecycle (one block per card) ──
// Example for card-01 [1.0, 7.5] with kinetic-chars at +0.3, grow-x at +0.65:
// Enter (fade in over 0.4s)
tl.set('.card-host[data-card-id="card-01"]', { visibility: "visible" }, 1.0);
tl.fromTo(
'.card-host[data-card-id="card-01"]',
{ opacity: 0 },
{ opacity: 1, duration: 0.4, ease: "power2.out" },
1.0,
);
// Card-internal anims (compile each data-anim-* declaration here)
tl.from(
'.card[data-card-id="card-01"] #card-01-title .char',
{ opacity: 0, y: 8, scale: 0.8, duration: 0.5, ease: "power2.out", stagger: 0.04 },
1.3,
);
tl.fromTo(
'.card[data-card-id="card-01"] #card-01-line',
{ width: 0 },
{ width: 420, duration: 0.5, ease: "power2.out" },
1.65,
);
// Exit (fade out over 0.35s, ending at endSec)
tl.to(
'.card-host[data-card-id="card-01"]',
{ opacity: 0, duration: 0.35, ease: "power2.in" },
7.15,
);
tl.set('.card-host[data-card-id="card-01"]', { visibility: "hidden" }, 7.5);
// ── Video framing transitions ──
// When the next card uses a different composition layout, animate the
// video-wrapper to its new bounds. Example: card-01 = fullscreen
// (video hidden behind), card-02 = split composition (zone="side-panel"
// → video on right, card on left).
// Card-02 enters at 8.0s with the split composition. Animate video to
// the right half during the card-01 → card-02 gap (between 7.5 and 8.0s).
tl.set("#video-wrap", { className: "video-wrapper framed" }, 7.5);
tl.to(
"#video-wrap",
{ left: 960, top: 0, width: 960, height: 1080, duration: 0.6, ease: "power2.inOut" },
7.5,
);
// Card-02 enter — same pattern as card-01
tl.set('.card-host[data-card-id="card-02"]', { visibility: "visible" }, 8.0);
tl.fromTo(
'.card-host[data-card-id="card-02"]',
{ opacity: 0 },
{ opacity: 1, duration: 0.4, ease: "power2.out" },
8.0,
);
// ...card-02 internal anims...
// ── repeat for each card; if the NEXT card's layout differs,
// insert another tl.to('#video-wrap', ...) tween before its enter ──
window.__timelines = window.__timelines || {};
window.__timelines["talking-head-recut"] = tl;
})();
</script>
</div>
</body>
</html>GSAP Statement Cheat Sheet
Compile each data-anim attribute into a GSAP statement. Times are absolute seconds = card.startSec + data-anim-at, quantized to 1/fps. Selector is .card[data-card-id="X"] #elementId.
| data-anim | GSAP statement template |
|---|---|
fade-in | tl.fromTo(SEL, { opacity: 0 }, { opacity: 1, duration: D, ease: 'power2.out' }, T); |
fade-out | tl.to(SEL, { opacity: 0, duration: D, ease: 'power2.in' }, T); |
slide-in (from=left, dist=80) | tl.fromTo(SEL, { opacity: 0, x: -80 }, { opacity: 1, x: 0, duration: D, ease: 'power2.out' }, T); |
kinetic-chars (pop) | tl.from(SEL + ' .char', { opacity: 0, y: 8, scale: 0.8, duration: D, ease: 'power2.out', stagger: S }, T); |
count-up | (function(){const o={v:FROM};tl.to(o,{v:TO,duration:D,ease:'power2.out',onUpdate:function(){const el=document.querySelector(SEL);if(el)el.textContent=__fmt(o.v,'FMT');}},T);})(); |
draw-path | (function(){const el=document.querySelector(SEL);if(el){const L=el.getTotalLength();tl.set(SEL,{strokeDasharray:L,strokeDashoffset:L},T);tl.to(SEL,{strokeDashoffset:0,duration:D,ease:'power2.inOut'},T);}})(); |
grow-x (target-w=W) | tl.fromTo(SEL, { width: 0 }, { width: W, duration: D, ease: 'power2.out' }, T); |
grow-y (target-h=H) | tl.fromTo(SEL, { height: 0 }, { height: H, duration: D, ease: 'power2.out' }, T); |
scale-pop | tl.fromTo(SEL, { opacity: 0, scale: 0.6 }, { opacity: 1, scale: 1, duration: D, ease: 'back.out(1.6)' }, T); |
mask-reveal (direction=left) | tl.fromTo(SEL, { clipPath: 'inset(0 100% 0 0)' }, { clipPath: 'inset(0 0 0 0)', duration: D, ease: 'power2.inOut' }, T); |
Quantize: T = Math.round(absSec * fps) / fps. At 30fps the smallest step is 1/30 ≈ 0.0333s; rounding to 4 decimals (.toFixed(4)) is fine inside the JS literal.
Video Framing Reference (per layout value)
The selector for the video container is #video-wrap. Animate its bounds between cards using tl.to('#video-wrap', { ...bounds }, T). Initial bounds should be set inline on the element to match card-01's layout. Pick a transition duration of 0.5–0.7s with ease: 'power2.inOut'.
Decorative frames (clean / hairline / polaroid) sit as a sibling of #video-wrap and follow it through layout transitions. See `references/frames/` for each frame's placement HTML, suggested CSS, and which layouts it pairs with. Quick rule: overlay layout suppresses decorative frames (the full-bleed video clashes with chrome); PiP layouts already have their own pill treatment (border-radius + white ring + shadow), so add a decorative frame only on top of split / stack.
GSAP target lookup table for #video-wrap per composition layout (landscape 1920×1080 — for portrait & 4:5 see references/layouts/*.html which list all three ratios):
| composition layout | typical card.zone | #video-wrap GSAP target | extra css class |
|---|---|---|---|
split | side-panel | { left: 960, top: 0, width: 960, height: 1080 } | — |
stack | lower-third | { left: 14, top: 14, width: 1892, height: 548 } (top 52%) | — |
pip (bottom-right) | fullscreen | { left: 1480, top: 760, width: 400, height: 300 } | pip-pill (border-radius + ring + shadow) |
pip (top-left) | fullscreen | { left: 40, top: 40, width: 400, height: 300 } | pip-pill |
overlay (video full-bleed) | video-overlay | { left: 0, top: 0, width: 1920, height: 1080 } (no change from default) | — |
| hide video (pure-graphic moment) | fullscreen | { opacity: 0 } (or move off-canvas) | — |
To toggle the pip-pill chrome (border-radius + white ring + drop shadow) when entering or leaving a pip moment:
// Enter pip — add chrome
tl.set("#video-wrap", { className: "video-wrapper pip-pill" }, T);
tl.to(
"#video-wrap",
{ left: 1480, top: 760, width: 400, height: 300, duration: 0.6, ease: "power2.inOut" },
T,
);
// Leave pip — back to clean full-bleed
tl.set("#video-wrap", { className: "video-wrapper" }, T_NEXT);
tl.to(
"#video-wrap",
{ left: 0, top: 0, width: 1920, height: 1080, duration: 0.6, ease: "power2.inOut" },
T_NEXT,
);Card-host bounds match the zone. Resolve the card's zone into pixel bounds using the table at the top of Step 6, then write those into the card-host's inline style="left:Xpx;top:Ypx;width:Wpx; height:Hpx;...". For video-overlay zone (overlay recipe), the card-host fills the full canvas — your CSS inside .card .root decides where the actual visible card sits.
HyperFrames Layout / Animation QA Rules
- Build each card's static hero frame first: the moment where the card is fully visible and readable.
- Confirm video, cards, subtitles/captions, and diagrams do not unintentionally overlap.
- Confirm hidden video areas are clipped by the frame and not visible outside intended bounds.
- Register one paused master timeline as
window.__timelines["talking-head-recut"]. - Build timelines synchronously at page load; no
async,setTimeout, Promises, or mediaplay()calls. - Do not use
Math.random()orDate.now()in render paths. - Do not use
repeat: -1; calculate finite repeats from the video duration. - Prefer GSAP transforms and opacity (
x,y,scale,rotation,opacity) over layout properties (top,left,width,height) for motion. - Animate wrappers such as
#video-wrap, not the video element dimensions directly. - Avoid animating the same property on the same element from multiple timelines at the same time.
- Use
data-track-index, notdata-layer; usedata-duration, notdata-end. - Every timed element (
card-host, sub-composition, etc.) MUST includeclass="clip"alongside its own classes — e.g.class="card-host clip". The HyperFrames runtime uses.clipto gate visibility to thedata-start … data-start+data-durationwindow. Without it the element is visible for the whole video (lint:timed_element_missing_clip_class). - For body / global
font-family, list concrete font names ('Inter', 'Caveat', …) — not a CSS variable likevar(--font-family). The HyperFrames font resolver doesn't expand CSS vars during static analysis (lint:font_family_without_font_face). Cards may still usevar(--font-family)internally since their@font-facedeclarations are loaded.
10. Render to MP4
cd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
--skill=talking-head-recut \
-o output.mp4 \
--fps 30hyperframes render <dir> reads <dir>/index.html and produces the MP4. The flag PRODUCER_BROWSER_GPU_MODE=hardware (or --browser-gpu) is strongly recommended on macOS — software-only Chrome rendering times out on most laptops.
For a sanity check before the full render, capture a single frame at a specific timestamp:
npx hyperframes snapshot public --at 5 # → public/snapshots/frame-00-at-5s.png (a single --at ignores --out)11. Report Results
Tell the user:
- Work directory path
storyboard.json(the card outline you designed)public/cards/*.html(one HTML per card)public/index.html(the assembled composition)output.mp4(the final video)- ASR provider used
- Card count + how you chose them (in 1 sentence)
- Any missing keys or quality caveats
Optional live preview (on request only). The clip plays unchanged inside public/index.html with the overlays on top, so it previews faithfully. Don't open it during the run. When the user asks, start a long-lived server after render and report the URL:
(cd "$WORK_DIR/public" && npx hyperframes preview) # or `npx hyperframes play` for a shareable linkDo not delete the work directory unless the user asks.
Attribution
The talking-head-recut skill (its card-based design system — styles, layouts, frames, fonts, and the GSAP-driven composition workflow) is adapted from the open-source vtake-skills project (vtake-cut):
https://github.com/notedit/vtake-skills
Adaptations for this repo: renamed to talking-head-recut; transcription repointed to local Whisper via hyperframes transcribe (dropping the third-party @notedit/vtake CLI and the vtake.app proxy); audio/metadata extraction inlined with ffmpeg/ffprobe; the fixed third-party brand outro removed in favour of an optional, neutral outro; artifacts aligned to the videos/<project>/ convention.
The original is MIT-licensed; its notice is retained below as required.
MIT License
Copyright (c) 2026 leeoxiang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.V—Take Visual Design Library
This directory is a reference library for the talking-head-recut skill. Style, layout, and video frame are three orthogonal dimensions you can freely mix when designing a takeaway video.
Style × Layout × VideoFrame
(10) (4) (3) = 120 possible combinationsRead a reference file when you decide to use that dimension. Each file is a self-contained HTML fragment that follows the talking-head-recut card-HTML contract (scoped <style>, no <script>, no external URLs, animations only via data-anim-*).
Layouts — how video and card share the canvas
| key | file | what it does | best for |
|---|---|---|---|
split | layouts/split.html | 50/50 side-by-side (landscape) or top/bottom (portrait) | speaker + data equal weight |
stack | layouts/stack.html | video on top (~52%), card below | talking-head with summary card |
pip | layouts/pip.html | card fills canvas, video rounded PiP in corner | content-heavy moment, speaker secondary |
overlay | layouts/overlay.html | video full-bleed, glass card floats on bottom | cinematic / dramatic moments |
A layout is a two-part recipe: pick a card.zone value to put in storyboard.json AND author a GSAP tween for #video-wrap to its target rect in the composition's <script>. Open the layout file's header for the recommended zone + the GSAP statement to paste. (Earlier docs referenced a card.layout field — that field does NOT exist in the real schema; the strict v3 schema only has card.zone.)
Styles — the card's visual language
| key | file | character | accent | suggested font |
|---|---|---|---|---|
academic | styles/academic.html | warm paper · grid · serif · blue highlight | #2557a7 | serif |
editorial | styles/editorial.html | cream · coral block · big italic quote | #ff3a2d | Playfair-like serif |
minimal | styles/minimal.html | pure black/white · huge type · generous space | #000 | Inter |
spotlight | styles/spotlight.html | dark purple gradient · glow · dramatic | #a78bfa | sans |
geom | styles/geom.html | chartreuse + hot pink + black collision | #d4ff00 | Inter bold |
whiteboard | styles/whiteboard.html | paper · Caveat handwriting · sketched borders | #ff6b35 | Caveat |
audit | styles/audit.html | manila paper · justified serif · APPROVED stamp | #8b1d1d | serif |
terminal | styles/terminal.html | dark · monospace · ASCII border · prompt cursor | #4ade80 | mono |
swiss | styles/swiss.html | white · Helvetica · strict double rules · red accent | #e8190f | Helvetica/Inter |
xhs | styles/xhs.html | cream + hot pink · chips · #hashtags · ❤️💬 row | #ff2e63 | sans |
Choose by content tone, not by content type — academic works for finance too if the tone is reflective; terminal works for non-tech if the tone is "engineering rigor".
Video Frames — decoration around the video element
| key | file | character | when to skip |
|---|---|---|---|
clean | frames/clean.html | no decoration; raw video | default; safest |
hairline | frames/hairline.html | double-stroke + four-corner viewfinder ticks | over overlay layout (clashes with full-bleed) |
polaroid | frames/polaroid.html | white photo frame + Caveat label + blue washi tape (no tilt) | over overlay layout; portrait PiP gets cramped |
A frame is a decorative div that sits next to the #video-wrap inside the composition's #stage. It is one-time HTML (not animated), but you can fade it in/out across cards. See each frame file for the placement snippet and the inline <style> it needs.
Decision guide (loose, not prescriptive)
| video content | suggested combos |
|---|---|
| interview / dialogue | academic × stack, audit × split |
| product launch / announcement | editorial × overlay, geom × pip |
| data analysis / financial report | audit × split, swiss × stack, terminal × pip |
| social clip (9:16) | xhs × overlay, editorial × stack |
| technical tutorial | terminal × split, whiteboard × pip |
| emotional story / narration | spotlight × overlay, whiteboard × overlay |
| minimalist presentation | minimal × split, swiss × overlay |
These are starting points only. Look at the transcript, pick the tone, then pick the visual.
Portrait sizing — bigger type for mobile
Every references/styles/*.html is sized for a 1920×1080 landscape preview. When the final composition is portrait (1080×1920) — the default for social / mobile — scale every visual size up so it reads on a phone held close.
| token | landscape | portrait | scale |
|---|---|---|---|
| hero title (h1/h2) | 64–96px | 88–132px | ×1.35 |
| detail / body | 24–30px | 30–40px | ×1.30 |
| kicker / chip / meta | 14–18px | 18–22px | ×1.25 |
| primary number / stat | 48–60px | 64–88px | ×1.40 |
| horizontal padding | 40–64px | 24–36px | ÷1.5 |
portraitPx ≈ round(landscapePx × 1.3). Hero headlines can go ×1.4; small meta stays at ×1.2. Padding shrinks in portrait since the card is narrower.
For a card that must work in both, use a container query on the card root: container-type: inline-size + font-size: clamp(64px, 8.5cqi, 132px).
Source aspect ratio independence
Output canvas is independent of source video aspect. Three supported output ratios (selected by the user in Step 7.0 of SKILL.md):
| ratio | canvas | storyboard.layout | best for |
|---|---|---|---|
16:9 | 1920×1080 | "landscape" | YouTube / TV / desktop playback |
9:16 | 1080×1920 | "portrait" | TikTok / Reels / short-form mobile |
4:5 | 1080×1350 | "portrait" (schema treats 4:5 as portrait since h>w) | Instagram feed / WeChat Moments / works for both platforms |
The layout reference files in layouts/ document landscape and portrait bounds only. For 4:5 derive bounds by proportional vertical scaling from portrait: 4:5 y/h = round(portrait y/h × 0.703), keep x/w identical. The composer doesn't care about the named layout value; it just uses composition.width × height.
- Landscape video on landscape canvas →
videoBoundsmatches video aspect, no letterbox - Portrait video on landscape canvas →
videoBoundsis a narrower box (e.g.pipbecomes 248×440); empty side filled by card or background - Landscape video on portrait canvas →
videoBoundsbecomes a wide-but-short band;stackandoverlaywork best - Portrait video on portrait canvas → most natural; any layout
The layout reference files show landscape values; for portrait you usually flip the long axis: split becomes top/bottom, pip video bubble shrinks ~20%, overlay card slot widens to full width.
Constraints you must obey when copying from these references
1. No `<script>` — animations only via data-anim-* attributes 2. No external URLs — no Google Fonts CDN, no remote images; the skill provides Caveat / LXGW WenKai TC / Inter / Virgil locally 3. All `<style>` rules must be prefixed with `.card[data-card-id="..."]` — the hyperframes sanitizer auto-scopes them, but write them already-scoped to stay readable 4. *No `on=` inline handlers 5. CSS variables for colors** when you want a card to switch theme cleanly; inline hex when you want this specific style's signature look
If a reference uses a font you don't have, fall back:
- Playfair Display / Noto Serif SC →
ui-serif, "Songti SC", "Times New Roman", serif - Noto Sans SC →
ui-sans-serif, system-ui, sans-serif - JetBrains Mono →
ui-monospace, "SF Mono", Menlo, monospace - Kalam →
'Caveat', cursive
<!--
Frame: clean — no decoration. Raw video edge.
─────────────────────────────────────────────────────────────────────
When to use
- Default; safest.
- Pairs with any layout and any style.
- Required over the `overlay` layout (decorative frames clash with full-bleed video).
How to apply in the composition
No extra HTML — just leave #video-wrap without the `framed` class.
.video-wrapper { border-radius: 0; box-shadow: none; }
How to apply on PiP layouts
Even with `clean`, PiPs benefit from a 14px border-radius + soft shadow
(see layouts/pip.html). That's a layout-level treatment, not a frame.
Placement
None. This reference exists as documentation of the "no-frame" option.
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>frame · clean (no decoration)</title>
<style>
html,
body {
margin: 0;
padding: 0;
background: #0a0c12;
color: #fff;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stage {
position: relative;
width: 1920px;
height: 1080px;
transform-origin: top left;
transform: scale(0.5);
background: #0a0c12;
overflow: hidden;
}
.video-wrap {
position: absolute;
left: 240px;
top: 140px;
width: 1440px;
height: 810px;
background: #1a1f2e;
background-image: repeating-linear-gradient(
45deg,
transparent 0 12px,
rgba(255, 255, 255, 0.05) 12px 13px
);
}
.v-label {
position: absolute;
left: 24px;
top: 24px;
padding: 8px 14px;
border: 1px solid rgba(255, 255, 255, 0.45);
border-radius: 3px;
color: rgba(255, 255, 255, 0.7);
font:
600 16px/1 ui-monospace,
monospace;
letter-spacing: 0.14em;
}
.note {
position: absolute;
left: 240px;
bottom: 60px;
right: 240px;
text-align: center;
color: rgba(255, 255, 255, 0.5);
font:
14px/1.4 ui-monospace,
monospace;
letter-spacing: 0.12em;
}
</style>
</head>
<body>
<div class="stage">
<div class="video-wrap">
<span class="v-label">VIDEO · no chrome</span>
</div>
<div class="note">CLEAN — let the video speak for itself.</div>
</div>
</body>
</html>
<!--
Frame: hairline — double stroke + four-corner viewfinder ticks
─────────────────────────────────────────────────────────────────────
When to use
- Professional / cinematographic feel without going full polaroid.
- Pairs well with: split, stack, pip layouts.
- Pairs well with: academic, audit, swiss, terminal styles.
- Skip on overlay layout — the decorative chrome clashes with full-bleed video.
How to apply in the composition
Add ONE decorative div as a sibling of #video-wrap (not a child — the
video element is muted/playsinline and the decorative div should sit
above it). The accent color is configurable per scene.
<div class="video-wrapper" id="video-wrap">
<video src="input-video.mp4" ...></video>
</div>
<div class="frame-deco frame-deco--hairline"
style="left:240px; top:140px; width:1440px; height:810px;
--frame-accent: #2557a7;">
<span class="tick tl"></span>
<span class="tick tr"></span>
<span class="tick bl"></span>
<span class="tick br"></span>
</div>
CSS (4 corners via explicit spans — preferred over ::before/::after
because pseudo-elements give you at most 2 corners cleanly):
.frame-deco { position: absolute; pointer-events: none; z-index: 3; }
.frame-deco--hairline {
box-shadow:
inset 0 0 0 2px var(--frame-accent),
inset 0 0 0 6px color-mix(in srgb, var(--frame-accent) 13%, transparent),
0 0 0 1px rgba(0,0,0,.1);
}
.frame-deco .tick { position: absolute; width: 24px; height: 24px; }
.frame-deco .tick.tl { top: -3px; left: -3px; border-top: 4px solid var(--frame-accent); border-left: 4px solid var(--frame-accent); }
.frame-deco .tick.tr { top: -3px; right: -3px; border-top: 4px solid var(--frame-accent); border-right: 4px solid var(--frame-accent); }
.frame-deco .tick.bl { bottom: -3px; left: -3px; border-bottom: 4px solid var(--frame-accent); border-left: 4px solid var(--frame-accent); }
.frame-deco .tick.br { bottom: -3px; right: -3px; border-bottom: 4px solid var(--frame-accent); border-right: 4px solid var(--frame-accent); }
When the layout changes mid-composition
Move + resize the frame-deco div together with #video-wrap using GSAP:
tl.to('#video-wrap, .frame-deco', { left:, top:, width:, height:, ... }, T);
Or fade out the deco entirely before transitioning to a clean PiP.
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>frame · hairline (double stroke + viewfinder ticks)</title>
<style>
html,
body {
margin: 0;
padding: 0;
background: #0a0c12;
color: #fff;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stage {
position: relative;
width: 1920px;
height: 1080px;
transform-origin: top left;
transform: scale(0.5);
background: #0a0c12;
overflow: hidden;
}
.video-wrap {
position: absolute;
left: 240px;
top: 140px;
width: 1440px;
height: 810px;
background: #1a1f2e;
background-image: repeating-linear-gradient(
45deg,
transparent 0 12px,
rgba(255, 255, 255, 0.05) 12px 13px
);
}
.v-label {
position: absolute;
left: 24px;
top: 24px;
padding: 8px 14px;
border: 1px solid rgba(255, 255, 255, 0.45);
border-radius: 3px;
color: rgba(255, 255, 255, 0.7);
font:
600 16px/1 ui-monospace,
monospace;
letter-spacing: 0.14em;
}
.frame-deco {
position: absolute;
left: 240px;
top: 140px;
width: 1440px;
height: 810px;
pointer-events: none;
z-index: 3;
--frame-accent: #2557a7;
box-shadow:
inset 0 0 0 2px var(--frame-accent),
inset 0 0 0 6px rgba(37, 87, 167, 0.13),
0 0 0 1px rgba(0, 0, 0, 0.1);
}
.tick {
position: absolute;
width: 24px;
height: 24px;
}
.tick.tl {
top: -3px;
left: -3px;
border-top: 4px solid var(--frame-accent, #2557a7);
border-left: 4px solid var(--frame-accent, #2557a7);
}
.tick.tr {
top: -3px;
right: -3px;
border-top: 4px solid var(--frame-accent, #2557a7);
border-right: 4px solid var(--frame-accent, #2557a7);
}
.tick.bl {
bottom: -3px;
left: -3px;
border-bottom: 4px solid var(--frame-accent, #2557a7);
border-left: 4px solid var(--frame-accent, #2557a7);
}
.tick.br {
bottom: -3px;
right: -3px;
border-bottom: 4px solid var(--frame-accent, #2557a7);
border-right: 4px solid var(--frame-accent, #2557a7);
}
.note {
position: absolute;
left: 240px;
bottom: 60px;
right: 240px;
text-align: center;
color: rgba(255, 255, 255, 0.5);
font:
14px/1.4 ui-monospace,
monospace;
letter-spacing: 0.12em;
}
</style>
</head>
<body>
<div class="stage">
<div class="video-wrap">
<span class="v-label">VIDEO</span>
</div>
<div class="frame-deco">
<span class="tick tl"></span>
<span class="tick tr"></span>
<span class="tick bl"></span>
<span class="tick br"></span>
</div>
<div class="note">
HAIRLINE — double stroke (2px inner + soft 6px halo) + four-corner viewfinder ticks. Accent
color matches the card style.
</div>
</div>
</body>
</html>
<!--
Frame: polaroid — white photo frame + Caveat label + blue washi tape (no tilt)
─────────────────────────────────────────────────────────────────────
When to use
- Casual, personal, warm feel. Looks great for interviews, memoirs, vlogs.
- Pairs well with: split, stack, pip layouts.
- Pairs well with: academic, whiteboard, editorial, xhs styles.
- Skip on overlay layout (clashes with full-bleed).
- In portrait PiP (narrow video bubble), polaroid gets cramped — prefer hairline.
IMPORTANT — NO TILT
Earlier iterations of the design tilted the polaroid by -1.4° / 5°. User
explicitly removed the tilt (chat L770–790). Keep the polaroid card,
label, and washi tape all UPRIGHT.
How to apply in the composition
Replace the bare #video-wrap with a wrapper div that draws the polaroid
around it. The polaroid card has its own background + padding; the video
sits inside a sub-region.
HTML pattern (use absolute positioning to fit the layout's video bounds):
<div class="frame-deco frame-deco--polaroid"
style="left:240px; top:140px; width:1440px; height:990px;">
<div class="polaroid-card">
<div class="polaroid-photo">
<!-- the actual #video-wrap element, sized to fill -->
<div class="video-wrapper" id="video-wrap">
<video src="input-video.mp4" ...></video>
</div>
</div>
<div class="polaroid-caption">speaker · 2026</div>
</div>
<div class="washi"></div>
</div>
The video bounds in storyboard.json should be the OUTER polaroid bounds.
The CSS inset values below carve out the inner photo area.
Suggested CSS
.frame-deco--polaroid { position: absolute; }
.polaroid-card {
position: absolute; inset: 24px;
background: #fefefe; padding: 14px 14px 64px;
box-shadow: 0 28px 60px -18px rgba(0,0,0,.55),
0 4px 12px rgba(0,0,0,.12);
box-sizing: border-box;
}
.polaroid-photo {
position: relative; width: 100%; height: calc(100% - 50px);
overflow: hidden; background: #000;
}
.polaroid-photo .video-wrapper {
position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 0;
}
.polaroid-caption {
position: absolute; bottom: 18px; left: 0; right: 0;
text-align: center;
font: 700 32px/1 "Caveat", ui-serif, cursive;
color: #1d1c1a;
}
.washi {
position: absolute; top: 12px; left: 20%;
width: 120px; height: 28px;
background: rgba(180,210,235,.85);
background-image: repeating-linear-gradient(90deg,
transparent 0 6px, rgba(255,255,255,.3) 6px 7px);
box-shadow: 0 3px 8px rgba(0,0,0,.15);
}
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>frame · polaroid (white photo frame + caveat label, no tilt)</title>
<style>
html, body { margin: 0; padding: 0; background: #0a0c12; color: #fff;
font-family: ui-sans-serif, system-ui, sans-serif; }
.stage { position: relative; width: 1920px; height: 1080px;
transform-origin: top left; transform: scale(0.5);
background: #0a0c12; overflow: hidden; }
.frame-deco { position: absolute; left: 240px; top: 70px; width: 1440px; height: 940px; }
.polaroid-card {
position: absolute; inset: 24px;
background: #fefefe; padding: 14px 14px 64px;
box-shadow: 0 28px 60px -18px rgba(0,0,0,.55), 0 4px 12px rgba(0,0,0,.12);
box-sizing: border-box;
}
.polaroid-photo {
position: relative; width: 100%; height: calc(100% - 50px);
overflow: hidden; background: #1a1f2e;
background-image: repeating-linear-gradient(45deg,
transparent 0 12px, rgba(255,255,255,.05) 12px 13px);
}
.v-label { position: absolute; left: 24px; top: 24px;
padding: 8px 14px; border: 1px solid rgba(255,255,255,.45);
border-radius: 3px; color: rgba(255,255,255,.7);
font: 600 16px/1 ui-monospace, monospace; letter-spacing: .14em; }
.polaroid-caption {
position: absolute; bottom: 18px; left: 0; right: 0;
text-align: center;
font: 700 32px/1 "Caveat", ui-serif, cursive;
color: #1d1c1a;
}
.washi {
position: absolute; top: 12px; left: 20%;
width: 120px; height: 28px;
background: rgba(180,210,235,.85);
background-image: repeating-linear-gradient(90deg,
transparent 0 6px, rgba(255,255,255,.3) 6px 7px);
box-shadow: 0 3px 8px rgba(0,0,0,.15);
}
.note { position: absolute; left: 240px; bottom: 18px; right: 240px;
text-align: center; color: rgba(255,255,255,.5);
font: 14px/1.4 ui-monospace, monospace; letter-spacing: .12em; }
</style>
</head>
<body>
<div class="stage">
<div class="frame-deco">
<div class="polaroid-card">
<div class="polaroid-photo">
<span class="v-label">VIDEO</span>
</div>
<div class="polaroid-caption">speaker · 2026</div>
</div>
<div class="washi"></div>
</div>
<div class="note">POLAROID — white frame, Caveat handwritten caption, blue washi tape on top. Upright (no tilt).</div>
</div>
</body>
</html>
<!--
Layout: overlay — video full-bleed, glass card floats on bottom
─────────────────────────────────────────────────────────────────────
Implementation: card.zone = "video-overlay" + leave #video-wrap at
full-bleed (no tween needed if it starts full-bleed). The "card" is
positioned by CSS inside the video-overlay zone's full-canvas wrapper.
Recommended storyboard.json card entry (strict v3):
{
"id": "card-XX",
"intent": "cinematic / dramatic moment with glass overlay",
"startSec": 8, "endSec": 14,
"accentIndex": 0,
"zone": "video-overlay",
"contentHints": { "kicker": "...", "title": "...", "detail": "..." }
}
GSAP target for #video-wrap (full-bleed — usually unchanged):
tl.to('#video-wrap',
{ left: 0, top: 0, width: COMPOSITION_W, height: COMPOSITION_H,
duration: 0.6, ease: 'power2.inOut' }, START_SEC);
// Use defaults: landscape 1920×1080 / portrait 1080×1920 / 4:5 1080×1350
Recommended card-host inline style (since zone="video-overlay" gives
full-canvas bounds, you typically inset the actual card visual via CSS):
landscape glass card, mid-left:
.card[data-card-id="X"] .root {
position: absolute; left: 48px; top: 460px;
width: 660px; height: 540px;
background: rgba(15,16,22,.78);
backdrop-filter: blur(22px) saturate(140%);
-webkit-backdrop-filter: blur(22px) saturate(140%);
border-radius: 18px;
box-shadow: 0 30px 80px -20px rgba(0,0,0,.45);
}
portrait glass card, bottom band:
.card[data-card-id="X"] .root {
position: absolute; left: 24px; top: 1280px;
width: 1032px; height: 564px;
/* same glass treatment */
}
4:5 glass card (y/h × 0.703):
.card[data-card-id="X"] .root {
position: absolute; left: 24px; top: 900px;
width: 1032px; height: 397px;
/* same glass treatment */
}
Legibility gradient (insert as a third absolute layer between video
and card-host, in the composition's <div id="stage">):
<div class="legibility"></div>
.legibility {
position: absolute; inset: 0; pointer-events: none; z-index: 2;
background:
linear-gradient(180deg, rgba(0,0,0,.55) 0%, transparent 22%,
transparent 50%, rgba(0,0,0,.72) 100%);
}
Notes
- Card root MUST be transparent or use rgba/glass background — see
Pattern A in SKILL.md "Transparent card backgrounds".
- Glass without legibility gradient = white-on-white catastrophe over
bright video. Always pair them.
- Glass on a light video also benefits from a darker base color (use
rgba(15,16,22,.78) over rgba(255,255,255,.10) when source has bright
daylight / interior shots).
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>layout · overlay (video full-bleed, glass card)</title>
<style>
html,
body {
margin: 0;
padding: 0;
background: #0a0c12;
color: #fff;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stage {
position: relative;
width: 1920px;
height: 1080px;
transform-origin: top left;
transform: scale(0.5);
background: #0a0c12;
overflow: hidden;
}
.video-region {
position: absolute;
left: 0;
top: 0;
width: 1920px;
height: 1080px;
background: #1a1f2e;
background-image: repeating-linear-gradient(
45deg,
transparent 0 12px,
rgba(255, 255, 255, 0.05) 12px 13px
);
}
.gradient {
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(135deg, rgba(0, 0, 0, 0.55) 0%, transparent 35%),
linear-gradient(180deg, transparent 50%, rgba(0, 0, 0, 0.55) 100%);
}
.card-region {
position: absolute;
left: 48px;
top: 460px;
width: 660px;
height: 540px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(18px) saturate(140%);
-webkit-backdrop-filter: blur(18px) saturate(140%);
border-radius: 18px;
box-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font:
600 24px/1.3 ui-monospace,
monospace;
letter-spacing: 0.12em;
}
.label {
padding: 12px 18px;
border: 1.5px solid rgba(255, 255, 255, 0.7);
border-radius: 4px;
}
.v-label {
position: absolute;
left: 28px;
top: 28px;
color: rgba(255, 255, 255, 0.55);
padding: 8px 14px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 3px;
font:
600 16px/1 ui-monospace,
monospace;
letter-spacing: 0.14em;
}
</style>
</head>
<body>
<div class="stage">
<div class="video-region"><span class="v-label">VIDEO · 1920×1080 (full-bleed)</span></div>
<div class="gradient"></div>
<div class="card-region"><span class="label">CARD · 660×540 glass</span></div>
</div>
</body>
</html>
<!--
Layout: pip — card fills canvas, video rounded PiP in corner
─────────────────────────────────────────────────────────────────────
Implementation: card.zone = "fullscreen" + shrink #video-wrap to a
corner pill via GSAP, and add a .pip-pill CSS class for the chrome.
Recommended storyboard.json card entry (strict v3):
{
"id": "card-XX",
"intent": "content-heavy moment with speaker as small accent",
"startSec": 8, "endSec": 14,
"accentIndex": 0,
"zone": "fullscreen",
"contentHints": { "kicker": "...", "title": "...", "detail": "..." }
}
GSAP target for #video-wrap (corner pill — pick one per source aspect):
landscape (1920×1080), 16:9 source video:
tl.set('#video-wrap', { className: 'video-wrapper pip-pill' }, START_SEC);
tl.to('#video-wrap',
{ left: 1432, top: 28, width: 460, height: 258,
duration: 0.6, ease: 'power2.inOut' }, START_SEC);
landscape, 9:16 source video (vertical phone footage):
tl.to('#video-wrap',
{ left: 1644, top: 28, width: 248, height: 440, ... }, START_SEC);
portrait (1080×1920), 16:9 source video:
tl.to('#video-wrap',
{ left: 690, top: 28, width: 360, height: 203, ... }, START_SEC);
portrait, 9:16 source video:
tl.to('#video-wrap',
{ left: 738, top: 28, width: 312, height: 555, ... }, START_SEC);
4:5 (1080×1350) — y values × 0.703, w/h × 0.703:
landscape-source: { left: 690, top: 28, width: 360, height: 203 } → keep, fits
portrait-source : { left: 738, top: 28, width: 312, height: 390 }
Recommended .pip-pill CSS (add to composition <style>):
.video-wrapper.pip-pill {
border-radius: 14px;
box-shadow:
0 24px 60px -20px rgba(0,0,0,.45),
0 0 0 4px rgba(255,255,255,.7),
0 0 0 5px rgba(0,0,0,.18);
overflow: hidden;
}
Notes
- Card-first composition for content-heavy talks.
- Default corner is top-right (above); for bottom-right flip y to
canvasH - videoH - 28 (e.g. portrait 16:9 source bottom-right:
{ left: 690, top: 1689, width: 360, height: 203 }).
- DOM order: card-host appears BEFORE video-wrapper for the pip window
so the video pill paints on top of the card. If you keep them in
natural DOM order (video first, card second), use z-index on the
card-host to push it behind.
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>layout · pip (card hero + video pill)</title>
<style>
html,
body {
margin: 0;
padding: 0;
background: #0a0c12;
color: #fff;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stage {
position: relative;
width: 1920px;
height: 1080px;
transform-origin: top left;
transform: scale(0.5);
background: #0a0c12;
overflow: hidden;
}
.region {
position: absolute;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font:
600 24px/1.3 ui-monospace,
monospace;
letter-spacing: 0.12em;
}
.card-region {
left: 0;
top: 0;
width: 1920px;
height: 1080px;
background: #f1ead8;
color: #1a1d2b;
}
.video-region {
left: 1432px;
top: 28px;
width: 460px;
height: 258px;
background: #1a1f2e;
color: #fff;
border-radius: 14px;
box-shadow:
0 24px 60px -20px rgba(0, 0, 0, 0.45),
0 0 0 4px rgba(255, 255, 255, 0.7),
0 0 0 5px rgba(0, 0, 0, 0.18);
background-image: repeating-linear-gradient(
45deg,
transparent 0 12px,
rgba(255, 255, 255, 0.05) 12px 13px
);
}
.label {
padding: 12px 18px;
border: 1.5px solid currentColor;
border-radius: 4px;
}
.v-label {
font:
600 14px/1 ui-monospace,
monospace;
letter-spacing: 0.14em;
padding: 6px 10px;
border: 1px solid currentColor;
border-radius: 3px;
}
</style>
</head>
<body>
<div class="stage">
<div class="region card-region"><span class="label">CARD · 1920×1080 (full-bleed)</span></div>
<div class="region video-region"><span class="v-label">VIDEO · 460×258 PIP</span></div>
</div>
</body>
</html>
<!--
Layout: split — 50/50 side-by-side
─────────────────────────────────────────────────────────────────────
Real schema only has `card.zone` (5 enum values) — there is NO
`card.layout` field, and video bounds are composition-level
(`videoTrack.bounds`, one-time). The "split look" is implemented by:
1. setting card.zone = "side-panel" for each card in this stretch
2. tweening #video-wrap to the other half via GSAP between cards
Recommended storyboard.json card entry (strict v3):
{
"id": "card-XX",
"intent": "speaker + data side-by-side",
"startSec": 8, "endSec": 14,
"accentIndex": 0,
"zone": "side-panel",
"contentHints": { "kicker": "...", "title": "...", "detail": "..." }
}
GSAP target for #video-wrap during this card (paste in <script>):
landscape (1920×1080):
tl.to('#video-wrap',
{ left: 960, top: 0, width: 960, height: 1080,
duration: 0.6, ease: 'power2.inOut' }, START_SEC);
portrait (1080×1920) — flips to top/bottom:
tl.to('#video-wrap',
{ left: 0, top: 960, width: 1080, height: 960,
duration: 0.6, ease: 'power2.inOut' }, START_SEC);
4:5 (1080×1350) — derive by ×0.703 on y/h:
tl.to('#video-wrap',
{ left: 0, top: 675, width: 1080, height: 675,
duration: 0.6, ease: 'power2.inOut' }, START_SEC);
Notes
- Card-host (from card.zone="side-panel") covers half the canvas;
opaque card background is fine — it only paints its own half.
- Flip card/video sides: swap which side gets the speaker tween.
- With 14–18px page-level gap, set composition bg to deep color
(e.g. #0a0c12) so the gap reads as a clean separator.
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>layout · split (50/50)</title>
<style>
html,
body {
margin: 0;
padding: 0;
background: #0a0c12;
color: #fff;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stage {
position: relative;
width: 1920px;
height: 1080px;
transform-origin: top left;
transform: scale(0.5);
background: #0a0c12;
overflow: hidden;
}
.region {
position: absolute;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font:
600 24px/1.3 ui-monospace,
monospace;
letter-spacing: 0.12em;
}
.card-region {
left: 18px;
top: 18px;
width: 942px;
height: 1044px;
background: #f1ead8;
color: #1a1d2b;
}
.video-region {
left: 978px;
top: 18px;
width: 924px;
height: 1044px;
background: #1a1f2e;
background-image: repeating-linear-gradient(
45deg,
transparent 0 12px,
rgba(255, 255, 255, 0.05) 12px 13px
);
}
.label {
padding: 12px 18px;
border: 1.5px solid currentColor;
border-radius: 4px;
}
.meta {
position: absolute;
left: 18px;
top: -36px;
font:
12px/1 ui-monospace,
monospace;
letter-spacing: 0.18em;
color: #888;
}
</style>
</head>
<body>
<div class="stage">
<div class="meta">SPLIT · 1920×1080 · card 960 | video 960</div>
<div class="region card-region"><span class="label">CARD · 960×1080</span></div>
<div class="region video-region"><span class="label">VIDEO · 960×1080</span></div>
</div>
</body>
</html>
<!--
Layout: stack — video on top, card below
─────────────────────────────────────────────────────────────────────
Implementation: card.zone = "lower-third" + tween #video-wrap to top band.
Recommended storyboard.json card entry (strict v3):
{
"id": "card-XX",
"intent": "talking-head + caption-style summary below",
"startSec": 8, "endSec": 14,
"accentIndex": 0,
"zone": "lower-third",
"contentHints": { "kicker": "...", "title": "...", "detail": "..." }
}
GSAP target for #video-wrap (top band):
landscape (1920×1080) — video top 52%:
tl.to('#video-wrap',
{ left: 14, top: 14, width: 1892, height: 548,
duration: 0.6, ease: 'power2.inOut' }, START_SEC);
portrait (1080×1920) — video top 44%:
tl.to('#video-wrap',
{ left: 0, top: 0, width: 1080, height: 844,
duration: 0.6, ease: 'power2.inOut' }, START_SEC);
4:5 (1080×1350) — y/h × 0.703:
tl.to('#video-wrap',
{ left: 0, top: 0, width: 1080, height: 593,
duration: 0.6, ease: 'power2.inOut' }, START_SEC);
Notes
- Sleek vertical: gives the speaker a wider stage on top.
- Card below is a full-width statement — great for one big sentence
or one big number.
- The `lower-third` zone resolves to bottom 30% by default
(`resolveZoneBounds`); for a 48% card adjust the GSAP tween to also
move card-host height, or use `zone="whiteboard-area"` + custom CSS.
- 14px gap between video and card looks clean — set composition bg to
a deep color (#0a0c12) so the gap reads as a separator.
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>layout · stack (video top, card bottom)</title>
<style>
html,
body {
margin: 0;
padding: 0;
background: #0a0c12;
color: #fff;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stage {
position: relative;
width: 1920px;
height: 1080px;
transform-origin: top left;
transform: scale(0.5);
background: #0a0c12;
overflow: hidden;
}
.region {
position: absolute;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font:
600 24px/1.3 ui-monospace,
monospace;
letter-spacing: 0.12em;
}
.video-region {
left: 14px;
top: 14px;
width: 1892px;
height: 548px;
background: #1a1f2e;
background-image: repeating-linear-gradient(
45deg,
transparent 0 12px,
rgba(255, 255, 255, 0.05) 12px 13px
);
}
.card-region {
left: 14px;
top: 576px;
width: 1892px;
height: 504px;
background: #f1ead8;
color: #1a1d2b;
}
.label {
padding: 12px 18px;
border: 1.5px solid currentColor;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="stage">
<div class="region video-region"><span class="label">VIDEO · 1920×562 (top 52%)</span></div>
<div class="region card-region"><span class="label">CARD · 1920×518 (bottom 48%)</span></div>
</div>
</body>
</html>
<!--
Style: academic — warm paper · serif · blue highlight · coral annotation
─────────────────────────────────────────────────────────────────────
Tokens
bg #f1ead8 (warm paper)
ink #1a1d2b
muted #5b5749
accent #2557a7 (blue, used for kicker chip + text highlight)
accent2#c14d2c (coral, used for dashed quote box)
Fonts
headline & body — serif. Falls back to "ui-serif, 'Songti SC', serif"
mono — "ui-monospace, monospace"
Best with
layouts: split, stack, pip
frames: clean, hairline
content: 访谈反思 / 学术总结 / 长视频拆解
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>style · academic</title>
<style>
/* preview wrapper — NOT part of the card fragment */
html,
body {
margin: 0;
padding: 0;
background: #0a0c12;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stage {
position: relative;
width: 1920px;
height: 1080px;
transform-origin: top left;
transform: scale(0.5);
overflow: hidden;
}
.card-host {
position: absolute;
left: 0;
top: 0;
width: 1920px;
height: 1080px;
}
</style>
</head>
<body>
<div class="stage">
<div class="card-host">
<div class="card" data-card-id="ref-academic">
<style>
.card[data-card-id="ref-academic"] .root {
--bg: #f1ead8;
--ink: #1a1d2b;
--muted: #5b5749;
--accent: #2557a7;
--accent2: #c14d2c;
--font-head: ui-serif, "Songti SC", "Times New Roman", serif;
--font-body: ui-serif, "Songti SC", "Times New Roman", serif;
--font-mono: ui-monospace, "SF Mono", Menlo, monospace;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background-color: var(--bg);
background-image:
linear-gradient(rgba(91, 87, 73, 0.2) 1px, transparent 1px),
linear-gradient(90deg, rgba(91, 87, 73, 0.2) 1px, transparent 1px),
radial-gradient(120% 80% at 50% 0%, #fff8e9 0%, transparent 60%);
background-size:
28px 28px,
28px 28px,
100% 100%;
padding: 32px 40px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
gap: 16px;
color: var(--ink);
font-family: var(--font-body);
}
.card[data-card-id="ref-academic"] .meta {
display: flex;
align-items: baseline;
gap: 14px;
}
.card[data-card-id="ref-academic"] .kicker {
padding: 7px 14px;
background: var(--accent);
color: #fff;
font: 700 16px/1 var(--font-mono);
letter-spacing: 0.16em;
}
.card[data-card-id="ref-academic"] .time {
font: 15px/1 var(--font-mono);
color: var(--muted);
letter-spacing: 0.1em;
}
.card[data-card-id="ref-academic"] .title {
margin: 0;
font: 700 92px/1.05 var(--font-head);
color: var(--ink);
letter-spacing: 0.01em;
display: inline;
background: linear-gradient(
transparent 62%,
rgba(37, 87, 167, 0.2) 62%,
rgba(37, 87, 167, 0.2) 92%,
transparent 92%
);
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
padding: 0 8px;
}
.card[data-card-id="ref-academic"] .detail {
margin: 0;
font: 30px/1.5 var(--font-body);
color: var(--muted);
max-width: 94%;
}
.card[data-card-id="ref-academic"] .hl {
background: linear-gradient(transparent 60%, rgba(37, 87, 167, 0.25) 60%);
padding: 0 4px;
}
.card[data-card-id="ref-academic"] .quote {
align-self: flex-start;
padding: 10px 18px;
border: 1.5px dashed var(--accent2);
color: var(--accent2);
font: italic 600 24px/1 var(--font-head);
transform: rotate(-1deg);
}
</style>
<div class="root">
<div class="meta">
<span class="kicker" data-anim="fade-in" data-anim-at="0.05" data-anim-duration="0.4"
>TAKEAWAY</span
>
<span class="time">00:18 → 00:46</span>
</div>
<h2
class="title"
data-anim="kinetic-chars"
data-anim-at="0.25"
data-anim-duration="0.55"
data-anim-stagger="0.04"
data-anim-pattern="pop"
>
<span class="char">少</span><span class="char">即</span><span class="char">是</span>
<span class="char">多</span>
</h2>
<p class="detail" data-anim="fade-in" data-anim-at="0.7" data-anim-duration="0.45">
复杂的解释往往是<span class="hl">未经压缩的思考</span
>。当你能用最少的词说清最重要的点时, 你也就真正想明白了这件事。
</p>
<div class="quote" data-anim="scale-pop" data-anim-at="1.05" data-anim-duration="0.5">
"Compression is comprehension."
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<!--
Style: audit — manila paper · justified serif · APPROVED stamp · brick + navy
─────────────────────────────────────────────────────────────────────
Tokens
bg #ebd9b4 (manila)
ink #1d1a14
muted #6b5e44
accent #8b1d1d (brick red — stamp + heading underline)
accent2#1c3a6b (navy — footnote markers)
Fonts
headline — serif; ui-serif, "Songti SC", "Times New Roman", serif
body — serif justified
Best with
layouts: split, stack
frames: hairline, clean
content: 财报分析 / 调查报告 / 数据稽核 / 严肃陈述
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>style · audit</title>
<style>
html,
body {
margin: 0;
padding: 0;
background: #0a0c12;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stage {
position: relative;
width: 1920px;
height: 1080px;
transform-origin: top left;
transform: scale(0.5);
overflow: hidden;
}
.card-host {
position: absolute;
left: 0;
top: 0;
width: 1920px;
height: 1080px;
}
</style>
</head>
<body>
<div class="stage">
<div class="card-host">
<div class="card" data-card-id="ref-audit">
<style>
.card[data-card-id="ref-audit"] .root {
--bg: #ebd9b4;
--ink: #1d1a14;
--muted: #6b5e44;
--accent: #8b1d1d;
--accent2: #1c3a6b;
--font-head: ui-serif, "Songti SC", "Times New Roman", serif;
--font-body: ui-serif, "Songti SC", "Times New Roman", serif;
--font-mono: ui-monospace, "SF Mono", Menlo, monospace;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background-color: var(--bg);
background-image:
repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.02) 0 1px, transparent 1px 28px),
radial-gradient(120% 80% at 20% 0%, rgba(255, 250, 235, 0.45) 0%, transparent 60%);
padding: 48px 64px 56px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 18px;
color: var(--ink);
font-family: var(--font-body);
}
.card[data-card-id="ref-audit"] .head {
display: flex;
justify-content: space-between;
align-items: baseline;
border-bottom: 2px solid var(--ink);
padding-bottom: 10px;
}
.card[data-card-id="ref-audit"] .kicker {
font: 700 14px/1 var(--font-mono);
letter-spacing: 0.3em;
text-transform: uppercase;
color: var(--ink);
}
.card[data-card-id="ref-audit"] .ref {
font: 13px/1 var(--font-mono);
color: var(--muted);
letter-spacing: 0.14em;
}
.card[data-card-id="ref-audit"] .title {
margin: 0;
font: 700 76px/1.1 var(--font-head);
color: var(--ink);
letter-spacing: 0.005em;
}
.card[data-card-id="ref-audit"] .title .accent {
color: var(--accent);
}
.card[data-card-id="ref-audit"] .body {
margin: 0;
font: 26px/1.55 var(--font-body);
color: var(--ink);
text-align: justify;
column-count: 2;
column-gap: 32px;
max-width: 100%;
}
.card[data-card-id="ref-audit"] .body .sup {
color: var(--accent2);
font-weight: 700;
}
.card[data-card-id="ref-audit"] .foot {
border-top: 1px solid var(--muted);
padding-top: 10px;
font: italic 14px/1.4 var(--font-body);
color: var(--muted);
}
.card[data-card-id="ref-audit"] .stamp {
position: absolute;
right: 56px;
top: 120px;
padding: 12px 22px;
border: 4px solid var(--accent);
color: var(--accent);
font: 800 36px/1 var(--font-head);
letter-spacing: 0.14em;
transform: rotate(-12deg);
opacity: 0.9;
box-shadow: 0 0 0 1px var(--accent) inset;
}
</style>
<div class="root">
<div class="head">
<span class="kicker" data-anim="fade-in" data-anim-at="0.05" data-anim-duration="0.35"
>EXHIBIT · TAKEAWAY</span
>
<span class="ref">REF / VT-2026-014</span>
</div>
<span class="stamp" data-anim="scale-pop" data-anim-at="0.7" data-anim-duration="0.4"
>APPROVED</span
>
<h2 class="title" data-anim="fade-in" data-anim-at="0.25" data-anim-duration="0.5">
第三季度<span class="accent">现金流</span>由负转正
</h2>
<p class="body" data-anim="fade-in" data-anim-at="0.55" data-anim-duration="0.5">
经营性现金流首次为正<span class="sup">①</span>,主要由订阅业务驱动。 自由现金流由 -2.1
亿改善至 +0.8 亿,按当前轨迹推算<span class="sup">②</span>,全年有望实现正向 1.5–2.0
亿。 与此同时,应收账款周转天数从 64 天压缩至 51 天,回款效率创近两年新高。
</p>
<div class="foot" data-anim="fade-in" data-anim-at="0.95" data-anim-duration="0.4">
① 含一次性税务返还 0.3 亿;② 假设 Q4 续约率不低于 91%。
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<!--
Style: editorial — cream · coral block · big italic quote · Playfair-like serif
─────────────────────────────────────────────────────────────────────
Tokens
bg #f1e8d5 (cream)
ink #0e1018
muted #6b6353
accent #ff3a2d (coral red — used as block accents + italic quote color)
accent2 #1f6bff (blue — used for ring/circle outline)
accent3 #f2cf3a (yellow dot)
Fonts
headline — Playfair Display fallback: ui-serif, "Songti SC", serif
body — ui-serif, serif
Best with
layouts: overlay, pip, stack
frames: clean, polaroid
content: 产品发布 / 编辑专栏 / 大字宣言
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>style · editorial</title>
<style>
html,
body {
margin: 0;
padding: 0;
background: #0a0c12;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stage {
position: relative;
width: 1920px;
height: 1080px;
transform-origin: top left;
transform: scale(0.5);
overflow: hidden;
}
.card-host {
position: absolute;
left: 0;
top: 0;
width: 1920px;
height: 1080px;
}
</style>
</head>
<body>
<div class="stage">
<div class="card-host">
<div class="card" data-card-id="ref-editorial">
<style>
.card[data-card-id="ref-editorial"] .root {
--bg: #f1e8d5;
--ink: #0e1018;
--muted: #6b6353;
--accent: #ff3a2d;
--accent2: #1f6bff;
--accent3: #f2cf3a;
/* IMPORTANT — this skill only ships Caveat / LXGW WenKai TC / Inter / Virgil.
Playfair Display is NOT bundled; if you list it first the font resolver
can't load it and you'll get inconsistent rendering across machines.
The editorial look uses italic-bold serif at large size — ui-serif on
macOS resolves to Times/New York which is close enough; for a real
Playfair you'd need to add the woff2 to public/fonts/. */
--font-head: ui-serif, "Songti SC", "Times New Roman", serif;
--font-body: ui-serif, "Songti SC", serif;
--font-mono: ui-monospace, "SF Mono", Menlo, monospace;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background: var(--bg);
padding: 56px 64px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
gap: 24px;
color: var(--ink);
font-family: var(--font-body);
}
.card[data-card-id="ref-editorial"] .kicker {
align-self: flex-start;
padding: 8px 16px;
background: var(--ink);
color: var(--bg);
font: 700 16px/1 var(--font-mono);
letter-spacing: 0.22em;
}
.card[data-card-id="ref-editorial"] .title {
margin: 0;
font: italic 800 132px/0.95 var(--font-head);
color: var(--ink);
letter-spacing: -0.01em;
max-width: 92%;
}
.card[data-card-id="ref-editorial"] .first {
font-style: italic;
color: var(--accent);
}
.card[data-card-id="ref-editorial"] .quote-mark {
font: 800 220px/0.5 var(--font-head);
color: var(--accent);
position: absolute;
top: 24px;
right: 64px;
opacity: 0.9;
}
.card[data-card-id="ref-editorial"] .detail {
margin: 0;
font: 28px/1.5 var(--font-body);
color: var(--muted);
max-width: 78%;
}
.card[data-card-id="ref-editorial"] .hl {
background: linear-gradient(transparent 62%, rgba(255, 58, 45, 0.22) 62%);
color: var(--ink);
padding: 0 4px;
}
.card[data-card-id="ref-editorial"] .accents {
position: absolute;
}
.card[data-card-id="ref-editorial"] .block-coral {
top: 64px;
right: 88px;
width: 96px;
height: 96px;
background: var(--accent);
transform: rotate(12deg);
}
.card[data-card-id="ref-editorial"] .dot-yellow {
top: 200px;
right: 60px;
width: 36px;
height: 36px;
background: var(--accent3);
border-radius: 50%;
}
.card[data-card-id="ref-editorial"] .ring-blue {
bottom: 80px;
right: 80px;
width: 80px;
height: 80px;
border: 4px solid var(--accent2);
border-radius: 50%;
}
</style>
<div class="root">
<span
class="accents block-coral"
data-anim="scale-pop"
data-anim-at="0.15"
data-anim-duration="0.5"
></span>
<span
class="accents dot-yellow"
data-anim="fade-in"
data-anim-at="0.35"
data-anim-duration="0.4"
></span>
<span
class="accents ring-blue"
data-anim="fade-in"
data-anim-at="0.55"
data-anim-duration="0.4"
></span>
<span class="kicker" data-anim="fade-in" data-anim-at="0.05" data-anim-duration="0.4"
>TAKEAWAY</span
>
<h1 class="title" data-anim="fade-in" data-anim-at="0.3" data-anim-duration="0.6">
<span class="first">真</span>正的创造,从拒绝套路开始。
</h1>
<p class="detail" data-anim="fade-in" data-anim-at="0.8" data-anim-duration="0.45">
停止模仿、停止套模板,把每一个想法都当作<span class="hl">第一次表达</span>。
只有这样,作品才会带上你的体温。
</p>
</div>
</div>
</div>
</div>
</body>
</html>
<!--
Style: minimal — pure black on white · huge type · generous space
─────────────────────────────────────────────────────────────────────
Tokens
bg #ffffff
ink #000000
muted #6b7280
accent #000000 (no color; weight & size do the work)
Fonts
headline & body — Inter, ui-sans-serif, system-ui, sans-serif
Best with
layouts: split, stack, overlay
frames: clean, hairline
content: 极简陈述 / 数据揭示 / 大字标题
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>style · minimal</title>
<style>
html,
body {
margin: 0;
padding: 0;
background: #0a0c12;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stage {
position: relative;
width: 1920px;
height: 1080px;
transform-origin: top left;
transform: scale(0.5);
overflow: hidden;
}
.card-host {
position: absolute;
left: 0;
top: 0;
width: 1920px;
height: 1080px;
}
</style>
</head>
<body>
<div class="stage">
<div class="card-host">
<div class="card" data-card-id="ref-minimal">
<style>
.card[data-card-id="ref-minimal"] .root {
--bg: #ffffff;
--ink: #000000;
--muted: #6b7280;
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background: var(--bg);
color: var(--ink);
padding: 80px 96px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
gap: 56px;
font-family: var(--font-sans);
}
.card[data-card-id="ref-minimal"] .kicker {
font: 600 14px/1 var(--font-sans);
letter-spacing: 0.32em;
color: var(--ink);
text-transform: uppercase;
}
.card[data-card-id="ref-minimal"] .title {
margin: 0;
font: 900 132px/0.95 var(--font-sans);
color: var(--ink);
letter-spacing: -0.04em;
}
.card[data-card-id="ref-minimal"] .detail {
margin: 0;
font: 400 36px/1.4 var(--font-sans);
color: var(--muted);
max-width: 82%;
letter-spacing: -0.005em;
}
.card[data-card-id="ref-minimal"] .rule {
width: 0;
height: 4px;
background: var(--ink);
}
</style>
<div class="root">
<span class="kicker" data-anim="fade-in" data-anim-at="0.05" data-anim-duration="0.4"
>TAKEAWAY</span
>
<h1
class="title"
data-anim="kinetic-chars"
data-anim-at="0.3"
data-anim-duration="0.6"
data-anim-stagger="0.05"
data-anim-pattern="pop"
>
<span class="char">五</span><span class="char">秒</span><span class="char">决</span
><span class="char">定</span> <span class="char">一</span><span class="char">切</span>
</h1>
<div
class="rule"
data-anim="grow-x"
data-anim-at="0.9"
data-anim-duration="0.5"
data-anim-target-w="240"
></div>
<p class="detail" data-anim="fade-in" data-anim-at="1.05" data-anim-duration="0.45">
用户给一个新界面的时间不超过五秒。这意味着第一屏不需要解释,需要让人立刻知道这是什么、为什么有用。
</p>
</div>
</div>
</div>
</div>
</body>
</html>
Related skills
How it compares
Pick talking-head-recut for designed overlay packaging of existing footage; use music-to-video when audio beats drive frame creation from scratch.
FAQ
Does talking-head-recut replace or trim the source video?
talking-head-recut plays the source talking-head, interview, or podcast video in full. The skill only adds timed graphic overlay cards on top, then renders the combined result to MP4.
When should I use talking-head-recut instead of embedded-captions?
talking-head-recut adds designed graphic cards - titles, lower-thirds, callouts, and PiP - not plain subtitles.
Is Talking Head Recut safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.