@opendirectory.dev/skills 0.1.60 → 0.1.62

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.
@@ -0,0 +1,504 @@
1
+ # Scene Library — vid-motion-graphics
2
+
3
+ 6 scene types with complete HTML/CSS templates. Read this before generating any HTML in Step 3.
4
+
5
+ Each template shows the HTML structure and required CSS classes. Apply colors and fonts from the chosen style preset (`references/style-presets.md`).
6
+
7
+ ---
8
+
9
+ ## Timing Architecture (read this first)
10
+
11
+ All scene timing is driven by `window.renderFrame(t)` — a pure JS function called by Playwright once per frame. It computes `opacity`/`transform` directly from the millisecond timestamp. No CSS `@keyframes`, no `animation-delay`, no Web Animations API seeking.
12
+
13
+ **Why renderFrame instead of CSS animations:**
14
+ CSS `@keyframes` `currentTime` seeking is silently ignored for backward seeks in Chromium. A 12s animation finishes during `waitUntil: 'networkidle'` (Google Fonts CDN), so all frames capture the final state. The renderFrame approach is deterministic: same `t` → same output, always.
15
+
16
+ **Scene CSS base (apply to ALL scenes):**
17
+ ```css
18
+ .scene {
19
+ position: absolute;
20
+ inset: 0;
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ justify-content: center;
25
+ padding: 80px;
26
+ opacity: 0; /* start hidden — renderFrame sets opacity per frame */
27
+ will-change: opacity, transform;
28
+ }
29
+ ```
30
+
31
+ **renderFrame helpers (include verbatim in every HTML):**
32
+ ```javascript
33
+ window.__videoReady = false;
34
+ window.TOTAL_DURATION_MS = N * 1000; // total duration in ms
35
+
36
+ function lerp(a, b, p) { return a + (b - a) * p; }
37
+ function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
38
+ function easeOutCubic(t) { return 1 - Math.pow(1 - clamp(t, 0, 1), 3); }
39
+
40
+ // Returns {opacity, ty} for scene window [startMs, endMs)
41
+ function sceneState(t, startMs, endMs) {
42
+ if (t < startMs || t >= endMs) return { opacity: 0, ty: 0 };
43
+ const prog = (t - startMs) / (endMs - startMs);
44
+ if (prog < 0.10) {
45
+ const p = easeOutCubic(prog / 0.10);
46
+ return { opacity: p, ty: lerp(24, 0, p) };
47
+ }
48
+ if (prog < 0.85) return { opacity: 1, ty: 0 };
49
+ const p = (prog - 0.85) / 0.15;
50
+ return { opacity: 1 - p, ty: lerp(0, -12, p) };
51
+ }
52
+
53
+ function applySceneState(el, state) {
54
+ el.style.opacity = state.opacity;
55
+ el.style.transform = state.ty !== 0 ? `translateY(${state.ty.toFixed(2)}px)` : '';
56
+ }
57
+
58
+ window.renderFrame = function(t) {
59
+ // Scene 1: 0ms → scene1DurMs
60
+ applySceneState(document.querySelector('.scene-1'), sceneState(t, 0, scene1EndMs));
61
+ // Scene 2: scene1EndMs → scene2EndMs
62
+ applySceneState(document.querySelector('.scene-2'), sceneState(t, scene1EndMs, scene2EndMs));
63
+ // ... repeat per scene
64
+ };
65
+
66
+ // Preview rAF loop — stopped by Playwright before frame capture
67
+ let __previewActive = false;
68
+ let __previewRafId = null;
69
+ window.__stopPreview = function() {
70
+ __previewActive = false;
71
+ if (__previewRafId !== null) { cancelAnimationFrame(__previewRafId); __previewRafId = null; }
72
+ };
73
+
74
+ document.fonts.ready.then(() => {
75
+ window.renderFrame(0);
76
+ window.__videoReady = true;
77
+ __previewActive = true;
78
+ const startTime = performance.now();
79
+ function previewTick() {
80
+ if (!__previewActive) return;
81
+ const elapsed = performance.now() - startTime;
82
+ if (elapsed < window.TOTAL_DURATION_MS) {
83
+ window.renderFrame(elapsed);
84
+ __previewRafId = requestAnimationFrame(previewTick);
85
+ } else {
86
+ window.renderFrame(window.TOTAL_DURATION_MS - 1);
87
+ __previewActive = false;
88
+ }
89
+ }
90
+ __previewRafId = requestAnimationFrame(previewTick);
91
+ });
92
+ ```
93
+
94
+ **Timing math for N scenes of D seconds each:**
95
+ ```
96
+ scene 1: startMs = 0, endMs = D*1000
97
+ scene 2: startMs = D*1000, endMs = D*2000
98
+ scene N: startMs = (N-1)*D*1000, endMs = N*D*1000
99
+ ```
100
+
101
+ Within each scene's `[startMs, endMs)` window:
102
+ - Enter: first 10% → opacity 0→1, translateY 24px→0 (easeOutCubic)
103
+ - Hold: 10%–85% → opacity 1, no transform
104
+ - Exit: 85%–100% → opacity 1→0, translateY 0→-12px
105
+
106
+ ---
107
+
108
+ ## 1. title-card
109
+
110
+ Large headline with optional eyebrow label and subtext. Best for scene 1 (hook/intro) or final scene.
111
+
112
+ **HTML:**
113
+ ```html
114
+ <div class="scene scene-N">
115
+ <div class="scene-inner title-card">
116
+ <div class="eyebrow">Q4 2024</div>
117
+ <h1 class="headline">Revenue Grew 85%</h1>
118
+ <p class="subtext">Fastest quarter in company history</p>
119
+ </div>
120
+ </div>
121
+ ```
122
+
123
+ **CSS:**
124
+ ```css
125
+ .title-card {
126
+ text-align: center;
127
+ max-width: 900px;
128
+ }
129
+ .title-card .eyebrow {
130
+ font-family: var(--font-body);
131
+ font-size: 14px;
132
+ font-weight: 500;
133
+ letter-spacing: 0.12em;
134
+ text-transform: uppercase;
135
+ color: var(--accent);
136
+ margin-bottom: 20px;
137
+ }
138
+ .title-card .headline {
139
+ font-family: var(--font-display);
140
+ font-size: clamp(72px, 10vw, 120px);
141
+ font-weight: 700;
142
+ color: var(--text);
143
+ line-height: 1.0;
144
+ letter-spacing: -0.03em;
145
+ }
146
+ .title-card .subtext {
147
+ font-family: var(--font-body);
148
+ font-size: 22px;
149
+ color: var(--text-muted);
150
+ margin-top: 24px;
151
+ line-height: 1.5;
152
+ }
153
+ ```
154
+
155
+ **Omit eyebrow or subtext if not needed** — the headline stands alone fine.
156
+
157
+ ---
158
+
159
+ ## 2. stat-reveal
160
+
161
+ Oversized number + label. Best for a single metric that needs maximum impact.
162
+
163
+ **HTML:**
164
+ ```html
165
+ <div class="scene scene-N">
166
+ <div class="scene-inner stat-reveal">
167
+ <div class="stat-number">$4.2M</div>
168
+ <div class="stat-label">New ARR this quarter</div>
169
+ <div class="stat-context">vs $2.3M same period last year</div>
170
+ </div>
171
+ </div>
172
+ ```
173
+
174
+ **CSS:**
175
+ ```css
176
+ .stat-reveal {
177
+ text-align: center;
178
+ }
179
+ .stat-reveal .stat-number {
180
+ font-family: var(--font-display);
181
+ font-size: clamp(120px, 18vw, 200px);
182
+ font-weight: 800;
183
+ color: var(--accent);
184
+ line-height: 0.9;
185
+ letter-spacing: -0.04em;
186
+ }
187
+ .stat-reveal .stat-label {
188
+ font-family: var(--font-body);
189
+ font-size: 28px;
190
+ font-weight: 500;
191
+ color: var(--text);
192
+ margin-top: 20px;
193
+ letter-spacing: -0.01em;
194
+ }
195
+ .stat-reveal .stat-context {
196
+ font-family: var(--font-body);
197
+ font-size: 18px;
198
+ color: var(--text-muted);
199
+ margin-top: 12px;
200
+ }
201
+ ```
202
+
203
+ **Omit `.stat-context` if not needed.**
204
+
205
+ ---
206
+
207
+ ## 3. bullet-list
208
+
209
+ 2–4 short bullet points. Best for listing factors, drivers, or steps. Bullets animate in staggered within scene's keyframe window.
210
+
211
+ **HTML:**
212
+ ```html
213
+ <div class="scene scene-N">
214
+ <div class="scene-inner bullet-list">
215
+ <h2 class="list-title">3 Drivers of Growth</h2>
216
+ <ul class="bullets">
217
+ <li class="bullet bullet-1">
218
+ <span class="bullet-marker">01</span>
219
+ <span class="bullet-text">Enterprise deals +120%</span>
220
+ </li>
221
+ <li class="bullet bullet-2">
222
+ <span class="bullet-marker">02</span>
223
+ <span class="bullet-text">Churn dropped to 1.2%</span>
224
+ </li>
225
+ <li class="bullet bullet-3">
226
+ <span class="bullet-marker">03</span>
227
+ <span class="bullet-text">Price increase fully absorbed</span>
228
+ </li>
229
+ </ul>
230
+ </div>
231
+ </div>
232
+ ```
233
+
234
+ **CSS:**
235
+ ```css
236
+ .bullet-list {
237
+ max-width: 800px;
238
+ width: 100%;
239
+ }
240
+ .bullet-list .list-title {
241
+ font-family: var(--font-display);
242
+ font-size: 52px;
243
+ font-weight: 700;
244
+ color: var(--text);
245
+ margin-bottom: 40px;
246
+ letter-spacing: -0.02em;
247
+ }
248
+ .bullet-list .bullets {
249
+ list-style: none;
250
+ display: flex;
251
+ flex-direction: column;
252
+ gap: 20px;
253
+ }
254
+ .bullet-list .bullet {
255
+ display: flex;
256
+ align-items: baseline;
257
+ gap: 20px;
258
+ opacity: 0; /* renderFrame sets per-bullet opacity */
259
+ will-change: opacity;
260
+ }
261
+ .bullet-list .bullet-marker {
262
+ font-family: var(--font-mono);
263
+ font-size: 14px;
264
+ color: var(--accent);
265
+ letter-spacing: 0.05em;
266
+ min-width: 28px;
267
+ }
268
+ .bullet-list .bullet-text {
269
+ font-family: var(--font-body);
270
+ font-size: 32px;
271
+ font-weight: 500;
272
+ color: var(--text);
273
+ line-height: 1.2;
274
+ }
275
+ ```
276
+
277
+ **Stagger bullets in renderFrame:**
278
+ Add a `bulletOpacity()` helper and drive bullets from within `window.renderFrame`:
279
+
280
+ ```javascript
281
+ function bulletOpacity(sceneProgress, showAtProg) {
282
+ if (sceneProgress < showAtProg) return 0;
283
+ if (sceneProgress < showAtProg + 0.06) return (sceneProgress - showAtProg) / 0.06;
284
+ if (sceneProgress < 0.85) return 1;
285
+ return clamp(1 - (sceneProgress - 0.85) / 0.15, 0, 1);
286
+ }
287
+
288
+ // Inside window.renderFrame(t), for a bullet-list scene (e.g. 6000ms–9000ms):
289
+ const sProgress = clamp((t - startMs) / (endMs - startMs), 0, 1);
290
+ const staggerOffsets = [0.10, 0.28, 0.46]; // scene-progress when each bullet appears
291
+ ['.bullet-1', '.bullet-2', '.bullet-3'].forEach((cls, i) => {
292
+ const el = document.querySelector(cls);
293
+ if (el) el.style.opacity = bulletOpacity(sProgress, staggerOffsets[i]);
294
+ });
295
+ ```
296
+
297
+ Bullet 1 appears at 10% into scene, bullet 2 at 28%, bullet 3 at 46%. All fade out with the scene exit at 85%.
298
+
299
+ ---
300
+
301
+ ## 4. split-screen
302
+
303
+ Left column: text/context. Right column: large number or metric. Side-by-side contrast.
304
+
305
+ **HTML:**
306
+ ```html
307
+ <div class="scene scene-N">
308
+ <div class="scene-inner split-screen">
309
+ <div class="split-left">
310
+ <div class="split-label">Before</div>
311
+ <div class="split-value dim">200 req/s</div>
312
+ <div class="split-desc">Legacy system capacity</div>
313
+ </div>
314
+ <div class="split-divider"></div>
315
+ <div class="split-right">
316
+ <div class="split-label accent">After</div>
317
+ <div class="split-value">2,000 req/s</div>
318
+ <div class="split-desc">Post-migration capacity</div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ ```
323
+
324
+ **CSS:**
325
+ ```css
326
+ .split-screen {
327
+ display: flex;
328
+ align-items: center;
329
+ gap: 0;
330
+ width: 100%;
331
+ padding: 80px;
332
+ }
333
+ .split-left,
334
+ .split-right {
335
+ flex: 1;
336
+ display: flex;
337
+ flex-direction: column;
338
+ align-items: center;
339
+ text-align: center;
340
+ gap: 12px;
341
+ }
342
+ .split-divider {
343
+ width: 1px;
344
+ height: 200px;
345
+ background: var(--divider);
346
+ margin: 0 40px;
347
+ flex-shrink: 0;
348
+ }
349
+ .split-label {
350
+ font-family: var(--font-body);
351
+ font-size: 13px;
352
+ letter-spacing: 0.10em;
353
+ text-transform: uppercase;
354
+ color: var(--text-muted);
355
+ }
356
+ .split-label.accent { color: var(--accent); }
357
+ .split-value {
358
+ font-family: var(--font-display);
359
+ font-size: clamp(60px, 9vw, 96px);
360
+ font-weight: 700;
361
+ color: var(--text);
362
+ letter-spacing: -0.03em;
363
+ line-height: 1.0;
364
+ }
365
+ .split-value.dim { opacity: 0.35; }
366
+ .split-desc {
367
+ font-family: var(--font-body);
368
+ font-size: 18px;
369
+ color: var(--text-muted);
370
+ }
371
+ ```
372
+
373
+ ---
374
+
375
+ ## 5. quote-card
376
+
377
+ Large pull quote with attribution. Best for testimonials, founder quotes, or key insights.
378
+
379
+ **HTML:**
380
+ ```html
381
+ <div class="scene scene-N">
382
+ <div class="scene-inner quote-card">
383
+ <div class="quote-mark">"</div>
384
+ <blockquote class="quote-text">
385
+ This tool saved us 20 hours a week and paid for itself on day one.
386
+ </blockquote>
387
+ <div class="quote-attribution">
388
+ <span class="quote-name">Sarah Chen</span>
389
+ <span class="quote-role">Head of Engineering, Acme Corp</span>
390
+ </div>
391
+ </div>
392
+ </div>
393
+ ```
394
+
395
+ **CSS:**
396
+ ```css
397
+ .quote-card {
398
+ max-width: 860px;
399
+ text-align: center;
400
+ position: relative;
401
+ }
402
+ .quote-card .quote-mark {
403
+ font-family: var(--font-display);
404
+ font-size: 120px;
405
+ line-height: 0.6;
406
+ color: var(--accent);
407
+ margin-bottom: 24px;
408
+ display: block;
409
+ }
410
+ .quote-card .quote-text {
411
+ font-family: var(--font-display);
412
+ font-size: clamp(32px, 5vw, 52px);
413
+ font-weight: 600;
414
+ color: var(--text);
415
+ line-height: 1.25;
416
+ letter-spacing: -0.02em;
417
+ font-style: normal;
418
+ }
419
+ .quote-card .quote-attribution {
420
+ margin-top: 36px;
421
+ display: flex;
422
+ flex-direction: column;
423
+ gap: 4px;
424
+ }
425
+ .quote-card .quote-name {
426
+ font-family: var(--font-body);
427
+ font-size: 18px;
428
+ font-weight: 600;
429
+ color: var(--text);
430
+ }
431
+ .quote-card .quote-role {
432
+ font-family: var(--font-body);
433
+ font-size: 15px;
434
+ color: var(--text-muted);
435
+ }
436
+ ```
437
+
438
+ ---
439
+
440
+ ## 6. cta-card
441
+
442
+ Final scene: brand name + call to action + URL or handle. Always the last scene.
443
+
444
+ **HTML:**
445
+ ```html
446
+ <div class="scene scene-N">
447
+ <div class="scene-inner cta-card">
448
+ <div class="cta-brand">Acme</div>
449
+ <div class="cta-divider"></div>
450
+ <p class="cta-message">See the full Q4 report</p>
451
+ <div class="cta-url">acme.com/q4-2024</div>
452
+ </div>
453
+ </div>
454
+ ```
455
+
456
+ **CSS:**
457
+ ```css
458
+ .cta-card {
459
+ text-align: center;
460
+ align-items: center;
461
+ gap: 0;
462
+ }
463
+ .cta-card .cta-brand {
464
+ font-family: var(--font-display);
465
+ font-size: 72px;
466
+ font-weight: 700;
467
+ color: var(--text);
468
+ letter-spacing: -0.04em;
469
+ }
470
+ .cta-card .cta-divider {
471
+ width: 48px;
472
+ height: 3px;
473
+ background: var(--accent);
474
+ margin: 28px auto;
475
+ }
476
+ .cta-card .cta-message {
477
+ font-family: var(--font-body);
478
+ font-size: 26px;
479
+ color: var(--text-muted);
480
+ margin-bottom: 16px;
481
+ }
482
+ .cta-card .cta-url {
483
+ font-family: var(--font-mono);
484
+ font-size: 28px;
485
+ font-weight: 600;
486
+ color: var(--accent);
487
+ letter-spacing: -0.01em;
488
+ }
489
+ ```
490
+
491
+ ---
492
+
493
+ ## Scene Selection Guide
494
+
495
+ | Content type | Recommended scene type |
496
+ |---|---|
497
+ | Hook / opening | title-card |
498
+ | Single metric / number | stat-reveal |
499
+ | Multiple points / list | bullet-list |
500
+ | Before vs after / two values | split-screen |
501
+ | Testimonial / quote | quote-card |
502
+ | Final / CTA / contact | cta-card |
503
+
504
+ **Max 6 scenes per video.** Beyond 6, the message fragments. If the brief has more than 6 ideas, consolidate similar points or promote the top 5 with a summary CTA.
@@ -0,0 +1,202 @@
1
+ # Style Presets — vid-motion-graphics
2
+
3
+ 5 motion-optimized style presets. Each includes CSS tokens, font stack, Google Fonts CDN link, and animation personality.
4
+
5
+ Read this file before generating any HTML in Step 3.
6
+
7
+ ---
8
+
9
+ ## 1. kinetic-dark (default)
10
+
11
+ **Feel:** High-energy. Electric yellow on near-black. Tight grotesque. Fast-in, slow-hold.
12
+ **Best for:** Product launches, growth metrics, startup content.
13
+
14
+ ```css
15
+ /* Font CDN */
16
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700;800&family=DM+Sans:wght@400;500&display=swap" rel="stylesheet">
17
+
18
+ :root {
19
+ --bg: #0A0A0A;
20
+ --text: #FAFAFA;
21
+ --text-muted: #888888;
22
+ --accent: #F5FF00;
23
+ --accent-dim: rgba(245,255,0,0.15);
24
+ --divider: rgba(255,255,255,0.10);
25
+ --font-display: 'Space Grotesk', sans-serif;
26
+ --font-body: 'DM Sans', sans-serif;
27
+ --font-mono: 'Space Grotesk', monospace;
28
+ }
29
+ ```
30
+
31
+ **Data palette:** `['#F5FF00', '#60A5FA', '#4ADE80', '#F87171', '#A78BFA']`
32
+
33
+ **Animation feel:**
34
+ - In: `translateY(24px) → 0, opacity 0 → 1` over first 8% of scene
35
+ - Hold: static for 80% of scene
36
+ - Out: `translateY(-12px), opacity → 0` over last 12% of scene
37
+ - Easing: `cubic-bezier(0.22, 1, 0.36, 1)` for in, `linear` for out
38
+
39
+ ---
40
+
41
+ ## 2. editorial-light
42
+
43
+ **Feel:** Refined. Ink on paper. Serif display. Long hold, gentle crossfade.
44
+ **Best for:** Thought leadership, executive communications, annual reports.
45
+
46
+ ```css
47
+ /* Font CDN */
48
+ <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;800&family=Source+Serif+4:wght@400;600&display=swap" rel="stylesheet">
49
+
50
+ :root {
51
+ --bg: #FAFAF8;
52
+ --text: #111111;
53
+ --text-muted: #777777;
54
+ --accent: #111111;
55
+ --accent-dim: rgba(17,17,17,0.08);
56
+ --divider: rgba(0,0,0,0.10);
57
+ --font-display: 'Playfair Display', serif;
58
+ --font-body: 'Source Serif 4', serif;
59
+ --font-mono: 'Source Serif 4', monospace;
60
+ }
61
+ ```
62
+
63
+ **Data palette:** `['#111111', '#3B82F6', '#16A34A', '#DC2626', '#7C3AED']`
64
+
65
+ **Animation feel:**
66
+ - In: `opacity 0 → 1` only, over first 10% of scene (no transform)
67
+ - Hold: static for 85% of scene
68
+ - Out: `opacity → 0` over last 5% of scene
69
+ - Easing: `ease-in-out` throughout
70
+
71
+ ---
72
+
73
+ ## 3. data-pulse
74
+
75
+ **Feel:** Terminal/dashboard. Deep navy, electric blue, monospaced. Scan-line entrance.
76
+ **Best for:** Technical metrics, engineering announcements, SaaS dashboards.
77
+
78
+ ```css
79
+ /* Font CDN */
80
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600;700&family=IBM+Plex+Sans:wght@400;500&display=swap" rel="stylesheet">
81
+
82
+ :root {
83
+ --bg: #050B1A;
84
+ --text: #E8F4FF;
85
+ --text-muted: #4A7FA5;
86
+ --accent: #00C2FF;
87
+ --accent-dim: rgba(0,194,255,0.12);
88
+ --divider: rgba(0,194,255,0.15);
89
+ --font-display: 'IBM Plex Mono', monospace;
90
+ --font-body: 'IBM Plex Sans', sans-serif;
91
+ --font-mono: 'IBM Plex Mono', monospace;
92
+ }
93
+ ```
94
+
95
+ **Data palette:** `['#00C2FF', '#4ADE80', '#F5FF00', '#F87171', '#A78BFA']`
96
+
97
+ **Animation feel:**
98
+ - In: `translateX(-8px) → 0, opacity 0 → 1` with slight stagger on text lines
99
+ - Hold: static with subtle `box-shadow` pulse on accent elements
100
+ - Out: `opacity → 0, translateX(8px)` — slide out opposite direction
101
+ - Easing: `steps(4)` for entrance (scan-line effect), `linear` for exit
102
+
103
+ ---
104
+
105
+ ## 4. bold-type
106
+
107
+ **Feel:** Brutalist typographic. White bg, black/red, maximum weight. Slam in.
108
+ **Best for:** Announcements, milestones, provocative statements.
109
+
110
+ ```css
111
+ /* Font CDN */
112
+ <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
113
+
114
+ :root {
115
+ --bg: #FFFFFF;
116
+ --text: #000000;
117
+ --text-muted: #555555;
118
+ --accent: #FF2B00;
119
+ --accent-dim: rgba(255,43,0,0.10);
120
+ --divider: #000000;
121
+ --font-display: 'Bebas Neue', sans-serif;
122
+ --font-body: 'Inter', sans-serif;
123
+ --font-mono: 'Inter', monospace;
124
+ }
125
+ ```
126
+
127
+ **Data palette:** `['#FF2B00', '#000000', '#1D4ED8', '#16A34A', '#9333EA']`
128
+
129
+ **Animation feel:**
130
+ - In: `scale(1.08) → 1, opacity 0 → 1` over first 5% of scene (slam)
131
+ - Hold: static for 90% of scene
132
+ - Out: `scale(0.96), opacity → 0` over last 5% of scene (snap)
133
+ - Easing: `cubic-bezier(0.34, 1.56, 0.64, 1)` for in (overshoot), `linear` for out
134
+
135
+ ---
136
+
137
+ ## 5. minimal-clean
138
+
139
+ **Feel:** Warm, refined. Off-white, ink-warm neutrals, thin serif display. Gentle rise.
140
+ **Best for:** People-focused content, hiring, testimonials, brand storytelling.
141
+
142
+ ```css
143
+ /* Font CDN */
144
+ <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600;700&family=Jost:wght@300;400;500&display=swap" rel="stylesheet">
145
+
146
+ :root {
147
+ --bg: #F5F4F0;
148
+ --text: #1A1A18;
149
+ --text-muted: #8A8A82;
150
+ --accent: #1A1A18;
151
+ --accent-dim: rgba(26,26,24,0.06);
152
+ --divider: rgba(26,26,24,0.12);
153
+ --font-display: 'Cormorant Garamond', serif;
154
+ --font-body: 'Jost', sans-serif;
155
+ --font-mono: 'Jost', monospace;
156
+ }
157
+ ```
158
+
159
+ **Data palette:** `['#1A1A18', '#6366F1', '#16A34A', '#CA8A04', '#DC2626']`
160
+
161
+ **Animation feel:**
162
+ - In: `translateY(32px) → 0, opacity 0 → 1` over first 12% of scene
163
+ - Hold: static for 80% of scene
164
+ - Out: `opacity → 0` only (no transform) over last 8%
165
+ - Easing: `cubic-bezier(0.25, 0.46, 0.45, 0.94)` throughout
166
+
167
+ ---
168
+
169
+ ## renderFrame Timing Quick Reference
170
+
171
+ All scene timing is computed in `window.renderFrame(t)` (milliseconds). No CSS `@keyframes`.
172
+
173
+ For a scene at `[startMs, endMs)`:
174
+ ```javascript
175
+ const prog = (t - startMs) / (endMs - startMs); // 0.0 → 1.0
176
+ // Enter: prog 0.00–0.10 → opacity 0→1, translateY 24px→0 (easeOutCubic)
177
+ // Hold: prog 0.10–0.85 → opacity 1, no transform
178
+ // Exit: prog 0.85–1.00 → opacity 1→0, translateY 0→-12px
179
+ // Hidden: t < startMs or t >= endMs → opacity 0
180
+ ```
181
+
182
+ **Example — 3 scenes, 3s each, 9s total:**
183
+ ```javascript
184
+ // scene 1: 0ms–3000ms | scene 2: 3000ms–6000ms | scene 3: 6000ms–9000ms
185
+ window.renderFrame = function(t) {
186
+ applySceneState(document.querySelector('.scene-1'), sceneState(t, 0, 3000));
187
+ applySceneState(document.querySelector('.scene-2'), sceneState(t, 3000, 6000));
188
+ applySceneState(document.querySelector('.scene-3'), sceneState(t, 6000, 9000));
189
+ };
190
+ ```
191
+
192
+ **Animation feel by preset (all implemented in sceneState/easing):**
193
+
194
+ | Preset | Enter transform | Enter easing | Exit transform |
195
+ |---|---|---|---|
196
+ | kinetic-dark | translateY(24px)→0 | easeOutCubic | translateY(0→-12px) |
197
+ | editorial-light | opacity only | ease-in-out | opacity only |
198
+ | data-pulse | translateX(-8px)→0 | easeOutCubic | translateX(0→8px) |
199
+ | bold-type | scale(1.08)→1 | easeOutCubic (overshoot) | scale(1→0.96) |
200
+ | minimal-clean | translateY(32px)→0 | easeOutCubic | opacity only |
201
+
202
+ Adapt `sceneState()` or add a style-specific `enterTransform`/`exitTransform` param to match the preset's feel.