hyperframes 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,10 +5,79 @@ description: Create video compositions, animations, title cards, overlays, capti
5
5
 
6
6
  # HyperFrames
7
7
 
8
- HTML is the source of truth for video. A composition is an HTML file with `data-*` attributes for timing, a GSAP timeline for animation, and CSS for appearance.
8
+ HTML is the source of truth for video. A composition is an HTML file with `data-*` attributes for timing, a GSAP timeline for animation, and CSS for appearance. The framework handles clip visibility, media playback, and timeline sync.
9
+
10
+ ## Approach
11
+
12
+ Before writing HTML, think at a high level:
13
+
14
+ 1. **What** — what should the viewer experience? Identify the narrative arc, key moments, and emotional beats.
15
+ 2. **Structure** — how many compositions, which are sub-compositions vs inline, what tracks carry what (video, audio, overlays, captions).
16
+ 3. **Timing** — which clips drive the duration, where do transitions land, what's the pacing.
17
+ 4. **Layout** — build the end-state first. See "Layout Before Animation" below.
18
+ 5. **Animate** — then add motion using the rules below.
19
+
20
+ For small edits (fix a color, adjust timing, add one element), skip straight to the rules.
9
21
 
10
22
  When no `visual-style.md` or animation direction is provided, follow [house-style.md](./house-style.md) for motion defaults, sizing, and color palettes.
11
23
 
24
+ ## Layout Before Animation
25
+
26
+ Position every element where it should be at its **most visible moment** — the frame where it's fully entered, correctly placed, and not yet exiting. Write this as static HTML+CSS first. No GSAP yet.
27
+
28
+ **Why this matters:** If you position elements at their animated start state (offscreen, scaled to 0, opacity 0) and tween them to where you think they should land, you're guessing the final layout. Overlaps are invisible until the video renders. By building the end state first, you can see and fix layout problems before adding any motion.
29
+
30
+ ### The process
31
+
32
+ 1. **Identify the hero frame** for each scene — the moment when the most elements are simultaneously visible. This is the layout you build.
33
+ 2. **Write static CSS** for that frame. Every element at its final `top`, `left`, `width`, `height`. Use the browser or `npx hyperframes preview` to visually verify nothing overlaps unintentionally.
34
+ 3. **Add entrances with `gsap.from()`** — animate FROM offscreen/invisible TO the CSS position. The CSS position is the ground truth; the tween describes the journey to get there.
35
+ 4. **Add exits with `gsap.to()`** — animate TO offscreen/invisible FROM the CSS position.
36
+
37
+ ### Example
38
+
39
+ ```css
40
+ /* Step 1-2: Layout the end state. This is what the viewer sees at peak visibility. */
41
+ .title {
42
+ position: absolute;
43
+ top: 200px;
44
+ left: 160px;
45
+ opacity: 1;
46
+ }
47
+ .subtitle {
48
+ position: absolute;
49
+ top: 320px;
50
+ left: 160px;
51
+ opacity: 1;
52
+ }
53
+ .logo {
54
+ position: absolute;
55
+ bottom: 80px;
56
+ right: 80px;
57
+ opacity: 1;
58
+ }
59
+ ```
60
+
61
+ ```js
62
+ // Step 3: Animate INTO those positions
63
+ tl.from(".title", { y: 60, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
64
+ tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power3.out" }, 0.2);
65
+ tl.from(".logo", { scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.out" }, 0.3);
66
+
67
+ // Step 4: Animate OUT from those positions
68
+ tl.to(".title", { y: -40, opacity: 0, duration: 0.4, ease: "power2.in" }, 3);
69
+ tl.to(".subtitle", { y: -30, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.1);
70
+ tl.to(".logo", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.2);
71
+ ```
72
+
73
+ ### When elements share space across time
74
+
75
+ If element A exits before element B enters in the same area, both should have correct CSS positions for their respective hero frames. The timeline ordering guarantees they never visually coexist — but if you skip the layout step, you won't catch the case where they accidentally overlap due to a timing error.
76
+
77
+ ### What counts as intentional overlap
78
+
79
+ Layered effects (glow behind text, shadow elements, background patterns) and z-stacked designs (card stacks, depth layers) are intentional. The layout step is about catching **unintentional** overlap — two headlines landing on top of each other, a stat covering a label, content bleeding off-frame.
80
+
12
81
  ## Data Attributes
