@litfamily/litmuse 0.1.1 → 0.1.2

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 (168) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +20 -0
  3. package/README_ko-KR.md +20 -0
  4. package/bin/litmuse.mjs +4 -0
  5. package/lib/stage-card.mjs +180 -5
  6. package/package.json +1 -1
  7. package/plugin/.muse-plugin/plugin.json +26 -11
  8. package/plugin/commands/lit-status.md +2 -1
  9. package/plugin/commands/lit.md +1 -1
  10. package/plugin/hooks/lib/digest.mjs +1 -1
  11. package/plugin/hooks/lib/next-step.mjs +68 -0
  12. package/plugin/hooks/lib/routes.mjs +59 -0
  13. package/plugin/hooks/lib/workflow-stack.mjs +318 -0
  14. package/plugin/hooks/session-start.mjs +29 -2
  15. package/plugin/hooks/user-prompt-submit.mjs +138 -7
  16. package/plugin/lib/capped-fetch.mjs +25 -0
  17. package/plugin/lib/pinned-fetch.mjs +79 -0
  18. package/plugin/lib/plan-task-rows.mjs +32 -0
  19. package/plugin/lib/render_pages.py +29 -1
  20. package/plugin/lib/rewind/collect.mjs +679 -0
  21. package/plugin/lib/rewind/gauge.mjs +72 -0
  22. package/plugin/lib/rewind/next-step.mjs +91 -0
  23. package/plugin/lib/rewind/redact.mjs +83 -0
  24. package/plugin/lib/rewind/transcripts.mjs +227 -0
  25. package/plugin/skills/frontend-ui-ux/SKILL.md +24 -9
  26. package/plugin/skills/frontend-ui-ux/kit/components.css +414 -0
  27. package/plugin/skills/frontend-ui-ux/kit/fonts.json +22 -0
  28. package/plugin/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
  29. package/plugin/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
  30. package/plugin/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
  31. package/plugin/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
  32. package/plugin/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
  33. package/plugin/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
  34. package/plugin/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
  35. package/plugin/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
  36. package/plugin/skills/frontend-ui-ux/kit/ui.js +226 -0
  37. package/plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -1
  38. package/plugin/skills/frontend-ui-ux/references/craft-floor.md +9 -4
  39. package/plugin/skills/frontend-ui-ux/references/default-editorial-pixel.json +59 -7
  40. package/plugin/skills/frontend-ui-ux/references/house-styles.md +234 -0
  41. package/plugin/skills/frontend-ui-ux/references/motion-guide.md +3 -3
  42. package/plugin/skills/frontend-ui-ux/references/slop-register.md +7 -7
  43. package/plugin/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
  44. package/plugin/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
  45. package/plugin/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +31 -10
  46. package/plugin/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +19 -7
  47. package/plugin/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +4 -0
  48. package/plugin/skills/frontend-ui-ux/scripts/interface-probe.mjs +59 -7
  49. package/plugin/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
  50. package/plugin/skills/lit-adhd/SKILL.md +188 -0
  51. package/plugin/skills/lit-adhd/references/card-examples.md +121 -0
  52. package/plugin/skills/lit-adhd/references/status-taxonomy.md +43 -0
  53. package/plugin/skills/lit-adhd/scripts/rewind.mjs +11 -0
  54. package/plugin/skills/lit-diagram-drawer/SKILL.md +3 -3
  55. package/plugin/skills/lit-diagram-drawer/scripts/diagram-craft-rules.mjs +1 -2
  56. package/plugin/skills/lit-diagram-drawer/scripts/visual-quality.mjs +0 -1
  57. package/plugin/skills/lit-docx/SKILL.md +1 -1
  58. package/plugin/skills/lit-hijack/SKILL.md +589 -0
  59. package/plugin/skills/lit-hijack/references/capture-and-provenance.md +123 -0
  60. package/plugin/skills/lit-hijack/references/clean-room-build.md +133 -0
  61. package/plugin/skills/lit-hijack/references/distil-and-licence.md +128 -0
  62. package/plugin/skills/lit-hijack/references/gap-and-port-design.md +152 -0
  63. package/plugin/skills/lit-hijack/references/parity-and-report.md +133 -0
  64. package/plugin/skills/lit-hijack/scripts/hijack.mjs +879 -0
  65. package/plugin/skills/lit-pptx/SKILL.md +20 -9
  66. package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
  67. package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
  68. package/plugin/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
  69. package/plugin/skills/lit-pptx/fonts/provenance.json +32 -1
  70. package/plugin/skills/lit-pptx/references/direction-step.md +19 -2
  71. package/plugin/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
  72. package/plugin/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
  73. package/plugin/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
  74. package/plugin/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
  75. package/plugin/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
  76. package/plugin/skills/lit-pptx/references/tonalities/bauhaus.md +112 -0
  77. package/plugin/skills/lit-pptx/references/tonalities/computational-brutalism.md +118 -0
  78. package/plugin/skills/lit-pptx/references/tonalities/neo-brutalism.md +125 -0
  79. package/plugin/skills/lit-pptx/references/tonalities/newsroom.md +115 -0
  80. package/plugin/skills/lit-pptx/references/tonalities/riso.md +109 -0
  81. package/plugin/skills/lit-pptx/scripts/compile-deck.js +1 -1
  82. package/plugin/skills/lit-pptx/scripts/craft_extras.py +136 -92
  83. package/plugin/skills/lit-pptx/scripts/embed_fonts.py +13 -1
  84. package/plugin/skills/lit-pptx/scripts/lib/grid-resolver.js +105 -116
  85. package/plugin/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
  86. package/plugin/skills/lit-pptx/scripts/lib/render-pack.js +368 -561
  87. package/plugin/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
  88. package/plugin/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
  89. package/plugin/skills/lit-pptx/scripts/qa_deck.py +4 -4
  90. package/plugin/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
  91. package/plugin/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
  92. package/plugin/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
  93. package/plugin/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
  94. package/plugin/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
  95. package/plugin/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
  96. package/plugin/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
  97. package/plugin/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
  98. package/plugin/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
  99. package/plugin/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
  100. package/plugin/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
  101. package/plugin/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
  102. package/plugin/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
  103. package/plugin/skills/lit-recap/SKILL.md +3 -0
  104. package/plugin/skills/lit-schematic/SKILL.md +241 -0
  105. package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +14 -0
  106. package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +8 -0
  107. package/plugin/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +22 -0
  108. package/plugin/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +73 -0
  109. package/plugin/skills/lit-schematic/references/cutaway-outputs.md +25 -0
  110. package/plugin/skills/lit-schematic/references/isometric-craft.md +24 -0
  111. package/plugin/skills/lit-schematic/references/journal-figures.md +19 -0
  112. package/plugin/skills/lit-schematic/references/kit-api.md +27 -0
  113. package/plugin/skills/lit-schematic/references/pfd-and-layout.md +20 -0
  114. package/plugin/skills/lit-schematic/references/pid-drafting.md +26 -0
  115. package/plugin/skills/lit-schematic/references/pid-symbols.md +27 -0
  116. package/plugin/skills/lit-schematic/references/verification.md +24 -0
  117. package/plugin/skills/lit-schematic/scripts/doctor.mjs +13 -0
  118. package/plugin/skills/lit-schematic/scripts/lib/figure.mjs +49 -0
  119. package/plugin/skills/lit-schematic/scripts/lib/isa.mjs +18 -0
  120. package/plugin/skills/lit-schematic/scripts/lib/iso.mjs +58 -0
  121. package/plugin/skills/lit-schematic/scripts/lib/legend-sheet.mjs +10 -0
  122. package/plugin/skills/lit-schematic/scripts/lib/qa-core.mjs +241 -0
  123. package/plugin/skills/lit-schematic/scripts/lib/sheet.mjs +446 -0
  124. package/plugin/skills/lit-schematic/scripts/lib/svg.mjs +39 -0
  125. package/plugin/skills/lit-schematic/scripts/lib/theme.mjs +25 -0
  126. package/plugin/skills/lit-schematic/scripts/motion.mjs +8 -0
  127. package/plugin/skills/lit-schematic/scripts/qa.mjs +21 -0
  128. package/plugin/skills/lit-schematic/scripts/render.mjs +38 -0
  129. package/plugin/skills/lit-schematic/scripts/scaffold.mjs +73 -0
  130. package/plugin/skills/lit-scientific-visualization/SKILL.md +2 -0
  131. package/plugin/skills/lit-typographic-motion/SKILL.md +12 -8
  132. package/plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +11 -0
  133. package/plugin/skills/lit-typographic-motion/engine/blender/runner.py +218 -0
  134. package/plugin/skills/lit-typographic-motion/engine/core/presets.mjs +1 -1
  135. package/plugin/skills/lit-typographic-motion/engine/node/assets.mjs +218 -0
  136. package/plugin/skills/lit-typographic-motion/engine/node/blender.mjs +144 -0
  137. package/plugin/skills/lit-typographic-motion/engine/node/board.mjs +136 -0
  138. package/plugin/skills/lit-typographic-motion/engine/node/done.mjs +42 -3
  139. package/plugin/skills/lit-typographic-motion/engine/node/gallery.mjs +120 -0
  140. package/plugin/skills/lit-typographic-motion/engine/node/house.mjs +44 -0
  141. package/plugin/skills/lit-typographic-motion/engine/node/pipeline.mjs +18 -7
  142. package/plugin/skills/lit-typographic-motion/engine/node/runtime.mjs +6 -3
  143. package/plugin/skills/lit-typographic-motion/engine/node/scaffold.mjs +210 -0
  144. package/plugin/skills/lit-typographic-motion/engine/node/score.mjs +639 -0
  145. package/plugin/skills/lit-typographic-motion/engine/node/sound-cli.mjs +21 -4
  146. package/plugin/skills/lit-typographic-motion/engine/node/sound.mjs +126 -27
  147. package/plugin/skills/lit-typographic-motion/engine/node/stage-serve.mjs +29 -5
  148. package/plugin/skills/lit-typographic-motion/engine/node/stage.mjs +82 -14
  149. package/plugin/skills/lit-typographic-motion/engine/node/styles.mjs +214 -0
  150. package/plugin/skills/lit-typographic-motion/engine/node/treatment.mjs +311 -10
  151. package/plugin/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
  152. package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
  153. package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
  154. package/plugin/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
  155. package/plugin/skills/lit-typographic-motion/recipes/hero-3d/expected.json +69 -0
  156. package/plugin/skills/lit-typographic-motion/recipes/hero-3d/index.html +139 -0
  157. package/plugin/skills/lit-typographic-motion/references/depth.md +122 -0
  158. package/plugin/skills/lit-typographic-motion/references/sound.md +74 -16
  159. package/plugin/skills/lit-typographic-motion/references/stage.md +29 -5
  160. package/plugin/skills/lit-typographic-motion/references/studio.md +245 -0
  161. package/plugin/skills/lit-typographic-motion/references/treatment.md +31 -8
  162. package/plugin/skills/lit-typographic-motion/scripts/motion.mjs +48 -7
  163. package/plugin/skills/lit-typographic-motion/styles/paper.json +41 -0
  164. package/plugin/skills/lit-typographic-motion/styles/swiss-signal.json +41 -0
  165. package/plugin/skills/lit-typographic-motion/styles/terminalcore.json +42 -0
  166. package/plugin/skills/lit-typographic-motion/styles/tidal.json +41 -0
  167. package/plugin/skills/readme-studio/SKILL.md +1 -1
  168. package/plugin/skills/start-work/SKILL.md +6 -0
