@litfamily/litgrok 1.0.26 → 1.0.28

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 (235) hide show
  1. package/.grok/hooks/lit-mark.mjs +42 -0
  2. package/.grok/hooks/lit-status-line.mjs +174 -21
  3. package/.grok/hooks/plan-gate.mjs +13 -3
  4. package/.grok/hooks/record-passive-event.mjs +40 -6
  5. package/.grok/hooks/session-end.json +15 -0
  6. package/.grok/hooks/session-end.mjs +6 -0
  7. package/.grok/hooks/session-liveness.mjs +102 -0
  8. package/.grok/hooks/session-rows.mjs +57 -0
  9. package/.grok/hooks/session-start.mjs +17 -0
  10. package/.grok/hooks/workflow-stage.mjs +5 -2
  11. package/.grok/rules/00-litgrok.md +14 -0
  12. package/.grok/skills/frontend-ui-ux/ATTRIBUTION.md +23 -0
  13. package/.grok/skills/frontend-ui-ux/SKILL.md +4 -1
  14. package/.grok/skills/frontend-ui-ux/examples/phase-3-readme-ab/README.md +9 -0
  15. package/.grok/skills/frontend-ui-ux/kit/components.css +414 -0
  16. package/.grok/skills/frontend-ui-ux/kit/fonts.json +22 -0
  17. package/.grok/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
  18. package/.grok/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
  19. package/.grok/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
  20. package/.grok/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
  21. package/.grok/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
  22. package/.grok/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
  23. package/.grok/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
  24. package/.grok/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
  25. package/.grok/skills/frontend-ui-ux/kit/ui.js +226 -0
  26. package/.grok/skills/frontend-ui-ux/references/adaptive-layout.md +10 -4
  27. package/.grok/skills/frontend-ui-ux/references/brand-and-imagery.md +11 -7
  28. package/.grok/skills/frontend-ui-ux/references/complete-contract.md +21 -0
  29. package/.grok/skills/frontend-ui-ux/references/composition.md +10 -6
  30. package/.grok/skills/frontend-ui-ux/references/craft-floor.md +256 -93
  31. package/.grok/skills/frontend-ui-ux/references/creative-directions.md +9 -6
  32. package/.grok/skills/frontend-ui-ux/references/default-editorial-pixel.json +28 -0
  33. package/.grok/skills/frontend-ui-ux/references/house-styles.md +234 -0
  34. package/.grok/skills/frontend-ui-ux/references/implementation-platforms.md +10 -6
  35. package/.grok/skills/frontend-ui-ux/references/inclusive-interface.md +11 -5
  36. package/.grok/skills/frontend-ui-ux/references/interaction-motion.md +34 -78
  37. package/.grok/skills/frontend-ui-ux/references/motion-guide.md +10 -7
  38. package/.grok/skills/frontend-ui-ux/references/performance-delivery.md +11 -8
  39. package/.grok/skills/frontend-ui-ux/references/probe-loop.md +1 -1
  40. package/.grok/skills/frontend-ui-ux/references/product-direction.md +9 -4
  41. package/.grok/skills/frontend-ui-ux/references/production-interview.md +17 -0
  42. package/.grok/skills/frontend-ui-ux/references/redesign-playbook.md +10 -6
  43. package/.grok/skills/frontend-ui-ux/references/slop-register.md +148 -73
  44. package/.grok/skills/frontend-ui-ux/references/system-foundations.md +10 -4
  45. package/.grok/skills/frontend-ui-ux/references/visual-language.md +10 -4
  46. package/.grok/skills/frontend-ui-ux/references/visual-reconstruction.md +39 -72
  47. package/.grok/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
  48. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-driver.mjs +33 -0
  49. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
  50. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +1555 -0
  51. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +153 -0
  52. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-server.mjs +57 -0
  53. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +139 -0
  54. package/.grok/skills/frontend-ui-ux/scripts/interface-probe.mjs +363 -0
  55. package/.grok/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
  56. package/.grok/skills/lit-adhd/SKILL.md +155 -0
  57. package/.grok/skills/lit-adhd/references/card-examples.md +131 -0
  58. package/.grok/skills/lit-adhd/references/status-taxonomy.md +50 -0
  59. package/.grok/skills/lit-adhd/scripts/lib/brief.mjs +421 -0
  60. package/.grok/skills/lit-adhd/scripts/lib/collect.mjs +395 -0
  61. package/.grok/skills/lit-adhd/scripts/lib/gauge.mjs +87 -0
  62. package/.grok/skills/lit-adhd/scripts/lib/next-step.mjs +22 -0
  63. package/.grok/skills/lit-adhd/scripts/lib/plan-task-rows.mjs +32 -0
  64. package/.grok/skills/lit-adhd/scripts/lib/project-state-root.mjs +55 -0
  65. package/.grok/skills/lit-adhd/scripts/lib/redact.mjs +64 -0
  66. package/.grok/skills/lit-adhd/scripts/lib/suggest.mjs +140 -0
  67. package/.grok/skills/lit-adhd/scripts/lib/transcripts.mjs +232 -0
  68. package/.grok/skills/lit-adhd/scripts/rewind.mjs +6 -0
  69. package/.grok/skills/lit-diagram-drawer/SKILL.md +17 -5
  70. package/.grok/skills/lit-diagram-drawer/references/output-spec.md +2 -2
  71. package/.grok/skills/lit-diagram-drawer/references/style-guide.md +20 -11
  72. package/.grok/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
  73. package/.grok/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
  74. package/.grok/skills/lit-diagram-drawer/references/type-tree.md +3 -3
  75. package/.grok/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
  76. package/.grok/skills/lit-diagram-drawer/references/verifier-guide.md +4 -0
  77. package/.grok/skills/lit-diagram-drawer/scripts/visual-quality.mjs +2 -2
  78. package/.grok/skills/lit-handoff/SKILL.md +2 -0
  79. package/.grok/skills/lit-hijack/SKILL.md +459 -0
  80. package/.grok/skills/lit-hijack/references/capture-and-provenance.md +124 -0
  81. package/.grok/skills/lit-hijack/references/clean-room-build.md +130 -0
  82. package/.grok/skills/lit-hijack/references/distil-and-licence.md +128 -0
  83. package/.grok/skills/lit-hijack/references/gap-and-port-design.md +150 -0
  84. package/.grok/skills/lit-hijack/references/parity-and-report.md +130 -0
  85. package/.grok/skills/lit-hijack/scripts/hijack.mjs +890 -0
  86. package/.grok/skills/lit-hijack/scripts/lib/safe-fetch.mjs +213 -0
  87. package/.grok/skills/lit-plan/SKILL.md +3 -1
  88. package/.grok/skills/lit-plan/references/plan-schema.md +4 -0
  89. package/.grok/skills/lit-plan/scripts/scaffold-plan.mjs +9 -2
  90. package/.grok/skills/lit-plan/scripts/scope-roots.mjs +83 -0
  91. package/.grok/skills/lit-pptx/SKILL.md +10 -5
  92. package/.grok/skills/lit-pptx/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
  93. package/.grok/skills/lit-pptx/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
  94. package/.grok/skills/lit-pptx/examples/15-design-lecture-bauhaus-ko.md +139 -0
  95. package/.grok/skills/lit-pptx/examples/16-community-programme-riso-ko.md +158 -0
  96. package/.grok/skills/lit-pptx/examples/17-public-data-briefing-newsroom-ko.md +179 -0
  97. package/.grok/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
  98. package/.grok/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
  99. package/.grok/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
  100. package/.grok/skills/lit-pptx/fonts/jetbrains-mono/provenance.json +1 -0
  101. package/.grok/skills/lit-pptx/references/direction-step.md +34 -8
  102. package/.grok/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
  103. package/.grok/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
  104. package/.grok/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
  105. package/.grok/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
  106. package/.grok/skills/lit-pptx/references/tonalities/riso.md +137 -0
  107. package/.grok/skills/lit-pptx/scripts/compile-deck.js +1 -1
  108. package/.grok/skills/lit-pptx/scripts/craft_extras.py +61 -3
  109. package/.grok/skills/lit-pptx/scripts/deck_output.py +76 -62
  110. package/.grok/skills/lit-pptx/scripts/embed_fonts.py +14 -1
  111. package/.grok/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
  112. package/.grok/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
  113. package/.grok/skills/lit-pptx/scripts/lib/render-pack.js +389 -582
  114. package/.grok/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
  115. package/.grok/skills/lit-pptx/scripts/lib/template-registry.js +60 -8
  116. package/.grok/skills/lit-pptx/scripts/qa_deck.py +4 -4
  117. package/.grok/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +2 -0
  118. package/.grok/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
  119. package/.grok/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +2 -0
  120. package/.grok/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
  121. package/.grok/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +2 -0
  122. package/.grok/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +2 -0
  123. package/.grok/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
  124. package/.grok/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
  125. package/.grok/skills/lit-pptx/templates/tonalities/night/pack.yaml +2 -0
  126. package/.grok/skills/lit-pptx/templates/tonalities/paper/pack.yaml +2 -0
  127. package/.grok/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
  128. package/.grok/skills/lit-pptx/templates/tonalities/signal/pack.yaml +3 -0
  129. package/.grok/skills/lit-pptx/templates/tonalities/studio/pack.yaml +2 -0
  130. package/.grok/skills/lit-recap/SKILL.md +1 -1
  131. package/.grok/skills/lit-schematic/SKILL.md +140 -0
  132. package/.grok/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
  133. package/.grok/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
  134. package/.grok/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
  135. package/.grok/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
  136. package/.grok/skills/lit-schematic/references/cutaway-outputs.md +70 -0
  137. package/.grok/skills/lit-schematic/references/isometric-craft.md +101 -0
  138. package/.grok/skills/lit-schematic/references/journal-figures.md +72 -0
  139. package/.grok/skills/lit-schematic/references/kit-api.md +134 -0
  140. package/.grok/skills/lit-schematic/references/pfd-and-layout.md +53 -0
  141. package/.grok/skills/lit-schematic/references/pid-drafting.md +103 -0
  142. package/.grok/skills/lit-schematic/references/pid-symbols.md +163 -0
  143. package/.grok/skills/lit-schematic/references/verification.md +100 -0
  144. package/.grok/skills/lit-schematic/scripts/doctor.mjs +57 -0
  145. package/.grok/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
  146. package/.grok/skills/lit-schematic/scripts/lib/fonts.mjs +27 -0
  147. package/.grok/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
  148. package/.grok/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
  149. package/.grok/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
  150. package/.grok/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
  151. package/.grok/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
  152. package/.grok/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
  153. package/.grok/skills/lit-schematic/scripts/lib/png.mjs +132 -0
  154. package/.grok/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
  155. package/.grok/skills/lit-schematic/scripts/lib/route.mjs +218 -0
  156. package/.grok/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
  157. package/.grok/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
  158. package/.grok/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
  159. package/.grok/skills/lit-schematic/scripts/motion.mjs +129 -0
  160. package/.grok/skills/lit-schematic/scripts/qa.mjs +92 -0
  161. package/.grok/skills/lit-schematic/scripts/render.mjs +233 -0
  162. package/.grok/skills/lit-schematic/scripts/scaffold.mjs +60 -0
  163. package/.grok/skills/lit-scientific-visualization/SKILL.md +36 -12
  164. package/.grok/skills/lit-typographic-motion/SKILL.md +7 -4
  165. package/.grok/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +11 -0
  166. package/.grok/skills/lit-typographic-motion/recipes/hero-3d/expected.json +62 -0
  167. package/.grok/skills/lit-typographic-motion/recipes/hero-3d/index.html +82 -0
  168. package/.grok/skills/lit-typographic-motion/references/choice-first.md +30 -0
  169. package/.grok/skills/lit-typographic-motion/references/cover.md +10 -1
  170. package/.grok/skills/lit-typographic-motion/references/depth.md +119 -0
  171. package/.grok/skills/lit-typographic-motion/references/treatment.md +9 -1
  172. package/.grok/skills/lit-typographic-motion/scripts/blender/runner.py +218 -0
  173. package/.grok/skills/lit-typographic-motion/scripts/lib/assets.mjs +230 -0
  174. package/.grok/skills/lit-typographic-motion/scripts/lib/blender.mjs +136 -0
  175. package/.grok/skills/lit-typographic-motion/scripts/lib/board.mjs +153 -0
  176. package/.grok/skills/lit-typographic-motion/scripts/lib/chrome.mjs +61 -32
  177. package/.grok/skills/lit-typographic-motion/scripts/lib/constants.mjs +4 -2
  178. package/.grok/skills/lit-typographic-motion/scripts/lib/cover-capture.mjs +20 -11
  179. package/.grok/skills/lit-typographic-motion/scripts/lib/cover-treatment.mjs +26 -2
  180. package/.grok/skills/lit-typographic-motion/scripts/lib/cover.mjs +12 -5
  181. package/.grok/skills/lit-typographic-motion/scripts/lib/frame-pool.mjs +78 -1
  182. package/.grok/skills/lit-typographic-motion/scripts/lib/gallery.mjs +160 -0
  183. package/.grok/skills/lit-typographic-motion/scripts/lib/house.mjs +43 -0
  184. package/.grok/skills/lit-typographic-motion/scripts/lib/look.mjs +39 -1
  185. package/.grok/skills/lit-typographic-motion/scripts/lib/scaffold.mjs +174 -0
  186. package/.grok/skills/lit-typographic-motion/scripts/lib/score.mjs +640 -0
  187. package/.grok/skills/lit-typographic-motion/scripts/lib/sound.mjs +143 -0
  188. package/.grok/skills/lit-typographic-motion/scripts/lib/soundtrack.mjs +12 -3
  189. package/.grok/skills/lit-typographic-motion/scripts/lib/stage-gate.mjs +44 -0
  190. package/.grok/skills/lit-typographic-motion/scripts/lib/stage-qa.mjs +132 -3
  191. package/.grok/skills/lit-typographic-motion/scripts/lib/stage-session.mjs +51 -3
  192. package/.grok/skills/lit-typographic-motion/scripts/lib/stage.mjs +74 -9
  193. package/.grok/skills/lit-typographic-motion/scripts/lib/styles.mjs +224 -0
  194. package/.grok/skills/lit-typographic-motion/scripts/lib/treatment.mjs +198 -4
  195. package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/LICENSE +21 -0
  196. package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/addons/environments/RoomEnvironment.js +144 -0
  197. package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
  198. package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/three.module.js +54571 -0
  199. package/.grok/skills/lit-typographic-motion/scripts/motion.mjs +56 -6
  200. package/.grok/skills/lit-typographic-motion/scripts/stage/cover-init.js +20 -8
  201. package/.grok/skills/lit-typographic-motion/scripts/stage/loop-kit.js +2 -2
  202. package/.grok/skills/lit-typographic-motion/scripts/stage/stage-kit.js +25 -0
  203. package/.grok/skills/lit-typographic-motion/styles/paper.json +1 -0
  204. package/.grok/skills/lit-typographic-motion/styles/swiss-signal.json +1 -0
  205. package/.grok/skills/lit-typographic-motion/styles/terminalcore.json +1 -0
  206. package/.grok/skills/lit-typographic-motion/styles/tidal.json +1 -0
  207. package/.grok/skills/litgrok/SKILL.md +1 -1
  208. package/.grok/skills/litresearch/SKILL.md +2 -0
  209. package/.grok/skills/readme-studio/SKILL.md +14 -12
  210. package/.grok/skills/readme-studio/references/choice-first.md +24 -0
  211. package/.grok/skills/start-work/SKILL.md +8 -1
  212. package/.grok-plugin/plugin.json +4 -1
  213. package/CHANGELOG.md +18 -0
  214. package/README.md +11 -11
  215. package/README_ko-KR.md +11 -11
  216. package/TRADEMARK.md +12 -0
  217. package/bin/litgrok.mjs +2 -0
  218. package/docs/assets/readme/badge-version.svg +1 -1
  219. package/docs/assets/showcase/brief.webp +0 -0
  220. package/docs/assets/showcase/deck.webp +0 -0
  221. package/docs/assets/showcase/page.webp +0 -0
  222. package/docs/assets/showcase/pid.webp +0 -0
  223. package/docs/assets/skills/lit-adhd.webp +0 -0
  224. package/docs/assets/skills/lit-hijack.webp +0 -0
  225. package/docs/assets/skills/lit-schematic.webp +0 -0
  226. package/docs/reference.md +11 -7
  227. package/docs/reference_ko-KR.md +11 -7
  228. package/hooks/hooks.json +11 -0
  229. package/package.json +6 -1
  230. package/plugin.json +1 -1
  231. package/.grok/skills/frontend-ui-ux/scripts/page-probe.mjs +0 -105
  232. package/.grok/skills/frontend-ui-ux/scripts/probe.mjs +0 -91
  233. package/.grok/skills/frontend-ui-ux/scripts/rule-data.mjs +0 -56
  234. package/.grok/skills/frontend-ui-ux/scripts/static-probe.mjs +0 -27
  235. package/.grok/skills/frontend-ui-ux/scripts/static-server.mjs +0 -19
