stillmade-sdk 0.1.48 → 0.1.50

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.
Files changed (60) hide show
  1. package/AGENTS.md +6 -2
  2. package/AGENT_CONTROL.md +15 -0
  3. package/FRAME_VIEWS.md +24 -0
  4. package/INTERFACE_DESIGN.md +40 -5
  5. package/MODULES.md +2 -2
  6. package/PASTE_TO_LLM.md +26 -19
  7. package/SDK.md +126 -39
  8. package/UI_AESTHETIC.md +15 -0
  9. package/blocks/generate/package.js +1 -1
  10. package/blocks/generate/versions/1.1.0/package.js +471 -0
  11. package/blocks/generate/versions/1.2.0/package.js +555 -0
  12. package/blocks/native/sm.board-editor/versions/3.0.0/package.js +50 -17
  13. package/blocks/native/sm.board-editor/versions/3.1.0/package.js +1037 -0
  14. package/blocks/native/sm.editor/versions/3.0.0/package.js +128 -83
  15. package/blocks/native/sm.editor/versions/3.1.0/package.js +1299 -0
  16. package/blocks/native/sm.panel-strip/versions/3.0.0/package.js +45 -12
  17. package/blocks/native/sm.panel-strip/versions/3.1.0/package.js +479 -0
  18. package/blocks/native/sm.voiceover/versions/3.0.0/package.js +9 -3
  19. package/docs/build/block-builder-contract.md +6 -2
  20. package/docs/build/frame-views.md +24 -0
  21. package/docs/build/interface-design.md +40 -5
  22. package/docs/build/modules.md +2 -2
  23. package/docs/reference/authoring-guide.md +4 -4
  24. package/docs/reference/execution.md +15 -1
  25. package/docs/reference/first-party-blocks.md +4 -4
  26. package/docs/reference/parity.md +1 -1
  27. package/docs/reference/ports.md +1 -1
  28. package/docs/reference/view-bridge.md +3 -0
  29. package/docs/tools/llm.md +26 -19
  30. package/package.json +1 -1
  31. package/packages/block-cli/dev-host/assets/host.js +161 -33
  32. package/packages/block-cli/dev-host/assets/index.css +1 -1
  33. package/packages/block-platform/block-readiness.js +16 -3
  34. package/packages/block-platform/canvas.js +13 -1
  35. package/packages/block-platform/catalog.js +32 -5
  36. package/packages/block-platform/flow-inspection.js +16 -2
  37. package/packages/block-platform/listings.js +17 -3
  38. package/packages/block-platform/steps.js +4 -1
  39. package/packages/block-sdk/admission.js +1 -1
  40. package/packages/block-sdk/agent-inputs.js +4 -2
  41. package/packages/block-sdk/builder-contract.js +7 -4
  42. package/packages/block-sdk/first-party-identities.js +9 -1
  43. package/packages/block-sdk/first-party-packages.js +11 -1
  44. package/packages/block-sdk/generation-catalog.js +55 -5
  45. package/packages/block-sdk/generation-models.js +60 -0
  46. package/packages/block-sdk/index.d.ts +15 -1
  47. package/packages/block-sdk/index.js +2 -1
  48. package/packages/block-sdk/input-source-policy.js +2 -1
  49. package/packages/block-sdk/module-hosted.js +13 -5
  50. package/packages/block-sdk/module-worker.js +1 -1
  51. package/packages/block-sdk/port-types.js +6 -2
  52. package/packages/block-sdk/semantic-contracts.d.ts +1 -1
  53. package/packages/block-sdk/semantic-relationships.js +3 -0
  54. package/packages/block-sdk/semantic-schema.js +4 -1
  55. package/packages/block-sdk/shared-controls.js +4 -1
  56. package/packages/block-sdk/staged-releases.js +2 -1
  57. package/packages/block-sdk/view-bridge.js +3 -0
  58. package/packages/block-sdk/view-design.js +54 -10
  59. package/packages/block-sdk/view-renderer.js +19 -6
  60. package/packages/design-system/block-kit.css +12 -3