13
82
 
14
83
  ### All Clips
@@ -89,6 +158,7 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
89
158
  - Register every timeline: `window.__timelines["<composition-id>"] = tl`
90
159
  - Framework auto-nests sub-timelines — do NOT manually add them
91
160
  - Duration comes from `data-duration`, not from GSAP timeline length
161
+ - Never create empty tweens to set duration
92
162
 
93
163
  ## Rules (Non-Negotiable)
94
164
 
@@ -100,7 +170,7 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
100
170
 
101
171
  **No `repeat: -1`:** Infinite-repeat timelines break the capture engine. Calculate the exact repeat count from composition duration: `repeat: Math.ceil(duration / cycleDuration) - 1`.
102
172
 
103
- **Synchronous timeline construction:** Never build timelines inside `async`/`await`, `setTimeout`, or Promises. The capture engine reads `window.__timelines` synchronously after page load. If you need fonts loaded first, use a synchronous `document.fonts.load()` call or rely on `font-display: block` — the engine waits for the page `load` event.
173
+ **Synchronous timeline construction:** Never build timelines inside `async`/`await`, `setTimeout`, or Promises. The capture engine reads `window.__timelines` synchronously after page load. Fonts are embedded by the compiler, so they're available immediately no need to wait for font loading.
104
174
 
105
175
  **Never do:**
106
176
 
@@ -116,8 +186,7 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
116
186
 
117
187
  ## Typography and Assets
118
188
 
119
- - Load fonts via `<link>` tags with `display=block` in `<head>`, NOT via CSS `@import` `@import` is async and may not complete before the first frame capture
120
- - Use `font-display: block` for `@font-face` declarations
189
+ - **Fonts:** Just write the `font-family` you want in CSS the compiler embeds supported fonts automatically via `@font-face` with inline data URIs. No `<link>` tags or `@import` needed. If a font isn't in the supported set, the compiler warns and you should add it to `deterministicFonts.ts`.
121
190
  - Add `crossorigin="anonymous"` to external media
122
191
  - **Minimum font sizes for rendered video (1080p at DPR 1):**
123
192
  - Body/label text: 20px minimum (landscape), 18px minimum (portrait)
@@ -148,7 +217,7 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
148
217
  - [ ] No `repeat: -1` on any tween or nested timeline
149
218
  - [ ] No text below 16px (data labels, footnotes) or 20px (body text)
150
219
  - [ ] No full-screen linear dark gradients (use radial or solid + localized glow)
151
- - [ ] Fonts loaded via `<link>` with `display=block`, not CSS `@import`
220
+ - [ ] Font families declared in CSS (compiler embeds them automatically)
152
221
  - [ ] 100% deterministic
153
222
  - [ ] Each composition includes GSAP script tag
154
223
  - [ ] `npx hyperframes lint` and `npx hyperframes validate` both pass
@@ -161,6 +230,8 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
161
230
  - **[references/tts.md](references/tts.md)** — Text-to-speech with Kokoro-82M. Voice selection, speed tuning, TTS+captions workflow. Read when generating narration or voiceover.
162
231
  - **[references/audio-reactive.md](references/audio-reactive.md)** — Audio-reactive animation: map frequency bands and amplitude to GSAP properties. Read when visuals should respond to music, voice, or sound.
163
232
  - **[references/marker-highlight.md](references/marker-highlight.md)** — Animated text highlighting via canvas overlays: marker pen, circle, burst, scribble, sketchout. Read when adding visual emphasis to text.
233
+ - **[references/fonts.md](references/fonts.md)** — Typography: typographic tension and contrast principles, font pairing theory, case studies from SSENSE/Acne/Stripe/Fly.io/Collins, failure modes, runtime font discovery. Read when picking and pairing typefaces.
234
+ - **[references/motion-principles.md](references/motion-principles.md)** — Motion design principles: easing as emotion, timing as weight, choreography as hierarchy, scene pacing, ambient motion, anti-patterns. Read when choreographing GSAP animations.
164
235
  - **[house-style.md](house-style.md)** — Default motion, sizing, and color palettes when no style is specified.