@@ -12,6 +12,7 @@ import { FlashDetector } from './flash.mjs';
12
12
  import { FramePool, measureFrame } from './frame-pool.mjs';
13
13
  import { encodePng } from './png.mjs';
14
14
  import { CONTENT_TYPES, openStage } from './stage-session.mjs';
15
+ import { readPlate } from './blender.mjs';
15
16
  import { stillsPlan, writeStills } from './stills.mjs';
16
17
  import { startEncode } from './encode.mjs';
17
18
  import { encodePreview, findTool, previewRung, promoteOrWithhold, clearDeliverables } from './render.mjs';
@@ -73,6 +74,10 @@ export function scanStage(stageDir) {
73
74
  const warnings = [];
74
75
  if (!existsSync(join(stageDir, 'index.html'))) return { contract: ['stage/index.html is missing'], network, warnings, files: [], rasters: [] };
75
76
  const files = walk(stageDir);
77
+ // A Blender plate and its preview stills are frames the runner wrote and plate.json lists by name
78
+ // and size; they are not page rasters. Anything not listed is held to the raster limits.
79
+ const plates = [readPlate(stageDir, 'plate'), readPlate(stageDir, 'plate-preview')].filter(Boolean);
80
+ const listed = new Set(plates.flatMap((p) => p.frames.map((f) => `${p.dir}/${String(f.name)}`)));
76
81
  const rasters = [];
77
82
  for (const file of files) {
78
83
  if (file.symlink) continue;
@@ -93,7 +98,7 @@ export function scanStage(stageDir) {
93
98
  const hint = text.match(/<link[^>]+rel\s*=\s*["']?(preconnect|prefetch|dns-prefetch|prerender)\b/i);
94
99
  if (hint) network.push(`stage/${file.rel}: <link rel=${hint[1]}>`);
95
100
  }
96
- if (RASTER_EXT.has(ext)) {
101
+ if (RASTER_EXT.has(ext) && !listed.has(file.rel)) {
97
102
  const bytes = readFileSync(file.path);
98
103
  let info;
99
104
  try { info = rasterInfo(bytes, ext === '.jpeg' ? '.jpg' : ext); } catch { info = { width: 0, height: 0, animated: false }; }
@@ -244,18 +249,31 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
244
249
  if (scan.contract.length) fail(new BlockedError(EXIT.STAGE_CONTRACT, `STAGE_CONTRACT_ERROR: ${scan.contract[0]}`));
245
250
  let fonts;
246
251
  try { fonts = stageFonts(treatment.typePlan.faces, env); } catch (error) { fail(error); }
252
+ let style = null;
253
+ if (treatment.style) {
254
+ const { readStyle, styleCss, styleScript } = await import('./styles.mjs');
255
+ const declared = readStyle(treatment.style);
256
+ style = { css: styleCss(declared, treatment.format), js: styleScript(declared, treatment.format, treatment) };
257
+ }
247
258
  const executable = resolveChrome(env);
248
259
  if (!executable) fail(new BlockedError(EXIT.NO_CHROME, 'BLOCKED_NO_CHROME: Chrome/Chromium not found; set CHROME_PATH to a Chrome executable'));
260
+ // The browser rung: a three.js world asks for the GPU; a Blender film shows its path-traced
261
+ // plate under HTML type, so its page needs only the software rung.
262
+ const rung = stageRung(treatment);
249
263
  const ffmpeg = findTool('ffmpeg', env);
250
264
  const seed = Number.isInteger(treatment.seed) ? treatment.seed : fnv1a32(treatment.idea);
251
265
  const warnings = [...scan.warnings];
252
266
 
253
267
  const pool = new FramePool();
254
268
  let master = null;
269
+ // The rung the master page actually got (a GPU ask can fall back to software); the replay, the
270
+ // QA and the manifest reuse it, not the requested rung.
271
+ let masterRung = null, masterRenderer = null, masterArgs = null;
255
272
  let encoder = null;
256
273
  let abortReplays = () => {};
257
274
  try {
258
- master = await openStage({ executable, outDir: out, stageDir, width, height, fps: 60, seed, fonts, label: 'm', log });
275
+ master = await openStage({ executable, outDir: out, stageDir, width, height, fps: 60, seed, fonts, style, label: 'm', rung, log });
276
+ if (master.notice) { warnings.push(master.notice); log(master.notice); }
259
277
  const prepared = await master.load();
260
278
  const contractError = contractCheck(prepared.contract, treatment, width, height);
261
279
  if (contractError) throw new BlockedError(EXIT.STAGE_CONTRACT, `STAGE_CONTRACT_ERROR: ${contractError}`);
@@ -286,7 +304,8 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
286
304
  // The QA replay only reads layout between samples and settles before its own captures.
287
305
  const stop = { aborted: false };
288
306
  const replay = async () => {
289
- const session = await openStage({ executable, outDir: out, stageDir, width, height, fps, seed, fonts, label: 'd', log });
307
+ // The replay shares the master's rung: a replay on another rung would flag the rung, not the page.
308
+ const session = await openStage({ executable, outDir: out, stageDir, width, height, fps, seed, fonts, style, label: 'd', rung: masterRung ?? rung, log });
290
309
  try {
291
310
  await session.load();
292
311
  const hashes = {};
@@ -309,7 +328,7 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
309
328
  const result = await job;
310
329
  if (!stillsOnly) {
311
330
  const flash = detector.push(result.grid);
312
- frames.push({ frame: f, ink: 0, lumP995: Math.round(result.lumP995 * 10000) / 10000, flash: { general: flash.general, red: flash.red, ...(flash.general2 ? { general2: flash.general2 } : {}), ...(flash.red2 ? { red2: flash.red2 } : {}), stepArea: flash.stepArea }, rgbaSha256: result.sha256 });
331
+ frames.push({ frame: f, ink: 0, lumP995: Math.round(result.lumP995 * 10000) / 10000, lumP005: Math.round(result.lumP005 * 10000) / 10000, busy: Math.round(result.busy * 10000) / 10000, flash: { general: flash.general, red: flash.red, ...(flash.general2 ? { general2: flash.general2 } : {}), ...(flash.red2 ? { red2: flash.red2 } : {}), stepArea: flash.stepArea }, rgbaSha256: result.sha256 });
313
332
  await encoder?.write(result.rgba);
314
333
  }
315
334
  if (keep.has(f)) kept.get(f).rgba = result.rgba;
@@ -330,6 +349,9 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
330
349
  while (queue.length) await drain();
331
350
  const masterDone = Date.now();
332
351
  const pageReport = await master.report();
352
+ masterRung = master.rung;
353
+ masterRenderer = master.renderer;
354
+ masterArgs = master.args;
333
355
  await master.close();
334
356
  master = null;
335
357
  if (pageReport.errors.length) warnings.push(`the page logged ${pageReport.errors.length} error(s); first: ${pageReport.errors[0]}`);
@@ -340,8 +362,27 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
340
362
  record({ exitCode: EXIT.NO_FFMPEG_FOR_VIDEO, state: 'blocked', reason: 'no ffmpeg', stillsIndexSha256: stills.sha256 });
341
363
  throw new BlockedError(EXIT.NO_FFMPEG_FOR_VIDEO, 'BLOCKED_NO_FFMPEG_FOR_VIDEO: ffmpeg not found on PATH; the stills and the contact sheet were written, install ffmpeg for the film');
342
364
  }
365
+ // The board round: judge the page's text on a QA replay, draw the storyboard, and stop when
366
+ // copy overlaps, leaves the style, or leaves its zone — fix the page, not the gate.
367
+ const boardQa = await runStageQa({ executable, out, stageDir, width, height, fps, seed, fonts, treatment, totalFrames, log, stepOrThrow, captureChecked, rung: masterRung, stop });
368
+ const { writeBoard, addToStills } = await import('./board.mjs');
369
+ const { assetUsed } = await import('./assets.mjs');
370
+ const { createHash } = await import('node:crypto');
371
+ const pad2 = (n) => String(n).padStart(2, '0');
372
+ const beatFiles = plan.mids.map((m) => join(out, 'stills', `beat-${pad2(m.beat + 1)}-mid.png`));
373
+ const assetRule = assetUsed(treatment, stageDir);
374
+ const check = { schema: 1, path: 'stage', round, rules: [...boardQa.results.filter((r) => /^(TEXT-|RICH-FILL)/u.test(r.id)), ...(assetRule ? [assetRule] : [])] };
375
+ writeFileSync(join(stageDir, 'check.json'), `${JSON.stringify(check, null, 2)}\n`);
376
+ const boardFile = await writeBoard({ out, treatment, beatFiles, check, env, log });
377
+ const sha = (b) => createHash('sha256').update(b).digest('hex');
378
+ addToStills(out, { file: 'stills/board.png', kind: 'board', check: 'stills' }, { sha256: sha });
379
+ const failed = check.rules.filter((x) => x.status === 'FAIL');
380
+ if (failed.length) {
381
+ record({ exitCode: EXIT.GATE_FAIL_QA, state: 'check-fail', stillsIndexSha256: stills.sha256, frames: lastNeeded + 1, perf: { p50Ms: pct(times, 0.5), p95Ms: pct(times, 0.95), totalSec: (Date.now() - started) / 1000 } });
382
+ throw new BlockedError(EXIT.GATE_FAIL_QA, `BOARD_CHECK_FAIL: ${failed.map((x) => `${x.id} ${x.detail}`).join('; ')}. Fix the page, render --stills-only again, then look at ${boardFile ?? join(out, 'stills', 'board.png')}`);
383
+ }
343
384
  record({ exitCode: EXIT.OK, state: 'stills-only', stillsIndexSha256: stills.sha256, frames: lastNeeded + 1, perf: { p50Ms: pct(times, 0.5), p95Ms: pct(times, 0.95), totalSec: (Date.now() - started) / 1000 } });
344
- log(`stills-only: ${stills.index.stills.length} files listed in ${join(out, 'stills', 'index.json')}; open every one, then record the look`);
385
+ log(`stills-only: board check PASS; open the storyboard ${boardFile ?? join(out, 'stills', 'board.png')} and every file in ${join(out, 'stills', 'index.json')}, record the look with look --round ${round}, make the change, then render the film with --round ${round + 1} (no --stills-only)`);
345
386
  return { exitCode: EXIT.OK, stillsOnly: true, stills };
346
387
  }
347
388
 
@@ -358,7 +399,7 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
358
399
  previewJob.catch(() => {});
359
400
  const [rerun, qa] = await Promise.all([
360
401
  replay(),
361
- runStageQa({ executable, out, stageDir, width, height, fps, seed, fonts, treatment, totalFrames, log, stepOrThrow, captureChecked, stop }),
402
+ runStageQa({ executable, out, stageDir, width, height, fps, seed, fonts, treatment, totalFrames, log, stepOrThrow, captureChecked, rung: masterRung, stop }),
362
403
  ]);
363
404
  timings.replaysSec = (Date.now() - replayStart) / 1000;
364
405
  const masterHash = new Map(frames.map((frame) => [frame.frame, frame.rgbaSha256]));
@@ -371,13 +412,29 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
371
412
  if (qa.minCopyFontPx && qa.minCopyFontPx < 32) preview = await encodePreview({ runDir, master: join(runDir, 'film.mp4'), encoder: previewRung(env), minFontPx: qa.minCopyFontPx, env, log, frame: { width, height } });
372
413
  const previewRecords = preview.records ? preview.records.map((r) => ({ preview: { frame: r.frame, fps: preview.fps }, flash: { general: r.general, red: r.red, ...(r.general2 ? { general2: r.general2 } : {}), ...(r.red2 ? { red2: r.red2 } : {}) } })) : [];
373
414
  const sound = muxGate({ mp4: join(runDir, 'film.mp4'), durationSec: totalFrames / fps, treatment, env, track });
415
+ // A film about a named product shows its real assets; without a product there is no verdict.
416
+ const { assetUsed } = await import('./assets.mjs');
417
+ const assetRule = assetUsed(treatment, stageDir);
418
+ if (assetRule) qa.results.push(assetRule);
374
419
 
420
+ // The manifest names the rung the master page got and its renderer string, so the report and
421
+ // the determinism replay can be read against the same hardware.
422
+ const { isSoftwareRenderer } = await import('./chrome.mjs');
375
423
  const manifest = {
376
424
  schemaVersion: 1, path: 'stage', format: treatment.format, resolution: [width, height], fps, durationSec: totalFrames / fps, targetDurationSec: treatment.durationSec,
377
- frames: totalFrames, seed, renderer: 'SwiftShader software rung (stage path)', softwareRenderer: true, chromeFlags: stageFlagsUsed(), previewEncoder: preview.encoder,
425
+ frames: totalFrames, seed, rung: masterRung, renderer: `${masterRenderer} on the ${masterRung} rung (stage path)`, softwareRenderer: masterRung === 'software' || isSoftwareRenderer(masterRenderer), chromeFlags: masterArgs.filter((arg) => !arg.startsWith('--user-data-dir') && !arg.startsWith('--window-size') && arg !== 'about:blank'), previewEncoder: preview.encoder,
378
426
  sound: sound.summary, round, warnings,
379
427
  };
380
428
  const stills = writeStills(out, { plan, frames: kept, width, height, path: 'stage', round, mode: 'full', poster: { bytes: posterBytes, frame: posterFrame }, treatmentSha256 });
429
+ // The final render ships its storyboard too: the done-check wants the board viewed on the final set.
430
+ const { writeBoard, addToStills } = await import('./board.mjs');
431
+ const { createHash } = await import('node:crypto');
432
+ const pad2 = (n) => String(n).padStart(2, '0');
433
+ const beatFiles = plan.mids.map((m) => join(out, 'stills', `beat-${pad2(m.beat + 1)}-mid.png`));
434
+ const boardCheck = { schema: 1, path: 'stage', round, rules: [...qa.results.filter((r) => /^(TEXT-|RICH-FILL)/u.test(r.id)), ...(assetRule ? [assetRule] : [])] };
435
+ await writeBoard({ out, treatment, beatFiles, check: boardCheck, env, log });
436
+ const sha = (b) => createHash('sha256').update(b).digest('hex');
437
+ stills.sha256 = addToStills(out, { file: 'stills/board.png', kind: 'board', check: 'stills' }, { sha256: sha });
381
438
  manifest.stillsIndexSha256 = stills.sha256;
382
439
  clearDeliverables(out);
383
440
  writeFileSync(join(out, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
@@ -415,8 +472,16 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
415
472
  }
416
473
  }
417
474
 
418
- export function stageFlagsUsed() {
419
- return stageArgs({ profileDir: '<profile>', width: 0, height: 0 }).filter((arg) => !arg.startsWith('--user-data-dir') && !arg.startsWith('--window-size') && arg !== 'about:blank');
475
+ // The browser rung for a treatment: a three.js world asks for the platform GPU rung, a Blender
476
+ // film shows its path-traced plate under HTML type on the software rung, and anything else renders
477
+ // on the software rung where a determinism mismatch is always a page leak.
478
+ export function stageRung(treatment) {
479
+ if (treatment.render3d === 'blender') return 'software';
480
+ return treatment.depth === '3d' ? 'gpu' : 'software';
481
+ }
482
+
483
+ export function stageFlagsUsed(rung = 'software') {
484
+ return stageArgs({ profileDir: '<profile>', width: 0, height: 0, rung }).filter((arg) => !arg.startsWith('--user-data-dir') && !arg.startsWith('--window-size') && arg !== 'about:blank');
420
485
  }
421
486
 
422
487
  // `gate --out <dir>` on a stage render: re-evaluate the recorded facts against the exports where they
@@ -0,0 +1,224 @@
1
+ // House styles as data. Four JSON documents under styles/ carry each style's palette, type
2
+ // scale, motion tokens, transitions, ground, default bed, bed list and bans; this module loads,
3
+ // validates and applies them. treatments name a style id; pickStyle() resolves one by rule.
4
+ import { readFileSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export const STYLE_SCHEMA = "litgrok.motion-style/v1";
8
+ export const STYLE_IDS = Object.freeze(["swiss-signal", "tidal", "terminalcore", "paper"]);
9
+ export const DEFAULT_STYLE = "swiss-signal";
10
+ export const TYPE_ROLES = Object.freeze(["display", "headline", "body", "label"]);
11
+ export const TRANSITIONS = Object.freeze(["cut", "match", "wipe", "push", "iris", "dissolve", "morph"]);
12
+ // Copy may sit this far (share of frame side) outside its zone, and this far off a scale step.
13
+ export const ZONE_TOLERANCE = 0.012;
14
+ export const SIZE_TOLERANCE = 0.04;
15
+
16
+ // [x, y, w, h] as frame fractions. `type` bounds copy; `subject` bounds the drawing.
17
+ export const LAYOUTS = Object.freeze({
18
+ "16:9": Object.freeze({
19
+ title: { type: [0.08, 0.2, 0.84, 0.6], subject: [0, 0, 1, 1] },
20
+ split: { type: [0.08, 0.16, 0.36, 0.68], subject: [0.48, 0.08, 0.46, 0.84] },
21
+ "split-alt": { type: [0.56, 0.16, 0.36, 0.68], subject: [0.06, 0.08, 0.46, 0.84] },
22
+ stage: { type: [0.08, 0.75, 0.84, 0.18], subject: [0.06, 0.07, 0.88, 0.64] },
23
+ hero: { type: [0.08, 0.76, 0.84, 0.17], subject: [0.03, 0.03, 0.94, 0.94] },
24
+ full: { type: null, subject: [0, 0, 1, 1] },
25
+ }),
26
+ "9:16": Object.freeze({
27
+ title: { type: [0.09, 0.26, 0.82, 0.48], subject: [0, 0, 1, 1] },
28
+ split: { type: [0.09, 0.08, 0.82, 0.26], subject: [0.06, 0.36, 0.88, 0.56] },
29
+ "split-alt": { type: [0.09, 0.66, 0.82, 0.26], subject: [0.06, 0.06, 0.88, 0.56] },
30
+ stage: { type: [0.09, 0.79, 0.82, 0.13], subject: [0.06, 0.06, 0.88, 0.68] },
31
+ hero: { type: [0.09, 0.8, 0.82, 0.12], subject: [0.03, 0.03, 0.94, 0.94] },
32
+ full: { type: null, subject: [0, 0, 1, 1] },
33
+ }),
34
+ });
35
+ export const LAYOUT_IDS = Object.freeze(Object.keys(LAYOUTS["16:9"]));
36
+ const FRAME = Object.freeze({ "16:9": [1920, 1080], "9:16": [1080, 1920] });
37
+
38
+ // With no name, house file or keyword, the genre picks where one reads better: a warm legible
39
+ // ground for explainers (stage only), the tidal flow for brand moods.
40
+ export const GENRE_STYLE = Object.freeze({ explainer: "paper", "brand-mood": "tidal" });
41
+
42
+ // Checked in order; the first row with a hit wins. A style the request names outright wins over
43
+ // every row. Paper has no type-engine preset, so the type path skips its row.
44
+ export const STYLE_PICK = Object.freeze([
45
+ { style: "terminalcore", korean: ["터미널", "CRT", "해커"], latin: ["terminal", "hacker", "crt"] },
46
+ { style: "tidal", korean: ["물결", "파도", "잔잔한", "흐름"], latin: ["gradient", "wave", "tide", "calm"] },
47
+ { style: "paper", korean: ["따뜻한", "밝은", "종이", "손그림", "포근한", "아기자기"], latin: ["warm", "cozy", "bright", "paper", "handmade", "hand-drawn"] },
48
+ ]);
49
+ const NAMED = Object.freeze({
50
+ "swiss-signal": ["swiss-signal", "swiss signal", "스위스 시그널"],
51
+ tidal: ["tidal", "타이달"],
52
+ terminalcore: ["terminalcore", "터미널코어"],
53
+ paper: ["paper style", "paper look", "페이퍼 스타일", "페이퍼"],
54
+ });
55
+
56
+ export class StyleError extends Error {}
57
+
58
+ const HEX = /^#[0-9a-f]{6}$/iu;
59
+ const isText = (v) => typeof v === "string" && v.trim().length > 0;
60
+ const linear = (hex) => [1, 3, 5].map((i) => {
61
+ const s = parseInt(hex.slice(i, i + 2), 16) / 255;
62
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
63
+ });
64
+ const luminance = (hex) => {
65
+ const [r, g, b] = linear(hex);
66
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
67
+ };
68
+ const ratio = (la, lb) => (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
69
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
70
+ const keywordHit = (text, keyword, korean) => {
71
+ const pattern = korean
72
+ ? new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp(keyword)}`, "u")
73
+ : new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp(keyword)}(?![\\p{L}\\p{N}])`, "iu");
74
+ return pattern.test(text);
75
+ };
76
+
77
+ /** A style document is valid, or throws StyleError naming the first bad field. */
78
+ export function validateStyle(style) {
79
+ const fail = (field, message) => {
80
+ throw new StyleError(`style ${style?.id ?? "?"}: ${field}: ${message}`);
81
+ };
82
+ if (!style || typeof style !== "object") fail("style", "must be an object");
83
+ if (style.schema !== STYLE_SCHEMA) fail("schema", `must be ${STYLE_SCHEMA}`);
84
+ if (!STYLE_IDS.includes(style.id)) fail("id", `must be one of ${STYLE_IDS.join(", ")}`);
85
+ for (const k of ["name", "summary"]) if (!isText(style[k])) fail(k, "must be a non-empty string");
86
+ const p = style.palette ?? {};
87
+ for (const role of ["ground", "surface", "ink", "muted", "signal", "line"]) if (!HEX.test(p[role] ?? "")) fail(`palette.${role}`, "must be #rrggbb");
88
+ if (!Array.isArray(style.textRoles) || style.textRoles.some((r) => !HEX.test(p[r] ?? ""))) fail("textRoles", "every text role names a palette colour");
89
+ const ground = luminance(p.ground);
90
+ if (ratio(luminance(p.ink), ground) < 7) fail("palette.ink", "ink on ground needs 7:1");
91
+ if (ratio(luminance(p.muted), ground) < 4.5) fail("palette.muted", "muted on ground needs 4.5:1");
92
+ for (const role of ["signal", "accent"]) if (p[role] && ratio(luminance(p[role]), ground) < 3) fail(`palette.${role}`, `${role} on ground needs 3:1 (large type)`);
93
+ if (!Array.isArray(style.accents) || style.accents.length < 3) fail("accents", "names at least three role accents a rich film may add");
94
+ style.accents.forEach((a, i) => {
95
+ if (!isText(a?.role)) fail(`accents[${i}].role`, "says what the colour marks");
96
+ if (!HEX.test(a?.color ?? "")) fail(`accents[${i}].color`, "must be #rrggbb");
97
+ if (ratio(luminance(a.color), ground) < 3) fail(`accents[${i}].color`, "an accent on ground needs 3:1 (large type)");
98
+ });
99
+ for (const role of TYPE_ROLES) if (!isText(style.faces?.[role]?.family)) fail(`faces.${role}`, "needs a family");
100
+ for (const format of Object.keys(FRAME)) {
101
+ const s = style.scale?.[format];
102
+ if (!s || TYPE_ROLES.some((r) => !(s[r] > 0))) fail(`scale.${format}`, "needs a size per type role");
103
+ if (!(s.display > s.headline && s.headline > s.body && s.body > s.label)) fail(`scale.${format}`, "steps must descend display > headline > body > label");
104
+ }
105
+ const m = style.motion ?? {};
106
+ for (const k of ["enter", "move", "exit"]) if (!Array.isArray(m.ease?.[k]) || m.ease[k].length !== 4) fail(`motion.ease.${k}`, "is a cubic-bezier [x1, y1, x2, y2]");
107
+ for (const k of ["micro", "move", "hero", "exit"]) if (!(m.dur?.[k] > 0)) fail(`motion.dur.${k}`, "seconds > 0");
108
+ if (!(m.dur.exit <= m.dur.hero)) fail("motion.dur.exit", "an exit is never longer than the hero move");
109
+ if (!Array.isArray(style.transitions) || !style.transitions.length || style.transitions.some((x) => !TRANSITIONS.includes(x))) fail("transitions", `a subset of ${TRANSITIONS.join(", ")}`);
110
+ if (!isText(style.score?.genre)) fail("score.genre", "names the genre a film's score takes by default");
111
+ if (!style.sound || !isText(style.sound.timbre)) fail("sound", "needs the generated bed's palette");
112
+ if (!Array.isArray(style.beds) || style.beds.length < 2 || style.beds.some((b) => !isText(b?.timbre) || !isText(b?.key) || !(b?.tempo > 0))) fail("beds", "lists at least two beds a film may pick, each { timbre, key, mode, tempo }");
113
+ const same = (a, b) => ["timbre", "key", "mode", "tempo"].every((k) => String(a[k]).toLowerCase() === String(b[k]).toLowerCase());
114
+ if (!style.beds.some((b) => same(b, style.sound))) fail("beds", "must include the default bed (sound)");
115
+ if (!Array.isArray(style.bans) || style.bans.length < 3) fail("bans", "names at least three defaults this style never uses");
116
+ return style;
117
+ }
118
+
119
+ const cache = new Map();
120
+ /** A shipped style by id, validated and frozen. */
121
+ export function readStyle(id) {
122
+ if (!STYLE_IDS.includes(id)) throw new StyleError(`unknown style: ${id} (choose ${STYLE_IDS.join(", ")})`);
123
+ if (!cache.has(id)) {
124
+ const url = new URL(`../../styles/${id}.json`, import.meta.url);
125
+ cache.set(id, Object.freeze(validateStyle(JSON.parse(readFileSync(fileURLToPath(url), "utf8")))));
126
+ }
127
+ return cache.get(id);
128
+ }
129
+
130
+ /** Every font family copy may use in this style (the role faces, the Hangul face, Pretendard). */
131
+ export const styleFamilies = (style) => new Set([...TYPE_ROLES.map((r) => style.faces[r].family), style.hangul?.family ?? "Pretendard", "Pretendard"]);
132
+ /** The hex colours copy may use. */
133
+ export const styleTextColors = (style) => style.textRoles.map((r) => style.palette[r].toLowerCase());
134
+ /** The beds a film in this style may pick from, the default first. */
135
+ export const styleBeds = (style) => style.beds;
136
+ /** The role accents a rich film may add to the core palette, each { role, color }. */
137
+ export const styleAccents = (style) => style.accents;
138
+ /** All palette colours of the style (what a stage treatment's palette may list); rich adds the accents. */
139
+ export const styleColors = (style, { rich = false } = {}) => [...Object.values(style.palette), ...(rich ? style.accents.map((a) => a.color) : [])].map((c) => c.toLowerCase());
140
+
141
+ /**
142
+ * Which style a film takes. A style the request names wins; then the project's house style; then
143
+ * the keyword auto-pick; then the genre's style; then the default. Returns { id, source, reason }.
144
+ */
145
+ export function pickStyle({ request = "", copy = [], path = "stage", house = null, genre = null } = {}) {
146
+ const text = String(request);
147
+ for (const [id, names] of Object.entries(NAMED)) {
148
+ if (path === "type" && !readStyle(id).typePreset) continue;
149
+ const hit = names.find((n) => keywordHit(text, n, /[가-힣]/u.test(n)));
150
+ if (hit) return { id, source: "request", reason: `the request names the ${id} style ("${hit}")` };
151
+ }
152
+ if (house?.style && STYLE_IDS.includes(house.style) && (path !== "type" || readStyle(house.style).typePreset)) {
153
+ return { id: house.style, source: "house", reason: `the project's house style is ${house.style} (motion-house.json)` };
154
+ }
155
+ const haystack = [text, ...copy].join("\n");
156
+ for (const row of STYLE_PICK) {
157
+ if (path === "type" && !readStyle(row.style).typePreset) continue;
158
+ for (const k of row.korean) if (keywordHit(haystack, k, true)) return { id: row.style, source: "auto", reason: `the request mentions "${k}"` };
159
+ for (const k of row.latin) if (keywordHit(haystack, k, false)) return { id: row.style, source: "auto", reason: `the request mentions "${k}"` };
160
+ }
161
+ const byGenre = GENRE_STYLE[genre];
162
+ if (byGenre && (path !== "type" || readStyle(byGenre).typePreset)) return { id: byGenre, source: "genre", reason: `no style is named and no keyword picks one, and a ${genre} film takes ${byGenre}` };
163
+ return { id: DEFAULT_STYLE, source: "default", reason: `no style is named and no keyword picks one, so the default ${DEFAULT_STYLE}` };
164
+ }
165
+
166
+ /** A layout's zones in pixels for a format: { type: {x, y, w, h} | null, subject: {...} }. */
167
+ export function zonesPx(format) {
168
+ const [W, H] = FRAME[format];
169
+ const px = (r) => (r ? { x: Math.round(r[0] * W), y: Math.round(r[1] * H), w: Math.round(r[2] * W), h: Math.round(r[3] * H) } : null);
170
+ return Object.fromEntries(Object.entries(LAYOUTS[format]).map(([id, z]) => [id, { type: px(z.type), subject: px(z.subject) }]));
171
+ }
172
+
173
+ /** The type step a rendered size sits on, or null when it is off the style's scale. */
174
+ export function scaleStep(style, format, sizePx) {
175
+ for (const role of TYPE_ROLES) {
176
+ const step = style.scale[format][role];
177
+ if (Math.abs(sizePx - step) <= step * SIZE_TOLERANCE) return role;
178
+ }
179
+ return null;
180
+ }
181
+
182
+ const familyStack = (style, role) => {
183
+ const f = style.faces[role];
184
+ const hangul = style.hangul?.family ?? "Pretendard";
185
+ return [f.family, hangul, "Pretendard"].filter((x, i, a) => a.indexOf(x) === i).map((x) => `"${x}"`).join(", ");
186
+ };
187
+
188
+ /** /lit/style.css for one style and format: custom properties, type-role classes, colour classes. */
189
+ export function styleCss(style, format) {
190
+ const s = style.scale[format];
191
+ const vars = [
192
+ ...Object.entries(style.palette).map(([k, v]) => ` --lit-${k}: ${v};`),
193
+ ...TYPE_ROLES.map((r) => ` --lit-size-${r}: ${s[r]}px;`),
194
+ ];
195
+ const roles = TYPE_ROLES.map((r) => {
196
+ const f = style.faces[r];
197
+ return `.lit-${r} { font-family: ${familyStack(style, r)}; font-size: ${s[r]}px; font-weight: ${f.weight}; ${f.stretch ? `font-stretch: ${f.stretch}; ` : ""}line-height: ${style.leading?.[r] ?? 1.3}; letter-spacing: 0; margin: 0; }`;
198
+ });
199
+ const colours = style.textRoles.map((r) => `.lit-${r === "ground" ? "knockout" : r} { color: var(--lit-${r}); }`);
200
+ return [
201
+ `/* lit-typographic-motion style: ${style.id} (${format}) */`,
202
+ `:root {\n${vars.join("\n")}\n}`,
203
+ // Korean breaks only between eojeol, never inside a word (type-craft.md, MO-A-13).
204
+ `html, body { margin: 0; overflow: hidden; background: var(--lit-ground); color: var(--lit-ink); font-family: ${familyStack(style, "body")}; word-break: keep-all; overflow-wrap: break-word; }`,
205
+ ...roles,
206
+ ...colours,
207
+ ".lit-zone { position: absolute; box-sizing: border-box; }",
208
+ "",
209
+ ].join("\n");
210
+ }
211
+
212
+ /** /lit/style.js: window.LitStyle (tokens and pixel zones) and window.LitTimeline (the treatment's shots). */
213
+ export function styleScript(style, format, treatment) {
214
+ const timeline = {
215
+ format, fps: treatment.fps ?? 60, duration: treatment.durationSec,
216
+ beats: (treatment.beats ?? []).map((b, i) => ({ index: i, t0: b.t0, t1: b.t1, layout: b.layout ?? null, transition: b.transition ?? null })),
217
+ };
218
+ const data = {
219
+ id: style.id, name: style.name, format, palette: style.palette, textRoles: style.textRoles,
220
+ faces: style.faces, hangul: style.hangul ?? null, scale: style.scale[format], leading: style.leading ?? {},
221
+ motion: style.motion, transitions: style.transitions, ground: style.ground, zones: zonesPx(format),
222
+ };
223
+ return `window.LitStyle = Object.freeze(${JSON.stringify(data)});\nwindow.LitTimeline = Object.freeze(${JSON.stringify(timeline)});\n`;
224
+ }