@@ -0,0 +1,1299 @@
1
+ // Generated by frame/build-package.mjs from the original Editor screen. Edit the screen or frame/, then rebuild.
2
+ export default {
3
+ "manifest": {
4
+ "schemaVersion": 1,
5
+ "sdkVersion": "0.1.0",
6
+ "id": "sm.editor",
7
+ "version": "3.1.0",
8
+ "name": "Editor",
9
+ "description": "Cut your video on a timeline: clips, pictures, captions, text, music, sound effects, transitions and versions of the cut, with the narration and pictures earlier Steps made.",
10
+ "kind": "workspace",
11
+ "runtime": "module",
12
+ "license": "MIT",
13
+ "inputs": {
14
+ "editor": {
15
+ "type": "timeline",
16
+ "primary": true,
17
+ "required": false,
18
+ "semantic": "editor_timeline",
19
+ "description": "The Editor document on this screen: every timeline, clip, caption and setting."
20
+ },
21
+ "cut": {
22
+ "type": "timeline",
23
+ "required": false,
24
+ "semantic": "editor_handoff",
25
+ "description": "An Editor cut from an earlier Step (White Board Motion's boards), opened when this Editor has no edit of its own yet."
26
+ },
27
+ "media": {
28
+ "type": "asset[]",
29
+ "required": false,
30
+ "semantic": "editable_media",
31
+ "description": "Clips and pictures from earlier Steps, shown in the Editor's media to put on the timeline."
32
+ },
33
+ "canvas": {
34
+ "type": "pipeline",
35
+ "required": false,
36
+ "semantic": "canvas_document",
37
+ "description": "A Canvas from an earlier Step: its pictures and clips go in the Editor's media and on a new timeline."
38
+ },
39
+ "narrationAudio": {
40
+ "type": "audio",
41
+ "required": false,
42
+ "semantic": "narration_audio",
43
+ "description": "The narration from an earlier Step, placed on the voiceover track."
44
+ },
45
+ "narrationTiming": {
46
+ "type": "transcript",
47
+ "required": false,
48
+ "semantic": "timed_transcript",
49
+ "description": "The narration's word timings, for captions and cuts."
50
+ },
51
+ "narrationScript": {
52
+ "type": "script",
53
+ "required": false,
54
+ "semantic": "narration_script",
55
+ "description": "The narration script."
56
+ },
57
+ "panelImages": {
58
+ "type": "asset[]",
59
+ "required": false,
60
+ "semantic": "panel_images",
61
+ "description": "The saved pictures, clips and GIFs of the Image Panels, for the timeline and its export."
62
+ },
63
+ "panels": {
64
+ "type": "panel-document",
65
+ "required": false,
66
+ "semantic": "image_panel_document",
67
+ "description": "Image Panels from an earlier Step, laid out on the timeline."
68
+ },
69
+ "make": {
70
+ "type": "text",
71
+ "required": false,
72
+ "semantic": "editor_action",
73
+ "description": "What a run makes: a picture, a clip, word timings, a saved file or the result.",
74
+ "sources": [
75
+ "view"
76
+ ]
77
+ },
78
+ "request": {
79
+ "type": "json",
80
+ "schema": "json",
81
+ "required": false,
82
+ "semantic": "editor_request",
83
+ "description": "The details of what to make.",
84
+ "sources": [
85
+ "view"
86
+ ]
87
+ },
88
+ "references": {
89
+ "type": "image[]",
90
+ "required": false,
91
+ "semantic": "reference_image",
92
+ "description": "Saved pictures a new picture keeps.",
93
+ "sources": [
94
+ "view"
95
+ ]
96
+ },
97
+ "firstFrame": {
98
+ "type": "image",
99
+ "required": false,
100
+ "semantic": "first_frame",
101
+ "description": "The picture a clip starts on.",
102
+ "sources": [
103
+ "view"
104
+ ]
105
+ },
106
+ "lastFrame": {
107
+ "type": "image",
108
+ "required": false,
109
+ "semantic": "last_frame",
110
+ "description": "The picture a clip ends on.",
111
+ "sources": [
112
+ "view"
113
+ ]
114
+ },
115
+ "audio": {
116
+ "type": "asset",
117
+ "required": false,
118
+ "semantic": "source_audio",
119
+ "description": "Saved sound to find the words of.",
120
+ "sources": [
121
+ "view"
122
+ ]
123
+ },
124
+ "files": {
125
+ "type": "asset[]",
126
+ "required": false,
127
+ "semantic": "export_media",
128
+ "description": "The saved clips, pictures and sound an export's timeline uses.",
129
+ "sources": [
130
+ "view"
131
+ ]
132
+ },
133
+ "dataUrl": {
134
+ "type": "text",
135
+ "required": false,
136
+ "semantic": "file_data",
137
+ "description": "A file the screen saves: a picture, a clip or a sound.",
138
+ "sources": [
139
+ "view"
140
+ ]
141
+ },
142
+ "key": {
143
+ "type": "text",
144
+ "required": false,
145
+ "semantic": "storage_key",
146
+ "description": "A file name or a saved setting of this Block.",
147
+ "sources": [
148
+ "view"
149
+ ]
150
+ },
151
+ "value": {
152
+ "type": "json",
153
+ "schema": "json",
154
+ "required": false,
155
+ "semantic": "storage_value",
156
+ "description": "The value of that setting.",
157
+ "sources": [
158
+ "view"
159
+ ]
160
+ }
161
+ },
162
+ "outputs": {
163
+ "editor": {
164
+ "type": "timeline",
165
+ "primary": true,
166
+ "required": false,
167
+ "role": "editor_timeline",
168
+ "semantic": "editor_timeline",
169
+ "description": "The complete Editor document for the next Step."
170
+ },
171
+ "export": {
172
+ "type": "video",
173
+ "required": false,
174
+ "semantic": "finished_video",
175
+ "description": "The exported video, to download or pass on."
176
+ },
177
+ "exportAudio": {
178
+ "type": "audio",
179
+ "required": false,
180
+ "semantic": "finished_audio",
181
+ "description": "The exported sound alone (MP3), to download or pass on."
182
+ },
183
+ "made": {
184
+ "type": "asset[]",
185
+ "required": false,
186
+ "semantic": "made_media",
187
+ "description": "The pictures, clips or files a run made."
188
+ },
189
+ "result": {
190
+ "type": "json",
191
+ "schema": "json",
192
+ "required": false,
193
+ "semantic": "editor_answer",
194
+ "description": "What the screen asked for: a picture, word timings, a saved file.",
195
+ "sources": [
196
+ "view"
197
+ ]
198
+ }
199
+ },
200
+ "permissions": {
201
+ "project": [],
202
+ "capabilities": [
203
+ "image.generate",
204
+ "video.generate",
205
+ "audio.transcribe",
206
+ "text.generate",
207
+ "audio.sfx",
208
+ "audio.music"
209
+ ],
210
+ "actions": [
211
+ "stock.search",
212
+ "final.render"
213
+ ],
214
+ "network": [],
215
+ "filesystem": [],
216
+ "secrets": []
217
+ },
218
+ "platforms": {
219
+ "phone": {
220
+ "supported": true
221
+ },
222
+ "browser": {
223
+ "supported": true
224
+ },
225
+ "desktop": {
226
+ "supported": true
227
+ }
228
+ },
229
+ "provenance": {
230
+ "notice": "MIT License\n\nCopyright (c) <year> <copyright holders>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
231
+ },
232
+ "features": [
233
+ {
234
+ "id": "motion",
235
+ "label": "Motion presets",
236
+ "does": "Zoom, pan, shake and slow-motion moves on a clip or picture.",
237
+ "asks": [
238
+ "slow-mo zoom on the wheels",
239
+ "ken burns pan over my photos",
240
+ "zoom in on the product"
241
+ ]
242
+ },
243
+ {
244
+ "id": "transitions",
245
+ "label": "Transitions",
246
+ "does": "Fades, whips, wipes, zooms and morphs between clips.",
247
+ "asks": [
248
+ "smooth transitions between clips",
249
+ "whip pan between shots",
250
+ "fade to black at the end"
251
+ ]
252
+ },
253
+ {
254
+ "id": "effects",
255
+ "label": "Effects and motion graphics",
256
+ "does": "One-hit flashes, glitches and light leaks, and animated titles, counting numbers and charts made from a prompt.",
257
+ "asks": [
258
+ "add a flash on the beat",
259
+ "glitch effect on the logo",
260
+ "count up to 10,000 sales",
261
+ "add a bar chart of sales"
262
+ ]
263
+ },
264
+ {
265
+ "id": "text",
266
+ "label": "Text, titles and captions",
267
+ "does": "Moving titles, lower thirds, on-screen words and synced captions with word-by-word highlight.",
268
+ "asks": [
269
+ "add a title at the start",
270
+ "lower third with my name",
271
+ "add captions to my video",
272
+ "karaoke style captions"
273
+ ]
274
+ },
275
+ {
276
+ "id": "roughcut",
277
+ "label": "Auto edit",
278
+ "does": "Cuts silences and picks the best moments into a first edit.",
279
+ "asks": [
280
+ "cut out the pauses",
281
+ "make a quick first cut",
282
+ "remove dead air"
283
+ ]
284
+ },
285
+ {
286
+ "id": "audio",
287
+ "label": "Music and sound",
288
+ "does": "Music, voiceover and sound effects on their own tracks with volume.",
289
+ "asks": [
290
+ "add background music",
291
+ "lower the music under the voice",
292
+ "add a whoosh sound"
293
+ ]
294
+ },
295
+ {
296
+ "id": "stickers",
297
+ "label": "Stickers",
298
+ "does": "Shapes, arrows and stickers placed over the video.",
299
+ "asks": [
300
+ "circle the car with an arrow",
301
+ "add a sale sticker"
302
+ ]
303
+ },
304
+ {
305
+ "id": "gifs",
306
+ "label": "Reaction GIFs",
307
+ "does": "Search GIFs and drop one on the timeline over a clip.",
308
+ "asks": [
309
+ "add a reaction gif",
310
+ "put a fire gif over my car",
311
+ "funny gif when he laughs"
312
+ ]
313
+ },
314
+ {
315
+ "id": "templates",
316
+ "label": "Templates",
317
+ "does": "Ready-made layouts and styles to start an edit from.",
318
+ "asks": [
319
+ "use a template for a tiktok ad",
320
+ "start from a ready layout"
321
+ ]
322
+ },
323
+ {
324
+ "id": "ai",
325
+ "label": "Edit with AI",
326
+ "does": "Describe a change in words and the edit is made on the timeline.",
327
+ "asks": [
328
+ "make it punchier",
329
+ "shorten it to 30 seconds",
330
+ "edit it for me"
331
+ ]
332
+ },
333
+ {
334
+ "id": "looks",
335
+ "label": "Color looks",
336
+ "does": "Cinematic looks such as Filmic or Teal and Orange, and brightness, contrast, saturation and warmth.",
337
+ "asks": [
338
+ "make it look cinematic",
339
+ "teal and orange grade",
340
+ "make it brighter"
341
+ ]
342
+ },
343
+ {
344
+ "id": "export",
345
+ "label": "Export",
346
+ "does": "Renders the video as an MP4, or only its audio as an MP3, in the size and quality chosen.",
347
+ "asks": [
348
+ "export my video",
349
+ "download the mp4",
350
+ "export vertical for tiktok",
351
+ "export just the audio"
352
+ ]
353
+ },
354
+ {
355
+ "id": "export-timeline",
356
+ "label": "Export timeline",
357
+ "does": "Downloads the timeline as XML, or a ZIP with its media, for another editing app.",
358
+ "asks": [
359
+ "export to premiere",
360
+ "download the project as xml",
361
+ "send my edit to final cut"
362
+ ]
363
+ }
364
+ ]
365
+ },
366
+ "module": {
367
+ "schemaVersion": 1,
368
+ "entry": "main.js",
369
+ "files": {
370
+ "main.js": {
371
+ "sha256": "6ef1b63b90441b9ab1b22c770a31c27db569831b43b8bd0037c04a63969ffd0b",
372
+ "bytes": 17367,
373
+ "mediaType": "text/javascript"
374
+ }
375
+ }
376
+ },
377
+ "view": {
378
+ "html": "<div id=\"root\" data-stillmade-layout=\"own\"></div>\n",
379
+ "css": "/* ============================================================\n StillMade AI — Foundations applied to the AI Video Workflow app.\n Two surfaces only: cream (#F7F0DB) and ink (#0A0A0A).\n Everything else is a tonal shift of one of those two.\n Dark is the default; [data-mode=\"light\"] flips the surface.\n ============================================================ */\n/* When the licensed Balgin Skinny Italic files are available, drop\n them into client/public/fonts and uncomment this block — the\n display face points at \"Balgin\" first and switches automatically. */\n/*\n@font-face {\n font-family: \"Balgin\";\n src: url(\"/fonts/Balgin-SkinnyItalic.woff2\") format(\"woff2\");\n font-weight: 100;\n font-style: italic;\n font-display: swap;\n}\n*/\n\n*, *::before, *::after {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n\n/* ============================================================\n Animated Studio — fixed slate theme (ignores light/dark).\n :root[...] specificity beats [data-mode], so it wins in both modes.\n ============================================================ */\n:root[data-anim-theme=\"1\"]:not([data-mode=\"white\"]) {\n color-scheme: dark;\n --bg: #0A0D13;\n --bg-elev-1: #0F141C;\n --bg-elev-2: #1A2230;\n --bg-inset: #0F141C;\n --fg: #D8DEE8;\n --fg-muted: #8B98AC;\n --fg-subtle: #64748B;\n --fg-on-accent: #0A0D13;\n --border-tok: #232D3D;\n --border-strong: #303D50;\n --divider: #1A2230;\n --hover-wash: rgba(139, 152, 172, 0.08);\n --press-wash: rgba(139, 152, 172, 0.16);\n --selection-bg: #303D50;\n --selection-fg: #D8DEE8;\n\n --bg-primary: #0A0D13;\n --bg-secondary: #0F141C;\n --bg-tertiary: #1A2230;\n --bg-elevated: #1A2230;\n --bg-hover: #232D3D;\n --border: #232D3D;\n --border-light: #303D50;\n --accent: #8B98AC;\n --accent-hover: #B6C0CE;\n --accent-dim: rgba(139, 152, 172, 0.12);\n --accent-glow: rgba(139, 152, 172, 0.25);\n --accent-contrast: #0A0D13;\n --editor-standout-bg: #D8DEE8;\n --editor-standout-fg: #0A0D13;\n --text-primary: #D8DEE8;\n --text-secondary: #8B98AC;\n --text-muted: #64748B;\n --success: #8B98AC;\n --warning: #B6C0CE;\n --danger: #B6C0CE;\n}\n\nhtml, body {\n height: 100%;\n width: 100%;\n overflow: hidden;\n background: var(--bg);\n color: var(--fg);\n font-family: var(--font-body);\n font-size: 14px;\n line-height: 1.6;\n letter-spacing: var(--tr-body);\n -webkit-font-smoothing: antialiased;\n text-rendering: optimizeLegibility;\n}\n\n#root {\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n/* App shell fills the dynamically-visible viewport. 100dvh (not 100vh) so the\n mobile browser's collapsing toolbar doesn't push the inner scroll container —\n and its last row of content — below the visible area where it can't be\n reached. vh fallback first for browsers without dvh. */\n.sm-app-shell { height: 100vh; height: 100dvh; }\n\n/* Full-screen mobile nav menu enters with a composited fade+slide (rule 6). */\n@keyframes sm-navmenu-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }\n.mobile-nav-menu { animation: sm-navmenu-in 200ms cubic-bezier(0.22, 1, 0.36, 1); }\n\n::selection { background: var(--selection-bg); color: var(--selection-fg); }\n\n/* Display lockups — always italic, always thin. This is the brand. */\n.display, h1, h2 {\n font-family: var(--font-display);\n font-style: italic;\n font-weight: 200;\n line-height: var(--lh-tight);\n letter-spacing: var(--tr-display);\n margin: 0;\n}\n.display { line-height: var(--lh-display); }\nh3, h4 {\n font-family: var(--font-body);\n font-weight: 500;\n line-height: var(--lh-tight);\n letter-spacing: -0.01em;\n margin: 0;\n}\n\n.eyebrow {font-size: 11px;\n color: var(--fg-muted);\n font-weight: 500;}\n\ncode, pre, .mono {\n font-family: var(--font-mono);\n font-size: 0.92em;\n}\n/* Listing IDs are interface text, not code. */\n.listing-identity code { font-family: inherit; font-size: inherit; }\n\nbutton {\n font-family: var(--font-body);\n cursor: pointer;\n border: none;\n outline: none;\n letter-spacing: var(--tr-body);\n}\n\ntextarea, input, select {\n font-family: var(--font-body);\n outline: none;\n}\n\n/* ─── Native <select> → app theme, applied GLOBALLY to every select (now + future),\n so individual call sites never need touching. This themes the CLOSED control via the\n theme tokens while keeping the browser's NATIVE open/close + selection behavior fully\n intact (so picking an option always works). The OPEN popup is drawn by the OS —\n macOS/Safari can't be CSS-themed — and `option` colors below theme it only where the\n browser honors them (Chrome/Firefox on Win/Linux). A truly themed popup everywhere\n needs a custom dropdown component, not native <select>. ─── */\nselect {\n background-color: var(--bg-elev-1);\n color: var(--text-primary);\n border: 1px solid var(--border);\n border-radius: var(--r-2);\n}\nselect option,\nselect optgroup {\n background-color: var(--bg-elevated);\n color: var(--text-primary);\n}\nselect option:checked { font-weight: 600; }\n\n/* ─── ThemedSelect — a custom dropdown whose OPEN popup is themed too (the one thing a\n native <select> can't do on macOS). The trigger mirrors the native `select` look above,\n so a converted call site is visually identical when closed; the popup + options are ours,\n including per-option disabled state and an \"ⓘ\" info tooltip (e.g. Coming Soon). ─── */\n.tsel-trigger {\n display: inline-flex; align-items: center; justify-content: space-between; gap: 8px;\n background-color: var(--bg-elev-1); color: var(--text-primary);\n border: 1px solid var(--border); border-radius: var(--r-2);\n font-family: var(--font-body); font-size: inherit; padding: 6px 10px;\n cursor: pointer; text-align: left;\n}\n.tsel-trigger:hover { border-color: var(--border-strong); }\n.tsel-trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }\n.tsel-trigger:disabled { opacity: 0.5; cursor: default; }\n.tsel-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.tsel-caret { flex: none; font-size: 10px; opacity: 0.7; }\n.tsel-pop {\n /* Above EVERY overlay that can contain a select, not just app dialogs (100000):\n the mobile BottomSheet is 100050, TemplateStudio 100090 and the PreviewGrid\n lightbox 100095. At the old 100010 the popup painted UNDERNEATH the sheet\n that opened it, so every dropdown inside a mobile sheet (motion easing,\n font, position, word-reveal swap/emphasis/trail layout) and the Whoosh\n transition's Direction slot in TemplateStudio silently did nothing. */\n z-index: 100100;\n max-height: 260px; overflow-y: auto;\n background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--r-2);\n box-shadow: 0 8px 28px rgba(0, 0, 0, 0.28); padding: 4px;\n}\n.tsel-opt {\n display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-radius: 6px;\n color: var(--text-primary); cursor: pointer; font-size: 13px; white-space: nowrap;\n}\n.tsel-opt[data-active=\"true\"] { background: var(--bg-elev-1); }\n.tsel-opt[aria-selected=\"true\"] { font-weight: 600; }\n.tsel-opt[aria-disabled=\"true\"] { color: var(--text-muted); cursor: default; }\n.tsel-opt[aria-disabled=\"true\"][data-active=\"true\"] { background: transparent; }\n.tsel-check { flex: none; width: 12px; font-size: 11px; opacity: 0.8; }\n.tsel-opt-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; }\n.tsel-note {\n position: relative;\n flex: none; width: 16px; height: 16px; font-size: 11px; cursor: help;\n color: var(--text-muted); border: 1px solid var(--border); border-radius: 999px;\n display: inline-flex; align-items: center; justify-content: center;\n}\n.tsel-note:hover { color: var(--text-primary); border-color: var(--border-strong); }\n/* Info tooltip (e.g. \"Coming Soon\"). Portaled to <body> and positioned to the RIGHT of the ⓘ\n so the popup's overflow can never clip it. */\n.tsel-tip {\n z-index: 100110; /* one step above .tsel-pop — it annotates an option inside it */\n background: var(--text-primary); color: var(--bg-elevated);\n padding: 4px 8px; border-radius: 5px; font-size: 11px; font-weight: 600;\n max-width: min(260px, calc(100vw - 16px)); box-sizing: border-box;\n white-space: normal; pointer-events: none;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);\n}\n@media (max-width: 767.98px), (max-height: 480px) and (pointer: coarse) {\n .tsel-opt { min-height: 44px; box-sizing: border-box; font-size: 14px; }\n .tsel-note { width: 44px; height: 44px; flex: 0 0 44px; font-size: 14px; }\n}\n\n.video-editor input,\n.video-editor select,\n.video-editor textarea {\n width: 100%;\n min-width: 0;\n background: var(--bg-inset);\n color: var(--fg);\n border: var(--bw-1) solid var(--border-strong);\n border-radius: var(--r-2);\n padding: 7px 8px;\n font-family: var(--font-body);\n font-size: 12px;\n accent-color: var(--signal);\n}\n\n.video-editor,\n.mobile-editor-root {\n user-select: none;\n -webkit-user-select: none;\n /* Kill the iOS long-press callout (Copy / Search) and the selection\n magnifier so a touch drag/trim/long-press on the timeline, labels, or\n buttons can't accidentally select UI text. Inputs re-enable both below. */\n -webkit-touch-callout: none;\n}\n\n.video-editor input,\n.video-editor select,\n.video-editor textarea,\n.video-editor [contenteditable=\"true\"],\n.mobile-editor-root input,\n.mobile-editor-root select,\n.mobile-editor-root textarea,\n.mobile-editor-root [contenteditable=\"true\"] {\n user-select: text;\n -webkit-user-select: text;\n -webkit-touch-callout: default;\n}\n\n.video-editor input[type=\"range\"] {\n padding: 0;\n border: 0;\n background: transparent;\n}\n\n.video-editor input[type=\"checkbox\"] {\n width: auto;\n min-width: auto;\n}\n\n.video-editor select:focus,\n.video-editor input:focus,\n.video-editor textarea:focus {\n border-color: var(--signal);\n box-shadow: 0 0 0 1px var(--signal-dim);\n}\n\n/* Focus — always visible, always the signal color */\n:focus-visible {\n outline: 2px solid var(--signal);\n outline-offset: 2px;\n}\n\n::-webkit-scrollbar {\n width: 6px;\n height: 6px;\n}\n::-webkit-scrollbar-track { background: transparent; }\n::-webkit-scrollbar-thumb {\n background: var(--border-strong);\n border-radius: var(--r-pill);\n}\n::-webkit-scrollbar-thumb:hover { background: var(--fg-subtle); }\n\n/* Hide the scrollbar chrome while keeping the element scrollable (wheel /\n trackpad). Used on the editor's left panel + tab rail for a clean column. */\n.sm-hide-scrollbar { scrollbar-width: none; -ms-overflow-style: none; }\n.sm-hide-scrollbar::-webkit-scrollbar { width: 0; height: 0; display: none; }\n\n/* ============================================================\n Buttons — primary is fg/bg inversion, not a color.\n Sharp by default; pill is a deliberate touch affordance.\n ============================================================ */\n.btn {\n display: inline-flex;\n align-items: center;\n gap: var(--s-2);\n padding: var(--s-2) var(--s-4);\n border-radius: var(--r-pill);\n font-size: 13px;\n font-weight: 500;\n letter-spacing: var(--tr-body);\n transition: transform var(--dur-fast) var(--ease-out),\n background-color var(--dur-fast) var(--ease-out),\n border-color var(--dur-fast) var(--ease-out),\n color var(--dur-fast) var(--ease-out),\n opacity var(--dur-fast) var(--ease-out);\n}\n.btn:active { transform: scale(0.98); }\n\n.btn-primary {\n background: var(--fg);\n color: var(--bg);\n}\n.btn-primary:hover { background: var(--fg); opacity: 0.86; }\n.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }\n\n.btn-secondary {\n background: transparent;\n color: var(--fg);\n border: var(--bw-1) solid var(--border-strong);\n}\n.btn-secondary:hover { background: var(--hover-wash); border-color: var(--fg); }\n\n.btn-ghost {\n background: transparent;\n color: var(--fg-muted);\n padding: 6px 10px;\n}\n.btn-ghost:hover { background: var(--hover-wash); color: var(--fg); }\n\n.btn-icon {\n background: transparent;\n color: var(--fg-muted);\n padding: 5px;\n border-radius: var(--r-2);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n transition: background-color var(--dur-fast) var(--ease-out),\n color var(--dur-fast) var(--ease-out),\n transform var(--dur-fast) var(--ease-out);\n}\n.btn-icon:hover { background: var(--hover-wash); color: var(--fg); }\n.btn-icon:active { transform: scale(0.98); }\n.btn-icon:disabled { opacity: 0.4; cursor: not-allowed; }\n\n.typing-indicator {\n display: flex;\n gap: 4px;\n align-items: center;\n padding: 4px 0;\n}\n.typing-indicator span {\n width: 6px;\n height: 6px;\n border-radius: var(--r-pill);\n background: var(--fg-subtle);\n animation: bounce 1.2s infinite;\n}\n.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }\n.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }\n\n@keyframes bounce {\n 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }\n 30% { transform: translateY(-5px); opacity: 1; }\n}\n\n@keyframes fadeIn {\n from { opacity: 0; transform: translateY(6px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@keyframes spin {\n to { transform: rotate(360deg); }\n}\n\n@keyframes pulse {\n 0%, 100% { opacity: 0.4; transform: scale(0.9); }\n 50% { opacity: 1; transform: scale(1.1); }\n}\n\n/* Toast slide-in for ToastHost (services/toast.js) */\n@keyframes toastIn {\n from { opacity: 0; transform: translateY(8px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* Q3 — keyboard focus rings (focus-visible only, so mouse users don't see them) */\nbutton:focus-visible,\n[role=\"button\"]:focus-visible,\na:focus-visible,\ninput:focus-visible,\nselect:focus-visible,\ntextarea:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n border-radius: 4px;\n}\n\n/* Typing fields never show a focus outline or ring (docs/UI_AESTHETIC.md):\n the caret marks focus. Buttons, links, switches and checkboxes keep theirs.\n !important so component focus-visible rules cannot bring the ring back. */\ninput:not([type=\"checkbox\"]):not([type=\"radio\"]):not([type=\"range\"]):not([type=\"color\"]):not([type=\"file\"]):not([type=\"button\"]):not([type=\"submit\"]):not([type=\"reset\"]):is(:focus, :focus-visible),\ntextarea:is(:focus, :focus-visible),\nselect:is(:focus, :focus-visible),\n[contenteditable]:not([contenteditable=\"false\"]):is(:focus, :focus-visible) {\n outline: none !important;\n}\n\n/* Skeleton shimmer for P2 loaders */\n@keyframes skeletonShimmer {\n 0% { background-position: -200% 0; }\n 100% { background-position: 200% 0; }\n}\n.skeleton {\n background: linear-gradient(90deg,\n rgba(255,255,255,0.03) 0%,\n rgba(255,255,255,0.10) 50%,\n rgba(255,255,255,0.03) 100%);\n background-size: 200% 100%;\n animation: skeletonShimmer 1.4s ease-in-out infinite;\n border-radius: 6px;\n}\n\n/* Node-state visuals (E1) — applied to NodeShell wrapper.\n States: empty (dashed ghost) / generating (shimmer ring) / failed (red stripe) / complete */\n/* opacity:0.78 removed — CSS opacity cascades to children and faded the\n image too. Dashed border alone is the \"pending\" signal now. */\n.node-state-empty { border-style: dashed !important; }\n/* The pulsing glow animates OPACITY on a pseudo-element (compositor-only)\n instead of keyframing box-shadow, which repainted the node's full shadow\n region on the main thread every frame for the whole generation — with\n batch generation pulsing many nodes at once. Same look: constant 2px\n accent ring + a breathing 18px glow. */\n.node-state-generating { box-shadow: 0 0 0 2px var(--accent); position: relative; }\n.node-state-generating::after {\n content: '';\n position: absolute;\n inset: -2px;\n border-radius: inherit;\n box-shadow: 0 0 18px rgba(247, 200, 50, 0.55);\n animation: nodePulse 1.6s ease-in-out infinite;\n pointer-events: none;\n will-change: opacity;\n}\n@keyframes nodePulse {\n 0%, 100% { opacity: 0.36; }\n 50% { opacity: 1; }\n}\n.node-state-warning { position: relative; border-color: var(--attention-warning) !important; }\n.node-state-warning::after {\n content: ''; position: absolute; inset: -2px; border-radius: inherit; pointer-events: none;\n box-shadow: 0 0 0 1px var(--attention-warning), 0 0 14px color-mix(in srgb, var(--attention-warning) 40%, transparent);\n}\n.node-state-failed { border-top: 4px solid #d04848 !important; }\n.node-state-failed::before {\n content: '⚠ '; color: #d04848; font-weight: 800;\n position: absolute; top: 6px; left: 8px; font-size: 12px; z-index: 2;\n pointer-events: none;\n}\n\n/* React Flow overrides */\n.react-flow__node { cursor: default; }\n/* Promote layers only WHILE a gesture is live (xyflow toggles these classes\n automatically at drag start/end) — the dragged card and the panning\n viewport move on the compositor instead of repainting their 8-layer\n shadows every frame. No idle memory cost: the hint drops with the class. */\n.react-flow__node.dragging { will-change: transform; }\n.react-flow__pane.dragging .react-flow__viewport { will-change: transform; }\n\n/* ── 3D node extrusion (StillMade design bundle) ────────────────────\n Stacked box-shadow that follows the corner radius, plus an ambient\n cast. Compositor-only: no per-frame JS, no extra DOM. Capped at 6\n solid layers so paint cost stays bounded on large scenes (147 nodes).\n Excludes layout-only node types (scene groups, shot labels, sticky\n notes) which aren't cards. */\n.react-flow__node:not(.react-flow__node-sceneGroupNode):not(.react-flow__node-shotLabelNode):not(.react-flow__node-stickyNoteNode):not(.react-flow__node-transitionNode) {\n border-radius: 10px;\n box-shadow:\n 1px 1px 0 var(--node-side, #d8cfb3),\n 2px 2px 0 var(--node-side, #d8cfb3),\n 3px 3px 0 var(--node-side, #d8cfb3),\n 4px 4px 0 var(--node-side, #d8cfb3),\n 5px 5px 0 var(--node-side, #d8cfb3),\n 6px 6px 0 var(--node-side, #d8cfb3),\n 7px 7px 0 var(--node-edge, rgba(10,10,10,0.18)),\n 14px 16px 18px -4px rgba(10,10,10,0.18);\n /* No transition / hover-lift / drag-lift — animating transform on\n react-flow's already-transformed wrapper compounds onto its pan\n matrix and makes pans/drags feel laggy. Static shadow only. */\n}\n[data-mode=\"dark\"] .react-flow__node:not(.react-flow__node-sceneGroupNode):not(.react-flow__node-shotLabelNode):not(.react-flow__node-stickyNoteNode):not(.react-flow__node-transitionNode) {\n --node-side: #1f1d18;\n --node-edge: rgba(247, 240, 219, 0.18);\n}\n/* Zoom controls laid out as a 2×2 square (positioned above the Feedback pill\n in JS). Overrides React Flow v12's default flex-column. */\n.react-flow__controls {\n display: grid !important;\n grid-template-columns: repeat(2, 1fr) !important;\n}\n.react-flow__controls button {\n background: var(--bg-elev-2) !important;\n border-color: var(--border-tok) !important;\n color: var(--fg-muted) !important;\n fill: var(--fg-muted) !important;\n}\n.react-flow__controls button:hover {\n background: var(--ink-300) !important;\n}\n.react-flow__edge-path {\n stroke: var(--border-strong);\n}\n\n/* While actively panning a large graph, skip painting the edge layer (the main\n big-graph pan cost). Edges stay mounted — only paint is suppressed — so they\n reappear instantly with no remount when motion stops. See WorkflowCanvas\n markPanning / EDGE_PAN_HIDE_MIN. */\n.react-flow.rf-edges-panning .react-flow__edges {\n display: none;\n}\n\n.react-flow__node.node-controls-locked input,\n.react-flow__node.node-controls-locked textarea,\n.react-flow__node.node-controls-locked select,\n.react-flow__node.node-controls-locked button,\n.react-flow__node.node-controls-locked [role=\"button\"] {\n pointer-events: none !important;\n}\n\n.react-flow__node.node-controls-locked {\n cursor: grab;\n}\n\n.react-flow__node.node-controls-locked:active {\n cursor: grabbing;\n}\n\n.react-flow__node.scene-add-available-node {\n filter: drop-shadow(0 0 10px rgba(247,240,219,0.22));\n}\n\n.react-flow__node.scene-add-available-node > div {\n outline: 1px solid var(--border-strong);\n outline-offset: 4px;\n border-radius: var(--r-3);\n}\n\n.react-flow__node.scene-add-available-node::after {\n content: \"\";\n position: absolute;\n inset: -8px;\n border: 1px dashed var(--border-strong);\n border-radius: var(--r-3);\n pointer-events: none;\n}\n.react-flow__node.node-replace-available {\n filter: drop-shadow(0 0 10px rgba(247,240,219,0.22));\n}\n\n.react-flow__node.node-replace-available > div {\n outline: 1px solid var(--border-strong);\n outline-offset: 4px;\n border-radius: var(--r-3);\n}\n\n.react-flow__node.node-replace-available::after {\n content: \"\";\n position: absolute;\n inset: -8px;\n border: 1px dashed var(--border-strong);\n border-radius: var(--r-3);\n pointer-events: none;\n}\n\nselect option {\n background: var(--bg-elev-2);\n color: var(--fg);\n}\n\n.spinner {\n width: 16px;\n height: 16px;\n border: 2px solid var(--border-tok);\n border-top-color: var(--fg);\n border-radius: var(--r-pill);\n animation: spin 0.7s linear infinite;\n}\n\n.media-upload-preview:hover .media-upload-overlay {\n opacity: 1 !important;\n}\n\n/* Reduced motion — respect it everywhere */\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after {\n animation-duration: 1ms !important;\n transition-duration: 1ms !important;\n }\n}\n\n/* All Assets grid: overlay shows on hover via CSS so the parent\n doesn't need a hoveredId state that re-renders the whole list. */\n.assets-card .assets-card-overlay { opacity: 0; transition: opacity 0.15s; }\n.assets-card:hover .assets-card-overlay { opacity: 1; }\n\n/* YouTube hub — Video performance rows: soft accent glow on hover so the rows\n feel clickable. Inset (not outer) shadow because the table clips overflow. */\n.yt-video-row { transition: background 0.12s ease, box-shadow 0.12s ease, color 0.12s ease; }\n.yt-video-row:hover {\n background: var(--hover-wash) !important;\n box-shadow: inset 2px 0 0 var(--accent), inset 0 0 14px var(--accent-glow);\n color: var(--text-primary);\n}\n\n/* Chat bubble text selection — the global ::selection token (which uses\n ink-600 in light mode) collides with the user bubble's dark accent\n background, making highlights invisible. Force the signal/amber color\n here so highlights pop on both user (dark bubble) and assistant\n (cream bubble) regardless of theme. */\n.chat-bubble *::selection { background: var(--signal); color: #0A0A0A; }\n.chat-bubble *::-moz-selection { background: var(--signal); color: #0A0A0A; }\n\n/* Chat bubble copy button — visible on hover/focus, and stays visible\n while the cursor is over the button itself (otherwise moving from\n bubble onto button would hide it before you could click). */\n.chat-bubble:hover .chat-bubble-copy,\n.chat-bubble:focus-within .chat-bubble-copy,\n.chat-bubble-copy:hover,\n.chat-bubble-copy:focus { opacity: 1 !important; }\n/* Subtle hover/active feedback so the user knows it's clickable. */\n.chat-bubble-copy:hover { background: var(--bg-elevated-2, var(--bg-elevated)); color: var(--text-primary) !important; }\n.chat-bubble-copy:active { transform: scale(0.94); }\n\n/* ── StillMade logo lockup — hover animation ──────────────────────────────\n Hovering the top-left lockup turns the isometric cube left-to-right around its\n vertical axis (a 3D rotateY) with a soft currentColor glow; it turns back on\n leave. Reduced-motion users get just a gentle scale + glow, no rotation. */\n.sm-brand .sm-brandmark {\n transform-origin: 50% 50%;\n transition: transform 760ms cubic-bezier(0.2, 0.8, 0.2, 1), filter 280ms ease;\n}\n.sm-brand:hover .sm-brandmark {\n transform: perspective(340px) rotateY(360deg) scale(1.06);\n filter: drop-shadow(0 0 7px color-mix(in srgb, currentColor 45%, transparent));\n will-change: transform, filter;\n}\n/* Wordmark resting position + a subtle lift on hover (a small nudge — gentler\n than the design's). Resting transform lives here (not inline) so :hover can\n override it. */\n.sm-brand .sm-word {\n transform: translate(-4px, 0px);\n transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);\n}\n.sm-brand:hover .sm-word { transform: translate(-4px, -1px); will-change: transform; }\n@media (prefers-reduced-motion: reduce) {\n .sm-brand .sm-brandmark { transition: transform 240ms ease, filter 240ms ease; }\n .sm-brand:hover .sm-brandmark { transform: scale(1.06); }\n .sm-brand:hover .sm-word { transform: translate(-4px, 0px); }\n}\n\n/* ── Mobile UI ergonomics ───────────────────────────────────────────────────\n Scoped to body.sm-mobile-ui, which App.jsx adds ONLY when the user chose the\n mobile site on a small screen, including a rotated touch phone. Ordinary\n desktop windows and the continue-anyway mode are unaffected.\n\n The app is inline-styled, so these few universal touch fixes are applied here\n once instead of edited into every input across every screen:\n - 16px text fields stop iOS from zooming the page on focus (needs !important\n to beat the inline font sizes).\n - comfortable minimum tap target on buttons/selects. */\nbody.sm-mobile-ui input,\nbody.sm-mobile-ui textarea,\nbody.sm-mobile-ui select {\n font-size: 16px !important;\n}\nbody.sm-mobile-ui button,\nbody.sm-mobile-ui select,\nbody.sm-mobile-ui [role=\"button\"] {\n min-height: 44px;\n}\n/* WebKit can ignore min-height on its native one-line select. Keep the\n physical control touch-sized and let enlarged text make it taller. */\nbody.sm-mobile-ui select:not([multiple]):not([size]) {\n height: max(44px, 2.75em);\n}\n/* Icon-only / chip-sized controls opt out of the min height so dense toolbars\n (timeline, inspector) don't balloon. */\nbody.sm-mobile-ui button.btn-icon,\nbody.sm-mobile-ui button[data-compact],\nbody.sm-mobile-ui .no-min-tap {\n min-height: 0;\n}\n/* YouTube hub analytics rows are dense multi-column grids; keep their columns\n readable and let the table scroll horizontally instead of squishing on a\n phone (the table container is overflow-x:auto). */\nbody.sm-mobile-ui .yt-video-row {\n min-width: 560px;\n}\n\n/* Frame view: the Editor fills the space StillMade gives its Step (at least 560 pixels tall). */\n#root { height: max(560px, var(--stillmade-space-height, 100vh)); }\n/* Text wraps as on StillMade's own pages (frame views break long words anywhere by default). */\nhtml { overflow-wrap: normal; }\n\n/* Stylesheets the screen's components import. */\n.bs-private-dialog{box-sizing:border-box;margin:auto;width:min(720px,calc(100vw - 32px));max-height:calc(100dvh - 32px);padding:0;border:1px solid var(--border);border-radius:var(--r-3);background:var(--bg-primary);color:var(--text-primary);font:16px/1.6 var(--font-body);overflow:auto;overflow-wrap:anywhere}\n.bs-private-dialog::backdrop{background:var(--scrim,rgba(0,0,0,.4))}\n.bs-private-dialog>header,.bs-private-dialog>footer{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:20px 24px;background:var(--bg-primary)}\n.bs-private-dialog>header{border-bottom:1px solid var(--border);position:sticky;top:0;z-index:1}.bs-private-dialog>header>div{min-width:0}.bs-private-dialog>header p{margin:4px 0 0;font-size:14px;color:var(--text-secondary)}\n.bs-private-dialog h2{margin:0;font:italic 200 30px/1.2 var(--font-display);letter-spacing:var(--tr-display)}\n.bs-private-body{padding:20px 24px}.bs-private-body>p:first-child{margin-top:0}.bs-private-dialog fieldset{min-width:0;border:1px solid var(--border);border-radius:var(--r-3);padding:16px}.bs-private-dialog legend{padding:0 6px;font-size:14px}\n.bs-private-check{display:flex;align-items:flex-start;gap:12px;margin:14px 0}.bs-private-check input{width:18px;height:18px;flex-shrink:0;margin:4px 0 0;accent-color:var(--fg)}.bs-private-note{font-size:14px;color:var(--text-secondary)}\n.bs-private-dialog>footer{position:sticky;bottom:0;border-top:1px solid var(--border);justify-content:flex-end;z-index:1}.bs-private-dialog button,.bs-team-library button{font:inherit;max-width:100%;padding:8px 16px;border:1px solid var(--border-strong);border-radius:var(--r-pill);background:transparent;color:inherit;cursor:pointer}.bs-private-dialog button:disabled{opacity:.5;cursor:default}.bs-private-dialog .bs-private-primary{background:var(--fg);color:var(--bg-primary);border-color:var(--fg)}.bs-private-dialog :is(button,input):focus-visible{outline:2px solid var(--signal);outline-offset:3px}\n.bs-private-review{width:min(1100px,calc(100vw - 32px))}.bs-private-review .bs-import-review{margin:0}.bs-private-review .bs-import-review input[type=checkbox]{width:auto}.bs-private-review .bs-actions{position:sticky;bottom:0;background:var(--bg-secondary);border-top:1px solid var(--border);padding:12px 0}.bs-private-review .bs-actions button{font-size:16px}\n.bs-team-library{border-top:1px solid var(--border);margin-top:28px;padding-top:20px}.bs-team-library>header{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}.bs-team-library h2{margin:0}.bs-team-library .bs-card{border:1px solid var(--border);background:var(--bg-secondary)}.bs-team-library h3{margin:0;font-weight:500}.bs-team-library .bs-card button{display:block;margin-top:16px}\n@media(max-width:600px){.bs-private-dialog{width:calc(100vw - 16px);max-height:calc(100dvh - 16px)}.bs-private-dialog>header,.bs-private-dialog>footer,.bs-private-body{padding:16px}.bs-private-dialog>header{align-items:flex-start}.bs-private-dialog h2{font-size:26px}.bs-private-dialog>header>button{flex-shrink:0}.bs-team-library>header{flex-direction:column}.bs-private-review .bs-import-review{padding:16px}.bs-private-review .bs-private-body{padding:8px}.bs-private-dialog :is(input,select,textarea){font-size:16px}}\n\n.bs-private-check span>small{display:block}\n\n.publish-build-review{width:min(760px,calc(100vw - 32px))}\n.publish-build-review .bs-private-body{display:grid;gap:20px}\n.publish-build-review h3{margin:0 0 8px;font-size:18px;font-weight:600}\n.publish-build-review p{margin:8px 0;color:var(--text-secondary)}\n.publish-build-review details{border-top:1px solid var(--border);padding-top:16px}\n.publish-build-review summary{cursor:pointer;font-weight:600}\n.publish-build-review form>footer{display:flex;gap:12px;justify-content:flex-end;position:sticky;bottom:0;padding:18px 24px;border-top:1px solid var(--border);background:var(--bg-primary)}\n.publish-build-review .bs-private-check{cursor:pointer}\n@media(max-width:540px){.publish-build-review form>footer{flex-direction:column;padding:16px}.publish-build-review form>footer button{width:100%}}\n\n.project-type-start-review .bs-private-body>label{display:grid;gap:8px}.project-type-start-review :is(input,select,textarea){box-sizing:border-box;width:100%;padding:10px 12px;background:var(--bg-primary);color:var(--text-primary);border:1px solid var(--border);border-radius:var(--r-3);font:inherit}.project-type-start-review textarea{resize:vertical}\n\n.block-audio-library{min-width:0;margin-top:20px;padding-top:16px;border-top:1px solid var(--border-tok);color:var(--fg);font:16px/1.5 var(--font-body);overflow-wrap:anywhere}\n.block-audio-library h3{font:200 italic 24px/1.2 var(--font-display);margin:0 0 14px}\n.bal-recording{display:grid;gap:8px;min-width:0;padding:12px 0}.bal-recording+.bal-recording{border-top:1px solid var(--border-tok)}.bal-recording p{margin:0;color:var(--fg-muted);font-size:13px}\n.bal-recording audio{display:block;min-width:0;width:100%;max-width:100%;height:44px}.bal-recording button{width:100%;min-width:0;font:inherit;padding:9px 14px;border:1px solid var(--border-tok);border-radius:var(--r-pill);background:var(--bg-elevated);color:var(--fg);cursor:pointer}.bal-recording button:disabled{opacity:.45;cursor:default}.bal-recording button:focus-visible{outline:2px solid var(--signal);outline-offset:3px}\n\n.timeline-edit-preview{margin:12px 0;padding:16px;border:1px solid var(--border-tok);border-radius:var(--r-card,12px);background:var(--bg-elevated);color:var(--fg);font:16px/1.5 var(--font-body);min-width:0;overflow-wrap:anywhere}\n.timeline-edit-preview figcaption{font:200 italic 24px/1.2 var(--font-display)}\n.timeline-edit-preview>p{color:var(--fg-muted);margin:10px 0;font-size:14px}\n.tep-change{padding:12px 0;border-top:1px solid var(--border-tok)}.tep-change>strong{display:block;margin-bottom:8px}\n.tep-row{display:grid;grid-template-columns:52px minmax(0,1fr);gap:6px 10px;align-items:center;margin:8px 0}.tep-row>small{grid-column:2;color:var(--fg-muted);font-size:12px}.tep-row>span{font-size:13px}\n.tep-lane{height:24px;position:relative;background:var(--bg);border-radius:4px;overflow:hidden;border:1px solid var(--border-tok)}.tep-clip{position:absolute;top:2px;bottom:2px;background:var(--signal);border:1px solid var(--fg-muted);border-radius:3px;min-width:2px}.tep-lane>small{position:absolute;inset:0;text-align:center;color:var(--fg-muted);font-size:12px}\n\n/* StillMade media player, photo button and full-screen viewer: option A \"Cream blocks\" (owner, November 2, 2026). Solid cream\n rounded squares with ink icons, a thick cream timeline, the time in the thin italic display face. The chrome sits on the\n picture, so it is the same in Light, Dark and White; the letterbox is ink. Rounded squares only, never circles\n (UI_AESTHETIC \"Icon preference\"). Everything that moves per frame moves by transform; nothing uses backdrop-filter.\n Built by media-player.js (attachMediaPlayer, openMediaViewer, attachPhotoExpand). */\n:where(.sm-player,.sm-viewer,.sm-expand-btn){--sm-cream:var(--cream-100,#F7F0DB);--sm-ink:var(--ink-600,#0A0A0A);--sm-gold:var(--signal,#E8B23A);--sm-out:var(--dur-fast,160ms);--sm-in:var(--dur-base,240ms);--sm-ease:var(--ease-out,cubic-bezier(.22,1,.36,1));--sm-spring:var(--ease-spring,cubic-bezier(.34,1.56,.64,1));--sm-display:var(--font-display,\"Balgin\",\"Hanken Grotesk\",ui-sans-serif,sans-serif)}\n\n/* ---------- The player box: shrinks to the video like a bare <video>; data-fill fills a box the caller sizes ---------- */\n.sm-player{position:relative;display:inline-block;vertical-align:top;max-width:100%;box-sizing:border-box;overflow:hidden;background:var(--sm-ink);color:var(--sm-cream);line-height:1;font-family:var(--font-body,\"Hanken Grotesk\",ui-sans-serif,system-ui,sans-serif);user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;touch-action:manipulation;outline:none}\n.sm-player:focus-visible{outline:2px solid var(--sm-gold);outline-offset:2px}\n.sm-player>video{display:block;width:auto;max-width:100%;height:auto;max-height:inherit;margin:0 auto;object-fit:contain;background:transparent}\n.sm-player[data-fill]>video,.sm-player:fullscreen>video,.sm-player[data-stage]>video{width:100%;height:100%;max-width:none;max-height:none}\n.sm-player[data-fill]{display:block}\n.sm-player[data-idle]{cursor:none}\n:where(.sm-stage)>.sm-player{max-width:100%;max-height:100%;border-radius:var(--r-2,4px)}\n\n/* The controls layer covers the picture; only the bar takes the pointer, so a click on the picture reaches the box. */\n.sm-player-ui{position:absolute;inset:0;z-index:1;pointer-events:none}\n.sm-player-shade{position:absolute;left:0;right:0;bottom:0;height:120px;max-height:100%;background:linear-gradient(to top,rgba(10,10,10,.55),rgba(10,10,10,0))}\n.sm-player-bar{position:absolute;left:12px;right:12px;bottom:12px;display:flex;align-items:center;gap:10px;pointer-events:auto}\n.sm-player-shade,.sm-player-bar{transition:opacity var(--sm-in) var(--sm-ease)}\n/* November 2, 2026 (owner): hover only. data-away (no pointer over it, no keyboard focus in it, no recent tap) and data-idle\n (the mouse resting on a playing video) hide them; hidden controls take no pointer, so a tap reaches the picture. */\n.sm-player[data-away] .sm-player-shade,.sm-player[data-away] .sm-player-bar,.sm-player[data-idle] .sm-player-shade,.sm-player[data-idle] .sm-player-bar{opacity:0;pointer-events:none;transition-duration:var(--sm-out)}\n\n/* ---------- Cream squares (player, viewer) ---------- */\n:where(.sm-player,.sm-viewer) .sm-media-btn{appearance:none;-webkit-appearance:none;box-sizing:border-box;flex:none;display:grid;place-items:center;width:44px;height:44px;min-width:0;min-height:0;margin:0;padding:0;border:0;border-radius:var(--r-3,8px);background:var(--sm-cream);color:var(--sm-ink);box-shadow:none;font:inherit;line-height:1;text-decoration:none;cursor:pointer;transition:transform var(--sm-out) var(--sm-spring)}\n:where(.sm-player,.sm-viewer) .sm-media-btn svg{display:block;width:20px;height:20px}\n:is(.sm-player,.sm-viewer) .sm-media-btn.sm-media-btn.sm-media-btn-text{width:auto;padding:0 14px;font-size:14px;font-weight:600}\n/* November 2, 2026: the hover rules restate the cream, so a page's own button:hover (the listing pages, a Block's CSS) never\n washes the squares out. */\n@media (hover:hover){:where(.sm-player,.sm-viewer) .sm-media-btn:hover{transform:translateY(-1px);background:var(--sm-cream);color:var(--sm-ink)}}\n:where(.sm-player,.sm-viewer) .sm-media-btn:active{transform:scale(.94)}\n:is(.sm-player,.sm-viewer) .sm-media-btn.sm-media-btn:focus-visible{outline:2px solid var(--sm-gold);outline-offset:2px;border-radius:var(--r-3,8px)}\n/* November 2, 2026: a container's own button rule outranked the squares and drew them as outlined pills on a Step\n (.bs-step-workspace button:not(.btn-primary) is 0,2,1 and 0,3,1 on hover; .block-playground and .bs-shell buttons). The\n look is restated at 0,3,0, and the cream at 0,4,0 on any hover (a tap leaves :hover on touch); size, place and display\n stay in the rules above, so the narrow-player and viewer rules keep working. */\n:is(.sm-player,.sm-viewer) .sm-media-btn.sm-media-btn{min-width:0;min-height:0;padding:0;border:0;border-radius:var(--r-3,8px);background:var(--sm-cream);color:var(--sm-ink);box-shadow:none;font:inherit;line-height:1;text-decoration:none;cursor:pointer}\n:is(.sm-player,.sm-viewer) .sm-media-btn.sm-media-btn:hover{background:var(--sm-cream);color:var(--sm-ink)}\n.sm-player .sm-player-bar [data-control=\"fullscreen\"]{margin-left:auto}\n\n/* ---------- Volume (owner, November 2, 2026): a vertical cream line above the sound button, shown while the button or the line\n is hovered or focused, or while it is dragged; the same track, fill and square knob as the timeline. Mouse only. ---------- */\n.sm-player-sound{position:relative;display:flex;flex:none}\n.sm-player-volume{position:absolute;left:50%;bottom:100%;padding-bottom:10px;opacity:0;pointer-events:none;transform:translate(-50%,6px);transition:opacity var(--sm-out) var(--sm-ease),transform var(--sm-out) var(--sm-ease)}\n.sm-player-sound:hover .sm-player-volume,.sm-player-sound:focus-within .sm-player-volume,.sm-player-volume[data-drag]{opacity:1;pointer-events:auto;transform:translate(-50%,0);transition-duration:var(--sm-in)}\n.sm-player-volume-rail{position:relative;box-sizing:border-box;width:44px;height:128px;padding:14px 0;border-radius:var(--r-3,8px);background:rgba(10,10,10,.64);cursor:pointer;touch-action:none;outline:none}\n.sm-player-volume-rail:focus-visible{outline:2px solid var(--sm-gold);outline-offset:2px}\n.sm-player-volume-track{position:relative;width:6px;height:100%;margin:0 auto;border-radius:3px;background:rgba(247,240,219,.28);overflow:visible}\n.sm-player-volume-level{position:absolute;inset:0;border-radius:3px;background:var(--sm-cream);transform-origin:center bottom;transform:scaleY(1)}\n.sm-player-volume-head{position:absolute;inset:0;pointer-events:none;transform:translateY(-100%)}\n.sm-player-volume-knob{position:absolute;left:50%;bottom:0;width:16px;height:16px;margin:0 0 -8px -8px;border-radius:4px;background:var(--sm-cream);box-shadow:0 0 0 3px rgba(10,10,10,.35)}\n.sm-player-volume[data-drag] .sm-player-volume-head{will-change:transform}\n/* The player box clips, and the line opens 194 px above its bottom: under 200 px tall it is shorter, under 150 px it stays\n closed and the button only mutes (data-volume-room, measured by the player's ResizeObserver). */\n.sm-player[data-volume-room=\"short\"] .sm-player-volume-rail{height:84px;padding:12px 0}\n.sm-player[data-volume-room=\"none\"] .sm-player-volume{display:none}\n@media (hover:none){.sm-player-volume{display:none}}\n\n/* ---------- Timeline: 24 px to point at, a 6 px track that grows, a cream square knob, the time under the pointer ---------- */\n.sm-player-rail{position:relative;flex:1 1 auto;min-width:48px;height:24px;cursor:pointer;touch-action:none;outline:none}\n.sm-player-rail:focus-visible{outline:2px solid var(--sm-gold);outline-offset:2px;border-radius:var(--r-2,4px)}\n.sm-player-track{position:absolute;left:0;right:0;top:50%;height:6px;margin-top:-3px;border-radius:3px;background:rgba(247,240,219,.28);overflow:hidden;transition:transform var(--sm-out) var(--sm-ease)}\n.sm-player-buffered,.sm-player-played{position:absolute;inset:0;transform-origin:left center;transform:scaleX(0)}\n.sm-player-buffered{background:rgba(247,240,219,.18)}\n.sm-player-played{background:var(--sm-cream)}\n.sm-player-head{position:absolute;inset:0;pointer-events:none;transform:translateX(0)}\n.sm-player[data-playing] .sm-player-head,.sm-player[data-playing] .sm-player-played,.sm-player-rail[data-drag] .sm-player-head{will-change:transform}\n.sm-player-knob{position:absolute;top:50%;left:0;width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:4px;background:var(--sm-cream);box-shadow:0 0 0 3px rgba(10,10,10,.35);transform:scale(0);transition:transform var(--sm-out) var(--sm-spring)}\n.sm-player-rail:hover .sm-player-knob,.sm-player-rail[data-drag] .sm-player-knob,.sm-player-rail:focus-visible .sm-player-knob{transform:scale(1)}\n.sm-player-rail:hover .sm-player-track,.sm-player-rail[data-drag] .sm-player-track{transform:scaleY(1.34)}\n.sm-player-tip{position:absolute;bottom:calc(100% + 4px);left:0;padding:3px 7px;border-radius:var(--r-2,4px);background:var(--sm-ink);border:1px solid rgba(247,240,219,.14);color:var(--sm-cream);font-size:12px;font-weight:500;font-variant-numeric:tabular-nums;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity var(--sm-out) var(--sm-ease)}\n.sm-player-rail:hover .sm-player-tip,.sm-player-rail[data-drag] .sm-player-tip{opacity:1}\n\n/* ---------- Time: \"0:12 / 0:34\", the total at 55% ---------- */\n.sm-player-time{flex:none;font-family:var(--sm-display);font-style:italic;font-weight:200;font-size:22px;line-height:1;letter-spacing:-.01em;font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--sm-cream);text-shadow:0 1px 8px rgba(10,10,10,.6)}\n:where(.sm-player,.sm-viewer) .sm-media-of{opacity:.55}\n\n/* ---------- Narrow players step down by measured width so the bar never overflows (November 2, 2026: a 320 px vertical\n result clipped full screen): under 400 px no total and a smaller clock, under 300 px no clock, under 240 px no sound,\n under 180 px only play and full screen. The timeline keeps at least 48 px. ---------- */\n.sm-player[data-size] .sm-player-bar{gap:8px}\n.sm-player[data-size] .sm-player-total{display:none}\n.sm-player[data-size] .sm-player-time{font-size:18px}\n.sm-player:is([data-size=\"compact\"],[data-size=\"tiny\"],[data-size=\"micro\"]) .sm-player-time{display:none}\n.sm-player:is([data-size=\"tiny\"],[data-size=\"micro\"]) [data-control=\"volume\"]{display:none}\n.sm-player[data-size=\"micro\"] .sm-player-bar>:not([data-control=\"play\"]):not([data-control=\"fullscreen\"]){display:none}\n\n/* ---------- Touch: 44 px targets ---------- */\n@media (hover:none){.sm-player-rail{height:44px}}\n\n/* ---------- Full screen, and theater mode where the page may not go full screen (top layer, safe areas) ---------- */\n.sm-player:fullscreen{width:100%;height:100%;max-width:none;max-height:none;border-radius:0}\n.sm-player[data-stage=\"theater\"]{position:fixed;inset:0;z-index:2147483000;display:block;width:auto;height:auto;max-width:none;max-height:none;margin:0;border:0;border-radius:0;padding:env(safe-area-inset-top,0px) env(safe-area-inset-right,0px) env(safe-area-inset-bottom,0px) env(safe-area-inset-left,0px);overflow:hidden;color:var(--sm-cream);background:var(--sm-ink)}\n.sm-player[data-stage=\"theater\"]::backdrop{background:var(--sm-ink)}\n.sm-player:fullscreen .sm-player-bar,.sm-player[data-stage] .sm-player-bar{left:max(12px,env(safe-area-inset-left,0px));right:max(12px,env(safe-area-inset-right,0px));bottom:max(12px,env(safe-area-inset-bottom,0px))}\n\n/* ---------- Photo button: the video's full-screen square in the same corner, bottom right. 44 px on photos at least 240 px\n wide, 32 px under that (data-size=\"small\"). Shown only while the photo is hovered or the button has keyboard focus, and for\n 3 s after a tap on touch (data-show); hidden, it takes no pointer, so the first tap lands on the photo. ---------- */\n.sm-expand-host{position:relative}\n.sm-expand-btn{position:absolute;right:12px;bottom:12px;z-index:2;box-sizing:border-box;display:grid;place-items:center;width:44px;height:44px;min-width:0;min-height:0;margin:0;padding:0;border:0;border-radius:var(--r-3,8px);background:var(--sm-cream);color:var(--sm-ink);box-shadow:none;line-height:1;cursor:pointer;opacity:0;pointer-events:none;transition:opacity var(--sm-out) var(--sm-ease),transform var(--sm-out) var(--sm-spring)}\n.sm-expand-btn svg{display:block;width:20px;height:20px}\n.sm-expand-btn[data-size=\"small\"]{right:6px;bottom:6px;width:32px;height:32px;border-radius:6px}\n.sm-expand-btn[data-size=\"small\"] svg{width:16px;height:16px}\n.sm-expand-btn[data-size=\"small\"]::before{content:\"\";position:absolute;inset:-6px}\n@media (hover:hover){:hover>.sm-expand-btn{opacity:1;pointer-events:auto}.sm-expand-btn:hover{transform:translateY(-1px);background:var(--sm-cream);color:var(--sm-ink)}}\n.sm-expand-btn:focus-visible,.sm-expand-btn[data-show]{opacity:1;pointer-events:auto}\n.sm-expand-btn:active{transform:scale(.94)}\n.sm-expand-btn.sm-expand-btn.sm-expand-btn:focus-visible{outline:2px solid var(--sm-gold);outline-offset:2px}\n/* The same guard as the squares above (November 2, 2026): the look at 0,3,0 and 0,4,0, never position, size or display. */\n.sm-expand-btn.sm-expand-btn.sm-expand-btn{min-width:0;min-height:0;padding:0;border:0;border-radius:var(--r-3,8px);background:var(--sm-cream);color:var(--sm-ink);box-shadow:none;font:inherit;line-height:1;cursor:pointer}\n.sm-expand-btn.sm-expand-btn.sm-expand-btn[data-size=\"small\"]{border-radius:6px}\n.sm-expand-btn.sm-expand-btn.sm-expand-btn:hover{background:var(--sm-cream);color:var(--sm-ink)}\n\n/* ---------- Viewer: a modal dialog on ink, the photo contained, cream squares to close and move ---------- */\n.sm-viewer{position:fixed;inset:0;box-sizing:border-box;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:0;border:0;overflow:hidden;background:var(--sm-ink);color:var(--sm-cream);font-family:var(--font-body,\"Hanken Grotesk\",ui-sans-serif,system-ui,sans-serif)}\n.sm-viewer[open]{display:grid;grid-template-rows:minmax(0,1fr) auto;animation:sm-viewer-in var(--sm-in) var(--sm-ease) both}\n.sm-viewer::backdrop{background:var(--ink-600,#0A0A0A)}\n@keyframes sm-viewer-in{from{opacity:0}}\n.sm-viewer [hidden],.sm-viewer .sm-media-btn[hidden]{display:none}\n.sm-viewer-stage{position:relative;display:grid;grid-template:minmax(0,1fr)/minmax(0,1fr);place-items:center;min-width:0;min-height:0;overflow:hidden;padding:max(12px,env(safe-area-inset-top,0px)) max(12px,env(safe-area-inset-right,0px)) 12px max(12px,env(safe-area-inset-left,0px))}\n/* The stage is one 1fr cell, so the photo is held to the screen in both directions (November 2, 2026: it ran off the bottom). */\n.sm-viewer-media{display:block;width:100%;height:100%;min-width:0;min-height:0;object-fit:contain}\n.sm-viewer-stage>.sm-player{width:100%;height:100%;max-width:100%;max-height:100%}\n/* What cannot open here (a file kept on another device) is a quiet tile with no words (November 2, 2026: \"notices, not\n prose\"); the app's single toast says why. */\n.sm-viewer-off,.sm-player-off{display:inline-grid;place-items:center;box-sizing:border-box;border:1px solid var(--border-tok,#58534D);border-radius:var(--r-2,4px);background:var(--bg-inset,#171716);color:var(--fg-muted,#BBB2A7)}\n.sm-viewer-off{width:min(60vw,320px);aspect-ratio:4/3}\n.sm-player-off{width:min(100%,320px);max-width:100%;aspect-ratio:16/9;vertical-align:top}\n.sm-viewer-off svg,.sm-player-off svg{display:block;width:24px;height:24px}\n.sm-viewer-top{position:absolute;top:max(12px,env(safe-area-inset-top,0px));right:max(12px,env(safe-area-inset-right,0px));z-index:2;display:flex;gap:10px}\n.sm-viewer .sm-viewer-nav{position:absolute;top:50%;margin-top:-22px;z-index:2}\n.sm-viewer .sm-viewer-previous{left:max(12px,env(safe-area-inset-left,0px))}\n.sm-viewer .sm-viewer-next{right:max(12px,env(safe-area-inset-right,0px))}\n.sm-viewer-foot{display:flex;align-items:baseline;justify-content:center;gap:12px;min-width:0;padding:12px max(12px,env(safe-area-inset-right,0px)) max(14px,env(safe-area-inset-bottom,0px)) max(12px,env(safe-area-inset-left,0px))}\n.sm-viewer-count{flex:none;font-family:var(--sm-display);font-style:italic;font-weight:200;font-size:24px;line-height:1;letter-spacing:-.01em;font-variant-numeric:tabular-nums;white-space:nowrap}\n.sm-viewer-name{min-width:0;font-size:13px;color:var(--fg-muted,#BBB2A7);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n\n@media (prefers-reduced-motion:reduce){\n .sm-player *,.sm-viewer,.sm-viewer *,.sm-expand-btn{transition:none!important;animation:none!important}\n}\n\n/* Preview shape tiles in the transport bar: a quiet row of outlines sized to the bar. */\n.ve-shape-choices{display:flex;flex-shrink:0;gap:2px}\n/* November 2, 2026: only unpicked tiles go transparent; the pick keeps the kit's ink fill, since\n a transparent pick drew its shape in the page colour (the kit's colour:var(--bg)) and vanished. */\n.ve-shape-choices>label{min-height:28px;padding:4px 6px}\n.ve-shape-choices>label:not(:has(input:checked)){border-color:transparent;background:transparent}\n.ve-shape-choices .sm-shape{--sm-shape-size:16px;border-width:1.5px}\n",
380
+ "javascript": "(function(){\"use strict\";var graph={\"view-X9ivhzWv.js\":[\"react-Cd33N7Zq.js\",\"subscribeAnim-Bg9ZWJrU.js\",\"view-Kb_SSysL.js\"],\"MediaViewer-CkF9dtek.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"react-Cd33N7Zq.js\",\"media-player-B5xtn5y5.js\"],\"MobileEditor-Djggxit0.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"react-Cd33N7Zq.js\",\"editorAdds-CTNeJ99r.js\",\"editorTimelines-VaiIR5FG.js\",\"subscribeAnim-Bg9ZWJrU.js\",\"assetStore-DtBUuqk3.js\",\"liveOverlaysCanvas-DfCRS-HH.js\",\"view-Kb_SSysL.js\",\"boxOpacity-xDTjxCwE.js\",\"bind-VF25SFei.js\",\"boxOpacity-CKnL9bqC.js\"],\"PanelBlocksPanel-Bf6aQ7yb.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"react-Cd33N7Zq.js\",\"editorAdds-CTNeJ99r.js\",\"contracts-5DwIypr0.js\",\"subscribeAnim-Bg9ZWJrU.js\",\"liveOverlaysCanvas-DfCRS-HH.js\",\"view-Kb_SSysL.js\",\"boxOpacity-xDTjxCwE.js\",\"boxOpacity-CKnL9bqC.js\"],\"VideoEditor-C_GiJNFB.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"react-Cd33N7Zq.js\",\"editorAdds-CTNeJ99r.js\",\"editorTimelines-VaiIR5FG.js\",\"subscribeAnim-Bg9ZWJrU.js\",\"assetStore-DtBUuqk3.js\",\"liveOverlaysCanvas-DfCRS-HH.js\",\"view-Kb_SSysL.js\",\"boxOpacity-xDTjxCwE.js\",\"bind-VF25SFei.js\",\"speedSample-BXiYiZ4E.js\",\"boxOpacity-CKnL9bqC.js\",\"media-player-B5xtn5y5.js\"],\"_frame-empty_wasm_vtracer_wasm_vtracer_bg_wasm_url-C085yj93.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"assetStore-DtBUuqk3.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"contracts-5DwIypr0.js\"],\"bind-VF25SFei.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"view-Kb_SSysL.js\",\"boxOpacity-xDTjxCwE.js\",\"sharedVault-mMOG3PsG.js\"],\"boxOpacity-CKnL9bqC.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"react-Cd33N7Zq.js\",\"editorAdds-CTNeJ99r.js\",\"editorTimelines-VaiIR5FG.js\",\"contracts-5DwIypr0.js\",\"subscribeAnim-Bg9ZWJrU.js\",\"assetStore-DtBUuqk3.js\",\"type-workspace-Atu8ZDza.js\",\"liveOverlaysCanvas-DfCRS-HH.js\",\"view-Kb_SSysL.js\",\"boxOpacity-xDTjxCwE.js\",\"bind-VF25SFei.js\",\"sharedVault-mMOG3PsG.js\",\"vectorizeClient-CSsuV7Ew.js\"],\"boxOpacity-xDTjxCwE.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"react-Cd33N7Zq.js\",\"subscribeAnim-Bg9ZWJrU.js\"],\"browser-CGfns6yT.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"contracts-5DwIypr0.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"editorAdds-CTNeJ99r.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"react-Cd33N7Zq.js\",\"subscribeAnim-Bg9ZWJrU.js\"],\"editorTimelines-VaiIR5FG.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"react-Cd33N7Zq.js\",\"editorAdds-CTNeJ99r.js\",\"contracts-5DwIypr0.js\",\"subscribeAnim-Bg9ZWJrU.js\",\"type-workspace-Atu8ZDza.js\"],\"exportRender-Pi5GkYGT.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"boxOpacity-xDTjxCwE.js\",\"bind-VF25SFei.js\",\"videoSource-C9Gkw8hE.js\"],\"exportRender-WS8SCtHj.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"exportRender-Pi5GkYGT.js\"],\"favorites-CT5VjClb.js\":[\"VideoEditor-C_GiJNFB.js\"],\"frameExtract-CpwOQS0p.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"imagetracer_v1.2.6-BBGerUPj.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"lamejs-Dix5aA0E.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"liveOverlaysCanvas-DfCRS-HH.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"media-player-B5xtn5y5.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"nativeRender-B0Nvt4hv.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"boxOpacity-xDTjxCwE.js\",\"videoSource-C9Gkw8hE.js\"],\"overlaysCanvas-e39098p3.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"subscribeAnim-Bg9ZWJrU.js\",\"boxOpacity-xDTjxCwE.js\"],\"panelExportProcess-BfyXrGVq.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"type-workspace-Atu8ZDza.js\",\"boxOpacity-CKnL9bqC.js\"],\"react-Cd33N7Zq.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"rolldown-runtime-BDP3x1s3.js\":[],\"sharedVault-mMOG3PsG.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"speedSample-BXiYiZ4E.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"subscribeAnim-Bg9ZWJrU.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"react-Cd33N7Zq.js\",\"contracts-5DwIypr0.js\"],\"type-workspace-Atu8ZDza.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"contracts-5DwIypr0.js\",\"assetStore-DtBUuqk3.js\"],\"v2AssetGen-041opegA.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"view-Kb_SSysL.js\",\"sharedVault-mMOG3PsG.js\",\"vectorizeClient-CSsuV7Ew.js\",\"vectorizeClient-BGAa1YU-.js\",\"boxOpacity-CKnL9bqC.js\"],\"vectorizeClient-BGAa1YU-.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"vectorizeClient-CSsuV7Ew.js\"],\"vectorizeClient-CSsuV7Ew.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"videoSource-C9Gkw8hE.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"liveOverlaysCanvas-DfCRS-HH.js\",\"view-Kb_SSysL.js\",\"boxOpacity-xDTjxCwE.js\",\"overlaysCanvas-e39098p3.js\",\"bind-VF25SFei.js\",\"speedSample-BXiYiZ4E.js\",\"sharedVault-mMOG3PsG.js\"],\"view-Kb_SSysL.js\":[\"rolldown-runtime-BDP3x1s3.js\",\"react-Cd33N7Zq.js\",\"editorAdds-CTNeJ99r.js\",\"editorTimelines-VaiIR5FG.js\",\"contracts-5DwIypr0.js\",\"subscribeAnim-Bg9ZWJrU.js\",\"assetStore-DtBUuqk3.js\",\"type-workspace-Atu8ZDza.js\"],\"wasm_vtracer_bg-BPZSQuDe.js\":[\"rolldown-runtime-BDP3x1s3.js\"],\"wavesurfer.esm-B4xOEftU.js\":[\"rolldown-runtime-BDP3x1s3.js\"]},dir=\"code/\",factories=Object.create(null),cache=Object.create(null),loading=Object.create(null);\nfunction name(id){return String(id).replace(/^\\.\\//,\"\")}\nfunction require(id){var key=name(id);if(cache[key])return cache[key].exports;var factory=factories[key];if(!factory)throw new Error(key+\" is not loaded\");var module={exports:{}};cache[key]=module;factory.call(undefined,module.exports,require,module,load);return module.exports}\nfunction needed(key,into){if(into.indexOf(key)<0){into.push(key);(graph[key]||[]).forEach(function(next){needed(next,into)})}return into}\nfunction get(key){if(factories[key])return Promise.resolve();return loading[key]||(loading[key]=StillMade.loadCode(dir+key).then(function(){if(!factories[key])throw new Error(dir+key+\" did not define its module\")},function(error){delete loading[key];throw error}))}\nfunction load(id){return Promise.all(needed(name(id),[]).map(get))}\nObject.defineProperty(globalThis,\"__stillmadeCode_4b57c10af8f8\",{value:Object.freeze({define:function(key,factory){if(!factories[key])factories[key]=factory}})});\nload(\"view-X9ivhzWv.js\").then(function(){require(\"view-X9ivhzWv.js\")}).catch(function(error){var note=document.createElement(\"p\");note.setAttribute(\"role\",\"alert\");note.style.cssText=\"margin:16px;color:var(--fg)\";note.textContent=\"This screen could not load: \"+(error&&error.message||error);(document.getElementById(\"root\")||document.body).appendChild(note);throw error})})();\n",
381
+ "runtime": "frame",
382
+ "files": {
383
+ "schemaVersion": 1,
384
+ "files": {
385
+ "code/MediaViewer-CkF9dtek.js": {
386
+ "sha256": "d22de50f6bc24c25f9851e9f9126aa10f0e700843f4a44093fdeda0c87a08c5c",
387
+ "bytes": 1053,
388
+ "mediaType": "text/javascript"
389
+ },
390
+ "code/MobileEditor-Djggxit0.js": {
391
+ "sha256": "d5a1982664e06c1098db24b2fb82689e830737c56d5a1d7219e012654849ebba",
392
+ "bytes": 168633,
393
+ "mediaType": "text/javascript"
394
+ },
395
+ "code/PanelBlocksPanel-Bf6aQ7yb.js": {
396
+ "sha256": "d52054ba59bc1b33800c7bcae1b796701b3d72c18064bec3f428be4a51de4062",
397
+ "bytes": 316361,
398
+ "mediaType": "text/javascript"
399
+ },
400
+ "code/VideoEditor-C_GiJNFB.js": {
401
+ "sha256": "f0d55fe2e9de52216f8f302ed5522463d02978d84fe1074fbe1311dc030dedc0",
402
+ "bytes": 483546,
403
+ "mediaType": "text/javascript"
404
+ },
405
+ "code/_frame-empty_wasm_vtracer_wasm_vtracer_bg_wasm_url-C085yj93.js": {
406
+ "sha256": "4f838d2631dc6d36e1db4cc54a934628161402e69cac183bf8a99ef76ecd130f",
407
+ "bytes": 269,
408
+ "mediaType": "text/javascript"
409
+ },
410
+ "code/assetStore-DtBUuqk3.js": {
411
+ "sha256": "859c7c3dea93b0f0f4f66487f1e5062249627b555a5f1ce074b88e573b018d33",
412
+ "bytes": 1511,
413
+ "mediaType": "text/javascript"
414
+ },
415
+ "code/bind-VF25SFei.js": {
416
+ "sha256": "f08528b1a2414ca651051afe2ace6689d8b8157a0628210318abce0ba36158e6",
417
+ "bytes": 349873,
418
+ "mediaType": "text/javascript"
419
+ },
420
+ "code/boxOpacity-CKnL9bqC.js": {
421
+ "sha256": "6bf96a047ef5980691404d40a8b6ea61074a2b551e43701c7d69cfede6567d08",
422
+ "bytes": 350824,
423
+ "mediaType": "text/javascript"
424
+ },
425
+ "code/boxOpacity-xDTjxCwE.js": {
426
+ "sha256": "52547adc114e7459579c8500998f9a87d72559ce67e814ba503e573f9e81ad5e",
427
+ "bytes": 14167,
428
+ "mediaType": "text/javascript"
429
+ },
430
+ "code/browser-CGfns6yT.js": {
431
+ "sha256": "d5dff9febef17d55751d880c145710e542d72d168f3fb03251df651f60c69161",
432
+ "bytes": 12691,
433
+ "mediaType": "text/javascript"
434
+ },
435
+ "code/contracts-5DwIypr0.js": {
436
+ "sha256": "8e778341a47b0c489036450ef874e9bfbaf069bc9a92093559dfcf21fad3de2a",
437
+ "bytes": 64083,
438
+ "mediaType": "text/javascript"
439
+ },
440
+ "code/editorAdds-CTNeJ99r.js": {
441
+ "sha256": "d2050e4453532d6e2bb7a8c2c1385cc80d696d1fdce4e2c18797dee7908f0798",
442
+ "bytes": 194975,
443
+ "mediaType": "text/javascript"
444
+ },
445
+ "code/editorTimelines-VaiIR5FG.js": {
446
+ "sha256": "4d6171c21a98c170c3ff41a084750282cf0bf1c125abcff25d4231b6b934a783",
447
+ "bytes": 69210,
448
+ "mediaType": "text/javascript"
449
+ },
450
+ "code/exportRender-Pi5GkYGT.js": {
451
+ "sha256": "ea355115929d77b4de64acc9d32c829d9eac957c8046246a2655715b372b3326",
452
+ "bytes": 47667,
453
+ "mediaType": "text/javascript"
454
+ },
455
+ "code/exportRender-WS8SCtHj.js": {
456
+ "sha256": "0f21041519c34c7d1e8fb70b18ee489dec94f3b6f7d4744254c9e775344e8d50",
457
+ "bytes": 348,
458
+ "mediaType": "text/javascript"
459
+ },
460
+ "code/favorites-CT5VjClb.js": {
461
+ "sha256": "c9ee4261821ad578bff983ea1422f32393c6840a0c6570f3d85046fc48efdf32",
462
+ "bytes": 182,
463
+ "mediaType": "text/javascript"
464
+ },
465
+ "code/frameExtract-CpwOQS0p.js": {
466
+ "sha256": "7328e7f1e934bd54740570807d5ded105e034694ade72487c8b89af921c52a6a",
467
+ "bytes": 1336,
468
+ "mediaType": "text/javascript"
469
+ },
470
+ "code/imagetracer_v1.2.6-BBGerUPj.js": {
471
+ "sha256": "f74ab6f60c682d71eb2ea9ab8245d23a50bfa9406c43c2ce9d23665c7b378f5d",
472
+ "bytes": 19877,
473
+ "mediaType": "text/javascript"
474
+ },
475
+ "code/lamejs-Dix5aA0E.js": {
476
+ "sha256": "c95f0b4564bcdca1120ffc7686bba7f24ddf3d4092270d834f1518018a6a8358",
477
+ "bytes": 163273,
478
+ "mediaType": "text/javascript"
479
+ },
480
+ "code/liveOverlaysCanvas-DfCRS-HH.js": {
481
+ "sha256": "174f64775b26cc8ece01c76229af403b648767d763c7a86ac3249a6ea57c8c95",
482
+ "bytes": 34146,
483
+ "mediaType": "text/javascript"
484
+ },
485
+ "code/media-player-B5xtn5y5.js": {
486
+ "sha256": "8d1e440d72a8a14be9ce0d44af24be18813881ebc140fd0941334bb22a8c97d2",
487
+ "bytes": 19849,
488
+ "mediaType": "text/javascript"
489
+ },
490
+ "code/nativeRender-B0Nvt4hv.js": {
491
+ "sha256": "3bda177ad91e81be0e2cc493a01c89b22a5a5f121c8070ceb250867776f0a7b3",
492
+ "bytes": 2601,
493
+ "mediaType": "text/javascript"
494
+ },
495
+ "code/overlaysCanvas-e39098p3.js": {
496
+ "sha256": "af2244a8107027306d90a314b7763ca853f71834cb84e4c65790a2d76c56509f",
497
+ "bytes": 11939,
498
+ "mediaType": "text/javascript"
499
+ },
500
+ "code/panelExportProcess-BfyXrGVq.js": {
501
+ "sha256": "e54493865ebb10447a8a60ce0411bb11c805d36bcc409c29e00b12255d1ab899",
502
+ "bytes": 4590,
503
+ "mediaType": "text/javascript"
504
+ },
505
+ "code/react-Cd33N7Zq.js": {
506
+ "sha256": "cb294c9600324a21646784b2568e2563cbc6e100e0d1d6161eb4c85094d629cb",
507
+ "bytes": 6674,
508
+ "mediaType": "text/javascript"
509
+ },
510
+ "code/rolldown-runtime-BDP3x1s3.js": {
511
+ "sha256": "03b053c12d4f3946a70738fbb49d7a545bcd5aa3349c593f95459c07e101d6ae",
512
+ "bytes": 1189,
513
+ "mediaType": "text/javascript"
514
+ },
515
+ "code/sharedVault-mMOG3PsG.js": {
516
+ "sha256": "77a2fc8f8fcbb182f174ee07fbf30b117421d1da4eea50df23c38a5b41572365",
517
+ "bytes": 3026,
518
+ "mediaType": "text/javascript"
519
+ },
520
+ "code/speedSample-BXiYiZ4E.js": {
521
+ "sha256": "52f35ba6a379fb2dd94454693418ae3bac0231bddca2aaaf261d155226641ebc",
522
+ "bytes": 1474,
523
+ "mediaType": "text/javascript"
524
+ },
525
+ "code/subscribeAnim-Bg9ZWJrU.js": {
526
+ "sha256": "160b77460a76c16469770a576013405ced7436d1b4ebca8be1e2bae50a01760b",
527
+ "bytes": 3786,
528
+ "mediaType": "text/javascript"
529
+ },
530
+ "code/type-workspace-Atu8ZDza.js": {
531
+ "sha256": "ea5f3780b413e074b2509221414bccc87b4d505ae5ce1efc7ca82d614575fe5b",
532
+ "bytes": 261807,
533
+ "mediaType": "text/javascript"
534
+ },
535
+ "code/v2AssetGen-041opegA.js": {
536
+ "sha256": "35c62f7e57d0416b34f3c39f266ebd1f035ab32af3c6e6ea6e4a7a9e7bb2a598",
537
+ "bytes": 3973,
538
+ "mediaType": "text/javascript"
539
+ },
540
+ "code/vectorizeClient-BGAa1YU-.js": {
541
+ "sha256": "a157e0985da722293b1f4ed865acbda8ab883e327c86b176e380a798ea7ba17b",
542
+ "bytes": 442,
543
+ "mediaType": "text/javascript"
544
+ },
545
+ "code/vectorizeClient-CSsuV7Ew.js": {
546
+ "sha256": "2b50371b3d0f18f3f38bcabbeeaaccca16f5fda0e09bd275f6dc64b2ecde87bc",
547
+ "bytes": 5278,
548
+ "mediaType": "text/javascript"
549
+ },
550
+ "code/videoSource-C9Gkw8hE.js": {
551
+ "sha256": "fa8dda02d3e52863cd416e23e9ef15fad106fedff58dcaf03723179240d8727f",
552
+ "bytes": 193830,
553
+ "mediaType": "text/javascript"
554
+ },
555
+ "code/view-Kb_SSysL.js": {
556
+ "sha256": "3f7936812de182001aa5c6f95a0dc310a0a511cfff92dda4c9ace2c1ca8200ec",
557
+ "bytes": 279173,
558
+ "mediaType": "text/javascript"
559
+ },
560
+ "code/view-X9ivhzWv.js": {
561
+ "sha256": "b01dbabe3be42c162e13efe9b3329fb29b5638a4ebdbdbe96a43ae28a51c4d72",
562
+ "bytes": 206,
563
+ "mediaType": "text/javascript"
564
+ },
565
+ "code/wasm_vtracer_bg-BPZSQuDe.js": {
566
+ "sha256": "743160c76c2327c227aad867230b79858b9fdf1043833b9c4c8753931e0d1e92",
567
+ "bytes": 6530,
568
+ "mediaType": "text/javascript"
569
+ },
570
+ "code/wavesurfer.esm-B4xOEftU.js": {
571
+ "sha256": "d3b801ee22676dd9d4b46ca4030a6ef4bb9646323dde6f6941b959c5dcca5164",
572
+ "bytes": 39687,
573
+ "mediaType": "text/javascript"
574
+ }
575
+ }
576
+ }
577
+ },
578
+ "tests": [
579
+ {
580
+ "name": "Opens with the narration, pictures and clips of earlier Steps",
581
+ "input": {
582
+ "make": "use",
583
+ "media": [
584
+ {
585
+ "assetId": "example-photo-1.jpg",
586
+ "versionId": "example-photo-1.jpg-v1",
587
+ "url": "/api/media/example/photo-1.jpg",
588
+ "kind": "image",
589
+ "mimeType": "image/jpeg",
590
+ "width": 1600,
591
+ "height": 1200
592
+ },
593
+ {
594
+ "assetId": "example-photo-3.jpg",
595
+ "versionId": "example-photo-3.jpg-v1",
596
+ "url": "/api/media/example/photo-3.jpg",
597
+ "kind": "image",
598
+ "mimeType": "image/jpeg",
599
+ "width": 1600,
600
+ "height": 1200
601
+ },
602
+ {
603
+ "assetId": "example-clip.mp4",
604
+ "versionId": "example-clip.mp4-v1",
605
+ "url": "/api/media/example/clip.mp4",
606
+ "kind": "video",
607
+ "mimeType": "video/mp4",
608
+ "width": 640,
609
+ "height": 360,
610
+ "duration": 2
611
+ }
612
+ ],
613
+ "narrationAudio": {
614
+ "assetId": "example-kitchen.wav",
615
+ "versionId": "example-kitchen.wav-v1",
616
+ "url": "/api/media/example/kitchen.wav",
617
+ "kind": "audio",
618
+ "mimeType": "audio/wav",
619
+ "duration": 2
620
+ },
621
+ "narrationTiming": {
622
+ "schemaVersion": 1,
623
+ "text": "A small idea.",
624
+ "words": [
625
+ {
626
+ "text": "A",
627
+ "start": 0,
628
+ "end": 0.2
629
+ },
630
+ {
631
+ "text": "small",
632
+ "start": 0.2,
633
+ "end": 0.6
634
+ },
635
+ {
636
+ "text": "idea.",
637
+ "start": 0.6,
638
+ "end": 1.1
639
+ }
640
+ ]
641
+ },
642
+ "narrationScript": {
643
+ "schemaVersion": 1,
644
+ "text": "A small idea."
645
+ }
646
+ },
647
+ "expected": {
648
+ "result": null
649
+ }
650
+ },
651
+ {
652
+ "name": "Opens an Image Panels cut with a pop where something is added and the sizzle offered as a sound effect",
653
+ "input": {
654
+ "make": "use",
655
+ "panels": {
656
+ "schemaVersion": 1,
657
+ "mode": "imagePanel",
658
+ "meta": {
659
+ "title": "Kitchen",
660
+ "aspect": "16:9"
661
+ },
662
+ "audio": {
663
+ "voPath": null,
664
+ "words": [
665
+ {
666
+ "text": "The",
667
+ "start": 0,
668
+ "end": 0.3,
669
+ "startMs": 0,
670
+ "endMs": 300
671
+ },
672
+ {
673
+ "text": "kitchen",
674
+ "start": 0.3,
675
+ "end": 0.9,
676
+ "startMs": 300,
677
+ "endMs": 900
678
+ },
679
+ {
680
+ "text": "an",
681
+ "start": 2,
682
+ "end": 2.2,
683
+ "startMs": 2000,
684
+ "endMs": 2200
685
+ },
686
+ {
687
+ "text": "apple",
688
+ "start": 2.2,
689
+ "end": 2.7,
690
+ "startMs": 2200,
691
+ "endMs": 2700
692
+ },
693
+ {
694
+ "text": "the",
695
+ "start": 4,
696
+ "end": 4.2,
697
+ "startMs": 4000,
698
+ "endMs": 4200
699
+ },
700
+ {
701
+ "text": "pan",
702
+ "start": 4.2,
703
+ "end": 4.6,
704
+ "startMs": 4200,
705
+ "endMs": 4600
706
+ },
707
+ {
708
+ "text": "sizzles.",
709
+ "start": 4.6,
710
+ "end": 5.2,
711
+ "startMs": 4600,
712
+ "endMs": 5200
713
+ }
714
+ ]
715
+ },
716
+ "styleBlock": "soft watercolor",
717
+ "sheets": {},
718
+ "lineages": {
719
+ "L": {
720
+ "sheetIds": [],
721
+ "place": ""
722
+ }
723
+ },
724
+ "scenes": [
725
+ {
726
+ "sceneId": "s1",
727
+ "title": "Kitchen",
728
+ "genre": "explainer"
729
+ }
730
+ ],
731
+ "strip": [
732
+ {
733
+ "panelId": "p1",
734
+ "type": "panel",
735
+ "sceneId": "s1",
736
+ "lineageId": "L",
737
+ "kind": "base",
738
+ "prompt": "a quiet kitchen counter",
739
+ "sourcePanelId": null,
740
+ "image": "/api/media/example/photo-1.jpg",
741
+ "startMs": 0,
742
+ "durationMs": 2000,
743
+ "transitionIn": "cut",
744
+ "overlayLayer": null,
745
+ "provenance": "auto"
746
+ },
747
+ {
748
+ "panelId": "p2",
749
+ "type": "panel",
750
+ "sceneId": "s1",
751
+ "lineageId": "L",
752
+ "kind": "edit",
753
+ "prompt": "keep identical; a red apple appears on the counter",
754
+ "sourcePanelId": "p1",
755
+ "image": "/api/media/example/photo-3.jpg",
756
+ "startMs": 2000,
757
+ "durationMs": 2000,
758
+ "transitionIn": "cut",
759
+ "overlayLayer": null,
760
+ "provenance": "auto"
761
+ },
762
+ {
763
+ "panelId": "p3",
764
+ "type": "panel",
765
+ "sceneId": "s1",
766
+ "lineageId": "L",
767
+ "kind": "edit",
768
+ "prompt": "keep identical; the pan starts sizzling",
769
+ "sourcePanelId": "p2",
770
+ "image": "/api/media/example/photo-3.jpg",
771
+ "startMs": 4000,
772
+ "durationMs": 2000,
773
+ "transitionIn": "cut",
774
+ "overlayLayer": null,
775
+ "provenance": "auto"
776
+ }
777
+ ]
778
+ },
779
+ "narrationAudio": {
780
+ "assetId": "example-kitchen.wav",
781
+ "versionId": "example-kitchen.wav-v1",
782
+ "url": "/api/media/example/kitchen.wav",
783
+ "kind": "audio",
784
+ "mimeType": "audio/wav",
785
+ "duration": 2
786
+ },
787
+ "narrationTiming": {
788
+ "schemaVersion": 1,
789
+ "text": "The kitchen an apple the pan sizzles.",
790
+ "words": [
791
+ {
792
+ "text": "The",
793
+ "start": 0,
794
+ "end": 0.3
795
+ },
796
+ {
797
+ "text": "kitchen",
798
+ "start": 0.3,
799
+ "end": 0.9
800
+ },
801
+ {
802
+ "text": "an",
803
+ "start": 2,
804
+ "end": 2.2
805
+ },
806
+ {
807
+ "text": "apple",
808
+ "start": 2.2,
809
+ "end": 2.7
810
+ },
811
+ {
812
+ "text": "the",
813
+ "start": 4,
814
+ "end": 4.2
815
+ },
816
+ {
817
+ "text": "pan",
818
+ "start": 4.2,
819
+ "end": 4.6
820
+ },
821
+ {
822
+ "text": "sizzles.",
823
+ "start": 4.6,
824
+ "end": 5.2
825
+ }
826
+ ]
827
+ }
828
+ },
829
+ "expected": {
830
+ "result": null
831
+ }
832
+ },
833
+ {
834
+ "name": "Gives the Editor document to the next Step",
835
+ "input": {
836
+ "make": "use",
837
+ "editor": {
838
+ "schemaVersion": 1,
839
+ "activeTimelineId": "timeline-1",
840
+ "tracks": [
841
+ {
842
+ "id": "video-1",
843
+ "type": "video"
844
+ }
845
+ ],
846
+ "clips": [
847
+ {
848
+ "id": "clip-1",
849
+ "trackId": "video-1",
850
+ "url": "https://media.example/picture-1.png",
851
+ "start": 0,
852
+ "duration": 4
853
+ }
854
+ ],
855
+ "audioItems": [],
856
+ "captions": [],
857
+ "textOverlays": [],
858
+ "effects": []
859
+ }
860
+ },
861
+ "expected": {
862
+ "editor": {
863
+ "schemaVersion": 1,
864
+ "activeTimelineId": "timeline-1",
865
+ "tracks": [
866
+ {
867
+ "id": "video-1",
868
+ "type": "video"
869
+ }
870
+ ],
871
+ "clips": [
872
+ {
873
+ "id": "clip-1",
874
+ "trackId": "video-1",
875
+ "url": "https://media.example/picture-1.png",
876
+ "start": 0,
877
+ "duration": 4
878
+ }
879
+ ],
880
+ "audioItems": [],
881
+ "captions": [],
882
+ "textOverlays": [],
883
+ "effects": []
884
+ },
885
+ "result": null
886
+ }
887
+ },
888
+ {
889
+ "name": "Makes a picture that keeps its reference",
890
+ "input": {
891
+ "make": "image",
892
+ "editor": {
893
+ "schemaVersion": 1,
894
+ "activeTimelineId": "timeline-1",
895
+ "tracks": [
896
+ {
897
+ "id": "video-1",
898
+ "type": "video"
899
+ }
900
+ ],
901
+ "clips": [
902
+ {
903
+ "id": "clip-1",
904
+ "trackId": "video-1",
905
+ "url": "https://media.example/picture-1.png",
906
+ "start": 0,
907
+ "duration": 4
908
+ }
909
+ ],
910
+ "audioItems": [],
911
+ "captions": [],
912
+ "textOverlays": [],
913
+ "effects": []
914
+ },
915
+ "request": {
916
+ "model": "gpt-image-2",
917
+ "readablePrompt": "A title card at dusk",
918
+ "aspectRatio": "16:9",
919
+ "quality": "low",
920
+ "resolution": "1K"
921
+ },
922
+ "references": [
923
+ {
924
+ "kind": "image",
925
+ "assetId": "picture-1",
926
+ "versionId": "picture-1-v1",
927
+ "url": "https://media.example/picture-1.png",
928
+ "mimeType": "image/png",
929
+ "width": 1024,
930
+ "height": 1024,
931
+ "bytes": 200000
932
+ }
933
+ ]
934
+ },
935
+ "mocks": {
936
+ "hosted": {
937
+ "image.generate": {
938
+ "kind": "image",
939
+ "assetId": "picture-2",
940
+ "versionId": "picture-2-v1",
941
+ "url": "https://media.example/picture-2.png",
942
+ "mimeType": "image/png",
943
+ "width": 1024,
944
+ "height": 1024,
945
+ "bytes": 200000
946
+ }
947
+ }
948
+ },
949
+ "expected": {
950
+ "editor": {
951
+ "schemaVersion": 1,
952
+ "activeTimelineId": "timeline-1",
953
+ "tracks": [
954
+ {
955
+ "id": "video-1",
956
+ "type": "video"
957
+ }
958
+ ],
959
+ "clips": [
960
+ {
961
+ "id": "clip-1",
962
+ "trackId": "video-1",
963
+ "url": "https://media.example/picture-1.png",
964
+ "start": 0,
965
+ "duration": 4
966
+ }
967
+ ],
968
+ "audioItems": [],
969
+ "captions": [],
970
+ "textOverlays": [],
971
+ "effects": []
972
+ },
973
+ "made": [
974
+ {
975
+ "kind": "image",
976
+ "assetId": "picture-2",
977
+ "versionId": "picture-2-v1",
978
+ "url": "https://media.example/picture-2.png",
979
+ "mimeType": "image/png",
980
+ "width": 1024,
981
+ "height": 1024,
982
+ "bytes": 200000
983
+ }
984
+ ],
985
+ "result": {
986
+ "kind": "image",
987
+ "assetId": "picture-2",
988
+ "versionId": "picture-2-v1",
989
+ "url": "https://media.example/picture-2.png",
990
+ "mimeType": "image/png",
991
+ "width": 1024,
992
+ "height": 1024,
993
+ "bytes": 200000
994
+ }
995
+ }
996
+ },
997
+ {
998
+ "name": "Finds the sound effects of an Image Panels cut from what is on screen and the words said then",
999
+ "input": {
1000
+ "make": "score",
1001
+ "editor": {
1002
+ "schemaVersion": 1,
1003
+ "activeTimelineId": "timeline-1",
1004
+ "tracks": [
1005
+ {
1006
+ "id": "video-1",
1007
+ "type": "video"
1008
+ }
1009
+ ],
1010
+ "clips": [
1011
+ {
1012
+ "id": "clip-1",
1013
+ "trackId": "video-1",
1014
+ "url": "https://media.example/picture-1.png",
1015
+ "start": 0,
1016
+ "duration": 4
1017
+ }
1018
+ ],
1019
+ "audioItems": [],
1020
+ "captions": [],
1021
+ "textOverlays": [],
1022
+ "effects": []
1023
+ },
1024
+ "request": {
1025
+ "schemaVersion": 1,
1026
+ "action": "sfx-detect",
1027
+ "body": {
1028
+ "transcript": "The pan sizzles as the oil heats.",
1029
+ "totalDuration": 6,
1030
+ "panelMode": true,
1031
+ "beats": [
1032
+ {
1033
+ "start": 0,
1034
+ "image": "A pan of oil on a stove",
1035
+ "narration": "The pan sizzles as the oil heats."
1036
+ }
1037
+ ]
1038
+ }
1039
+ }
1040
+ },
1041
+ "mocks": {
1042
+ "hosted": {
1043
+ "text.generate": "[{\"timestamp\":0,\"trigger\":\"oil sizzling\",\"prompt\":\"oil sizzling in a hot pan, diegetic, realistic, no music\"}]"
1044
+ }
1045
+ },
1046
+ "expected": {
1047
+ "editor": {
1048
+ "schemaVersion": 1,
1049
+ "activeTimelineId": "timeline-1",
1050
+ "tracks": [
1051
+ {
1052
+ "id": "video-1",
1053
+ "type": "video"
1054
+ }
1055
+ ],
1056
+ "clips": [
1057
+ {
1058
+ "id": "clip-1",
1059
+ "trackId": "video-1",
1060
+ "url": "https://media.example/picture-1.png",
1061
+ "start": 0,
1062
+ "duration": 4
1063
+ }
1064
+ ],
1065
+ "audioItems": [],
1066
+ "captions": [],
1067
+ "textOverlays": [],
1068
+ "effects": []
1069
+ },
1070
+ "result": {
1071
+ "sfx": [
1072
+ {
1073
+ "timestamp": 0,
1074
+ "trigger": "oil sizzling",
1075
+ "prompt": "oil sizzling in a hot pan, diegetic, realistic, no music"
1076
+ }
1077
+ ]
1078
+ }
1079
+ }
1080
+ },
1081
+ {
1082
+ "name": "Makes a sound effect for a moment of the cut",
1083
+ "input": {
1084
+ "make": "score",
1085
+ "editor": {
1086
+ "schemaVersion": 1,
1087
+ "activeTimelineId": "timeline-1",
1088
+ "tracks": [
1089
+ {
1090
+ "id": "video-1",
1091
+ "type": "video"
1092
+ }
1093
+ ],
1094
+ "clips": [
1095
+ {
1096
+ "id": "clip-1",
1097
+ "trackId": "video-1",
1098
+ "url": "https://media.example/picture-1.png",
1099
+ "start": 0,
1100
+ "duration": 4
1101
+ }
1102
+ ],
1103
+ "audioItems": [],
1104
+ "captions": [],
1105
+ "textOverlays": [],
1106
+ "effects": []
1107
+ },
1108
+ "request": {
1109
+ "schemaVersion": 1,
1110
+ "action": "sfx",
1111
+ "body": {
1112
+ "prompt": "oil sizzling in a hot pan",
1113
+ "durationSec": 1
1114
+ }
1115
+ }
1116
+ },
1117
+ "mocks": {
1118
+ "hosted": {
1119
+ "audio.sfx": {
1120
+ "kind": "audio",
1121
+ "assetId": "sfx-1",
1122
+ "versionId": "sfx-1-v1",
1123
+ "url": "https://media.example/sfx-1.wav",
1124
+ "mimeType": "audio/wav",
1125
+ "duration": 1.1,
1126
+ "bytes": 52844,
1127
+ "sampleRate": 24000,
1128
+ "channels": 1
1129
+ }
1130
+ }
1131
+ },
1132
+ "expected": {
1133
+ "editor": {
1134
+ "schemaVersion": 1,
1135
+ "activeTimelineId": "timeline-1",
1136
+ "tracks": [
1137
+ {
1138
+ "id": "video-1",
1139
+ "type": "video"
1140
+ }
1141
+ ],
1142
+ "clips": [
1143
+ {
1144
+ "id": "clip-1",
1145
+ "trackId": "video-1",
1146
+ "url": "https://media.example/picture-1.png",
1147
+ "start": 0,
1148
+ "duration": 4
1149
+ }
1150
+ ],
1151
+ "audioItems": [],
1152
+ "captions": [],
1153
+ "textOverlays": [],
1154
+ "effects": []
1155
+ },
1156
+ "made": [
1157
+ {
1158
+ "kind": "audio",
1159
+ "assetId": "sfx-1",
1160
+ "versionId": "sfx-1-v1",
1161
+ "url": "https://media.example/sfx-1.wav",
1162
+ "mimeType": "audio/wav",
1163
+ "duration": 1.1,
1164
+ "bytes": 52844,
1165
+ "sampleRate": 24000,
1166
+ "channels": 1
1167
+ }
1168
+ ],
1169
+ "result": {
1170
+ "kind": "audio",
1171
+ "assetId": "sfx-1",
1172
+ "versionId": "sfx-1-v1",
1173
+ "url": "https://media.example/sfx-1.wav",
1174
+ "mimeType": "audio/wav",
1175
+ "duration": 1.1,
1176
+ "bytes": 52844,
1177
+ "sampleRate": 24000,
1178
+ "channels": 1
1179
+ }
1180
+ }
1181
+ },
1182
+ {
1183
+ "name": "Finds the words of the narration",
1184
+ "input": {
1185
+ "make": "transcribe",
1186
+ "editor": {
1187
+ "schemaVersion": 1,
1188
+ "activeTimelineId": "timeline-1",
1189
+ "tracks": [
1190
+ {
1191
+ "id": "video-1",
1192
+ "type": "video"
1193
+ }
1194
+ ],
1195
+ "clips": [
1196
+ {
1197
+ "id": "clip-1",
1198
+ "trackId": "video-1",
1199
+ "url": "https://media.example/picture-1.png",
1200
+ "start": 0,
1201
+ "duration": 4
1202
+ }
1203
+ ],
1204
+ "audioItems": [],
1205
+ "captions": [],
1206
+ "textOverlays": [],
1207
+ "effects": []
1208
+ },
1209
+ "audio": {
1210
+ "kind": "audio",
1211
+ "assetId": "voice-1",
1212
+ "versionId": "voice-1-v1",
1213
+ "url": "https://media.example/voice-1.wav",
1214
+ "mimeType": "audio/wav",
1215
+ "duration": 1.1,
1216
+ "bytes": 52844,
1217
+ "sampleRate": 24000,
1218
+ "channels": 1
1219
+ }
1220
+ },
1221
+ "mocks": {
1222
+ "hosted": {
1223
+ "audio.transcribe": {
1224
+ "schemaVersion": 1,
1225
+ "text": "A small idea.",
1226
+ "words": [
1227
+ {
1228
+ "text": "A",
1229
+ "start": 0,
1230
+ "end": 0.2
1231
+ },
1232
+ {
1233
+ "text": "small",
1234
+ "start": 0.2,
1235
+ "end": 0.6
1236
+ },
1237
+ {
1238
+ "text": "idea.",
1239
+ "start": 0.6,
1240
+ "end": 1.1
1241
+ }
1242
+ ]
1243
+ }
1244
+ }
1245
+ },
1246
+ "expected": {
1247
+ "editor": {
1248
+ "schemaVersion": 1,
1249
+ "activeTimelineId": "timeline-1",
1250
+ "tracks": [
1251
+ {
1252
+ "id": "video-1",
1253
+ "type": "video"
1254
+ }
1255
+ ],
1256
+ "clips": [
1257
+ {
1258
+ "id": "clip-1",
1259
+ "trackId": "video-1",
1260
+ "url": "https://media.example/picture-1.png",
1261
+ "start": 0,
1262
+ "duration": 4
1263
+ }
1264
+ ],
1265
+ "audioItems": [],
1266
+ "captions": [],
1267
+ "textOverlays": [],
1268
+ "effects": []
1269
+ },
1270
+ "result": {
1271
+ "text": "A small idea.",
1272
+ "words": [
1273
+ {
1274
+ "word": "A",
1275
+ "text": "A",
1276
+ "start": 0,
1277
+ "end": 0.2
1278
+ },
1279
+ {
1280
+ "word": "small",
1281
+ "text": "small",
1282
+ "start": 0.2,
1283
+ "end": 0.6
1284
+ },
1285
+ {
1286
+ "word": "idea.",
1287
+ "text": "idea.",
1288
+ "start": 0.6,
1289
+ "end": 1.1
1290
+ }
1291
+ ]
1292
+ }
1293
+ }
1294
+ }
1295
+ ],
1296
+ "moduleSource": {
1297
+ "main.js": "//#region packages/block-platform/audio-score-prompts.js\n/** JSON from a model's answer: a fenced block, the whole text, or the first array/object in it. */\nfunction parseScoreJson(text, fallback) {\n\tif (!text) return fallback;\n\tconst m = text.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\n\tconst raw = (m ? m[1] : text).trim();\n\ttry {\n\t\treturn JSON.parse(raw);\n\t} catch {}\n\tconst a = raw.indexOf(\"[\"), o = raw.indexOf(\"{\");\n\tconst s = a >= 0 && (o < 0 || a < o) ? a : o;\n\tconst e = Math.max(raw.lastIndexOf(\"]\"), raw.lastIndexOf(\"}\"));\n\tif (s >= 0 && e > s) try {\n\t\treturn JSON.parse(raw.slice(s, e + 1));\n\t} catch {}\n\treturn fallback;\n}\nfunction buildScoreContext(bible, voiceoverShotMap) {\n\tconst vo = {};\n\tconst vsScenes = voiceoverShotMap?.scenes || voiceoverShotMap || [];\n\t(Array.isArray(vsScenes) ? vsScenes : []).forEach((sc) => {\n\t\t(sc?.shots || sc?.shotList || []).forEach((sh) => {\n\t\t\tif (sh?.id) vo[sh.id] = sh.voiceover || sh.line || sh.text || \"\";\n\t\t});\n\t});\n\treturn (bible?.scenes || []).map((sc) => ({\n\t\tid: sc.id,\n\t\tsceneNumber: sc.sceneNumber,\n\t\ttitle: sc.title || `Scene ${sc.sceneNumber ?? \"\"}`,\n\t\ttone: sc.emotionalTone || sc.tone || \"\",\n\t\ttimeOfDay: sc.timeOfDay || \"\",\n\t\tlocation: sc.location || \"\",\n\t\twhatHappens: sc.whatHappens || sc.action || \"\",\n\t\tshots: (sc.shotList || []).map((sh) => ({\n\t\t\tid: sh.id,\n\t\t\ttype: sh.type || \"\",\n\t\t\tdescription: sh.description || \"\",\n\t\t\timagePrompt: sh.imagePromptReadable || sh.imagePrompt || \"\",\n\t\t\tvideoPrompt: sh.videoPromptReadable || sh.videoPrompt || \"\",\n\t\t\tvoiceover: vo[sh.id] || \"\",\n\t\t\tduration: Number(sh.intendedDuration || sh.generationLength || 4)\n\t\t}))\n\t}));\n}\nfunction moodMapRequest({ bible, voiceoverShotMap, visionFrames }) {\n\tconst ctx = buildScoreContext(bible, voiceoverShotMap);\n\tlet t = 0;\n\tconst fallback = ctx.map((sc) => {\n\t\tconst dur = sc.shots.reduce((n, s) => n + (s.duration || 4), 0) || 8;\n\t\tconst start = t;\n\t\tt += dur;\n\t\treturn {\n\t\t\tsceneId: sc.id,\n\t\t\ttitle: sc.title,\n\t\t\temotionalTone: sc.tone || \"neutral\",\n\t\t\tenergy: 5,\n\t\t\tmusicalMood: sc.tone ? `${sc.tone}, cinematic` : \"cinematic, atmospheric\",\n\t\t\tinstrumentation: \"strings, piano, soft percussion\",\n\t\t\tbpm: \"70-90\",\n\t\t\tstart,\n\t\t\tend: t\n\t\t};\n\t});\n\tconst vision = (visionFrames || []).slice(0, 8);\n\treturn {\n\t\tempty: !ctx.length && !vision.length,\n\t\tfallback,\n\t\tvision,\n\t\tmaxTokens: 3e3,\n\t\tsystem: \"You are a film score supervisor. Output strict JSON only.\",\n\t\tprompt: `Build a scene mood map for scoring. ${vision.length ? \"Frames from an uploaded video are attached. \" : \"\"}Use this story context (timestamps in seconds, derive start/end from shot durations in order):\n\n${JSON.stringify(ctx)}\n\nReturn JSON array, one object per scene:\n[{\"sceneId\",\"title\",\"emotionalTone\",\"energy\"(1-10 int),\"musicalMood\",\"instrumentation\",\"bpm\"(e.g. \"70-90\"),\"start\"(sec),\"end\"(sec)}]`\n\t};\n}\nfunction musicPromptsRequest({ moodMap, bible, styleNotes }) {\n\tconst style = bible?.visualStyle || bible?.styleGuide || bible?.tone || \"\";\n\treturn {\n\t\tfallback: (moodMap || []).map((s) => ({\n\t\t\tsceneId: s.sceneId,\n\t\t\tprompt: `${s.musicalMood || \"cinematic\"}, ${s.instrumentation || \"orchestral\"}, ${s.bpm || \"80\"} BPM, ${s.emotionalTone || \"neutral\"} mood, no vocals, film score`\n\t\t})),\n\t\tmaxTokens: 2500,\n\t\tsystem: \"You write concise music-generation prompts for a film score. Output strict JSON only.\",\n\t\tprompt: `Visual style: ${JSON.stringify(style)}\nUser direction (optional): ${styleNotes || \"none\"}\nMood map: ${JSON.stringify(moodMap)}\n\nFor each scene return a single rich music-gen prompt (instrumentation, tempo BPM, energy, mood, \"no vocals\"). JSON: [{\"sceneId\",\"prompt\"}]`\n\t};\n}\nconst SFX_CUES = [\n\t[/thunder|lightning/i, \"a thunderclap\"],\n\t[/storm/i, \"storm wind and rain\"],\n\t[/\\brain\\b|downpour/i, \"steady rain\"],\n\t[/ocean|\\bsea\\b|waves?|surf|shore/i, \"ocean waves on the shore\"],\n\t[/wind|gust|breeze/i, \"gusting wind\"],\n\t[/\\bfire\\b|flames?|campfire|burning/i, \"crackling fire\"],\n\t[/door|gate/i, \"a door opening and closing\"],\n\t[/crowd|applause|cheer/i, \"a murmuring crowd\"],\n\t[/explosion|blast|detonat/i, \"an explosion\"],\n\t[/footsteps?|walking|running/i, \"footsteps\"],\n\t[/engine|motor|car|truck|machine/i, \"a running engine\"],\n\t[/birds?|seagull/i, \"birdsong\"],\n\t[/forest|woods|leaves/i, \"rustling forest ambience\"],\n\t[/\\bgun|gunshot|rifle|pistol/i, \"a gunshot\"],\n\t[/crash|shatter|glass break/i, \"shattering glass\"],\n\t[/\\bbell\\b|chime/i, \"a tolling bell\"],\n\t[/clock|ticking/i, \"a ticking clock\"],\n\t[/water|river|stream|drip/i, \"flowing water\"]\n];\n/**\n* The sound-effect scan of a cut. `result` set: answer it as is (nothing to analyze). Otherwise `fallback` is the\n* heuristic answer (no model, or an answer that does not parse) and system/prompt/maxTokens are the model call.\n*/\nfunction sfxDetectRequest({ bible, voiceoverShotMap, transcript = \"\", totalDuration = 0, panelMode = false, beats = null }) {\n\tconst ctx = buildScoreContext(bible, voiceoverShotMap);\n\tlet t = 0;\n\tconst flat = [];\n\tctx.forEach((sc) => sc.shots.forEach((sh) => {\n\t\tflat.push({\n\t\t\t...sh,\n\t\t\tsceneTitle: sc.title,\n\t\t\tstart: t\n\t\t});\n\t\tt += sh.duration || 4;\n\t}));\n\tconst shotText = (s) => `${s.imagePrompt || \"\"} ${s.videoPrompt || \"\"} ${s.description || \"\"} ${s.voiceover || \"\"}`.trim();\n\tconst hasShotContent = flat.some((s) => shotText(s).length > 0);\n\tconst script = String(transcript || \"\").trim();\n\tconst panelBeats = Array.isArray(beats) ? beats.map((b) => ({\n\t\tstart: Math.max(0, Math.round(Number(b?.start) || 0)),\n\t\tonScreen: String(b?.image ?? b?.onScreen ?? \"\").replace(/\\s+/g, \" \").trim().slice(0, 300),\n\t\tsaid: String(b?.narration ?? b?.said ?? b?.words ?? \"\").replace(/\\s+/g, \" \").trim().slice(0, 300)\n\t})).filter((b) => b.onScreen || b.said).slice(0, 240) : [];\n\tconst hasBeats = panelBeats.length > 0;\n\tif (!hasShotContent && !script && !hasBeats) return { result: {\n\t\tsfx: [],\n\t\treason: \"no-content\"\n\t} };\n\tconst seen = /* @__PURE__ */ new Set();\n\tconst heuristic = [];\n\tfor (const s of flat) {\n\t\tconst hay = `${s.imagePrompt} ${s.videoPrompt} ${s.description}`;\n\t\tconst cue = SFX_CUES.find(([re]) => re.test(hay));\n\t\tif (!cue) continue;\n\t\tif (seen.has(cue[1])) continue;\n\t\tseen.add(cue[1]);\n\t\theuristic.push({\n\t\t\ttimestamp: s.start,\n\t\t\ttrigger: cue[1],\n\t\t\tprompt: `${cue[1]}, diegetic, realistic, no music`\n\t\t});\n\t\tif (heuristic.length >= 8) break;\n\t}\n\tconst span = t || Number(totalDuration) || 0;\n\tconst shotPayload = flat.filter((s) => shotText(s).length > 0).map((s) => ({\n\t\tid: s.id,\n\t\tstart: s.start,\n\t\timage: s.imagePrompt,\n\t\tvideo: s.videoPrompt\n\t}));\n\tconst parts = [];\n\tif (hasBeats) parts.push(`This is a still-image / panel project (~${Math.round(span)}s). Below is every beat in time order — \"start\" is seconds into the video, \"onScreen\" is what that panel depicts, \"said\" is the narration spoken during it. Decide each beat from BOTH together: a concrete real-world sound the image implies (a match strikes, a coin drops, glass breaks) OR a sound the narration makes literal at that exact moment. Do NOT invent a sound that neither the image nor the words support:\n${JSON.stringify(panelBeats)}`);\n\telse {\n\t\tif (shotPayload.length) parts.push(`Shots in order (start = seconds into the video). Decide per shot from its image/video prompt:\n${JSON.stringify(shotPayload)}`);\n\t\tif (script) parts.push(`This is a still-image / panel project narrated by a voiceover — there are NO per-shot visual prompts, so infer diegetic sound moments from what the NARRATION actually describes (e.g. a lighter flicking, an exhale, bedsheets, a phone). Video length is about ${Math.round(span)} seconds; spread the timestamps across that span. Narration:\n${script.slice(0, 6e3)}`);\n\t}\n\tif (panelMode) parts.push(\"IMPORTANT (panel project): only flag moments with an INHERENT real-world sound (something sizzles, cracks, explodes, rings, pours). Purely visual changes — something appears, a color/lighting/mood shifts, a shadow falls, something glows or changes size — are handled by a separate \\\"pop\\\" system: return NOTHING for those, no matter how vivid the description is.\");\n\treturn {\n\t\tfallback: heuristic,\n\t\tmaxTokens: 2500,\n\t\tsystem: \"You are a sound designer placing diegetic sound effects on a video timeline. Read what is actually on screen or described in the narration, and flag ONLY the moments where a specific, concrete sound is clearly happening. Be selective: most moments need NO sound effect. Skip talking-head/dialogue-only moments and anything where music alone would carry it. Never put the same generic effect on everything — each effect must be specific to what is actually happening. If the content does not describe any concrete sound, return an empty array.\",\n\t\tprompt: `${parts.join(\"\\n\\n\")}\n\nReturn ONLY moments that genuinely need a sound effect, as JSON:\n[{\"timestamp\": <seconds>, \"trigger\": \"<the concrete source, short>\", \"prompt\": \"<a specific sound-effect description, diegetic, realistic, no music or speech>\"}]\nReturn few, targeted moments — not one per shot. If nothing clearly needs SFX, return [].`\n\t};\n}\n/** The scan's answer from the model's text: its list, or the heuristic when it does not parse. */\nfunction sfxDetectAnswer(request, text) {\n\tconst parsed = parseScoreJson(text, null);\n\treturn { sfx: Array.isArray(parsed) ? parsed : request.fallback };\n}\n\n//#endregion\n//#region blocks/native/sm.editor/frame/main.js\nconst fail = (message) => {\n\tthrow Error(message);\n};\nconst plain = (value) => !!value && typeof value === \"object\" && !Array.isArray(value);\nconst isMedia = (value, kind) => plain(value) && typeof value.url === \"string\" && typeof value.assetId === \"string\" && (!kind || value.kind === kind);\nconst dataUrlBytes = (dataUrl) => {\n\tconst match = /^data:([a-z]+\\/[a-z0-9.+-]+)(?:;[^,;]*)*;base64,(.+)$/i.exec(String(dataUrl || \"\"));\n\tif (!match) fail(\"Choose a file to save.\");\n\tconst text = atob(match[2]), bytes = new Uint8Array(text.length);\n\tfor (let index = 0; index < text.length; index++) bytes[index] = text.charCodeAt(index);\n\treturn {\n\t\tmediaType: match[1].toLowerCase(),\n\t\tbuffer: bytes.buffer\n\t};\n};\nconst EXTENSIONS = {\n\t\"image/png\": \"png\",\n\t\"image/jpeg\": \"jpg\",\n\t\"image/webp\": \"webp\",\n\t\"image/gif\": \"gif\",\n\t\"video/mp4\": \"mp4\",\n\t\"video/webm\": \"webm\",\n\t\"video/quicktime\": \"mov\",\n\t\"audio/mpeg\": \"mp3\",\n\t\"audio/wav\": \"wav\",\n\t\"audio/x-wav\": \"wav\",\n\t\"audio/mp4\": \"m4a\",\n\t\"audio/ogg\": \"ogg\",\n\t\"audio/webm\": \"webm\"\n};\nconst SCORE_SECONDS = Object.freeze({\n\tmusic: Object.freeze({\n\t\tmin: 5,\n\t\tmax: 240,\n\t\tdefault: 30\n\t}),\n\tsfx: Object.freeze({\n\t\tmin: .5,\n\t\tmax: 22,\n\t\tdefault: 4\n\t})\n});\nasync function run(input, stillmade) {\n\tconst make = typeof input.make === \"string\" ? input.make : \"use\";\n\tconst request = plain(input.request) ? input.request : {};\n\tconst references = (Array.isArray(input.references) ? input.references : []).filter((value) => isMedia(value, \"image\"));\n\tconst editor = plain(input.editor) ? {\n\t\t...input.editor,\n\t\tschemaVersion: input.editor.schemaVersion ?? 1\n\t} : null;\n\tconst done = (result, made = []) => ({\n\t\t...editor ? { editor } : {},\n\t\t...made.length ? { made } : {},\n\t\tresult: result === void 0 ? null : result\n\t});\n\tswitch (make) {\n\t\tcase \"use\": return done(null);\n\t\tcase \"image\": {\n\t\t\tif (typeof request.readablePrompt !== \"string\" || !request.readablePrompt.trim()) fail(\"Write what the picture shows first.\");\n\t\t\tstillmade.progress(.2, \"Making the picture\");\n\t\t\tconst settings = Object.fromEntries(Object.entries({\n\t\t\t\tmodel: request.model,\n\t\t\t\taspectRatio: request.aspectRatio,\n\t\t\t\tquality: request.quality,\n\t\t\t\tresolution: request.resolution\n\t\t\t}).filter(([, value]) => typeof value === \"string\" && value));\n\t\t\tconst made = await stillmade.hosted.run(\"image.generate\", {\n\t\t\t\tprompt: request.readablePrompt,\n\t\t\t\t...references.length ? { references: references.slice(0, 8) } : {},\n\t\t\t\t...Object.keys(settings).length ? { settings } : {}\n\t\t\t});\n\t\t\treturn done(made, [made]);\n\t\t}\n\t\tcase \"video\": {\n\t\t\tstillmade.progress(.1, \"Making the clip\");\n\t\t\tconst settings = Object.fromEntries(Object.entries({\n\t\t\t\tmodel: request.model,\n\t\t\t\taspectRatio: request.aspectRatio,\n\t\t\t\tquality: request.quality,\n\t\t\t\tresolution: request.resolution,\n\t\t\t\tduration: request.duration,\n\t\t\t\tseed: request.seed,\n\t\t\t\tgenerateAudio: request.generateAudio\n\t\t\t}).filter(([, value]) => value !== void 0 && value !== null && value !== \"\"));\n\t\t\tconst made = await stillmade.hosted.run(\"video.generate\", {\n\t\t\t\tprompt: request.readablePrompt || \"\",\n\t\t\t\t...isMedia(input.firstFrame, \"image\") ? { firstFrame: input.firstFrame } : {},\n\t\t\t\t...isMedia(input.lastFrame, \"image\") ? { lastFrame: input.lastFrame } : {},\n\t\t\t\t...request.negativePrompt ? { negativePrompt: request.negativePrompt } : {},\n\t\t\t\t...Object.keys(settings).length ? { settings } : {}\n\t\t\t});\n\t\t\treturn done(made, [made]);\n\t\t}\n\t\tcase \"transcribe\": {\n\t\t\tif (!isMedia(input.audio)) fail(\"Choose the sound to transcribe.\");\n\t\t\tstillmade.progress(.2, \"Finding the words\");\n\t\t\tconst timing = await stillmade.hosted.run(\"audio.transcribe\", { audio: input.audio });\n\t\t\tconst words = (Array.isArray(timing?.words) ? timing.words : []).map((word) => ({\n\t\t\t\tword: word.text ?? word.word,\n\t\t\t\ttext: word.text ?? word.word,\n\t\t\t\tstart: word.start,\n\t\t\t\tend: word.end\n\t\t\t}));\n\t\t\treturn done({\n\t\t\t\ttext: String(timing?.text || words.map((word) => word.text).join(\" \")),\n\t\t\t\twords\n\t\t\t});\n\t\t}\n\t\tcase \"upload\": {\n\t\t\tconst { mediaType, buffer } = dataUrlBytes(input.dataUrl);\n\t\t\tconst extension = EXTENSIONS[mediaType] || fail(\"Save a picture, a clip or a sound file.\");\n\t\t\tconst name = String(input.key || `upload.${extension}`).replace(/[^\\w.-]+/g, \"-\").slice(0, 120) || `upload.${extension}`;\n\t\t\tconst saved = await stillmade.files.write(buffer, {\n\t\t\t\tmediaType,\n\t\t\t\tname: /\\.[a-z0-9]{2,5}$/i.test(name) ? name : `${name}.${extension}`\n\t\t\t});\n\t\t\treturn done(saved, [saved]);\n\t\t}\n\t\tcase \"export\": {\n\t\t\tconst kind = request.kind === \"audio\" ? \"audio\" : \"video\";\n\t\t\tstillmade.progress(.05, kind === \"audio\" ? \"Mixing the sound\" : \"Rendering the video\");\n\t\t\tconst { asset } = await stillmade.actions.run({\n\t\t\t\tschemaVersion: 1,\n\t\t\t\tkind: \"stillmade.media-action\",\n\t\t\t\toperation: \"final.render\",\n\t\t\t\tresultPort: null,\n\t\t\t\trequest: {\n\t\t\t\t\tschemaVersion: 1,\n\t\t\t\t\tkind,\n\t\t\t\t\ttimeline: request.timeline,\n\t\t\t\t\tsettings: request.settings,\n\t\t\t\t\tsource: plain(request.source) ? request.source : {\n\t\t\t\t\t\tid: \"sm.editor\",\n\t\t\t\t\t\tversion: \"3.1.0\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t\tconst kept = {\n\t\t\t\tkind: asset.kind,\n\t\t\t\tassetId: asset.assetId,\n\t\t\t\tversionId: asset.versionId,\n\t\t\t\turl: asset.url,\n\t\t\t\t...asset.mimeType ? { mimeType: asset.mimeType } : {},\n\t\t\t\t...Number(asset.duration) > 0 ? { duration: Number(asset.duration) } : {}\n\t\t\t};\n\t\t\treturn {\n\t\t\t\t...done(kept, [kept]),\n\t\t\t\t[kind === \"audio\" ? \"exportAudio\" : \"export\"]: kept\n\t\t\t};\n\t\t}\n\t\tcase \"score\": {\n\t\t\tconst action = String(request.action || \"\"), body = plain(request.body) ? request.body : {};\n\t\t\tconst ask = (built) => stillmade.hosted.run(\"text.generate\", {\n\t\t\t\tsystem: built.system,\n\t\t\t\tprompt: built.prompt,\n\t\t\t\tmaxTokens: built.maxTokens\n\t\t\t});\n\t\t\tconst seconds = (value, { min, max, default: fallback }) => Math.max(min, Math.min(max, Number(value) || fallback));\n\t\t\tconst sound = async (operation, limits, label) => {\n\t\t\t\tconst prompt = String(body.prompt || \"\").trim().slice(0, 4e3) || fail(\"Write the sound to make first.\");\n\t\t\t\tstillmade.progress(.1, label);\n\t\t\t\tconst made = await stillmade.hosted.run(operation, {\n\t\t\t\t\tprompt,\n\t\t\t\t\tdurationSec: seconds(body.durationSec ?? body.duration, limits)\n\t\t\t\t});\n\t\t\t\treturn done({\n\t\t\t\t\t...made,\n\t\t\t\t\t...Number(made?.duration) > 0 ? { duration: Number(made.duration) } : {}\n\t\t\t\t}, [made]);\n\t\t\t};\n\t\t\tswitch (action) {\n\t\t\t\tcase \"sfx-detect\": {\n\t\t\t\t\tconst built = sfxDetectRequest(body);\n\t\t\t\t\tif (built.result) return done(built.result);\n\t\t\t\t\tstillmade.progress(.2, \"Finding the sound effects\");\n\t\t\t\t\treturn done(sfxDetectAnswer(built, await ask(built)));\n\t\t\t\t}\n\t\t\t\tcase \"moodmap\": {\n\t\t\t\t\tconst built = moodMapRequest({\n\t\t\t\t\t\t...body,\n\t\t\t\t\t\tvisionFrames: []\n\t\t\t\t\t});\n\t\t\t\t\tif (built.empty) return done({ moodMap: built.fallback });\n\t\t\t\t\tstillmade.progress(.2, \"Reading the mood of each scene\");\n\t\t\t\t\treturn done({ moodMap: parseScoreJson(await ask(built), built.fallback) || built.fallback });\n\t\t\t\t}\n\t\t\t\tcase \"music-prompts\": {\n\t\t\t\t\tconst built = musicPromptsRequest(body);\n\t\t\t\t\tstillmade.progress(.2, \"Writing the music for each scene\");\n\t\t\t\t\treturn done({ prompts: parseScoreJson(await ask(built), built.fallback) || built.fallback });\n\t\t\t\t}\n\t\t\t\tcase \"music\": return sound(\"audio.music\", SCORE_SECONDS.music, \"Making the music\");\n\t\t\t\tcase \"sfx\": return sound(\"audio.sfx\", SCORE_SECONDS.sfx, \"Making the sound effect\");\n\t\t\t}\n\t\t\tfail(\"Choose a supported Audio Score step.\");\n\t\t}\n\t\tcase \"stock-status\": return done({ available: true });\n\t\tcase \"stock-grid\": {\n\t\t\tconst { results } = await stillmade.actions.run({\n\t\t\t\tschemaVersion: 1,\n\t\t\t\tkind: \"stillmade.media-action\",\n\t\t\t\toperation: \"stock.search\",\n\t\t\t\tresultPort: null,\n\t\t\t\trequest: {\n\t\t\t\t\tquery: String(request.query || request.q || \"\").slice(0, 500) || \"scene\",\n\t\t\t\t\ttype: request.type === \"video\" ? \"video\" : \"photo\"\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn done(results || []);\n\t\t}\n\t\tcase \"storage-get\": return done(await stillmade.storage.get(String(input.key || \"\")));\n\t\tcase \"storage-set\":\n\t\t\tawait stillmade.storage.set(String(input.key || \"\"), input.value ?? null);\n\t\t\treturn done(true);\n\t}\n\tfail(`${make} is not something the Editor Block does yet.`);\n}\n\n//#endregion\nexport { run as default };"
1298
+ }
1299
+ };