165
236
  - **[patterns.md](patterns.md)** — PiP, title cards, slide show patterns.
166
237
  - **[data-in-motion.md](data-in-motion.md)** — Data, stats, and infographic patterns.
@@ -6,7 +6,7 @@ Defaults when no `visual-style.md` or animation direction is provided. These rai
6
6
 
7
7
  1. **Interpret the prompt.** Generate real content for the topic — don't use the prompt text as body copy. A recipe lists real ingredients. A stats dashboard shows the actual numbers given. A product showcase names real features and specs. A sci-fi HUD has actual crosshairs and readouts, not a heading that says "sci-fi HUD."
8
8
  2. **Pick a palette.** First decide: does this content call for a light or dark canvas? Then load the file most appropriate for the theme and pick one palette at random from the file. Declare your bg, fg, and accent colors before writing any code.
9
- 3. **Pick a typeface.** Don't reach for Sora, Space Grotesk, Outfit, Playfair Display, Cormorant Garamond, or Bodoni Moda — they're overused. Explore the full range of Google Fonts. Serif for editorial, mono for technical, display for impact, handwritten for personal.
9
+ 3. **Pick a typeface.** Don't reach for Sora, Space Grotesk, Outfit, Playfair Display, Cormorant Garamond, or Bodoni Moda — they're overused. Read [references/fonts.md](references/fonts.md) and pick a font that matches the content mood. Serif for editorial, mono for technical, display for impact, handwritten for personal. Just write the `font-family` in CSS — the compiler embeds supported fonts automatically.
10
10
  4. **Pick a layout approach.** Don't default to the same structure every time.
11
11
  5. **Pick your entrance patterns.** Plan how elements enter — never use the same entrance pattern twice in a composition.
12
12
 
@@ -129,4 +129,4 @@ tl.seek(0);
129
129
  - Sync to transcript timestamps.
130
130
  - One group visible at a time.
131
131
  - Every group must have a hard `tl.set` kill at `group.end`.
132
- - Check project root for font files before defaulting to Google Fonts.
132
+ - The compiler embeds supported fonts automatically just declare `font-family` in CSS.
@@ -49,7 +49,7 @@
49
49
  style="
50
50
  position: absolute; inset: 0;
51
51
  display: flex; align-items: center; justify-content: center;
52
- font-family: 'Inter', sans-serif; font-size: 72px; color: #fff;
52
+ font-family: 'Bricolage Grotesque', sans-serif; font-size: 72px; color: #fff;
53
53
  background: #111;
54
54
  "
55
55
  >
