@shotstack/cli 0.6.0 → 0.7.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotstack/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Command-line interface for the Shotstack video rendering API.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/shotstack/shotstack-cli",
@@ -17,9 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "dist/shotstack.js",
20
- "SKILL.md",
21
- "shared",
22
- "references"
20
+ "skills"
23
21
  ],
24
22
  "scripts": {
25
23
  "start": "bun run src/index.ts",
@@ -158,7 +158,7 @@ This skill ships sub-references for the gnarly bits:
158
158
  - [`references/svg.md`](references/svg.md) — required attrs, supported elements
159
159
  - [`references/motion.md`](references/motion.md) — **the house motion language**: one duration scale, one ease, one stagger; choreography recipes (GSAP/CSS), the rich-text/transition mappings, and the brand kit. Read before composing any animation.
160
160
  - [`references/html5.md`](references/html5.md) — HTML5 asset: fields, preloaded libs (gsap/d3/anime/lottie), browser harness, sizing, worked examples
161
- - [`references/html5-snippets.md`](references/html5-snippets.md) — copy-paste motion-graphic clips: kinetic headline, count-up/price odometer, shine sweep, pulsing CTA, film grain
161
+ - [`references/html5-snippets.md`](references/html5-snippets.md) — copy-paste motion-graphic clips: kinetic headline, value reveal, shine sweep, pulsing CTA, film grain
162
162
  - [`references/fonts.md`](references/fonts.md) — built-in fonts, Google Fonts URL pattern, custom-font workflow
163
163
  - [`references/asset-library.md`](references/asset-library.md) — placeholder videos, images, music
164
164
  - [`references/troubleshooting.md`](references/troubleshooting.md) — common errors and fixes
@@ -1,6 +1,6 @@
1
1
  # HTML5 snippet pack — drop-in motion graphics
2
2
 
3
- Copy-paste `html5` clips that "pop" — kinetic type, count-ups, shine sweeps,
3
+ Copy-paste `html5` clips that "pop" — kinetic type, value reveals, shine sweeps,
4
4
  pulsing CTAs, grain. Each is a **single clip**: paste it into a track's
5
5
  `clips[]`, set `start`/`length`, and position with `offset`.
6
6
 
@@ -14,12 +14,14 @@ easings and durations. Read [`motion.md`](motion.md) for the why and the full re
14
14
 
15
15
  Read [`html5.md`](html5.md) for the rules these obey. The non-negotiables:
16
16
 
17
- - **Seekable animation only.** GSAP timelines, GSAP tweens (incl. `onUpdate`), anime.js, Lottie, or CSS `@keyframes`. **Never** `setTimeout`/`setInterval`/`requestAnimationFrame`/`Date.now()`/`gsap.call()` — the renderer seeks by absolute time, it doesn't play.
17
+ - **Seekable animation only.** GSAP timelines, GSAP tweens, anime.js, Lottie, or CSS `@keyframes`. **Never** `setTimeout`/`setInterval`/`requestAnimationFrame`/`Date.now()`/`gsap.call()` — the renderer seeks by absolute time, it doesn't play.
18
+ - **`onUpdate` does not fire under seek.** GSAP `onUpdate` callbacks are **not** invoked when the harness seeks to a frame, so any DOM mutations made inside them (`textContent`, `innerHTML`, class swaps) won't appear in the rendered video. Animate **CSS properties only** (opacity, transform, filter, scale). Bake final values into the HTML at generation time and reveal them with opacity/transform tweens.
18
19
  - **Size the clip to the content, not the canvas.** `html, body` pinned to the clip's `width`/`height`; place with `offset` (`{x:0,y:0}` is centred, `y` positive is up). Use **px**, never `vw`/`vh`/`%`.
19
20
  - **The body is transparent by default** — the clip composites over the layers below (only set an opaque background if you want one).
20
21
  - **No `<canvas>`** — Studio capture serialises the DOM; canvas bitmaps come through empty. Use SVG or positioned DOM.
21
22
  - **No network.** gsap/anime/d3/lottie are preloaded; external `<script src>`, `fetch`, remote `<img>`, and remote fonts are all CSP-blocked — inline everything as `data:` URIs.
22
23
  - **Fonts:** `timeline.fonts[]` does **not** reach `html5` and remote `@font-face` is blocked. These snippets name a display font first but render in the `system-ui` fallback unless you inline the font as a `data:` `@font-face`.
24
+ - **Use unique element IDs for GSAP targets, not `:nth-child` or compound CSS selectors.** Studio preview doesn't resolve `.parent:nth-child(n) .child` patterns; `#id` works everywhere. When animating repeated elements (list rows, bars, cards), generate a unique ID per item (`id="row0"`, `id="bar0"`) rather than relying on structural selectors.
23
25
 
24
26
  Coordinates below assume a **1080×1920 vertical** canvas; adjust `offset` for other sizes.
25
27
 
@@ -79,19 +81,19 @@ Each word starts `translateY(120%)`, opacity 0; `0.6 s` rise on `power3.out`, wo
79
81
 
80
82
  ---
81
83
 
82
- ## 3. Count-up number / price odometer
84
+ ## 3. Value reveal — bake and fade-in
83
85
 
84
- **Category** data · **Use when** revealing a value, price, stat or metric · **Canvas** 620×220 · **Tags** number, count, price, data · **Merge-friendly** target value
86
+ **Category** data · **Use when** revealing a value, price, stat or metric · **Canvas** 620×220 · **Tags** number, price, stat, data · **Merge-friendly** target value
85
87
 
86
- Animate a value from 0 to its target. Seek-safe because the count lives in a **tweened object with `onUpdate`** (fires on seek), never a timer.
88
+ Bake the final value into the HTML and reveal it with opacity + blur + rise. The value is always present in the DOM — the animation controls only its visibility — so every captured frame shows the correct number. **Never use `onUpdate` to mutate `textContent`**: the seek harness doesn't fire `onUpdate` callbacks, so the value stays at its initial state (`$0`) in every frame.
87
89
 
88
90
  ```json
89
91
  {
90
92
  "asset": {
91
93
  "type": "html5",
92
- "html": "<div class=\"wrap\"><span class=\"cur\">$</span><span id=\"n\">0</span></div>",
93
- "css": "html,body{margin:0;width:620px;height:220px;overflow:hidden;background:transparent;font-family:system-ui,sans-serif}.wrap{display:flex;align-items:baseline;justify-content:center;width:620px;height:220px;color:#141414;font-weight:800;font-variant-numeric:tabular-nums}.cur{font-size:70px;margin-right:6px}#n{font-size:150px;letter-spacing:-2px}",
94
- "js": "const o={v:0};const out=document.getElementById('n');const tl=gsap.timeline();tl.to(o,{v:395,duration:0.8,ease:'power2.out',onUpdate:()=>{out.textContent=Math.round(o.v)}});tl.to({},{duration:1.5});"
94
+ "html": "<div class=\"wrap\"><span class=\"cur\">$</span><span class=\"n\">395</span></div>",
95
+ "css": "html,body{margin:0;width:620px;height:220px;overflow:hidden;background:transparent;font-family:system-ui,sans-serif}.wrap{display:flex;align-items:baseline;justify-content:center;width:620px;height:220px;color:#141414;font-weight:800;font-variant-numeric:tabular-nums;opacity:0;transform:translateY(16px);filter:blur(10px)}.cur{font-size:70px;margin-right:6px}.n{font-size:150px;letter-spacing:-2px}",
96
+ "js": "gsap.to('.wrap',{opacity:1,y:0,filter:'blur(0px)',duration:0.8,ease:'power3.out'});gsap.to({},{duration:1.5});"
95
97
  },
96
98
  "start": 0,
97
99
  "length": 3,
@@ -101,7 +103,7 @@ Animate a value from 0 to its target. Seek-safe because the count lives in a **t
101
103
  }
102
104
  ```
103
105
 
104
- `slow` (0.8 s) count on `power2.out` (a decelerating settle reads right for a value), then a `hold`. Swap `v:395` for any target; for thousands separators use `Math.round(o.v).toLocaleString()`. Pair with a static label on an adjacent `rich-text` track ("FROM", "AUD").
106
+ `slow` (0.8 s) reveal on `power3.out` (a decelerating settle reads right for a value), then a `hold`. Swap `395` for any value at generation time; for thousands separators format the string before baking it (`$63,642` not `63642`). Pair with a static label on an adjacent `rich-text` track ("FROM", "AUD").
105
107
 
106
108
  ---
107
109
 
@@ -185,7 +187,7 @@ A `data:` URI is fine **inside an html5 asset's CSS** (it's iframe content, not
185
187
 
186
188
  ## Brand kit — re-skin every snippet at once
187
189
 
188
- The snippets share one palette (ink `#141414`, accent `#D96B82`) so a set already looks coherent. To re-skin a whole edit to a brand in one place, lift the colours/font into top-level `merge[]` and reference the tokens in each clip's `css` — `merge` find/replace runs over the `html`/`css` strings too:
190
+ The snippets share one palette (ink `#141414`, accent `#D96B82`) so a set already looks coherent. To re-skin a whole edit to a brand in one place, lift the colours/font into top-level `merge[]` and reference the tokens in each clip's `html`, `css`, and `js` — `merge` find/replace runs over all three strings:
189
191
 
190
192
  ```json
191
193
  "merge": [
@@ -202,5 +204,5 @@ Then in any snippet's CSS, swap the literal hex for the token: `color:{{ink}}`,
202
204
  - Each snippet is one clip on its own track. Layer order is top-track-first (see `agent-core.md`) — grain and shine go in **early** tracks, backgrounds in **late** ones.
203
205
  - They don't overlap on a single track, so `shotstack validate <file>` stays clean. Run it before rendering.
204
206
  - Reuse text via top-level `merge[]` (`{{title}}` in the HTML) — see the lower-third example in `html5.md`.
205
- - Mix calm and punchy deliberately: a `blur-reveal` title, a `kinetic-headline` hero line, a `count-up` stat, a `shine` on the product, a pulsing CTA — all on the same tokens, so the set reads as one piece.
207
+ - Mix calm and punchy deliberately: a `blur-reveal` title, a `kinetic-headline` hero line, a `value-reveal` stat, a `shine` on the product, a pulsing CTA — all on the same tokens, so the set reads as one piece.
206
208
  - Heavier motion = longer render. Preview in `shotstack studio <file>` before spending credits.
@@ -23,8 +23,8 @@ This is the **modern replacement for the deprecated `html` asset.** `html5` runs
23
23
  |---|---|---|---|
24
24
  | `type` | Yes | `"html5"` | Discriminator. |
25
25
  | `html` | Yes | string | Body markup. Supports merge fields (`{{title}}`). |
26
- | `css` | No | string | Stylesheet. Inlined into the iframe `<head>`. |
27
- | `js` | No | string | Script. Runs after libraries are preloaded. |
26
+ | `css` | No | string | Stylesheet. Inlined into the iframe `<head>`. Supports merge fields. |
27
+ | `js` | No | string | Script. Runs after libraries are preloaded. Supports merge fields. |
28
28
 
29
29
  Clip-level `width` and `height` set the iframe's pixel dimensions. They default to the edit's natural size.
30
30
 
@@ -56,7 +56,9 @@ These cover ~95% of motion-graphics use cases. **You can't load other libraries
56
56
 
57
57
  ## The browser harness (deterministic auto-seek)
58
58
 
59
- Frames are captured by **seeking** the animation to each timestamp, not by playing in real time — so your animation must be **seekable**. GSAP (timelines or tweens), anime.js, Lottie, and CSS (`@keyframes`, transitions, `Element.animate()`) are all driven automatically. Anything time-driven that isn't seekable gives a frozen or wrong frame: never use `setTimeout`, `setInterval`, `requestAnimationFrame` loops, `Date.now()` / `performance.now()`, or `gsap.call()`. For "different content at different times" (countdowns, tickers, scene swaps) use the staggered-CSS pattern (see the countdown example) or an `onUpdate` tween (see the count-up snippet).
59
+ Frames are captured by **seeking** the animation to each timestamp, not by playing in real time — so your animation must be **seekable**. GSAP (timelines or tweens), anime.js, Lottie, and CSS (`@keyframes`, transitions, `Element.animate()`) are all driven automatically. Anything time-driven that isn't seekable gives a frozen or wrong frame: never use `setTimeout`, `setInterval`, `requestAnimationFrame` loops, `Date.now()` / `performance.now()`, or `gsap.call()`. For "different content at different times" (countdowns, tickers, scene swaps) use the staggered-CSS pattern (see the countdown example) or bake values into the HTML and animate their visibility (see the value-reveal snippet).
60
+
61
+ **`onUpdate` callbacks do not fire under seek.** The harness seeks the GSAP timeline to each frame's timestamp without playing through, so `onUpdate` handlers are not invoked. Any DOM mutation made inside an `onUpdate` callback (`textContent`, `innerHTML`, class swaps, attribute changes) will not appear in the rendered video — the element stays at its initial state. **Animate CSS properties only** (opacity, transform, filter, scale) — those are applied directly by GSAP's seek. To display a value, bake it into the HTML at generation time and reveal it with an opacity/transform tween.
60
62
 
61
63
  **Duration comes from the clip's `length`** — there's no animation-duration auto-detection. Size your animation to run within (or fill) the clip's `length`.
62
64
 
@@ -156,7 +158,7 @@ Slide-in name + role bar with subtle accent. **Clip sized to the bar (560×120),
156
158
  - **Clip is the size of the bar, not the canvas** — placement is one `offset` change.
157
159
  - **`html, body, .bar` all 560×120.** No absolute positioning inside the iframe — the bar IS the iframe content.
158
160
  - One GSAP timeline drives every animation.
159
- - Merge fields (`{{name}}`, `{{role}}`) in the HTML, populated by **top-level** `merge[]` (sibling of `timeline`/`output`, NOT a clip property). Keeps the asset reusable.
161
+ - Merge fields (`{{name}}`, `{{role}}`) in the HTML, populated by **top-level** `merge[]` (sibling of `timeline`/`output`, NOT a clip property). Keeps the asset reusable. Merge fields also resolve in `css` and `js` — use `{{accent}}` in CSS for brand colours, or `{{targetValue}}` inside a JS string literal for data-driven animation targets.
160
162
  - A trailing `.to({}, { duration: 3.5 })` holds the final state before the clip ends.
161
163
 
162
164
  ## Worked example: animated bar chart (D3 + GSAP)
@@ -236,6 +238,7 @@ The same pattern scales to scene transitions (each scene is a `<section>` with i
236
238
 
237
239
  If you're building something that genuinely cannot be expressed without canvas, render it as a `<video>` or `<image>` asset instead of an `html5` clip.
238
240
  2. **Mismatched dimensions.** If `clip.width = 1920` and your CSS sets `body { width: 1280px }`, content gets cropped or stretched. Pin the iframe's `html, body` dimensions to the clip dimensions.
241
+ 3. **JS syntax or runtime errors produce a blank clip with no render error.** If `asset.js` throws (syntax error or uncaught runtime error), the entire clip renders as a blank frame. The render still reports `status: "done"` with no error — there is no feedback loop. `shotstack validate` catches JS syntax errors offline; runtime errors (e.g. referencing a DOM element that doesn't exist) are silent. If a clip is blank, check the JS first: run `node --check` on the string, or wrap suspect code in `try/catch` to surface the error.
239
242
 
240
243
  ## When to use `html5` vs `rich-text`/`svg`
241
244
 
@@ -113,7 +113,7 @@ tl.from('.el', { opacity: 0, y: 12, duration: 0.6, ease: 'power3.out' }) // in
113
113
 
114
114
  ## Brand kit — set the palette once, re-skin everything
115
115
 
116
- Don't hardcode hex and font per snippet. Declare the brand as top-level `merge[]` fields and reference the tokens in every clip's `css`/`html` — one edit re-skins the whole video — brand tokens applied once, via Shotstack merge:
116
+ Don't hardcode hex and font per snippet. Declare the brand as top-level `merge[]` fields and reference the tokens in every clip's `html`/`css`/`js` — one edit re-skins the whole video — brand tokens applied once, via Shotstack merge:
117
117
 
118
118
  ```json
119
119
  "merge": [
@@ -129,7 +129,7 @@ Don't hardcode hex and font per snippet. Declare the brand as top-level `merge[]
129
129
  .cta { background: {{accent}}; }
130
130
  ```
131
131
 
132
- `merge` find/replace runs over the whole edit, including `html`/`css` strings, so `{{accent}}` resolves everywhere. Keep one accent and use it sparingly — a headline word, a number, a CTA, one glow — everything else neutral. **Colour is earned, never sprinkled.**
132
+ `merge` find/replace runs over the whole edit, including `html`/`css`/`js` strings, so `{{accent}}` resolves everywhere. Keep one accent and use it sparingly — a headline word, a number, a CTA, one glow — everything else neutral. **Colour is earned, never sprinkled.**
133
133
 
134
134
  ## Motion checklist
135
135
 
@@ -6,12 +6,14 @@ Common errors and the fix for each. **Catch most of them before rendering:** `sh
6
6
 
7
7
  - "Unknown property: alignment" / wrong property names
8
8
  - "Invalid option: expected one of top|middle|bottom"
9
- - Video looks stretched / squished after rendering
9
+ - Video looks stretched / squishing after rendering
10
10
  - "Font not found"
11
11
  - Captions cover the whole frame
12
12
  - Clips flicker / "clips overlap" on one track
13
13
  - Timeline renders but layers are wrong
14
14
  - "Invalid asset URL"
15
+ - html5 clip is blank but render succeeded
16
+ - html5 clip animates in the rendered MP4 but not in Studio preview
15
17
  - "Render failed" with no other detail
16
18
  - Render takes much longer than expected
17
19
  - Credits exhausted on stage environment
@@ -82,6 +84,29 @@ The `src` is not a public HTTPS URL, or it's a `data:` URI, or it's a local path
82
84
 
83
85
  **Fix:** host the asset at a public HTTPS URL or use a presigned URL with credentials in the URL itself. For tests, pull from `references/asset-library.md`.
84
86
 
87
+ ## html5 clip is blank but render succeeded
88
+
89
+ An `html5` clip renders as a completely blank frame, but the render reports `status: "done"` with no error. This is a **silent failure** — there is no feedback loop.
90
+
91
+ Two causes:
92
+
93
+ 1. **JS syntax error in `asset.js`.** A missing semicolon, unbalanced brace, or any other parse error crashes the entire script before any animation runs. `shotstack validate <file>` catches this offline — it runs each `asset.js` string through a syntax check and reports the error with the clip path. **Always validate before rendering.**
94
+
95
+ 2. **JS runtime error.** Referencing a DOM element that doesn't exist (`document.getElementById("missing")`), calling a method on `null`, or any other uncaught exception — the script stops mid-execution and the clip stays in its initial state (typically invisible if elements start at `opacity:0`). These are not caught by `validate` (they require the runtime DOM). To debug, wrap suspect code in `try/catch` and log to a visible element, or simplify the JS to isolate the failing line.
96
+
97
+ A third variant: values stuck at `$0` or their initial state. This happens when `onUpdate` callbacks are used to mutate `textContent` — the seek harness doesn't fire `onUpdate`, so the DOM never updates. Bake final values into the HTML and animate opacity/transform instead. See `references/html5.md` → "The browser harness".
98
+
99
+ ## html5 clip animates in the rendered MP4 but not in Studio preview
100
+
101
+ Studio preview and the cloud render use different capture mechanisms. The cloud render (puppeteer) properly handles all GSAP properties; Studio's client-side preview does not. This is a parity gap, not a render bug — the MP4 is the ground truth.
102
+
103
+ Known differences:
104
+
105
+ - **`:nth-child` and compound CSS selectors** as GSAP targets don't resolve in Studio preview. `.brow:nth-child(1) .bar` silently matches nothing; rows and bars stay in their initial state. Use unique element IDs (`#row0`, `#bar0`) instead — they work in both.
106
+ - **`scale` / `scaleX` / `scaleY` transforms** don't animate in Studio preview. Elements starting at `scale(0)` or `scaleX(0)` stay invisible. Use `width` (for bars), `opacity`, or `y` transitions instead.
107
+
108
+ If a clip looks wrong in Studio but the rendered MP4 is correct, don't waste credits re-rendering — check whether the JS uses structural selectors or scale transforms, and swap for ID-based targets and width/opacity animations.
109
+
85
110
  ## "Render failed" with no other detail
86
111
 
87
112
  `shotstack status <id> --output json` returns the full response including the error message. The `--watch` text view truncates.
@@ -128,7 +128,7 @@ Compose from this rather than round-tripping the full schema — these are the v
128
128
 
129
129
  `align.horizontal` = `left|center|right`; `align.vertical` = `top|middle|bottom` (**not** `center`). `animation.preset` = `fadeIn` `slideIn` `typewriter` `ascend` `shift` `movingLetters`; `animation.style` = `character|word`; `animation.direction` = `left|right|up|down`. Give the clip a `width`/`height` box so text wraps and aligns where you expect.
130
130
 
131
- For motion beyond this (kinetic type, count-ups, shine sweeps, grain, pulsing CTAs) reach for `html5` — see [`references/html5-snippets.md`](../references/html5-snippets.md).
131
+ For motion beyond this (kinetic type, value reveals, shine sweeps, grain, pulsing CTAs) reach for `html5` — see [`references/html5-snippets.md`](../references/html5-snippets.md).
132
132
 
133
133
  ## Motion language (house tokens)
134
134
 
@@ -163,6 +163,51 @@ Pick `output.resolution` (preset) OR `output.size.width`+`output.size.height` (c
163
163
 
164
164
  Custom sizes must be divisible by 2.
165
165
 
166
+ ## Merge fields
167
+
168
+ Top-level `merge[]` (sibling of `timeline`/`output`, NOT a clip property) is a simple find-and-replace that runs over the entire Edit JSON **before** any asset is processed. Each entry is `{ "find": "<TOKEN>", "replace": "<value>" }` — the `find` value is the token name **without** braces; every `{{TOKEN}}` occurrence in any string throughout the Edit is swapped for the value.
169
+
170
+ ```json
171
+ {
172
+ "merge": [
173
+ { "find": "TITLE", "replace": "Q4 Results" },
174
+ { "find": "REVENUE", "replace": "$1,247,890" },
175
+ { "find": "ACCENT", "replace": "#22D3EE" }
176
+ ],
177
+ "timeline": {
178
+ "tracks": [{
179
+ "clips": [{
180
+ "asset": { "type": "rich-text", "text": "{{TITLE}}", "font": { "family": "Roboto", "size": 60, "color": "{{ACCENT}}" } },
181
+ "start": 0, "length": 3
182
+ }]
183
+ }]
184
+ },
185
+ "output": { "format": "mp4", "resolution": "1080" }
186
+ }
187
+ ```
188
+
189
+ **Rules:**
190
+ - Whitespace inside braces is ignored: `{{ TITLE }}` and `{{TITLE}}` are the same token.
191
+ - Token names are **case sensitive**. Use `UPPER_SNAKE_CASE` (the Shotstack convention).
192
+ - The `{{` delimiter cannot be changed.
193
+ - One `merge[]` entry can replace multiple `{{TOKEN}}` occurrences across the entire Edit.
194
+ - The replacement is a dumb string swap — no conditionals, no loops, no escaping.
195
+
196
+ **Where merge fields resolve:** every string value in the Edit JSON — `rich-text` `text` and `font.color`; `html5` `html`, `css`, and `js`; `svg` markup; `asset.src` URLs; `clip.transition` names; any string anywhere. This is an Edit-wide capability, not an html5 feature.
197
+
198
+ **When to use merge vs baking at generation time:**
199
+
200
+ | Scenario | Use |
201
+ |---|---|
202
+ | Reusable template rendered many times with different data (reports, personalised videos, per-user wraps) | `merge[]` — keep one Edit JSON, swap the `merge[]` per render |
203
+ | Brand re-skinning (palette, font, accent across every clip) | `merge[]` — change one `merge[]` entry to re-skin the whole video |
204
+ | One-off render or data unique to a single video | Bake values directly into the asset at generation time — simpler, no merge indirection |
205
+ | Generating multiple edits programmatically (e.g. one per user from a CSV) | Either — merge keeps the template reusable, baking is more direct. If the template won't be reused, bake. |
206
+
207
+ **Checking merge results:** pass `?data=true&merged=true` on the status request to see the merged Edit JSON in the response — useful for debugging unresolved placeholders.
208
+
209
+ **Validate caveat:** `shotstack validate` checks `html5` JS syntax **before** merge resolution. A `{{TOKEN}}` inside a JS string literal (e.g. `color: "{{ACCENT}}"`) is valid JS and passes. A `{{TOKEN}}` in JS *code* position (not inside a string) may fail validation pre-merge but work at render time post-merge — keep merge fields inside string literals to avoid false positives.
210
+
166
211
  ## Asset types
167
212
 
168
213
  Use only the **current** asset types; the deprecated ones still parse but should not be used in new templates.
@@ -211,9 +256,9 @@ One overlay is a `rich-text` job. **Several videos "in different styles" is not*
211
256
  |---|---|---|
212
257
  | 1 — type & layout | `rich-text` | Titles, lower-thirds, kickers, captions, price/CTA pills. Fast and reliable; the right default for static styled text. |
213
258
  | 2 — shapes | `svg` | Colour panels, rules, badges, frames, geometric accents behind or around type. |
214
- | 3 — motion graphics | `html5` | Kinetic type, count-ups / price odometers, shine sweeps, animated gradients, film grain, masked reveals, data-driven overlays — anything that should *move* beyond a `transition` or a Ken-Burns `effect`. gsap / anime / d3 / lottie are preloaded. See [`references/html5.md`](../references/html5.md) and the copy-paste clips in [`references/html5-snippets.md`](../references/html5-snippets.md). |
259
+ | 3 — motion graphics | `html5` | Kinetic type, value reveals, shine sweeps, animated gradients, film grain, masked reveals, data-driven overlays — anything that should *move* beyond a `transition` or a Ken-Burns `effect`. gsap / anime / d3 / lottie are preloaded. See [`references/html5.md`](../references/html5.md) and the copy-paste clips in [`references/html5-snippets.md`](../references/html5-snippets.md). |
215
260
 
216
- **When the brief asks for a *range*, deliberately spread across the ladder.** A strong set: a couple of clean `rich-text` studio cuts, one or two `svg` colour-block promos, and several `html5` pieces carrying the real motion (kinetic headline, price odometer, shine-swept CTA, grain-graded teaser). Reserve the elaborate `html5` treatments for the hero / hype cuts where motion sells the product. If every clip in a "variety" brief is `rich-text`, you have not delivered variety.
261
+ **When the brief asks for a *range*, deliberately spread across the ladder.** A strong set: a couple of clean `rich-text` studio cuts, one or two `svg` colour-block promos, and several `html5` pieces carrying the real motion (kinetic headline, value reveal, shine-swept CTA, grain-graded teaser). Reserve the elaborate `html5` treatments for the hero / hype cuts where motion sells the product. If every clip in a "variety" brief is `rich-text`, you have not delivered variety.
217
262
 
218
263
  ## Fonts
219
264