@@ -0,0 +1,214 @@
1
+ // House styles as data: palette, faces, type scale, layout zones, motion tokens, transitions
2
+ // and sound for one look, so every film in a style shares them. A style lives in
3
+ // `styles/<id>.json`; the treatment names one, the validator checks it against the rules
4
+ // (named in the request, the project's house style, or the fixed keyword auto-pick), the
5
+ // renderer serves it to the stage page as /lit/style.css and /lit/style.js, and the text QA
6
+ // judges copy against it. Zones are one grid per layout, named per shot and mapped per
7
+ // format, so a story re-lays itself per format instead of cropping. Three styles carry a
8
+ // `typePreset`, which means the same thing on the type path.
9
+ import { readFileSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import { matchesKeyword } from "../core/presets.mjs";
12
+ import { contrastRatio, relativeLuminance } from "../core/util.mjs";
13
+
14
+ export const STYLE_SCHEMA = "litmuse.motion-style/v1";
15
+ export const STYLE_IDS = Object.freeze(["swiss-signal", "tidal", "terminalcore", "paper"]);
16
+ export const DEFAULT_STYLE = "swiss-signal";
17
+ export const TYPE_ROLES = Object.freeze(["display", "headline", "body", "label"]);
18
+ export const TRANSITIONS = Object.freeze(["cut", "match", "wipe", "push", "iris", "dissolve", "morph"]);
19
+ /** Copy may sit this far (a share of the frame side) outside its zone, and this far off a scale step. */
20
+ export const ZONE_TOLERANCE = 0.012;
21
+ export const SIZE_TOLERANCE = 0.04;
22
+
23
+ // [x, y, w, h] as frame fractions. `type` bounds where copy may sit; `subject` bounds where the
24
+ // drawing leads (a `title` or `full` subject fills the frame as backdrop). Every type zone sits
25
+ // inside the central 90 % title-safe area.
26
+ export const LAYOUTS = Object.freeze({
27
+ "16:9": Object.freeze({
28
+ title: { type: [0.08, 0.2, 0.84, 0.6], subject: [0, 0, 1, 1] },
29
+ split: { type: [0.08, 0.16, 0.36, 0.68], subject: [0.48, 0.08, 0.46, 0.84] },
30
+ "split-alt": { type: [0.56, 0.16, 0.36, 0.68], subject: [0.06, 0.08, 0.46, 0.84] },
31
+ stage: { type: [0.08, 0.75, 0.84, 0.18], subject: [0.06, 0.07, 0.88, 0.64] },
32
+ hero: { type: [0.08, 0.76, 0.84, 0.17], subject: [0.03, 0.03, 0.94, 0.94] },
33
+ full: { type: null, subject: [0, 0, 1, 1] },
34
+ }),
35
+ "9:16": Object.freeze({
36
+ title: { type: [0.09, 0.26, 0.82, 0.48], subject: [0, 0, 1, 1] },
37
+ split: { type: [0.09, 0.08, 0.82, 0.26], subject: [0.06, 0.36, 0.88, 0.56] },
38
+ "split-alt": { type: [0.09, 0.66, 0.82, 0.26], subject: [0.06, 0.06, 0.88, 0.56] },
39
+ stage: { type: [0.09, 0.79, 0.82, 0.13], subject: [0.06, 0.06, 0.88, 0.68] },
40
+ hero: { type: [0.09, 0.8, 0.82, 0.12], subject: [0.03, 0.03, 0.94, 0.94] },
41
+ full: { type: null, subject: [0, 0, 1, 1] },
42
+ }),
43
+ });
44
+ export const LAYOUT_IDS = Object.freeze(Object.keys(LAYOUTS["16:9"]));
45
+ const FRAME = Object.freeze({ "16:9": [1920, 1080], "9:16": [1080, 1920] });
46
+
47
+ // With no name, house file or keyword, the genre chooses the style that reads best: an
48
+ // explainer on the warm, legible paper ground (stage only), a brand mood on the tidal flow.
49
+ export const GENRE_STYLE = Object.freeze({ explainer: "paper", "brand-mood": "tidal" });
50
+
51
+ // Checked in order; the first row with a whole-word hit wins. A style named outright wins over
52
+ // every row. `paper` has no type-engine preset, so the type path skips its row.
53
+ export const STYLE_PICK = Object.freeze([
54
+ { style: "terminalcore", korean: ["터미널", "CRT", "해커"], latin: ["terminal", "hacker", "crt"] },
55
+ { style: "tidal", korean: ["물결", "파도", "잔잔한", "흐름"], latin: ["gradient", "wave", "tide", "calm"] },
56
+ { style: "paper", korean: ["따뜻한", "밝은", "종이", "손그림", "포근한", "아기자기"], latin: ["warm", "cozy", "bright", "paper", "handmade", "hand-drawn"] },
57
+ ]);
58
+ const NAMED = Object.freeze({
59
+ "swiss-signal": ["swiss-signal", "swiss signal", "스위스 시그널"],
60
+ tidal: ["tidal", "타이달"],
61
+ terminalcore: ["terminalcore", "터미널코어"],
62
+ paper: ["paper style", "paper look", "페이퍼 스타일", "페이퍼"],
63
+ });
64
+
65
+ export class StyleError extends Error {}
66
+
67
+ const HEX = /^#[0-9a-f]{6}$/iu;
68
+ const isText = (v) => typeof v === "string" && v.trim().length > 0;
69
+
70
+ /** Validate a style document; returns it, or throws StyleError naming the first bad field. */
71
+ export function validateStyle(style) {
72
+ const fail = (field, message) => {
73
+ throw new StyleError(`style ${style?.id ?? "?"}: ${field}: ${message}`);
74
+ };
75
+ if (!style || typeof style !== "object") fail("style", "must be an object");
76
+ if (style.schema !== STYLE_SCHEMA) fail("schema", `must be ${STYLE_SCHEMA}`);
77
+ if (!STYLE_IDS.includes(style.id)) fail("id", `must be one of ${STYLE_IDS.join(", ")}`);
78
+ for (const k of ["name", "summary"]) if (!isText(style[k])) fail(k, "must be a non-empty string");
79
+ const p = style.palette ?? {};
80
+ for (const role of ["ground", "surface", "ink", "muted", "signal", "line"]) if (!HEX.test(p[role] ?? "")) fail(`palette.${role}`, "must be #rrggbb");
81
+ if (!Array.isArray(style.textRoles) || style.textRoles.some((r) => !HEX.test(p[r] ?? ""))) fail("textRoles", "every text role names a palette colour");
82
+ const ground = relativeLuminance(p.ground);
83
+ if (contrastRatio(relativeLuminance(p.ink), ground) < 7) fail("palette.ink", "ink on ground needs 7:1");
84
+ if (contrastRatio(relativeLuminance(p.muted), ground) < 4.5) fail("palette.muted", "muted on ground needs 4.5:1");
85
+ for (const role of ["signal", "accent"]) if (p[role] && contrastRatio(relativeLuminance(p[role]), ground) < 3) fail(`palette.${role}`, `${role} on ground needs 3:1 (large type)`);
86
+ if (!Array.isArray(style.accents) || style.accents.length < 3) fail("accents", "names at least three role accents a rich film may add");
87
+ style.accents.forEach((a, i) => {
88
+ if (!isText(a?.role)) fail(`accents[${i}].role`, "says what the colour marks");
89
+ if (!HEX.test(a?.color ?? "")) fail(`accents[${i}].color`, "must be #rrggbb");
90
+ if (contrastRatio(relativeLuminance(a.color), ground) < 3) fail(`accents[${i}].color`, "an accent on ground needs 3:1 (large type)");
91
+ });
92
+ for (const role of TYPE_ROLES) if (!isText(style.faces?.[role]?.family)) fail(`faces.${role}`, "needs a family");
93
+ for (const format of Object.keys(FRAME)) {
94
+ const s = style.scale?.[format];
95
+ if (!s || TYPE_ROLES.some((r) => !(s[r] > 0))) fail(`scale.${format}`, "needs a size per type role");
96
+ if (!(s.display > s.headline && s.headline > s.body && s.body > s.label)) fail(`scale.${format}`, "steps must descend display > headline > body > label");
97
+ }
98
+ const m = style.motion ?? {};
99
+ 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]");
100
+ for (const k of ["micro", "move", "hero", "exit"]) if (!(m.dur?.[k] > 0)) fail(`motion.dur.${k}`, "seconds > 0");
101
+ if (!(m.dur.exit <= m.dur.hero)) fail("motion.dur.exit", "an exit is never longer than the hero move");
102
+ if (!Array.isArray(style.transitions) || !style.transitions.length || style.transitions.some((x) => !TRANSITIONS.includes(x))) fail("transitions", `a subset of ${TRANSITIONS.join(", ")}`);
103
+ if (!isText(style.score?.genre)) fail("score.genre", "names the genre a film's score takes by default");
104
+ if (!style.sound || !isText(style.sound.timbre)) fail("sound", "needs the generated bed's palette");
105
+ 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 }");
106
+ const same = (a, b) => ["timbre", "key", "mode", "tempo"].every((k) => String(a[k]).toLowerCase() === String(b[k]).toLowerCase());
107
+ if (!style.beds.some((b) => same(b, style.sound))) fail("beds", "must include the default bed (sound)");
108
+ if (!Array.isArray(style.bans) || style.bans.length < 3) fail("bans", "names at least three defaults this style never uses");
109
+ return style;
110
+ }
111
+
112
+ const cache = new Map();
113
+ /** Load a shipped style by id (validated, frozen). */
114
+ export function readStyle(id) {
115
+ if (!STYLE_IDS.includes(id)) throw new StyleError(`unknown style: ${id} (choose ${STYLE_IDS.join(", ")})`);
116
+ if (!cache.has(id)) cache.set(id, Object.freeze(validateStyle(JSON.parse(readFileSync(fileURLToPath(new URL(`../../styles/${id}.json`, import.meta.url)), "utf8")))));
117
+ return cache.get(id);
118
+ }
119
+
120
+ /** Every font family copy may use in this style (the role faces, the Hangul face, Pretendard). */
121
+ export const styleFamilies = (style) => new Set([...TYPE_ROLES.map((r) => style.faces[r].family), style.hangul?.family ?? "Pretendard", "Pretendard"]);
122
+ /** The hex colours copy may use. */
123
+ export const styleTextColors = (style) => style.textRoles.map((r) => style.palette[r].toLowerCase());
124
+ /** The beds a film in this style may pick from, the default first. */
125
+ export const styleBeds = (style) => style.beds;
126
+ /** The role accents a rich film may add to the core palette, each { role, color }. */
127
+ export const styleAccents = (style) => style.accents;
128
+ /** All palette colours of the style (what a stage treatment's palette may list); rich adds the accents. */
129
+ export const styleColors = (style, { rich = false } = {}) => [...Object.values(style.palette), ...(rich ? style.accents.map((a) => a.color) : [])].map((c) => c.toLowerCase());
130
+
131
+ /**
132
+ * Which style a film takes. A style named in the request wins; then the project's house style;
133
+ * then the keyword auto-pick; then the genre's style; then the default. Returns { id, source, reason }.
134
+ */
135
+ export function pickStyle({ request = "", copy = [], path = "stage", house = null, genre = null } = {}) {
136
+ const text = String(request);
137
+ for (const [id, names] of Object.entries(NAMED)) {
138
+ if (path === "type" && !readStyle(id).typePreset) continue;
139
+ const hit = names.find((n) => matchesKeyword(text, n, /[가-힣]/u.test(n)));
140
+ if (hit) return { id, source: "request", reason: `the request names the ${id} style ("${hit}")` };
141
+ }
142
+ if (house?.style && STYLE_IDS.includes(house.style) && (path !== "type" || readStyle(house.style).typePreset)) {
143
+ return { id: house.style, source: "house", reason: `the project's house style is ${house.style} (motion-house.json)` };
144
+ }
145
+ const haystack = [text, ...copy].join("\n");
146
+ for (const row of STYLE_PICK) {
147
+ if (path === "type" && !readStyle(row.style).typePreset) continue;
148
+ for (const k of row.korean) if (matchesKeyword(haystack, k, true)) return { id: row.style, source: "auto", reason: `the request mentions "${k}"` };
149
+ for (const k of row.latin) if (matchesKeyword(haystack, k, false)) return { id: row.style, source: "auto", reason: `the request mentions "${k}"` };
150
+ }
151
+ const byGenre = GENRE_STYLE[genre];
152
+ 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}` };
153
+ return { id: DEFAULT_STYLE, source: "default", reason: `no style is named and no keyword picks one, so the default ${DEFAULT_STYLE}` };
154
+ }
155
+
156
+ /** A layout's zones in pixels for a format: { type: {x, y, w, h} | null, subject: {...} }. */
157
+ export function zonesPx(format) {
158
+ const [W, H] = FRAME[format];
159
+ 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);
160
+ return Object.fromEntries(Object.entries(LAYOUTS[format]).map(([id, z]) => [id, { type: px(z.type), subject: px(z.subject) }]));
161
+ }
162
+
163
+ /** The type step a rendered size sits on, or null when it is off the style's scale. */
164
+ export function scaleStep(style, format, sizePx) {
165
+ for (const role of TYPE_ROLES) {
166
+ const step = style.scale[format][role];
167
+ if (Math.abs(sizePx - step) <= step * SIZE_TOLERANCE) return role;
168
+ }
169
+ return null;
170
+ }
171
+
172
+ const familyStack = (style, role) => {
173
+ const f = style.faces[role];
174
+ const hangul = style.hangul?.family ?? "Pretendard";
175
+ return [f.family, hangul, "Pretendard"].filter((x, i, a) => a.indexOf(x) === i).map((x) => `"${x}"`).join(", ");
176
+ };
177
+
178
+ /** /lit/style.css for one style and format: custom properties, type-role classes, colour classes. */
179
+ export function styleCss(style, format) {
180
+ const s = style.scale[format];
181
+ const vars = [
182
+ ...Object.entries(style.palette).map(([k, v]) => ` --lit-${k}: ${v};`),
183
+ ...TYPE_ROLES.map((r) => ` --lit-size-${r}: ${s[r]}px;`),
184
+ ];
185
+ const roles = TYPE_ROLES.map((r) => {
186
+ const f = style.faces[r];
187
+ 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; }`;
188
+ });
189
+ const colours = style.textRoles.map((r) => `.lit-${r === "ground" ? "knockout" : r} { color: var(--lit-${r}); }`);
190
+ return [
191
+ `/* lit-typographic-motion style: ${style.id} (${format}) */`,
192
+ `:root {\n${vars.join("\n")}\n}`,
193
+ // Korean breaks between eojeol, never inside a word (type-craft.md).
194
+ `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; }`,
195
+ ...roles,
196
+ ...colours,
197
+ ".lit-zone { position: absolute; box-sizing: border-box; }",
198
+ "",
199
+ ].join("\n");
200
+ }
201
+
202
+ /** /lit/style.js: window.LitStyle (tokens and pixel zones) and window.LitTimeline (the treatment's shots). */
203
+ export function styleScript(style, format, treatment) {
204
+ const timeline = {
205
+ format, fps: treatment.fps ?? 60, duration: treatment.durationSec,
206
+ beats: (treatment.beats ?? []).map((b, i) => ({ index: i, t0: b.t0, t1: b.t1, layout: b.layout ?? null, transition: b.transition ?? null })),
207
+ };
208
+ const data = {
209
+ id: style.id, name: style.name, format, palette: style.palette, textRoles: style.textRoles,
210
+ faces: style.faces, hangul: style.hangul ?? null, scale: style.scale[format], leading: style.leading ?? {},
211
+ motion: style.motion, transitions: style.transitions, ground: style.ground, zones: zonesPx(format),
212
+ };
213
+ return `window.LitStyle = Object.freeze(${JSON.stringify(data)});\nwindow.LitTimeline = Object.freeze(${JSON.stringify(timeline)});\n`;
214
+ }