@@ -0,0 +1,134 @@
1
+ # Typography
2
+
3
+ The compiler embeds supported fonts — just write `font-family` in CSS.
4
+
5
+ ## Banned
6
+
7
+ Inter, Roboto, Open Sans, Noto Sans, Arimo, Lato, Source Sans, PT Sans, Nunito, Poppins, Outfit, Sora, Playfair Display, Cormorant Garamond, Bodoni Moda, EB Garamond, Cinzel, Prata
8
+
9
+ ## Guardrails
10
+
11
+ You know these rules but you violate them. Stop.
12
+
13
+ - **Don't pair two sans-serifs.** You do this constantly — one for headlines, one for body. Cross the boundary: serif + sans, or sans + mono.
14
+ - **One expressive font per scene.** You pick two interesting fonts trying to make it "better." One performs, one recedes.
15
+ - **Weight contrast must be extreme.** You default to 400 vs 700. Video needs 300 vs 900. The difference must be visible in motion at a glance.
16
+ - **Video sizes, not web sizes.** Body: 20px minimum. Headlines: 60px+. Data labels: 16px. You will try to use 14px. Don't.
17
+
18
+ ## What You Don't Do Without Being Told
19
+
20
+ - **Tension should mean something.** Don't pattern-match pairings. Ask WHY these two fonts disagree. The pairing should embody the content's contradiction — mechanical vs human, public vs private, institutional vs personal. If you can't articulate the tension, it's arbitrary.
21
+ - **Register switching.** Assign different fonts to different communicative modes — one voice for statements, another for data, another for attribution. Not hierarchy on a page. Voices in a conversation.
22
+ - **Tension can live inside a single font.** A font that looks familiar but is secretly strange creates tension with the viewer's expectations, not with another font.
23
+ - **One variable changed = dramatic contrast.** Same letterforms, monospaced vs proportional. Same family at different optical sizes. Changing only rhythm while everything else stays constant.
24
+ - **Double personality works.** Two expressive fonts can coexist if they share an attitude (both irreverent, both precise) even when their forms are completely different.
25
+ - **Time is hierarchy.** The first element to appear is the most important. In video, sequence replaces position.
26
+ - **Motion is typography.** How a word enters carries as much meaning as the font. A 0.1s slam vs a 2s fade — same font, completely different message.
27
+ - **Fixed reading time.** 3 seconds on screen = must be readable in 2. Fewer words, larger type.
28
+ - **Tracking tighter than web.** -0.03em to -0.05em on display sizes. Video encoding compresses letter detail.
29
+
30
+ ## Finding Fonts
31
+
32
+ Don't default to what you know. If the content is luxury, a grotesque sans might create more tension than the expected Didone serif. Decide the register first, then search.
33
+
34
+ Save this script to `/tmp/fontquery.py` and run with `curl -s 'https://fonts.google.com/metadata/fonts' > /tmp/gfonts.json && python3 /tmp/fontquery.py /tmp/gfonts.json`:
35
+
36
+ ```python
37
+ import json, sys
38
+ from collections import OrderedDict
39
+
40
+ with open(sys.argv[1]) as f:
41
+ data = json.load(f)
42
+ fonts = data.get("familyMetadataList", [])
43
+
44
+ ban = {"Inter","Roboto","Open Sans","Noto Sans","Lato","Poppins","Source Sans 3",
45
+ "PT Sans","Nunito","Outfit","Sora","Playfair Display","Cormorant Garamond",
46
+ "Bodoni Moda","EB Garamond","Cinzel","Prata","Arimo","Source Sans Pro"}
47
+ skip_pfx = ("Roboto","Noto ","Google Sans","Bpmf","Playwrite","Anek","BIZ ",
48
+ "Nanum","Shippori","Sawarabi","Zen ","Kaisei","Kiwi ","Yuji ","Radio ")
49
+
50
+ def ok(f):
51
+ if f["family"] in ban: return False
52
+ if any(f["family"].startswith(b) for b in skip_pfx): return False
53
+ if "latin" not in (f.get("subsets") or []): return False
54
+ return True
55
+
56
+ seen = set()
57
+ R = OrderedDict()
58
+
59
+ # Trending Sans — recent (2022+), popular (<300)
60
+ R["Trending Sans"] = []
61
+ for f in fonts:
62
+ if not ok(f) or f["family"] in seen: continue
63
+ if f.get("category") in ("Sans Serif","Display") and f.get("dateAdded","") >= "2022-01-01" and f.get("popularity",9999) < 300:
64
+ R["Trending Sans"].append(f); seen.add(f["family"])
65
+
66
+ # Trending Serif — recent (2018+), popular (<600)
67
+ R["Trending Serif"] = []
68
+ for f in fonts:
69
+ if not ok(f) or f["family"] in seen: continue
70
+ if f.get("category") == "Serif" and f.get("dateAdded","") >= "2018-01-01" and f.get("popularity",9999) < 600:
71
+ R["Trending Serif"].append(f); seen.add(f["family"])
72
+
73
+ # Monospace — recent (2018+), popular (<600)
74
+ R["Monospace"] = []
75
+ for f in fonts:
76
+ if not ok(f) or f["family"] in seen: continue
77
+ if f.get("category") == "Monospace" and f.get("dateAdded","") >= "2018-01-01" and f.get("popularity",9999) < 600:
78
+ R["Monospace"].append(f); seen.add(f["family"])
79
+
80
+ # Impact & Condensed — curated names + heavy display fonts
81
+ R["Impact & Condensed"] = []
82
+ impact = {"Bebas Neue","Archivo Black","Big Shoulders Display","Teko","League Gothic",
83
+ "Barlow Condensed","Staatliches","Anton","Oswald","Saira","Syne",
84
+ "Titillium Web","Alumni Sans","Advent Pro"}
85
+ for f in fonts:
86
+ if not ok(f) or f["family"] in seen: continue
87
+ is_impact = f["family"] in impact
88
+ is_heavy_display = ("Display" in (f.get("classifications") or [])
89
+ and any(k in list(f.get("fonts",{}).keys()) for k in ("800","900"))
90
+ and f.get("popularity",9999) < 400
91
+ and f.get("category") in ("Sans Serif","Display"))
92
+ if is_impact or is_heavy_display:
93
+ R["Impact & Condensed"].append(f); seen.add(f["family"])
94
+
95
+ # Bold Geometric Display — curated
96
+ R["Bold Geometric Display"] = []
97
+ for f in fonts:
98
+ if not ok(f) or f["family"] in seen: continue
99
+ if f["family"] in {"DM Serif Display","Abril Fatface","Righteous","Orbitron","Black Ops One"}:
100
+ R["Bold Geometric Display"].append(f); seen.add(f["family"])
101
+
102
+ # Script & Handwriting — popular (<300)
103
+ R["Script & Handwriting"] = []
104
+ for f in fonts:
105
+ if not ok(f) or f["family"] in seen: continue
106
+ if f.get("category") == "Handwriting" and f.get("popularity",9999) < 300:
107
+ R["Script & Handwriting"].append(f); seen.add(f["family"])
108
+
109
+ # Established Classics — good older fonts
110
+ R["Established Classics"] = []
111
+ classics = {"Josefin Sans","Raleway","Montserrat","Abel","Exo","Red Hat Display",
112
+ "Rubik","Alegreya","Arvo","Besley","Crimson Text","Fraunces",
113
+ "Lora","Merriweather","Vollkorn"}
114
+ for f in fonts:
115
+ if f["family"] in classics and f["family"] not in seen:
116
+ R["Established Classics"].append(f); seen.add(f["family"])
117
+
118
+ # Print
119
+ for cat in R:
120
+ R[cat].sort(key=lambda x: x.get("popularity",9999))
121
+ limits = {"Trending Sans":15,"Trending Serif":12,"Monospace":8,
122
+ "Impact & Condensed":12,"Bold Geometric Display":8,
123
+ "Script & Handwriting":10,"Established Classics":20}
124
+ for cat in R:
125
+ items = R[cat][:limits.get(cat,10)]
126
+ if not items: continue
127
+ print(f"--- {cat} ({len(items)}) ---")
128
+ for ff in items:
129
+ var = "VAR" if ff.get("axes") else " "
130
+ print(f' {ff.get("popularity"):4d} | {var} | {ff["family"]}')
131
+ print()
132
+ ```
133
+
134
+ Seven categories: trending sans, trending serif, monospace, impact/condensed, bold geometric, script/handwriting, and established classics. Cross classification boundaries when pairing.
@@ -0,0 +1,69 @@
1
+ # Motion Principles
2
+
3
+ ## Guardrails
4
+
5
+ You know these rules but you violate them. Stop.
6
+
7
+ - **Don't use the same ease on every tween.** You default to `power2.out` on everything. Vary eases like you vary font weights — no more than 2 independent tweens with the same ease in a scene.
8
+ - **Don't use the same speed on everything.** You default to 0.4-0.5s for everything. The slowest scene should be 3× slower than the fastest. Vary duration deliberately.
9
+ - **Don't enter everything from the same direction.** You default to `y: 30, opacity: 0` on every element. Vary: from left, from right, from scale, opacity-only, letter-spacing.
10
+ - **Don't use the same stagger on every scene.** Each scene needs its own rhythm.
11
+ - **Don't use ambient zoom on every scene.** Pick different ambient motion per scene: slow pan, subtle rotation, scale push, color shift, or nothing. Stillness after motion is powerful.
12
+ - **Don't start at t=0.** Offset the first animation 0.1-0.3s. Zero-delay feels like a jump cut.
13
+
14
+ ## What You Don't Do Without Being Told
15
+
16
+ ### Easing is emotion, not technique
17
+
18
+ The transition is the verb. The easing is the adverb. A slide-in with `expo.out` = confident. With `sine.inOut` = dreamy. With `elastic.out` = playful. Same motion, different meaning. Choose the adverb deliberately.
19
+
20
+ **Direction rules — these are not optional:**
21
+
22
+ - `.out` for elements entering. Starts fast, decelerates. Feels responsive. This is your default.
23
+ - `.in` for elements leaving. Starts slow, accelerates away. Throws them off.
24
+ - `.inOut` for elements moving between positions.
25
+
26
+ You get this backwards constantly. Ease-in for entrances feels sluggish. Ease-out for exits feels reluctant.
27
+
28
+ ### Speed communicates weight
29
+
30
+ - Fast (0.15-0.3s) — energy, urgency, confidence
31
+ - Medium (0.3-0.5s) — professional, most content
32
+ - Slow (0.5-0.8s) — gravity, luxury, contemplation
33
+ - Very slow (0.8-2.0s) — cinematic, emotional, atmospheric
34
+
35
+ ### Scene structure: build / breathe / resolve
36
+
37
+ Every scene has three phases. You dump everything in the build and leave nothing for breathe or resolve.
38
+
39
+ - **Build (0-30%)** — elements enter, staggered. Don't dump everything at once.
40
+ - **Breathe (30-70%)** — content visible, alive with ONE ambient motion.
41
+ - **Resolve (70-100%)** — exit or decisive end. Exits are faster than entrances.
42
+
43
+ ### Transitions are meaning
44
+
45
+ - **Crossfade** = "this continues"
46
+ - **Hard cut** = "wake up" / disruption
47
+ - **Slow dissolve** = "drift with me"
48
+
49
+ You crossfade everything. Use hard cuts for disruption and register shifts.
50
+
51
+ ### Choreography is hierarchy
52
+
53
+ The element that moves first is perceived as most important. Stagger in order of importance, not DOM order. Don't wait for completion — overlap entries. Total stagger sequence under 500ms regardless of item count.
54
+
55
+ ### Asymmetry
56
+
57
+ Entrances need longer than exits. A card takes 0.4s to appear but 0.25s to disappear.
58
+
59
+ ## Visual Composition
60
+
61
+ You build for the web. Video frames are not pages.
62
+
63
+ - **Two focal points minimum per scene.** The eye needs somewhere to travel. Never a single text block floating in empty space.
64
+ - **Fill the frame.** Hero text: 60-80% of width. You will try to use web-sized elements. Don't.
65
+ - **Three layers minimum per scene.** Background treatment (glow, oversized faded type, color panel). Foreground content. Accent elements (dividers, labels, data bars).
66
+ - **Background is not empty.** Radial glows, oversized faded type bleeding off-frame, subtle border panels, hairline rules. Pure solid #000 reads as "nothing loaded."
67
+ - **Anchor to edges.** Pin content to left/top or right/bottom. Centered-and-floating is a web pattern.
68
+ - **Split frames.** Data panel on the left, content on the right. Top bar with metadata, full-width below. Zone-based layouts, not centered stacks.
69
+ - **Use structural elements.** Rules, dividers, border panels. They create paths for the eye and animate well (scaleX from 0).
@@ -8,7 +8,7 @@ These cause real bugs if violated.
8
8
 
9
9
  **Scene visibility:** Scene 1 visible by default (no `opacity: 0`). Scenes 2+ have `opacity: 0` on the CONTAINER div. GSAP reveals them. No visibility shim (`timedEls`).
10
10
 
11
- **Iframe compatibility:** No external font links (`<link>` to Google Fonts, `@import`). They block sandboxed iframes. Use system fonts.
11
+ **Fonts:** Just write the `font-family` you want — the compiler embeds supported fonts automatically via `@font-face` with inline data URIs. No need for `<link>` tags or `@import`. Works in all contexts including sandboxed iframes.
12
12
 
13
13
  **Element structure:** No `class="clip"` on scene divs in standalone compositions. Only the root div gets `data-composition-id`/`data-start`/`data-duration`.
14
14
 
@@ -57,7 +57,7 @@ Read [shader-setup.md](./shader-setup.md) for the full setup code these rules ap
57
57
  height: 1080px;
58
58
  overflow: hidden;
59
59
  background: #000;
60
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
60
+ font-family: "YOUR FONT", sans-serif; /* compiler embeds supported fonts automatically */
61
61
  }
62
62
  .scene {
63
63
  position: absolute;