slidemind 0.3.20 → 0.4.0

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 (185) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +591 -15
  4. package/README.md +202 -7
  5. package/bin/slidemind.mjs +50 -2
  6. package/package.json +2 -2
  7. package/skills/create-slides/SKILL.md +484 -886
  8. package/skills/search/SKILL.md +46 -23
  9. package/src/bridge/build-and-push.mjs +548 -73
  10. package/src/bridge/bundle.mjs +100 -31
  11. package/src/bridge/capture.mjs +14 -17
  12. package/src/bridge/client/brands.mjs +157 -18
  13. package/src/bridge/client/engine-routing.mjs +48 -13
  14. package/src/bridge/client/engine-search.mjs +24 -1
  15. package/src/bridge/client/render.mjs +27 -1
  16. package/src/bridge/client/transport.mjs +499 -23
  17. package/src/bridge/client.mjs +10 -0
  18. package/src/bridge/config.mjs +67 -6
  19. package/src/bridge/engine-download.mjs +0 -10
  20. package/src/bridge/graph-auth.mjs +55 -3
  21. package/src/bridge/image-intent-envelope.mjs +239 -0
  22. package/src/bridge/package-preview.mjs +67 -0
  23. package/src/bridge/sharepoint.mjs +914 -0
  24. package/src/builders/asset-fetch.mjs +203 -0
  25. package/src/builders/block-dividers.mjs +83 -0
  26. package/src/builders/build.mjs +171 -87
  27. package/src/builders/card-height.mjs +294 -0
  28. package/src/builders/chart-hole.mjs +183 -0
  29. package/src/builders/chart-jsx.mjs +410 -0
  30. package/src/builders/chart-labels.mjs +326 -0
  31. package/src/builders/concept-geometry.mjs +370 -0
  32. package/src/builders/concept-intent.mjs +200 -0
  33. package/src/builders/concept-layout.mjs +79 -0
  34. package/src/builders/concept-lines.mjs +509 -0
  35. package/src/builders/concept-lists.mjs +270 -0
  36. package/src/builders/concept-marks.mjs +84 -0
  37. package/src/builders/concept-relationships.mjs +12 -0
  38. package/src/builders/concept-spec.mjs +645 -0
  39. package/src/builders/concept-text.mjs +140 -0
  40. package/src/builders/concept-waves.mjs +57 -0
  41. package/src/builders/copy-fit.mjs +139 -0
  42. package/src/builders/design-tokens.mjs +347 -0
  43. package/src/builders/draft-check.mjs +552 -340
  44. package/src/builders/draft-lines.mjs +369 -0
  45. package/src/builders/edit-route.mjs +29 -0
  46. package/src/builders/figure-sketch.mjs +375 -0
  47. package/src/builders/fold-sets.mjs +639 -0
  48. package/src/builders/gate-server.mjs +9 -4
  49. package/src/builders/gate-session.js +273 -12
  50. package/src/builders/gate-shell.js +1494 -0
  51. package/src/builders/image-intent-check.mjs +308 -0
  52. package/src/builders/image-use-reconcile.mjs +107 -0
  53. package/src/builders/kit-helpers.mjs +121 -0
  54. package/src/builders/layout-names.mjs +32 -0
  55. package/src/builders/legacy-reference-uses.mjs +179 -0
  56. package/src/builders/line-fit.mjs +224 -0
  57. package/src/builders/line-native.mjs +895 -0
  58. package/src/builders/line-relations.mjs +230 -0
  59. package/src/builders/lint-fix.mjs +117 -9
  60. package/src/builders/lint.mjs +25 -20
  61. package/src/builders/list-jsx.mjs +88 -0
  62. package/src/builders/placement.mjs +133 -0
  63. package/src/builders/plan-checks.mjs +135 -0
  64. package/src/builders/plan-comments.mjs +19 -0
  65. package/src/builders/plan-handlers.mjs +95 -0
  66. package/src/builders/plan-markdown-edit.mjs +3 -3
  67. package/src/builders/plan-overlay.js +1530 -1608
  68. package/src/builders/plan.mjs +689 -186
  69. package/src/builders/preset-definitions.json +1 -0
  70. package/src/builders/preset-eval.mjs +244 -0
  71. package/src/builders/preview.html +51 -120
  72. package/src/builders/red-thread-backbone.mjs +135 -36
  73. package/src/builders/repair.mjs +205 -0
  74. package/src/builders/repeat-groups.mjs +540 -0
  75. package/src/builders/resolve-skill.mjs +5 -4
  76. package/src/builders/review-overlay.js +296 -293
  77. package/src/builders/review.mjs +305 -84
  78. package/src/builders/rule-shift.mjs +96 -0
  79. package/src/builders/seat.mjs +354 -0
  80. package/src/builders/settle-driver.mjs +236 -0
  81. package/src/builders/settle.mjs +1448 -0
  82. package/src/builders/slide-prep.mjs +67 -0
  83. package/src/builders/spec-assets.mjs +236 -0
  84. package/src/builders/spec-match.mjs +56 -0
  85. package/src/builders/stack.mjs +413 -0
  86. package/src/builders/storyboard-markdown.mjs +70 -27
  87. package/src/builders/storyboard-schema.mjs +54 -114
  88. package/src/builders/text-blocks.mjs +212 -0
  89. package/src/builders/text-card-fit.mjs +552 -0
  90. package/src/builders/text-stack.mjs +276 -0
  91. package/src/builders/transcribe.mjs +1069 -0
  92. package/src/cli/assets.mjs +164 -0
  93. package/src/cli/common.mjs +173 -12
  94. package/src/cli/concept-spec-support.mjs +76 -0
  95. package/src/cli/concepts.mjs +1002 -103
  96. package/src/cli/deck.mjs +401 -65
  97. package/src/cli/diagnose.mjs +4 -2
  98. package/src/cli/engine-helpers.mjs +17 -10
  99. package/src/cli/engine-results.mjs +76 -3
  100. package/src/cli/help.mjs +232 -28
  101. package/src/cli/install.mjs +9 -0
  102. package/src/cli/maintenance.mjs +104 -0
  103. package/src/cli/read.mjs +2 -1
  104. package/src/cli/ready.mjs +6 -3
  105. package/src/cli/record.mjs +522 -0
  106. package/src/cli/remote.mjs +818 -0
  107. package/src/cli/revision.mjs +686 -0
  108. package/src/cli/search.mjs +30 -11
  109. package/src/cli/session.mjs +56 -13
  110. package/src/cli/setup.mjs +46 -2
  111. package/src/cli/status.mjs +142 -33
  112. package/src/cli/tasks.mjs +246 -0
  113. package/src/cli/transcribe.mjs +854 -0
  114. package/src/cli/workspace-ui.mjs +260 -0
  115. package/src/cli/workspace.mjs +212 -34
  116. package/src/install-channel.mjs +64 -7
  117. package/src/lib/adopt.mjs +346 -25
  118. package/src/lib/concept-spec-binding.mjs +96 -0
  119. package/src/lib/conversions.mjs +70 -6
  120. package/src/lib/convert-folded.mjs +107 -0
  121. package/src/lib/deck-paths.mjs +418 -0
  122. package/src/lib/durable-ids.mjs +47 -0
  123. package/src/lib/edit-operations.mjs +316 -0
  124. package/src/lib/edit-state.mjs +1244 -0
  125. package/src/lib/edit-transitions.mjs +120 -0
  126. package/src/lib/extensions.mjs +501 -0
  127. package/src/lib/image-assets.mjs +465 -0
  128. package/src/lib/image-derivatives.mjs +234 -0
  129. package/src/lib/image-intent-fingerprint.mjs +70 -0
  130. package/src/lib/image-intent-tags.mjs +176 -0
  131. package/src/lib/image-intent.mjs +802 -0
  132. package/src/lib/image-use-overrides.mjs +279 -0
  133. package/src/lib/inline-components.mjs +613 -0
  134. package/src/lib/lint-converted.mjs +68 -4
  135. package/src/lib/merge-json.mjs +163 -0
  136. package/src/lib/overwrite-guard.mjs +3 -3
  137. package/src/lib/presentation-context.mjs +531 -0
  138. package/src/lib/preservation-tags.mjs +157 -0
  139. package/src/lib/preservation-verdict.mjs +1342 -0
  140. package/src/lib/publish.mjs +1557 -0
  141. package/src/lib/rebase.mjs +945 -0
  142. package/src/lib/reconcile-ack.mjs +4 -6
  143. package/src/lib/reconcile.mjs +62 -24
  144. package/src/lib/record-client.mjs +175 -0
  145. package/src/lib/record-state.mjs +151 -0
  146. package/src/lib/remote.mjs +661 -0
  147. package/src/lib/revision.mjs +1210 -0
  148. package/src/lib/slide-guids.mjs +4 -6
  149. package/src/lib/slide-preservation.mjs +423 -0
  150. package/src/lib/slide-selection.mjs +9 -0
  151. package/src/lib/workspace-transaction.mjs +117 -0
  152. package/src/pptx/content-digest.mjs +334 -0
  153. package/src/pptx/deck-assemble.mjs +1278 -0
  154. package/src/pptx/fill/engine.mjs +2 -3
  155. package/src/pptx/fill/freeform.mjs +6 -3
  156. package/src/pptx/ooxml.mjs +20 -0
  157. package/src/pptx/package-evidence.mjs +383 -0
  158. package/src/pptx/tags.mjs +163 -11
  159. package/src/update-check.mjs +22 -4
  160. package/src/workspace/adapters.mjs +222 -0
  161. package/src/workspace/auth.mjs +256 -0
  162. package/src/workspace/autostart.mjs +204 -0
  163. package/src/workspace/daemon.mjs +360 -0
  164. package/src/workspace/discovery.mjs +180 -0
  165. package/src/workspace/edit.mjs +167 -0
  166. package/src/workspace/events.mjs +52 -0
  167. package/src/workspace/gate-client.mjs +290 -0
  168. package/src/workspace/handback.mjs +73 -0
  169. package/src/workspace/lifecycle.mjs +635 -0
  170. package/src/workspace/operations.mjs +200 -0
  171. package/src/workspace/pick-folder.mjs +152 -0
  172. package/src/workspace/registry.mjs +359 -0
  173. package/src/workspace/remote-links.mjs +397 -0
  174. package/src/workspace/remote-status.mjs +359 -0
  175. package/src/workspace/routes.mjs +1331 -0
  176. package/src/workspace/server.mjs +159 -0
  177. package/src/workspace/sync.mjs +1119 -0
  178. package/src/workspace/task-actions.mjs +215 -0
  179. package/src/workspace/tasks.mjs +507 -0
  180. package/src/workspace/ui/app.js +1886 -0
  181. package/src/workspace/ui/index.html +141 -0
  182. package/src/workspace/ui/plan-bridge.js +120 -0
  183. package/src/workspace/ui/sharepoint.svg +59 -0
  184. package/src/workspace/ui/styles.css +423 -0
  185. package/src/builders/visual-families.mjs +0 -46
@@ -1,63 +1,56 @@
1
- // Plan sign-off overlay — the CLI's plan-review gate, styled as the same
2
- // branded slidemind "app" as the preview-review overlay (review-overlay.js).
3
- // Dormant unless `window.__SLIDEMIND_PLAN_REVIEW__` is set (only the
4
- // `slidemind plan` local server injects it), and dormant inside an iframe /
5
- // thumbnail render, so nothing runs on an ordinary page.
1
+ // Plan sign-off overlay — the CLI's plan-review gate, laid out like the
2
+ // preview-review gate (review-overlay.js): the storyline as a slide RAIL on the
3
+ // left, the selected slide's storyboard on a fitted STAGE in the middle, its
4
+ // layout gallery + details in an ASIDE on the right, and the decision composer
5
+ // pinned at the bottom. Dormant unless `window.__SLIDEMIND_PLAN_REVIEW__` is
6
+ // set (only the `slidemind plan` local server injects it), and dormant inside
7
+ // an iframe / thumbnail render, so nothing runs on an ordinary page.
6
8
  //
7
- // The gate is a storyboard "dossier": the plan.md body is rendered into ONE
8
- // scrollable document surface. A plan that carries ```storyboard-canvas```
9
- // fences renders as the dossier — a red-thread tab strip + the selected
10
- // slide's editable storyboard canvas + a details aside — instead of a raw
11
- // editor. A plain-markdown plan renders its blocks inline-editable
12
- // (contenteditable) directly. Both paths edit the SAME markdown string, mapped
13
- // back through the plan modules exposed on window.__SLIDEMIND_PLAN__ (bundled
14
- // alongside window.__SLIDEMIND_RENDER_MD__ by plan.mjs).
9
+ // The chrome (bar, buttons, strips, hand-off dialog, veil, rail styling) is the
10
+ // SHARED gate shell (gate-shell.js → window.__SLIDEMIND_SHELL__); the session
11
+ // (ledger, rounds, heartbeat) is the shared gate session (gate-session.js).
12
+ // This file owns only what is plan-specific: the storyline edits, the
13
+ // storyboard canvas, the layout pick, the composer and the decision POST.
15
14
  //
16
- // The decision lives in ONE always-visible chat-style composer bar pinned at
17
- // the bottom: a scope control (This slide / Whole story), a comment input, a
18
- // round send button (a "story iteration" pill while storyline edits are staged)
19
- // and — fused to its right — the Approve button, whose sub-label says the agent
20
- // builds the slides next. Above it, the LEDGER rail lists every comment of this
21
- // session with its anchor and status (sent → addressed).
15
+ // A plan that carries ```storyboard-canvas``` fences renders as rail + stage +
16
+ // aside; a plain-markdown plan renders its blocks inline-editable
17
+ // (contenteditable) as one document. Both paths edit the SAME markdown string,
18
+ // mapped back through the plan modules on window.__SLIDEMIND_PLAN__ (bundled
19
+ // by plan.mjs).
22
20
  //
23
21
  // TWO MODES, not "reviewing vs. locked out":
24
22
  // - PARKED (the command is blocking on this tab): everything is live. Send
25
23
  // resolves the gate at once (decision "comments"); Approve resolves it as
26
24
  // "approve"; direct edits and story iteration are allowed.
27
- // - WORKING (the agent is revising): the plan stays readable and the strip
25
+ // - WORKING (the agent is revising): the plan stays readable and the rail
28
26
  // stays navigable; the composer, direct edits, story iteration and Approve
29
- // are off (the displayed round is already superseded). A status strip says
30
- // so — the add-in's "Revising the plan / this window refreshes when it's
31
- // ready". Nothing is queued behind the agent's back: the tab polls the
32
- // heartbeat and reloads itself when the next `slidemind plan` run presents
33
- // the revised round.
27
+ // are off (the displayed round is already superseded). Nothing is queued
28
+ // behind the agent's back: the tab polls the heartbeat and reloads itself
29
+ // when the next `slidemind plan` run presents the revised round.
34
30
  //
35
- // A round transition is detected from the SERVER's per-run nonce (CFG.round):
36
- // a different nonce means a newer `slidemind plan` run served this page. It
37
- // flips "sent" ledger rows to "addressed", restores the selected tab + the
31
+ // A round transition is detected from the SERVER's per-run nonce (CFG.round).
32
+ // It flips "sent" ledger rows to "addressed", restores the selected slide + the
38
33
  // unsent draft, shows a transient banner, and diffs the cached previous plan
39
- // body per slide to badge the changed tabs "updated" — the body diff is
40
- // COSMETIC only; it never decides whether a round arrived. The web notification
41
- // fires when the arriving round parks (a plain refresh of a sent round stays
42
- // silent).
34
+ // body per slide to badge the changed rail cards "updated" — the body diff is
35
+ // COSMETIC only; it never decides whether a round arrived.
43
36
  //
44
- // A fresh window (no session of its own) seeds its ledger from CFG.ledgerSeed,
45
- // the deck's persisted plan-review.json round, so the reviewer still sees the
46
- // comments the agent is currently working.
47
- //
48
- // The decision POSTs {decision, comments[], editedPlan} to the local server so
49
- // the `slidemind plan` command unblocks.
50
- //
51
- // Branding is lifted verbatim from review-overlay.js: the embossed "raised
52
- // slide" logo (inline-SVG fallback when no per-brand orb PNG is bundled), the
53
- // drifting "slide"/"mind" wordmark, Poppins/Urbanist type, Lucide icons, and
54
- // the neutral dark product palette.
37
+ // The decision POSTs {decision, comments[], editedPlan, operationId,
38
+ // expectedHash} through the shell's decision wire (SHELL.decide) so the
39
+ // `slidemind plan` command — or, inside the workspace page, the agent's waiting
40
+ // request — gets it. A refusal never drops what the reviewer wrote: a busy deck
41
+ // re-sends it, a newer version reloads with the drafts kept, and the card edits
42
+ // that have not been sent yet are persisted per plan version (EDITS_KEY), so a
43
+ // reload or a crash brings them back.
55
44
 
56
45
  (function () {
57
46
  var CFG = window.__SLIDEMIND_PLAN_REVIEW__;
58
47
  if (!CFG) return;
59
- // Dormant inside an iframe / thumbnail render (mirrors review-overlay).
60
- if (window.top !== window.self) return;
48
+ // The copy of this page saved at the deck root: a browser cannot write back
49
+ // to a file, so the gate stays dormant and the plan reads as a document.
50
+ if (CFG.readOnly) return;
51
+ // Dormant inside an iframe / thumbnail render — except the workspace page,
52
+ // which frames the LIVE gate on purpose (CFG.embedded).
53
+ if (window.top !== window.self && !CFG.embedded) return;
61
54
  if (new URLSearchParams(location.search).get("thumb") === "1") return;
62
55
 
63
56
  // The bundled plan modules (schema / storyboard-markdown / backbone / edit /
@@ -67,22 +60,23 @@
67
60
  var APPROVE_CLOSE_DELAY_MS = 2400; // long enough to read the build animation
68
61
  var WAITING_TITLE = "⏳ Agent revising — slidemind";
69
62
  // The plan body of the round that is on screen, cached for the NEXT round's
70
- // per-slide diff. Its own key on purpose: it is ~60 KB and never changes
71
- // during a page's life, so it is written ONCE at boot — the round context
72
- // (selection, unsent draft) is the small blob a keystroke may rewrite.
73
- var BODY_KEY = "slidemind-plan-body:" + (CFG.deck || "");
74
-
75
- // The shared gate session (src/builders/gate-session.js, inlined into the page
76
- // as window.__SLIDEMIND_GATE__): the deck-keyed sessionStorage ledger, the
77
- // server-nonce round transition, the round-ready announce and the heartbeat
78
- // poll — one implementation, shared with the preview gate.
63
+ // per-slide diff. Its own key: ~60 KB, written ONCE at boot.
64
+ // The storage namespace: inside the workspace the registry id (two decks may
65
+ // share a name, and one tab's sessionStorage serves every deck it frames);
66
+ // standalone the deck name, as before.
67
+ var DECK_KEY = CFG.embedded && CFG.deckId ? "id:" + CFG.deckId : CFG.deck || "";
68
+ var BODY_KEY = "slidemind-plan-body:" + DECK_KEY;
69
+
79
70
  var SES = window.__SLIDEMIND_GATE__.createGateSession({
80
71
  name: "plan",
81
- deck: CFG.deck || "",
72
+ deck: DECK_KEY,
82
73
  storage: sessionStorage,
83
74
  fetch: function (u, o) {
84
75
  return fetch(u, o);
85
76
  },
77
+ endpoint: CFG.endpoint || "/plan-review",
78
+ embedded: !!CFG.embedded,
79
+ subject: CFG.subjectHash || null, // the content version this page shows (rides in ctx)
86
80
  round: CFG.round || "",
87
81
  service: "slidemind-plan",
88
82
  heartbeat: CFG.heartbeat || "/__plan/heartbeat",
@@ -92,455 +86,370 @@
92
86
  notifyTitle: "slidemind — plan ready",
93
87
  notifyBody: "The revised plan is ready for your review. Click to jump back to the tab.",
94
88
  });
95
- var BANNER_MS = 7000; // how long the "Updated · Round N" banner stays
96
- var BEAT_DRAG_THRESHOLD = 4; // px before a tab press becomes a drag
89
+ var BANNER_MS = 7000; // how long the "Updated by your agent" banner stays
90
+ var BEAT_DRAG_THRESHOLD = 4; // px before a rail card press becomes a drag
97
91
  var BLOCK_DRAG_THRESHOLD = 3; // px before a block press becomes a drag
98
92
 
99
93
  // ---------- state ----------
100
- var submitted = false;
101
- var approveArmed = false; // inline "discard unsent feedback?" confirm state
102
94
  var working = false; // the agent is revising → edits, composer and approve off
103
95
  var baseline = CFG.planBody || ""; // the served plan body; edits diff against it
104
- var ledger = []; // every comment of this session: {…, status:sent|addressed}
96
+ var ledger = []; // every comment of this session: {…, status:draft|sent|addressed}
105
97
  var roundNo = 1; // this tab's round counter (sessionStorage, not server state)
106
- var changedSlides = {}; // 1-based tab position → true ("updated" badges this round)
98
+ var changedSlides = {}; // slide number → true ("updated" badges this round)
107
99
  var bannerText = null; // transient round-arrival banner, null once dismissed
108
100
  var commentScope = "slide"; // composer scope control: "slide" | "story"
101
+ var pendingBullet = false; // a content point being typed, not in the record yet
109
102
 
110
103
  // The single source of truth the whole overlay renders from. `value` is the
111
- // frontmatter-less plan body (edits index into it); `selectedIndex` is the
112
- // beat on stage; `storyDraft` is a staged red-thread restructure (null when
113
- // none), which blocks Approve and turns "send" into a story-iteration round.
114
- var S = { value: baseline, selectedIndex: 0, storyDraft: null };
104
+ // frontmatter-less plan body; `selectedIndex` is the slide on stage;
105
+ // `storyDraft` is a staged red-thread restructure (null when none), which
106
+ // blocks Approve and turns "send" into a story-iteration round.
107
+ // `design` is a cached design the reviewer picked (its presFP), null while
108
+ // the deck's own design is shown; it rides with the answer, like the edits.
109
+ var S = { value: baseline, selectedIndex: 0, storyDraft: null, design: null };
110
+ // The bound design's palette; a picked design's comes from CFG.designs.
111
+ var boundLayouts = Array.isArray(CFG.layouts) ? CFG.layouts : [];
112
+ // The plan text before the last design switch moved its layouts, and right
113
+ // after: switching again over an untouched remap starts from `before`, so
114
+ // A → B → A gives back exactly the plan the reviewer had.
115
+ var designUndo = null; // { before, from, after }
115
116
 
116
117
  var feedbackText = ""; // the composer input, kept across composer rebuilds
117
- var editingKey = null; // the beat whose move text is in rename mode
118
+ var editingKey = null; // the rail card whose move text is in rename mode
118
119
  var newSlideCounter = 0; // unique keys for inserted slides
119
120
  var focusAfterRender = null; // an element to focus once a render settles
120
-
121
- var bar, wsEl, surfaceEl, composerEl, commentEl;
122
- var editedBadge, approveBtn, sendBtn, hintEl;
123
121
  var workingNote = null; // headline of the working status strip
124
122
  var pendingComments = []; // the batch the next decision POST carries
125
123
  var pendingEdited = null; // the edited plan markdown riding with it
126
- var notifyPermPromise = null; // resolved Notification permission, asked at the send gesture
124
+ var arrivedAfterRevising = false; // this load is the round the reviewer was waiting for
125
+ var askedAgainUnchanged = false; // …and the agent asked again without changing anything
127
126
 
128
- // Read-only surface: working mode is the ONLY thing that locks the plan text.
129
- // Navigation (tab selection) and the composer stay live regardless.
127
+ var wsEl, surfaceEl, composerEl, commentEl;
128
+ var editedBadge, approveBtn, sendBtn, hintEl, barTools;
129
+
130
+ // The shared chrome: bar, strips, hand-off dialog, veil, buttons.
131
+ var SHELL = window.__SLIDEMIND_SHELL__.createGateShell({
132
+ CFG: CFG,
133
+ session: SES,
134
+ service: "slidemind-plan",
135
+ what: { revising: "the plan", approved: "Plan" },
136
+ isWorking: function () {
137
+ return working;
138
+ },
139
+ workingNote: function () {
140
+ return workingNote;
141
+ },
142
+ onSaveCtx: function () {
143
+ saveCtx();
144
+ },
145
+ onChange: function () {
146
+ // an answer started or ended: the cards lock / unlock with it
147
+ if (locked() !== surfaceLocked) renderSurface();
148
+ refreshChrome();
149
+ },
150
+ onDecided: function (decision, ack, body) {
151
+ decided(decision, body);
152
+ },
153
+ onDecisionFailed: function () {
154
+ refreshChrome();
155
+ },
156
+ onAlreadyAnswered: function (a) {
157
+ enterWorking(a && a.action === "approve" ? "Already approved." : a && a.action ? "Comments already sent." : "Already answered in another window.");
158
+ },
159
+ onApprovalPickedUp: function () {
160
+ showApproved();
161
+ },
162
+ onLost: function () {
163
+ startNextRoundPolling(); // a re-run of the plan gate reloads this tab, drafts and all
164
+ },
165
+ // Unsent card edits are the one thing a reload into a newer version would
166
+ // drop (a comment being typed is kept, but the reload would yank the field
167
+ // from under the cursor): the newer version waits for both.
168
+ reloadBlocker: function () {
169
+ if (working) return null;
170
+ if (planWasEdited() || S.storyDraft || S.design) return "edits";
171
+ var a = document.activeElement;
172
+ if (a && (a.isContentEditable || (a === commentEl && commentEl.value.trim()))) return "typing";
173
+ return null;
174
+ },
175
+ reloadIntoNewRound: function () {
176
+ SES.write(EDITS_KEY, null); // reached only without edits, or by the reviewer's own "Reload"
177
+ saveLedger();
178
+ saveCtx();
179
+ SES.markRoundReadyAndReload();
180
+ },
181
+ });
182
+ var el = SHELL.el;
183
+ var ICONS = SHELL.ICONS;
184
+
185
+ // Read-only surface: working mode — and an answer on its way (a busy deck or
186
+ // a stopped workspace re-sends it) — lock the plan text: the request carries
187
+ // the edits made BEFORE it, so an edit made meanwhile would ride on nothing
188
+ // and be wiped when it lands. Navigation (rail selection) and the composer
189
+ // stay live regardless.
130
190
  function locked() {
131
- return working;
132
- }
133
-
134
- // ---------- brand assets (identical to review-overlay.js) ----------
135
- var LOGO_SVG =
136
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="160 160 960 540" preserveAspectRatio="none">' +
137
- "<defs>" +
138
- '<linearGradient id="rimTop" gradientUnits="userSpaceOnUse" x1="160" y1="0" x2="1120" y2="0">' +
139
- '<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.85"/>' +
140
- '<stop offset="35%" stop-color="#fff" stop-opacity="0.5"/>' +
141
- '<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
142
- '<linearGradient id="rimLeft" gradientUnits="userSpaceOnUse" x1="0" y1="160" x2="0" y2="700">' +
143
- '<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.6"/>' +
144
- '<stop offset="40%" stop-color="#fff" stop-opacity="0.28"/>' +
145
- '<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
146
- '<linearGradient id="rimBottom" gradientUnits="userSpaceOnUse" x1="1120" y1="0" x2="160" y2="0">' +
147
- '<stop offset="0%" stop-color="#000" stop-opacity="0.55"/>' +
148
- '<stop offset="60%" stop-color="#000" stop-opacity="0.2"/>' +
149
- '<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
150
- '<linearGradient id="rimRight" gradientUnits="userSpaceOnUse" x1="0" y1="700" x2="0" y2="160">' +
151
- '<stop offset="0%" stop-color="#000" stop-opacity="0.45"/>' +
152
- '<stop offset="60%" stop-color="#000" stop-opacity="0.16"/>' +
153
- '<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
154
- '<linearGradient id="faceFill" gradientUnits="userSpaceOnUse" x1="160" y1="160" x2="1120" y2="700">' +
155
- '<stop offset="0%" stop-color="#fff" stop-opacity="0.08"/>' +
156
- '<stop offset="50%" stop-color="#808080" stop-opacity="0.02"/>' +
157
- '<stop offset="100%" stop-color="#000" stop-opacity="0.10"/></linearGradient>' +
158
- "</defs>" +
159
- '<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="url(#faceFill)"/>' +
160
- '<path d="M 160,200 A 40,40 0 0 1 200,160 L 1080,160 A 40,40 0 0 1 1120,200" fill="none" stroke="url(#rimTop)" stroke-width="3" stroke-linecap="round"/>' +
161
- '<path d="M 160,200 L 160,660 A 40,40 0 0 0 200,700" fill="none" stroke="url(#rimLeft)" stroke-width="2.4" stroke-linecap="round"/>' +
162
- '<path d="M 1120,660 A 40,40 0 0 1 1080,700 L 200,700 A 40,40 0 0 1 160,660" fill="none" stroke="url(#rimBottom)" stroke-width="3" stroke-linecap="round"/>' +
163
- '<path d="M 1120,660 L 1120,200 A 40,40 0 0 0 1080,160" fill="none" stroke="url(#rimRight)" stroke-width="2.4" stroke-linecap="round"/>' +
164
- '<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.4"/>' +
165
- "</svg>";
166
-
167
- // Give a copy of the logo unique gradient ids so two instances (header + done
168
- // veil) can coexist without id collisions.
169
- function logoWithIds(suffix) {
170
- return LOGO_SVG.replace(/id="([^"]+)"/g, 'id="$1' + suffix + '"').replace(
171
- /url\(#([^)]+)\)/g,
172
- "url(#$1" + suffix + ")",
173
- );
191
+ return working || SHELL.decisionPending();
174
192
  }
193
+ var surfaceLocked = false; // what the surface was last rendered as
175
194
 
176
- // Lucide icons (MIT) — the same family review-overlay uses.
177
- var ICONS = {
178
- check:
179
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
180
- send:
181
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>',
182
- sparkles:
183
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.582a.5.5 0 0 1 0 .962L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>',
184
- plus:
185
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>',
186
- x:
187
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
188
- ruler:
189
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>',
190
- };
191
-
192
- // ---------- styles (chrome copied from review-overlay.js; the editor/preview
193
- // split is replaced by the storyboard dossier — plan-markdown.css ported and
194
- // remapped onto the overlay's dark --sm-* palette; the two structural hues
195
- // #3565d4 (blueprint blue) and #b41830 (storyline red) are literals) ----------
195
+ // ---------- styles (plan-specific; the chrome comes from the shell) ----------
196
196
  var CSS =
197
- "@keyframes sm-drift-a{0%,100%{transform:translateX(-5px)}50%{transform:translateX(5px)}}" +
198
- "@keyframes sm-drift-b{0%,100%{transform:translateX(5px)}50%{transform:translateX(-5px)}}" +
199
- "@keyframes sm-bar-in{from{transform:translateY(-100%);opacity:0}to{transform:none;opacity:1}}" +
200
- "@keyframes sm-veil-in{from{opacity:0}to{opacity:1}}" +
201
- "@keyframes sm-rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}" +
202
- "@keyframes sm-pulse{0%,100%{opacity:.45}50%{opacity:.85}}" +
203
- ":root{--sm-bg:var(--deck-chrome-bg,#111113);--sm-surface:var(--deck-chrome-bg-2,#161618);" +
204
- "--sm-border:var(--deck-chrome-border,#2b2b30);--sm-fg:var(--deck-chrome-fg,#ededed);" +
205
- "--sm-dim:var(--deck-chrome-dim,#9a9aa0);--sm-muted:rgba(255,255,255,.05);--sm-accent:#a9c2ff;" +
206
- "--sm-success:#2ecc71;--sm-danger:#f87171;--sm-warn:#f6c454;" +
207
- "--sm-font:'Poppins',ui-sans-serif,system-ui,sans-serif;--sm-word:'Urbanist',var(--sm-font);" +
208
- /* the two structural hues, brightened against the dark surface */
209
- "--plan-annotation:color-mix(in srgb,#3565d4 60%,var(--sm-fg));" +
197
+ ":root{--plan-annotation:color-mix(in srgb,#3565d4 60%,var(--sm-fg));" +
210
198
  "--plan-annotation-line:color-mix(in srgb,#3565d4 52%,transparent);" +
211
199
  "--plan-annotation-wash:color-mix(in srgb,#3565d4 12%,transparent);" +
212
200
  "--plan-story:color-mix(in srgb,#b41830 72%,var(--sm-fg));" +
213
201
  "--plan-story-line:color-mix(in srgb,#b41830 45%,transparent);}" +
214
- ".sm-o,.sm-o *{box-sizing:border-box;}" +
215
- /* header */
216
- ".sm-bar{position:fixed;top:0;left:0;right:0;z-index:2147483600;display:flex;align-items:center;" +
217
- "gap:16px;height:var(--deck-chrome-h,62px);padding:0 18px;color:var(--sm-fg);font-family:var(--sm-font);font-size:13px;" +
218
- "background:var(--sm-bg);border-bottom:1px solid var(--sm-border);" +
219
- "user-select:none;animation:sm-bar-in .5s cubic-bezier(.2,.7,.2,1);}" +
220
- ".sm-brand{display:flex;align-items:center;gap:11px;}" +
221
- ".sm-logo{position:relative;width:48px;height:27px;border-radius:5px;overflow:hidden;flex-shrink:0;background:#0a0c11;box-shadow:0 1px 3px rgba(0,0,0,.5);}" +
222
- ".sm-logo-svg{position:absolute;inset:0;z-index:1;}.sm-logo-svg svg{width:100%;height:100%;display:block;}" +
223
- ".sm-logo-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;}" +
224
- ".sm-word{display:flex;flex-direction:column;line-height:.98;font-family:var(--sm-word);" +
225
- "font-weight:300;font-size:13px;letter-spacing:.46em;color:var(--sm-fg);}" +
226
- ".sm-word b{font-weight:300;display:block;will-change:transform;}" +
227
- ".sm-word .top{animation:sm-drift-a 20s ease-in-out infinite;}" +
228
- ".sm-word .bot{margin-left:.18em;animation:sm-drift-b 20s ease-in-out infinite;}" +
229
- ".sm-sep{width:1px;height:28px;background:var(--sm-border);}" +
230
- ".sm-meta{display:flex;flex-direction:column;gap:2px;min-width:0;}" +
231
- ".sm-meta .t{font-weight:600;font-size:13px;display:flex;align-items:center;gap:8px;}" +
232
- ".sm-meta .h{font-size:11.5px;color:var(--sm-dim);}" +
233
- ".sm-edited{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--sm-warn);" +
234
- "border:1px solid rgba(246,196,84,.4);border-radius:999px;padding:1px 7px;}" +
235
- ".sm-grow{flex:1;}" +
236
- /* buttons */
237
- ".sm-btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:9px;" +
238
- "padding:8px 14px;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;" +
239
- "transition:transform .14s ease,background .14s ease,box-shadow .14s ease,border-color .14s ease;white-space:nowrap;}" +
240
- ".sm-btn svg{width:15px;height:15px;flex-shrink:0;}" +
241
- /* workspace: one scrollable document surface + composer dock */
242
- ".sm-ws{position:fixed;left:0;right:0;top:var(--deck-chrome-h,62px);bottom:0;z-index:2147483540;" +
243
- "display:flex;flex-direction:column;background:var(--sm-bg);}" +
244
- ".sm-ws-surface{flex:1;min-height:0;overflow-y:auto;background:var(--sm-bg);padding:22px 22px 30px;}" +
245
- /* working mode: read-only, NOT locked out — the surface stays scrollable and
246
- the strip stays clickable; the dashed frame is the read-only signal */
247
- ".sm-ws.is-waiting .plan-dossier{border-style:dashed;}" +
248
- /* round-arrival banner */
249
- ".sm-round-banner{display:flex;align-items:center;gap:9px;max-width:1120px;margin:0 auto 14px;padding:9px 13px;" +
250
- "border:1px solid color-mix(in srgb,var(--sm-accent) 35%,transparent);border-radius:10px;" +
251
- "background:color-mix(in srgb,var(--sm-accent) 9%,transparent);color:var(--sm-dim);font-size:12.5px;line-height:1.5;" +
252
- "animation:sm-rise .45s cubic-bezier(.2,.7,.2,1) both;}" +
253
- ".sm-round-banner svg{width:15px;height:15px;flex-shrink:0;color:var(--sm-accent);}" +
254
- ".sm-round-banner b{color:var(--sm-fg);font-weight:600;}" +
255
- /* markdown document (Mode A + the storyboard headings share this scale) */
256
- ".plan-md-root{max-width:min(1120px,100%);margin:0 auto;line-height:1.65;font-size:15.5px;color:var(--sm-fg);}" +
257
- ".plan-md-root--storyboards{max-width:100%;display:flex;flex-direction:column;gap:0;}" +
258
- ".plan-md-root h1,.plan-md-root h2,.plan-md-root h3,.plan-md-root h4,.plan-md-root h5,.plan-md-root h6{" +
259
- "color:var(--sm-fg);font-weight:600;line-height:1.28;margin:1.6em 0 .5em;}" +
202
+ "html,body{overflow:hidden;}" +
203
+ /* the workspace: surface + composer under the bar */
204
+ ".sm-ws{position:fixed;left:0;right:0;top:var(--deck-chrome-h,62px);bottom:0;display:flex;flex-direction:column;" +
205
+ "background:var(--sm-bg);color:var(--sm-fg);font-family:var(--sm-font);font-size:13px;}" +
206
+ ".sm-ws-surface{flex:1;min-height:0;display:flex;flex-direction:column;}" +
207
+ /* Mode A: one scrolling document */
208
+ ".sm-doc{flex:1;min-height:0;overflow-y:auto;padding:28px 28px 40px;}" +
209
+ ".plan-md-root{max-width:760px;margin:0 auto;line-height:1.65;font-size:15.5px;color:var(--sm-fg);}" +
210
+ ".plan-md-root h1,.plan-md-root h2,.plan-md-root h3,.plan-md-root h4,.plan-md-root h5,.plan-md-root h6{color:var(--sm-fg);font-weight:600;line-height:1.28;margin:1.6em 0 .5em;}" +
260
211
  ".plan-md-root h1{font-size:1.95em;margin-top:.2em;}" +
261
212
  ".plan-md-root h2{font-size:1.5em;padding-bottom:.24em;border-bottom:1px solid var(--sm-border);}" +
262
213
  ".plan-md-root h3{font-size:1.22em;}.plan-md-root h4{font-size:1.05em;}" +
263
214
  ".plan-md-root h5,.plan-md-root h6{font-size:.95em;color:var(--sm-dim);}" +
264
- ".plan-md-root p{margin:.7em 0;}" +
265
- ".plan-md-root ul,.plan-md-root ol{margin:.6em 0;padding-left:1.5em;}" +
266
- ".plan-md-root ul{list-style:disc;}.plan-md-root ol{list-style:decimal;}" +
267
- ".plan-md-root li{margin:.3em 0;}" +
268
- ".plan-md-root blockquote{margin:.95em 0;padding:.45em 1em;border-left:3px solid var(--sm-border);" +
269
- "color:var(--sm-dim);background:var(--sm-muted);border-radius:0 6px 6px 0;}" +
270
- ".plan-md-root code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;" +
271
- "background:var(--sm-muted);padding:.12em .42em;border-radius:5px;}" +
215
+ ".plan-md-root p{margin:.7em 0;}.plan-md-root ul,.plan-md-root ol{margin:.6em 0;padding-left:1.5em;}" +
216
+ ".plan-md-root ul{list-style:disc;}.plan-md-root ol{list-style:decimal;}.plan-md-root li{margin:.3em 0;}" +
217
+ ".plan-md-root blockquote{margin:.95em 0;padding:.45em 1em;border-left:3px solid var(--sm-border);color:var(--sm-dim);background:var(--sm-muted);border-radius:0 6px 6px 0;}" +
218
+ ".plan-md-root code{font-family:var(--sm-mono);font-size:.88em;background:var(--sm-muted);padding:.12em .42em;border-radius:5px;}" +
272
219
  ".plan-md-root pre{background:#0c0c0e;border:1px solid var(--sm-border);border-radius:10px;padding:14px 16px;overflow-x:auto;line-height:1.5;}" +
273
220
  ".plan-md-root pre code{background:none;padding:0;font-size:.86em;}" +
274
- ".plan-md-root strong{color:var(--sm-fg);font-weight:600;}.plan-md-root em{font-style:italic;}" +
275
- ".plan-md-root a{color:var(--sm-accent);}" +
221
+ ".plan-md-root strong{color:var(--sm-fg);font-weight:600;}.plan-md-root em{font-style:italic;}.plan-md-root a{color:var(--sm-accent);}" +
276
222
  ".plan-md-root [contenteditable=\"true\"]{border:0;background:transparent;border-radius:0;box-shadow:none;caret-color:currentColor;outline:none;}" +
277
- ".plan-md-root [contenteditable=\"true\"]:focus{outline:none;box-shadow:none;background:transparent;}" +
278
- /* dossier card */
279
- ".plan-dossier{max-width:1120px;margin:0 auto;border:1px solid var(--sm-border);border-radius:14px;" +
280
- "background:var(--sm-surface);overflow:hidden;}" +
281
- ".plan-dossier-body{display:flex;flex-direction:column;gap:16px;padding:18px 20px 20px;}" +
282
- ".plan-dossier-new-slide{display:flex;align-items:flex-start;gap:10px;border:1.5px dashed var(--plan-story-line);" +
283
- "border-radius:10px;background:color-mix(in srgb,#b41830 6%,transparent);color:var(--sm-dim);font-size:13px;line-height:1.5;padding:22px 18px;}" +
284
- ".plan-dossier-new-slide svg{width:14px;height:14px;flex:none;margin-top:2px;color:var(--plan-story);}" +
285
- ".plan-dossier-new-slide p{margin:0;}.plan-dossier-new-slide strong{color:var(--sm-fg);font-weight:600;}" +
286
- /* hand-off note */
287
- ".plan-storyboard-draft-note{display:flex;align-items:flex-start;gap:10px;margin:0;padding:9px 13px;" +
288
- "border:1px dashed color-mix(in srgb,var(--sm-dim) 45%,transparent);border-radius:9px;background:var(--sm-muted);" +
289
- "color:var(--sm-dim);font-size:12px;line-height:1.5;}" +
290
- ".plan-storyboard-draft-note svg{width:14px;height:14px;flex:none;margin-top:2px;}" +
291
- ".plan-storyboard-draft-note p{margin:0;}.plan-storyboard-draft-note strong{color:var(--sm-fg);font-weight:600;}" +
292
- /* storyline tab strip */
293
- ".plan-storyline{margin:0;}" +
294
- ".plan-storyline-head{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:10px 20px;padding:16px 20px 12px;}" +
295
- ".plan-storyline-head-text{display:flex;flex:1 1 240px;flex-direction:column;gap:3px;min-width:0;}" +
296
- /* deck-level layout knob: one pick binds every slide's layout */
297
- ".plan-storyline-deck-layout{display:flex;flex:0 1 280px;flex-direction:column;gap:4px;min-width:200px;}" +
298
- ".plan-storyline-deck-layout-label{color:var(--sm-dim);font-size:9px;font-weight:700;letter-spacing:.18em;line-height:1.2;text-transform:uppercase;}" +
299
- ".plan-storyline-deck-layout .plan-storyboard-layout-picker{margin:0;}" +
300
- ".plan-storyline-deck-layout .plan-storyboard-layout-menu{left:auto;right:0;min-width:340px;}" +
301
- ".plan-storyline-label{color:var(--plan-story);font-size:9px;font-weight:700;letter-spacing:.18em;line-height:1.2;text-transform:uppercase;}" +
302
- ".plan-storyline-track{display:flex;flex-wrap:wrap;align-items:stretch;margin:0;padding:4px 14px 10px;list-style:none;" +
303
- "border-bottom:1px solid var(--sm-border);row-gap:2px;}" +
304
- /* grid: numeral + move text share a baseline row; the layout chip fills the
305
- second row and pins to the bottom, so cards in a row stay one height */
306
- ".plan-storyline-beat{position:relative;display:grid;flex:1 1 150px;min-width:132px;max-width:260px;grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto 1fr;" +
307
- "align-items:baseline;column-gap:9px;padding:8px 8px 9px;border:0;border-radius:8px;background:transparent;cursor:pointer;transition:background-color 130ms ease;}" +
308
- ".plan-storyline-beat:hover{background:var(--sm-muted);}" +
309
- ".plan-storyline-beat--active{background:color-mix(in srgb,var(--sm-fg) 8%,transparent);}" +
310
- ".plan-storyline-beat--draggable{touch-action:none;}" +
311
- ".plan-storyline-beat--dragging{z-index:20;cursor:grabbing;background:var(--sm-surface);box-shadow:0 16px 36px rgba(0,0,0,.5);transition:none;}" +
312
- ".plan-storyline-beat--editing{cursor:text;}" +
313
- ".plan-storyline-beat-slide{grid-row:1;grid-column:1;color:color-mix(in srgb,var(--sm-dim) 55%,transparent);" +
314
- "font-family:Georgia,'Times New Roman',serif;font-size:21px;font-weight:400;line-height:1;transition:color 130ms ease;}" +
315
- ".plan-storyline-beat--active .plan-storyline-beat-slide{color:var(--plan-story);}" +
316
- ".plan-storyline-beat-text{grid-row:1;grid-column:2;display:-webkit-box;min-width:0;overflow:hidden;color:var(--sm-dim);font-size:12px;" +
317
- "font-weight:500;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2;}" +
318
- ".plan-storyline-beat--active .plan-storyline-beat-text{color:var(--sm-fg);font-weight:600;}" +
319
- ".plan-storyline-beat-text[contenteditable=\"true\"]{display:block;border-radius:4px;cursor:text;outline:none;-webkit-line-clamp:unset;}" +
320
- ".plan-storyboard-layout-picker--beat{grid-row:2;grid-column:2;align-self:end;min-width:0;margin:5px 0 0;max-width:100%;}" +
321
- ".plan-storyboard-layout-picker--beat .plan-storyboard-layout-current{width:auto;max-width:100%;gap:6px;border-color:transparent;border-radius:5px;padding:2px 4px;margin-left:-4px;color:var(--sm-dim);font-size:10.5px;font-weight:500;}" +
322
- ".plan-storyboard-layout-picker--beat .plan-storyboard-layout-current:not(:disabled):hover{border-color:var(--sm-line);}" +
323
- ".plan-storyboard-layout-picker--beat .plan-storyboard-layout-current>.plan-storyboard-layout-thumb{width:36px;height:20px;}" +
324
- ".plan-storyboard-layout-picker--beat .plan-storyboard-layout-menu{right:auto;min-width:320px;}" +
325
- /* "updated" badge — this slide changed in the round that just arrived */
326
- ".plan-storyline-beat-updated{position:absolute;top:-6px;left:6px;border-radius:999px;padding:0 6px;" +
327
- "background:color-mix(in srgb,var(--sm-accent) 22%,var(--sm-bg));color:var(--sm-accent);" +
328
- "border:1px solid color-mix(in srgb,var(--sm-accent) 45%,transparent);" +
329
- "font-size:8.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;line-height:1.6;}" +
330
- ".plan-storyline-gap{position:relative;display:flex;flex:none;align-items:center;justify-content:center;width:22px;}" +
331
- ".plan-storyline-gap[data-active=\"true\"]::before{position:absolute;top:18%;bottom:18%;left:50%;width:3px;border-radius:2px;background:var(--plan-story);content:\"\";transform:translateX(-50%);}" +
332
- ".plan-storyline-insert{position:relative;display:flex;align-items:center;justify-content:center;width:18px;height:18px;" +
333
- "border:1px solid var(--sm-border);border-radius:999px;background:var(--sm-surface);color:var(--sm-dim);cursor:pointer;" +
334
- "opacity:0;padding:0;pointer-events:none;transform:scale(.85);transition:opacity 140ms ease,transform 140ms ease,background-color 140ms ease,border-color 140ms ease,color 140ms ease;}" +
223
+ /* the dossier: rail | stage | aside */
224
+ /* A wider rail than the preview gate's: these cards carry a story beat, not
225
+ a filename, and the beat is what the reviewer navigates by. */
226
+ ".sm-plan{--sm-rail-w:272px;flex:1;min-height:0;display:grid;grid-template-columns:var(--sm-rail-w) minmax(0,1fr);}" +
227
+ /* the header's deck-wide choices, each with its name: Design, Slide Layouts */
228
+ ".sm-bar-tools{display:flex;align-items:center;gap:16px;min-width:0;flex:0 1 auto;}" +
229
+ ".sm-bar-tools:empty{display:none;}" +
230
+ ".sm-bar-tool{display:flex;align-items:center;gap:8px;min-width:0;}" +
231
+ ".sm-bar-tool-label{flex:none;font-size:10px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--sm-dim);}" +
232
+ ".sm-bar-tool .plan-design-picker,.sm-bar-tool .plan-storyboard-layout-picker{min-width:0;max-width:280px;}" +
233
+ "@media (max-width:760px){.sm-bar-tool-label{display:none;}}" +
234
+ ".sm-rail-gap{position:relative;height:10px;margin:-2px 0 0;display:flex;align-items:center;justify-content:center;}" +
235
+ ".sm-rail-gap[data-active=\"true\"]::before{content:\"\";position:absolute;left:8%;right:8%;top:50%;height:3px;border-radius:2px;background:var(--plan-story);transform:translateY(-50%);}" +
236
+ ".plan-storyline-insert{position:relative;z-index:2;display:flex;align-items:center;justify-content:center;width:18px;height:18px;border:1px solid var(--sm-border);border-radius:999px;" +
237
+ "background:var(--sm-surface);color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;transform:scale(.85);transition:opacity 140ms ease,transform 140ms ease,background-color 140ms ease,border-color 140ms ease,color 140ms ease;}" +
335
238
  ".plan-storyline-insert svg{width:12px;height:12px;}" +
336
- ".plan-storyline-track:hover .plan-storyline-insert,.plan-storyline-insert:focus-visible{opacity:1;pointer-events:auto;transform:scale(1);}" +
239
+ ".sm-rail-gap:hover .plan-storyline-insert,.plan-storyline-insert:focus-visible{opacity:1;transform:scale(1);}" +
337
240
  ".plan-storyline-insert:hover{background:var(--plan-story);border-color:var(--plan-story);color:var(--sm-bg);transform:scale(1.1);}" +
338
- ".plan-storyline-beat-remove{position:absolute;top:-7px;right:-7px;display:inline-flex;align-items:center;justify-content:center;" +
339
- "width:19px;height:19px;border:1px solid var(--sm-border);border-radius:999px;background:var(--sm-surface);box-shadow:0 2px 6px rgba(0,0,0,.35);" +
340
- "color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;pointer-events:none;transition:opacity 120ms ease;}" +
241
+ ".sm-rail-add{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;margin-top:4px;padding:8px;border:1px dashed var(--sm-border);border-radius:8px;background:transparent;color:var(--sm-dim);font:500 11.5px var(--sm-font);cursor:pointer;}" +
242
+ ".sm-rail-add svg{width:12px;height:12px;}.sm-rail-add:hover{color:var(--sm-fg);border-color:#3a3a40;background:var(--sm-muted);}" +
243
+ ".plan-beat{touch-action:none;}" +
244
+ ".plan-beat.is-dragging{z-index:20;cursor:grabbing;box-shadow:0 16px 36px rgba(0,0,0,.5);transition:none;}" +
245
+ ".plan-beat.is-editing{cursor:text;}" +
246
+ ".plan-beat .sm-thumb-name[contenteditable=\"true\"]{display:block;-webkit-line-clamp:unset;border-radius:3px;cursor:text;outline:none;background:var(--sm-muted);padding:1px 3px;margin:-1px -3px;}" +
247
+ ".plan-beat.is-active .sm-thumb-number{color:var(--plan-story);font-weight:600;}" +
248
+ ".plan-storyline-beat-updated{position:absolute;top:-6px;left:8px;z-index:2;border-radius:999px;padding:0 6px;background:color-mix(in srgb,var(--sm-accent) 22%,var(--sm-bg));color:var(--sm-accent);" +
249
+ "border:1px solid color-mix(in srgb,var(--sm-accent) 45%,transparent);font-size:8.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;line-height:1.6;}" +
250
+ ".plan-storyline-beat-remove{position:absolute;top:-7px;right:-7px;z-index:2;display:inline-flex;align-items:center;justify-content:center;width:19px;height:19px;border:1px solid var(--sm-border);border-radius:999px;" +
251
+ "background:var(--sm-surface);box-shadow:0 2px 6px rgba(0,0,0,.35);color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;pointer-events:none;transition:opacity 120ms ease;}" +
341
252
  ".plan-storyline-beat-remove svg{width:12px;height:12px;}" +
342
- ".plan-storyline-beat:hover .plan-storyline-beat-remove{opacity:1;pointer-events:auto;}" +
343
- ".plan-storyline-beat--dragging .plan-storyline-beat-remove,.plan-storyline-beat--editing .plan-storyline-beat-remove{opacity:0;pointer-events:none;}" +
344
- ".plan-storyline-beat-remove:disabled{cursor:default;opacity:0;}" +
345
- /* storyboard canvas */
346
- ".plan-storyboard{margin:0;}" +
347
- ".plan-storyboard-header{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:8px;" +
348
- "font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--sm-dim);}" +
349
- ".plan-storyboard-legend{display:inline-flex;flex:none;align-items:baseline;gap:10px;letter-spacing:0;text-transform:none;}" +
350
- ".plan-storyboard-legend-item--element{border:1px dashed var(--plan-annotation-line);border-radius:4px;background:var(--plan-annotation-wash);" +
351
- "color:var(--plan-annotation);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:8.5px;font-weight:650;letter-spacing:.08em;padding:2px 6px;text-transform:uppercase;}" +
352
- ".plan-storyboard-legend-item--content{color:var(--sm-fg);font-size:10.5px;font-weight:500;}" +
353
- ".plan-storyboard-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(200px,26%);gap:14px;align-items:start;}" +
354
- ".plan-storyboard-frame{position:relative;aspect-ratio:16/9;width:100%;overflow:hidden;border:0;border-radius:8px;" +
355
- "background:color-mix(in srgb,var(--sm-bg) 78%,white);}" +
356
- ".slide-emboss{box-shadow:0 1px 2px rgba(0,0,0,.45),0 10px 26px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.06);}" +
357
- ".plan-storyboard-reserved{position:absolute;left:0;right:0;display:flex;align-items:center;justify-content:center;" +
358
- "background:repeating-linear-gradient(-45deg,var(--sm-muted),var(--sm-muted) 8px,transparent 8px,transparent 16px);" +
359
- "color:var(--sm-dim);font-size:clamp(9px,1.6vw,11px);font-weight:500;}" +
360
- ".plan-storyboard-reserved-top{top:0;height:12.2%;border-bottom:1px solid var(--sm-border);}" +
361
- ".plan-storyboard-reserved-bottom{bottom:0;height:13.3%;border-top:1px solid var(--sm-border);}" +
362
- ".plan-storyboard-body{position:absolute;inset:12.2% 5% 13.3%;" +
363
- "background:linear-gradient(color-mix(in srgb,var(--plan-annotation) 13%,transparent) 1px,transparent 1px)," +
364
- "linear-gradient(90deg,color-mix(in srgb,var(--plan-annotation) 13%,transparent) 1px,transparent 1px);" +
365
- "background-size:calc(100%/12) calc(100%/6);background-color:color-mix(in srgb,#3565d4 3%,transparent);}" +
366
- ".plan-storyboard-block{position:absolute;display:flex;min-width:0;min-height:0;flex-direction:column;gap:clamp(4px,.72vw,7px);" +
367
- "overflow:hidden;border:1.5px dashed color-mix(in srgb,var(--plan-annotation) 55%,transparent);border-radius:6px;" +
368
- "background:color-mix(in srgb,var(--sm-surface) 94%,transparent);box-shadow:0 1px 4px rgba(15,23,42,.12);padding:clamp(6px,.95vw,10px);}" +
369
- ".plan-storyboard-block[data-emphasis=\"primary\"]{border-width:2px;border-color:color-mix(in srgb,var(--plan-annotation) 70%,transparent);}" +
370
- ".plan-storyboard-block[data-emphasis=\"tertiary\"]{opacity:.78;}" +
371
- ".plan-storyboard-block--movable{cursor:grab;touch-action:none;}" +
372
- ".plan-storyboard-block--movable:hover{border-color:color-mix(in srgb,var(--plan-annotation) 66%,transparent);}" +
373
- ".plan-storyboard-block--dragging{z-index:10;cursor:grabbing;border-style:solid;box-shadow:0 10px 26px rgba(0,0,0,.45);}" +
374
- ".plan-storyboard-block-remove{position:absolute;top:3px;right:3px;display:inline-flex;align-items:center;justify-content:center;" +
375
- "width:18px;height:18px;border:1px solid var(--sm-border);border-radius:999px;background:var(--sm-surface);box-shadow:0 2px 6px rgba(0,0,0,.35);" +
376
- "color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;pointer-events:none;transition:opacity 120ms ease;}" +
377
- ".plan-storyboard-block-remove svg{width:12px;height:12px;}" +
378
- ".plan-storyboard-block:hover .plan-storyboard-block-remove{opacity:1;pointer-events:auto;}" +
379
- ".plan-storyboard-block--dragging .plan-storyboard-block-remove{opacity:0;pointer-events:none;}" +
380
- ".plan-storyboard-block-resize{position:absolute;right:0;bottom:0;width:16px;height:16px;border:0;border-radius:0 0 5px 0;" +
381
- "background:linear-gradient(135deg,transparent 0 52%,var(--plan-annotation-line) 52% 60%,transparent 60% 72%,var(--plan-annotation-line) 72% 80%,transparent 80%);" +
382
- "cursor:nwse-resize;opacity:0;padding:0;touch-action:none;transition:opacity 120ms ease;}" +
383
- ".plan-storyboard-block:hover .plan-storyboard-block-resize,.plan-storyboard-block--dragging .plan-storyboard-block-resize{opacity:1;}" +
384
- ".plan-storyboard-reserved-top .plan-storyboard-add{position:absolute;top:50%;right:10px;transform:translateY(-50%);}" +
385
- ".plan-storyboard-add{display:inline-flex;align-items:center;gap:4px;border:1px dashed var(--plan-annotation-line);border-radius:5px;" +
386
- "background:var(--plan-annotation-wash);color:var(--plan-annotation);cursor:pointer;font-size:10px;font-weight:600;letter-spacing:.02em;padding:2px 7px;" +
387
- "font-family:var(--sm-font);transition:background-color 130ms ease,border-color 130ms ease;}" +
388
- ".plan-storyboard-add svg{width:11px;height:11px;}" +
389
- ".plan-storyboard-add:hover:not(:disabled){border-color:var(--plan-annotation);background:color-mix(in srgb,#3565d4 18%,transparent);}" +
390
- ".plan-storyboard-add:disabled{cursor:default;opacity:.45;}" +
391
- ".plan-storyboard-field{min-width:0;}" +
392
- ".plan-storyboard-field-control{display:block;width:100%;min-height:1.25em;overflow:hidden;border:0;background:transparent;padding:0;" +
393
- "color:var(--sm-fg);font:inherit;line-height:1.16;outline:none;box-shadow:none;white-space:normal;word-break:break-word;}" +
394
- ".plan-storyboard-field-control:focus{outline:none;box-shadow:none;background:transparent;}" +
395
- ".plan-storyboard-field[data-field=\"element\"]{display:inline-flex;width:fit-content;max-width:100%;flex-direction:column;" +
396
- "border:1px dashed var(--plan-annotation-line);border-radius:4px;background:var(--plan-annotation-wash);padding:3px 6px;}" +
397
- ".plan-storyboard-field[data-field=\"element\"] .plan-storyboard-field-control{color:var(--plan-annotation);" +
398
- "font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.07em;line-height:1.15;text-transform:uppercase;}" +
399
- ".plan-storyboard-field[data-field=\"content\"] .plan-storyboard-field-control{color:var(--sm-fg);font-weight:500;line-height:1.18;}" +
400
- ".plan-storyboard-detail{position:sticky;top:0;max-height:min(62vh,620px);overflow:auto;padding:30px 0 0;}" +
401
- ".plan-storyboard-detail-label{margin:0 0 5px;color:var(--sm-dim);font-size:10px;font-weight:700;letter-spacing:.04em;line-height:1.2;text-transform:uppercase;}" +
402
- ".plan-storyboard-frame--chrome{background:#fff;}" +
403
- ".plan-storyboard-chrome{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;pointer-events:none;opacity:.95;}" +
404
- ".plan-storyboard-zones{position:absolute;inset:0;pointer-events:none;}" +
405
- ".plan-storyboard-zone-wash{position:absolute;background:rgba(15,23,42,.16);}" +
406
- ".plan-storyboard-zone-notice-layer{position:absolute;inset:0;z-index:30;pointer-events:none;}" +
407
- ".plan-storyboard-zone-canvas{position:absolute;border:2px solid rgba(53,101,212,.8);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.55);}" +
408
- ".plan-storyboard-zone-label{position:absolute;top:-1px;left:-1px;border-radius:3px 0 4px 0;background:rgba(53,101,212,.85);padding:1px 7px;color:#fff;font-size:9px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;}" +
409
- ".plan-storyboard-zone-notice{position:absolute;top:10px;left:50%;transform:translateX(-50%);max-width:78%;border-radius:999px;background:rgba(15,23,42,.82);padding:5px 14px;color:#fff;font-size:11px;font-weight:600;line-height:1.35;text-align:center;}" +
410
- ".plan-storyboard-ph{position:absolute;border:1px solid rgba(15,23,42,.5);border-radius:3px;background:rgba(255,255,255,.12);box-shadow:0 0 0 1px rgba(255,255,255,.55);}" +
411
- ".plan-storyboard-ph-label{position:absolute;top:-1px;left:-1px;border-radius:2px 0 3px 0;background:rgba(15,23,42,.78);padding:0 5px;color:#fff;font-size:8px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;box-shadow:0 0 0 1px rgba(255,255,255,.5);}" +
412
- ".plan-storyboard-image-layer{position:absolute;inset:0;pointer-events:none;}" +
413
- ".plan-storyboard-image-slot{position:absolute;display:flex;flex-direction:column;pointer-events:auto;border:1px dashed rgba(53,101,212,.75);border-radius:4px;background:rgba(255,255,255,.55);overflow:hidden;}" +
414
- ".plan-storyboard-image-slot--filled{border-style:solid;background:rgba(255,255,255,.86);}" +
415
- ".plan-storyboard-image-caption{flex:none;padding:1px 5px;background:rgba(53,101,212,.85);color:#fff;font-size:8px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;}" +
416
- ".plan-storyboard-image-input{flex:1;width:100%;min-height:0;resize:none;border:0;background:transparent;padding:4px 5px;font:inherit;font-size:10px;line-height:1.3;color:#0f172a;}" +
417
- ".plan-storyboard-image-input:focus{outline:none;}" +
418
- ".plan-storyboard-image-input::placeholder{color:rgba(15,23,42,.45);font-style:italic;}" +
419
- ".plan-storyboard-layout-picker{position:relative;margin:0 0 18px;}" +
420
- ".plan-storyboard-layout-current{display:flex;align-items:center;gap:8px;width:100%;border:1px solid var(--sm-line);border-radius:8px;background:transparent;padding:6px 8px;color:var(--sm-fg);font-size:12.5px;font-weight:550;text-align:left;cursor:pointer;font-family:inherit;}" +
253
+ ".plan-beat:hover .plan-storyline-beat-remove{opacity:1;pointer-events:auto;}" +
254
+ ".plan-beat.is-dragging .plan-storyline-beat-remove,.plan-beat.is-editing .plan-storyline-beat-remove,.plan-storyline-beat-remove:disabled{opacity:0;pointer-events:none;}" +
255
+ /* the mini storyboard on a rail card */
256
+ ".sm-mini{position:absolute;inset:0;}" +
257
+ ".sm-mini-chrome{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}" +
258
+ ".sm-mini-block{position:absolute;border:1px dashed color-mix(in srgb,#3565d4 70%,transparent);border-radius:2px;background:rgba(53,101,212,.14);}" +
259
+ ".sm-mini-block[data-emphasis=\"primary\"]{border-style:solid;background:rgba(53,101,212,.22);}" +
260
+ ".sm-mini-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#8a8a90;font-size:10px;background:#f4f4f5;}" +
261
+ /* the stage: the selected storyboard, fitted to the column */
262
+ ".sm-stage{container-type:size;min-width:0;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px 22px;overflow:auto;background:#0a0a0b;}" +
263
+ /* The PANEL is the subject, not the slide. Letting the slide fill the
264
+ column made a wide grey field around a small box, which read as "look
265
+ at the slide" when the job is "correct the text" — so the slide is
266
+ capped to a reading size and the panel takes a large fraction OF it. */
267
+ ".sm-stage-fit{width:min(100%,980px,calc((100cqh - 96px) * 16 / 9));min-width:min(100%,360px);}" +
268
+ ".sm-stage-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--sm-dim);}" +
269
+ ".sm-stage-head .t{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}" +
270
+ ".sm-stage-head .t b{color:var(--sm-fg);text-transform:none;letter-spacing:0;font-weight:500;}" +
271
+ ".sm-round-banner{display:flex;align-items:center;gap:9px;margin:0 0 12px;padding:9px 13px;border:1px solid color-mix(in srgb,var(--sm-accent) 35%,transparent);border-radius:10px;" +
272
+ "background:color-mix(in srgb,var(--sm-accent) 9%,transparent);color:var(--sm-dim);font-size:12.5px;line-height:1.5;animation:sm-rise .45s cubic-bezier(.2,.7,.2,1) both;}" +
273
+ ".sm-round-banner svg{width:15px;height:15px;flex-shrink:0;color:var(--sm-accent);}.sm-round-banner b{color:var(--sm-fg);font-weight:600;}" +
274
+ ".plan-dossier-new-slide{display:flex;align-items:flex-start;gap:10px;border:1.5px dashed var(--plan-story-line);border-radius:10px;background:color-mix(in srgb,#b41830 6%,transparent);color:var(--sm-dim);font-size:13px;line-height:1.5;padding:22px 18px;}" +
275
+ ".plan-dossier-new-slide svg{width:14px;height:14px;flex:none;margin-top:2px;color:var(--plan-story);}" +
276
+ ".plan-dossier-new-slide p{margin:0;}.plan-dossier-new-slide strong{color:var(--sm-fg);font-weight:600;}" +
277
+ /* the slide on stage: the layout's own render with its slot geometry on
278
+ top, a light veil pushing it back, and ONE panel floating over both. The
279
+ panel is the only thing on the stage with elevation — that is what makes
280
+ it read as an overlay ON the slide rather than content IN it. */
281
+ ".plan-stage-frame{position:relative;width:100%;container-type:inline-size;}" +
282
+ /* The slide itself: the shared embossed card treatment, and the only
283
+ element here that clips — the panel above it has a menu that escapes. */
284
+ ".plan-stage-slide{position:relative;overflow:hidden;border-radius:12px;background:#fff;}" +
285
+ ".slide-emboss{box-shadow:0 1px 2px rgba(0,0,0,.5),0 10px 26px rgba(0,0,0,.42),inset 0 1px 0 rgba(255,255,255,.07);}" +
286
+ ".plan-stage-chrome{display:block;width:100%;aspect-ratio:16/9;object-fit:cover;border:0;background:#fff;}" +
287
+ ".plan-stage-chrome--blank{background:#16161a;border:1px solid var(--sm-border);}" +
288
+ ".plan-stage-slots{position:absolute;inset:0;pointer-events:none;}" +
289
+ ".plan-stage-slot{position:absolute;border:1px solid rgba(15,23,42,.45);border-radius:3px;background:rgba(255,255,255,.1);box-shadow:0 0 0 1px rgba(255,255,255,.45);}" +
290
+ ".plan-stage-slot-label{position:absolute;top:-1px;left:-1px;border-radius:2px 0 3px 0;background:rgba(15,23,42,.72);padding:0 5px;color:#fff;font-size:8px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;}" +
291
+ ".sm-mini-slots .plan-stage-slot{border-width:.5px;box-shadow:none;}" +
292
+ ".sm-mini-slots .plan-stage-slot-label{display:none;}" +
293
+ ".sm-mini-slots .plan-stage-zone{border-width:1px;}" +
294
+ ".plan-stage-zone{position:absolute;border:1.5px dashed rgba(53,101,212,.7);border-radius:4px;background:rgba(53,101,212,.07);}" +
295
+ ".plan-stage-veil{position:absolute;inset:0;background:rgba(9,11,16,.22);pointer-events:none;}" +
296
+ ".plan-stage-card{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;width:84%;max-height:88%;flex-direction:column;gap:clamp(10px,1.7cqw,18px);" +
297
+ /* Glass, not a lid: the slide stays readable through it, so the reviewer
298
+ can see the layout they are choosing copy for. The blur is what keeps
299
+ text legible over a busy render — opacity alone would not. */
300
+ "border:1px solid color-mix(in srgb,var(--sm-fg) 18%,transparent);border-radius:12px;background:color-mix(in srgb,var(--sm-surface) 76%,transparent);box-shadow:inset 0 1px 0 color-mix(in srgb,var(--sm-fg) 11%,transparent),0 2px 6px rgba(0,0,0,.3),0 26px 64px rgba(0,0,0,.46);padding:clamp(16px,2.4cqw,26px) clamp(16px,2.6cqw,28px) clamp(12px,1.8cqw,20px);backdrop-filter:blur(16px) saturate(115%);}" +
301
+ "@supports not (backdrop-filter:blur(1px)){.plan-stage-card{background:color-mix(in srgb,var(--sm-surface) 97%,transparent);}}" +
302
+ /* label gutter + content column: the label names which KIND of thing this
303
+ is, which is why it earns the space */
304
+ ".plan-stage-row{display:grid;min-height:0;margin-left:clamp(-9px,-.8cqw,-5px);grid-template-columns:calc(clamp(68px,10.5cqw,104px) - clamp(-9px,-.8cqw,-5px)) minmax(0,1fr);gap:4px clamp(10px,1.6cqw,18px);}" +
305
+ ".plan-stage-label{color:var(--sm-dim);font-size:clamp(10.5px,1.25cqw,12.5px);font-weight:500;line-height:1.5;padding-top:clamp(4px,.7cqw,7px);}" +
306
+ ".plan-stage-beat{color:var(--sm-fg);font-size:clamp(14px,2.25cqw,21px);font-weight:600;line-height:1.3;outline:none;white-space:pre-wrap;word-break:break-word;}" +
307
+ ".plan-stage-points{display:flex;min-height:0;flex-direction:column;gap:2px;overflow-y:auto;scrollbar-width:none;}" +
308
+ ".plan-stage-points::-webkit-scrollbar{display:none;}" +
309
+ ".plan-stage-bullets{display:flex;flex-direction:column;gap:clamp(2px,.5cqw,6px);margin:0;padding:0;list-style:none;}" +
310
+ ".plan-stage-bullet{display:flex;align-items:flex-start;gap:8px;color:var(--sm-fg);font-size:clamp(12px,1.55cqw,15px);line-height:1.5;}" +
311
+ ".plan-stage-bullet::before{flex:none;color:var(--plan-story);content:\"\\2022\";font-weight:700;line-height:inherit;}" +
312
+ ".plan-stage-bullet-text{min-width:0;flex:1;outline:none;white-space:pre-wrap;word-break:break-word;}" +
313
+ /* These two fields ARE the gate. At rest they read as copy; the moment a
314
+ pointer crosses them they read as inputs, and focus draws a real ring. */
315
+ ".plan-stage-beat[contenteditable=\"true\"],.plan-stage-bullet-text[contenteditable=\"true\"]{border-radius:6px;cursor:text;padding:clamp(3px,.5cqw,6px) clamp(5px,.8cqw,9px);transition:background 120ms ease,box-shadow 120ms ease;}" +
316
+ ".plan-stage-beat[contenteditable=\"true\"]:hover,.plan-stage-bullet-text[contenteditable=\"true\"]:hover{background:var(--sm-muted);}" +
317
+ ".plan-stage-beat:focus,.plan-stage-bullet-text:focus{background:color-mix(in srgb,var(--sm-fg) 8%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--sm-fg) 26%,transparent);}" +
318
+ ".plan-stage-bullet-remove{display:inline-flex;flex:none;align-items:center;justify-content:center;width:16px;height:16px;margin-top:2px;border:0;border-radius:999px;background:transparent;color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;transition:opacity 120ms ease;}" +
319
+ ".plan-stage-bullet-remove svg{width:11px;height:11px;}" +
320
+ ".plan-stage-bullet:hover .plan-stage-bullet-remove,.plan-stage-bullet:focus-within .plan-stage-bullet-remove{opacity:1;}" +
321
+ ".plan-stage-bullet-remove:hover{background:var(--sm-muted);color:var(--sm-fg);}" +
322
+ ".plan-stage-bullet-add{display:inline-flex;align-self:flex-start;align-items:center;gap:5px;margin-top:5px;border:0;border-radius:6px;background:transparent;color:var(--sm-dim);cursor:pointer;font-family:var(--sm-font);font-size:clamp(11.5px,1.35cqw,13.5px);font-weight:500;padding:clamp(3px,.5cqw,6px) clamp(5px,.8cqw,9px);}" +
323
+ ".plan-stage-bullet-add svg{width:11px;height:11px;}" +
324
+ ".plan-stage-bullet-add:hover{color:var(--sm-fg);}" +
325
+ /* the slide's approved image uses: read-only, one row per use — the file
326
+ the user handed over, shown, next to what it is FOR in plain words */
327
+ ".plan-stage-uses{display:flex;min-height:0;flex-direction:column;gap:clamp(5px,.8cqw,9px);overflow-y:auto;scrollbar-width:none;}" +
328
+ ".plan-stage-uses::-webkit-scrollbar{display:none;}" +
329
+ ".plan-stage-use{display:flex;align-items:flex-start;gap:clamp(7px,1.1cqw,11px);}" +
330
+ ".plan-stage-use-thumb{flex:none;width:clamp(38px,5.4cqw,54px);height:clamp(26px,3.7cqw,37px);border:1px solid var(--sm-border);border-radius:5px;background:var(--sm-muted);object-fit:contain;}" +
331
+ ".plan-stage-use-body{min-width:0;flex:1;display:flex;flex-direction:column;gap:1px;}" +
332
+ ".plan-stage-use-what{color:var(--sm-fg);font-size:clamp(11.5px,1.45cqw,14px);font-weight:600;line-height:1.4;}" +
333
+ ".plan-stage-use-name{color:var(--sm-dim);font-weight:500;}" +
334
+ ".plan-stage-use-instruction{color:var(--sm-fg);font-size:clamp(11px,1.35cqw,13.5px);line-height:1.45;word-break:break-word;}" +
335
+ ".plan-stage-use-framing{color:var(--sm-dim);font-size:clamp(10.5px,1.2cqw,12.5px);line-height:1.45;}" +
336
+ ".plan-stage-foot{display:grid;flex:none;align-items:center;margin-left:clamp(-9px,-.8cqw,-5px);grid-template-columns:calc(clamp(68px,10.5cqw,104px) - clamp(-9px,-.8cqw,-5px)) minmax(0,1fr);gap:clamp(10px,1.6cqw,18px);border-top:1px solid color-mix(in srgb,var(--sm-fg) 12%,transparent);padding-top:clamp(10px,1.5cqw,16px);}" +
337
+ ".plan-stage-foot .plan-stage-label{padding-top:0;}" +
338
+ ".plan-stage-card .plan-stage-layout{width:auto;max-width:100%;margin:0;}" +
339
+ /* A real button, not a caption: bordered, filled, and it lifts on hover. */
340
+ ".plan-stage-card .plan-stage-layout .plan-storyboard-layout-current{width:auto;gap:9px;border-color:color-mix(in srgb,var(--sm-fg) 26%,transparent);border-radius:8px;background:color-mix(in srgb,var(--sm-fg) 7%,transparent);padding:clamp(4px,.6cqw,7px) clamp(9px,1.1cqw,13px) clamp(4px,.6cqw,7px) clamp(4px,.6cqw,7px);font-size:clamp(11.5px,1.35cqw,13.5px);font-weight:600;}" +
341
+ ".plan-stage-card .plan-stage-layout .plan-storyboard-layout-current:not(:disabled):hover{border-color:color-mix(in srgb,var(--sm-fg) 42%,transparent);background:color-mix(in srgb,var(--sm-fg) 12%,transparent);}" +
342
+ ".plan-stage-card .plan-stage-layout .plan-storyboard-layout-thumb{width:clamp(34px,4.4cqw,46px);height:clamp(19px,2.5cqw,26px);}" +
343
+ /* a narrow panel has no room for a gutter: the label sits above its content */
344
+ "@container (max-width:620px){" +
345
+ ".plan-stage-card{width:90%;gap:12px;}" +
346
+ ".plan-stage-row,.plan-stage-foot{grid-template-columns:minmax(0,1fr);gap:2px;}" +
347
+ ".plan-stage-foot .plan-stage-layout{margin-top:4px;}" +
348
+ ".plan-stage-beat{font-size:15px;}" +
349
+ "}" +
350
+ ".plan-storyboard-warnings{margin:.75em 0 0;padding-left:1.2em;font-size:.86em;color:var(--sm-dim);}" +
351
+ ".plan-storyboard-error{margin:0;border:1px solid var(--sm-danger);border-radius:8px;background:color-mix(in srgb,var(--sm-danger) 10%,transparent);padding:12px;}" +
352
+ ".plan-storyboard-error p{margin:0 0 8px;font-weight:600;}.plan-storyboard-error pre{margin:0;overflow-x:auto;font-size:.82em;color:var(--sm-dim);}" +
353
+ ".plan-storyboard-layout-unknown{display:inline-block;border-radius:999px;background:rgba(217,75,75,.14);padding:1px 7px;color:#e07272;font-size:10px;font-weight:650;}" +
354
+ ".plan-storyboard-layout-purpose{color:var(--sm-dim);font-size:11px;line-height:1.35;margin:0 0 6px;}" +
355
+ ".plan-storyboard-draft-note{display:flex;align-items:center;justify-content:center;gap:7px;margin:10px 0 0;color:var(--sm-dim);font-size:11.5px;line-height:1.4;}" +
356
+ ".plan-storyboard-draft-note svg{width:14px;height:14px;flex:none;}" +
357
+ ".plan-storyboard-draft-note p{margin:0;}.plan-storyboard-draft-note strong{color:var(--sm-fg);font-weight:600;}" +
358
+ /* the layout picker: the deck default in the header, the slide pick on the card */
359
+ ".plan-storyboard-layout-picker{position:relative;margin:0;}" +
360
+ ".plan-storyboard-layout-current{display:flex;align-items:center;gap:8px;width:100%;border:1px solid var(--sm-border);border-radius:8px;background:transparent;padding:5px 8px;color:var(--sm-fg);font-size:11.5px;font-weight:500;text-align:left;cursor:pointer;font-family:inherit;}" +
421
361
  ".plan-storyboard-layout-current:disabled{cursor:default;opacity:.85;}" +
422
362
  ".plan-storyboard-layout-current:not(:disabled):hover{background:rgba(255,255,255,.05);}" +
423
- ".plan-storyboard-layout-thumb{width:52px;height:30px;flex:none;border:1px solid var(--sm-line);border-radius:4px;object-fit:cover;background:#fff;}" +
363
+ ".plan-storyboard-layout-thumb{width:36px;height:20px;flex:none;border:1px solid var(--sm-border);border-radius:3px;object-fit:cover;background:#fff;}" +
424
364
  ".plan-storyboard-layout-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}" +
425
- ".plan-storyboard-layout-chevron{flex:none;margin-left:auto;color:var(--sm-dim);font-size:11px;line-height:1;}" +
426
- ".plan-storyboard-layout-unknown{flex:none;border-radius:999px;background:rgba(217,75,75,.14);padding:1px 7px;color:#e07272;font-size:10px;font-weight:650;}" +
427
- ".plan-storyboard-layout-menu{position:absolute;z-index:30;top:calc(100% + 4px);left:0;right:0;max-height:300px;overflow:auto;border:1px solid var(--sm-line);border-radius:10px;background:var(--sm-bg);box-shadow:0 10px 32px rgba(0,0,0,.35);padding:4px;}" +
365
+ /* the same drawn chevron as the comment target: a "▾" glyph rendered as a dot */
366
+ ".plan-storyboard-layout-chevron{display:flex;flex:none;margin-left:auto;color:var(--sm-dim);transition:transform .15s ease;}.plan-storyboard-layout-chevron svg{width:10px;height:6px;}" +
367
+ "[aria-expanded=\"true\"]>.plan-storyboard-layout-chevron{transform:rotate(180deg);}" +
368
+ ".plan-storyboard-layout-menu{position:absolute;z-index:30;top:calc(100% + 4px);left:0;min-width:280px;max-height:320px;overflow:auto;border:1px solid var(--sm-border);border-radius:10px;background:var(--sm-surface);box-shadow:0 10px 32px rgba(0,0,0,.45);padding:4px;}" +
428
369
  ".plan-storyboard-layout-option{display:flex;align-items:center;gap:8px;width:100%;border:0;border-radius:7px;background:transparent;padding:6px;color:var(--sm-fg);text-align:left;cursor:pointer;font-family:inherit;}" +
429
- ".plan-storyboard-layout-option:hover{background:rgba(255,255,255,.06);}" +
430
- ".plan-storyboard-layout-option--selected{background:rgba(255,255,255,.09);}" +
431
- ".plan-storyboard-layout-thumb--option{width:72px;height:41px;}" +
370
+ ".plan-storyboard-layout-option:hover{background:rgba(255,255,255,.06);}.plan-storyboard-layout-option--selected{background:rgba(255,255,255,.09);}" +
371
+ ".plan-storyboard-layout-thumb--option{width:64px;height:36px;}" +
432
372
  ".plan-storyboard-layout-option-text{display:flex;min-width:0;flex-direction:column;gap:1px;}" +
433
- ".plan-storyboard-layout-option-text .plan-storyboard-layout-name{font-size:12.5px;font-weight:600;}" +
434
- ".plan-storyboard-layout-purpose{overflow:hidden;color:var(--sm-dim);font-size:11px;line-height:1.3;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;}" +
373
+ ".plan-storyboard-layout-option-text .plan-storyboard-layout-name{font-size:12px;font-weight:600;}" +
435
374
  ".plan-storyboard-layout-canvas{color:var(--sm-dim);font-size:10px;font-variant-numeric:tabular-nums;}" +
436
- ".plan-storyboard-key-message{margin:0 0 18px;color:var(--sm-fg);font-size:15px;font-weight:650;line-height:1.34;white-space:pre-wrap;word-break:break-word;outline:none;}" +
437
- ".plan-storyboard-detail-text{min-height:9em;border:0;background:transparent;color:var(--sm-fg);font-size:13.5px;font-weight:400;line-height:1.42;outline:none;white-space:pre-wrap;word-break:break-word;}" +
438
- ".plan-storyboard-warnings{margin:.75em 0 0;padding-left:1.2em;font-size:.86em;color:var(--sm-dim);}" +
439
- ".plan-storyboard-error{margin:1em 0;border:1px solid var(--sm-danger);border-radius:8px;background:color-mix(in srgb,var(--sm-danger) 10%,transparent);padding:12px;}" +
440
- ".plan-storyboard-error p{margin:0 0 8px;font-weight:600;}" +
441
- ".plan-storyboard-error pre{margin:0;overflow-x:auto;font-size:.82em;color:var(--sm-dim);}" +
442
- "@media (max-width:900px){.plan-storyline-beat{max-width:none;}.plan-storyboard-layout{grid-template-columns:1fr;}" +
443
- ".plan-storyboard-detail{position:static;max-height:none;padding-top:8px;}}" +
444
- /* chat composer bar */
445
- ".sm-composer{flex-shrink:0;display:flex;flex-direction:column;gap:8px;padding:11px 16px;" +
446
- "border-top:1px solid var(--sm-border);background:var(--sm-surface);font-family:var(--sm-font);}" +
447
- ".sm-story-banner{display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:12px;line-height:1.5;" +
448
- "border:1px solid color-mix(in srgb,#b41830 30%,transparent);border-radius:9px;background:color-mix(in srgb,#b41830 8%,transparent);}" +
375
+ /* the design picker: name, then the design's accent colours */
376
+ ".plan-design-picker{position:relative;margin:0;}" +
377
+ ".plan-design-current,.plan-design-option{display:flex;align-items:center;gap:8px;width:100%;border:1px solid var(--sm-border);border-radius:8px;background:transparent;padding:6px 8px;color:var(--sm-fg);font:500 11.5px var(--sm-font);text-align:left;cursor:pointer;}" +
378
+ ".plan-design-current:disabled{cursor:default;opacity:.85;}" +
379
+ ".plan-design-current:not(:disabled):hover{background:rgba(255,255,255,.05);}" +
380
+ ".plan-design-current .plan-storyboard-layout-chevron{margin-left:0;}" +
381
+ ".plan-design-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}" +
382
+ ".plan-design-dots{display:inline-flex;flex:none;gap:3px;margin-left:auto;}" +
383
+ ".plan-design-dot{display:block;width:10px;height:10px;border-radius:50%;box-shadow:inset 0 0 0 1px rgba(255,255,255,.16);}" +
384
+ /* a design the backend serves that this machine has not downloaded yet */
385
+ ".plan-design-get{flex:none;margin-left:auto;color:var(--sm-dim);font-size:10.5px;font-weight:500;}" +
386
+ ".plan-design-current.is-busy{cursor:progress;}.plan-design-current.is-busy .plan-design-name{color:var(--sm-dim);}" +
387
+ ".plan-design-menu{position:absolute;z-index:30;top:calc(100% + 4px);left:0;min-width:100%;max-height:320px;overflow:auto;border:1px solid var(--sm-border);border-radius:10px;background:var(--sm-surface);box-shadow:0 10px 32px rgba(0,0,0,.45);padding:4px;}" +
388
+ ".plan-design-option{border:0;border-radius:7px;padding:8px;font-size:12px;}" +
389
+ ".plan-design-option:hover,.plan-design-option:focus-visible{background:rgba(255,255,255,.06);outline:none;}.plan-design-option.is-selected{background:rgba(255,255,255,.09);}" +
390
+ /* designs not on this computer yet: last, smaller and quieter, under one divider */
391
+ ".plan-design-option.is-remote{padding:4px 8px;font-size:11px;color:var(--sm-dim);}" +
392
+ ".plan-design-option.is-first-remote{position:relative;margin-top:9px;}" +
393
+ ".plan-design-option.is-first-remote::before{content:'';position:absolute;left:8px;right:8px;top:-5px;border-top:1px solid var(--sm-border);}" +
394
+ /* small screens: the rail becomes a horizontal strip above the stage */
395
+ "@media (max-width:960px){" +
396
+ ".sm-plan{display:block;overflow-y:auto;}" +
397
+ ".sm-rail{border-right:0;border-bottom:1px solid var(--sm-border);}" +
398
+ ".sm-rail-list{display:flex;align-items:flex-start;overflow-x:auto;overflow-y:hidden;padding:4px 12px 10px;}" +
399
+ ".sm-rail-gap{width:12px;height:auto;align-self:stretch;margin:0;flex:none;}" +
400
+ ".sm-rail-gap[data-active=\"true\"]::before{left:50%;right:auto;top:8%;bottom:8%;height:auto;width:3px;transform:translateX(-50%);}" +
401
+ ".sm-thumb{width:150px;flex:none;margin:0;}" +
402
+ ".sm-rail-add{width:auto;flex:none;align-self:center;margin:0 0 0 6px;white-space:nowrap;}" +
403
+ ".sm-stage{container-type:normal;min-height:0;overflow:visible;justify-content:flex-start;padding:14px;}" +
404
+ ".sm-stage-fit{width:100%;}" +
405
+ "}" +
406
+ /* the composer: the unsent comments, then ONE row — what the comment is
407
+ about, the comment itself, Send (only once there is something to send)
408
+ and Approve */
409
+ ".sm-composer{flex-shrink:0;display:flex;flex-direction:column;gap:8px;padding:12px 16px;border-top:1px solid var(--sm-border);background:var(--sm-surface);font-family:var(--sm-font);}" +
410
+ ".sm-composer[hidden]{display:none;}" +
411
+ ".sm-story-banner{display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:12px;line-height:1.5;border:1px solid color-mix(in srgb,#b41830 30%,transparent);border-radius:9px;background:color-mix(in srgb,#b41830 8%,transparent);}" +
449
412
  ".sm-story-banner svg{width:14px;height:14px;flex-shrink:0;color:var(--plan-story);}" +
450
- ".sm-story-banner .txt{flex:1;min-width:0;color:var(--sm-dim);}" +
451
- ".sm-story-banner .txt b{color:var(--sm-fg);font-weight:600;}" +
452
- ".sm-story-discard{flex-shrink:0;border:0;background:transparent;color:var(--sm-dim);font-family:var(--sm-font);font-weight:500;" +
453
- "font-size:12px;padding:4px 8px;border-radius:6px;cursor:pointer;transition:background .12s,color .12s;}" +
413
+ ".sm-story-banner .txt{flex:1;min-width:0;color:var(--sm-dim);}.sm-story-banner .txt b{color:var(--sm-fg);font-weight:600;}" +
414
+ ".sm-story-discard{flex-shrink:0;border:0;background:transparent;color:var(--sm-dim);font-family:var(--sm-font);font-weight:500;font-size:12px;padding:4px 8px;border-radius:6px;cursor:pointer;transition:background .12s,color .12s;}" +
454
415
  ".sm-story-discard:hover{background:var(--sm-muted);color:var(--sm-fg);}" +
455
- /* comment ledger rail (supersedes the old single "previous round" note) */
456
- ".sm-ledger{display:flex;flex-direction:column;gap:3px;max-height:118px;overflow-y:auto;" +
457
- "padding:7px 10px;border:1px dashed #3a3a40;border-radius:9px;background:var(--sm-muted);}" +
458
- ".sm-ledger-row{display:flex;align-items:baseline;gap:8px;font-size:12px;line-height:1.45;color:#b9b9be;}" +
459
- ".sm-ledger-anchor{flex:none;font-weight:600;color:#a1a1aa;white-space:nowrap;}" +
460
- ".sm-ledger-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}" +
461
- ".sm-ledger-status{flex:none;border-radius:999px;padding:1px 8px;font-size:10px;font-weight:600;" +
462
- "letter-spacing:.04em;text-transform:uppercase;border:1px solid var(--sm-border);color:var(--sm-dim);}" +
463
- ".sm-ledger-status[data-state=\"sent\"]{color:var(--sm-accent);border-color:rgba(169,194,255,.4);}" +
464
- ".sm-ledger-status[data-state=\"addressed\"]{color:var(--sm-success);border-color:rgba(46,204,113,.4);}" +
465
- /* scope control */
466
- ".sm-composer-meta{display:flex;align-items:center;gap:9px;font-size:11.5px;color:var(--sm-dim);}" +
467
- ".sm-scope-label{font-weight:600;letter-spacing:.04em;text-transform:uppercase;font-size:9.5px;}" +
468
- ".sm-scope{display:inline-flex;gap:2px;padding:2px;border:1px solid var(--sm-border);border-radius:999px;background:#0c0c0e;}" +
469
- ".sm-scope-btn{border:0;border-radius:999px;padding:3px 11px;background:transparent;color:var(--sm-dim);" +
470
- "font-family:var(--sm-font);font-size:11.5px;font-weight:500;cursor:pointer;max-width:220px;overflow:hidden;" +
471
- "text-overflow:ellipsis;white-space:nowrap;transition:background .12s,color .12s;}" +
472
- ".sm-scope-btn:hover{color:var(--sm-fg);}" +
473
- ".sm-scope-btn.is-on{background:var(--sm-accent);color:#0f1220;font-weight:600;}" +
474
- ".sm-composer-row{display:flex;align-items:flex-end;gap:10px;}" +
475
- ".sm-chat-ta{flex:1;min-height:40px;max-height:150px;resize:none;background:#0c0c0e;color:var(--sm-fg);" +
476
- "border:1px solid var(--sm-border);border-radius:14px;padding:10px 15px;font:13px/1.5 var(--sm-font);outline:none;transition:border-color .15s;}" +
477
- ".sm-chat-ta:focus{border-color:var(--sm-accent);}.sm-chat-ta::placeholder{color:#6b6b72;}" +
478
- ".sm-send{width:38px;height:38px;border-radius:50%;border:0;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;" +
479
- "background:var(--sm-accent);color:#0f1220;transition:transform .14s ease,box-shadow .14s ease,opacity .14s ease;}" +
480
- ".sm-send svg{width:16px;height:16px;margin:1px 1px 0 0;}" +
481
- ".sm-send:hover{transform:translateY(-1px) scale(1.05);box-shadow:0 8px 22px rgba(169,194,255,.35);}" +
482
- ".sm-send:disabled,.sm-chat-ta:disabled{opacity:.45;cursor:not-allowed;transform:none;box-shadow:none;}" +
483
- ".sm-send.is-off{opacity:.35;cursor:not-allowed;pointer-events:none;}" +
484
- /* story-iteration pill (send while a storyline draft is staged) */
485
- ".sm-send-pill{display:inline-flex;align-items:center;gap:7px;height:38px;flex-shrink:0;border:0;border-radius:999px;" +
486
- "padding:0 16px;background:var(--sm-accent);color:#0f1220;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;white-space:nowrap;" +
487
- "transition:transform .14s ease,box-shadow .14s ease,opacity .14s ease;}" +
488
- ".sm-send-pill svg{width:15px;height:15px;}" +
416
+ ".sm-drafts{display:flex;flex-direction:column;gap:3px;max-height:128px;overflow-y:auto;margin:0;padding:0;list-style:none;}" +
417
+ ".sm-draft{display:flex;align-items:center;gap:10px;padding:4px 4px 4px 11px;border-radius:8px;background:var(--sm-muted);font-size:12.5px;line-height:1.45;}" +
418
+ ".sm-draft-anchor{flex:none;color:var(--sm-dim);font-size:11.5px;font-weight:600;white-space:nowrap;}" +
419
+ ".sm-draft-text{flex:1;min-width:0;overflow:hidden;color:var(--sm-fg);text-overflow:ellipsis;white-space:nowrap;}" +
420
+ ".sm-draft-del{flex:none;display:flex;align-items:center;justify-content:center;width:22px;height:22px;border:0;border-radius:50%;background:transparent;color:var(--sm-dim);cursor:pointer;}" +
421
+ ".sm-draft-del svg{width:12px;height:12px;}.sm-draft-del:hover{background:rgba(255,255,255,.08);color:var(--sm-fg);}" +
422
+ ".sm-composer-row{display:flex;align-items:flex-end;gap:8px;}" +
423
+ ".sm-chat-box{flex:1;min-width:0;display:flex;align-items:flex-end;gap:4px;padding:4px 4px 4px 5px;border:1px solid var(--sm-border);border-radius:14px;background:#0c0c0e;transition:border-color .15s;}" +
424
+ ".sm-chat-box:focus-within{border-color:var(--sm-accent);}" +
425
+ /* what the comment is about: a small menu of its own — a native select
426
+ opens the system's list, white on this dark page */
427
+ ".sm-target-wrap{position:relative;flex:none;}" +
428
+ ".sm-target{display:inline-flex;align-items:center;gap:7px;height:30px;max-width:170px;margin:0;border:0;border-radius:10px;padding:0 10px;background:var(--sm-muted);color:var(--sm-fg);font:600 11.5px var(--sm-font);cursor:pointer;transition:background .12s;}" +
429
+ ".sm-target:hover,.sm-target[aria-expanded=\"true\"]{background:rgba(255,255,255,.09);}.sm-target:focus-visible{outline:2px solid var(--sm-accent);outline-offset:1px;}" +
430
+ ".sm-target-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}" +
431
+ ".sm-target-chevron{display:flex;flex:none;color:var(--sm-dim);transition:transform .15s ease;}.sm-target-chevron svg{width:10px;height:6px;}" +
432
+ ".sm-target[aria-expanded=\"true\"] .sm-target-chevron{transform:rotate(180deg);}" +
433
+ ".sm-target-menu{position:absolute;z-index:40;left:-5px;bottom:calc(100% + 10px);width:max-content;min-width:220px;max-width:min(340px,calc(100vw - 32px));padding:4px;" +
434
+ "border:1px solid var(--sm-border);border-radius:12px;background:var(--sm-surface-2);box-shadow:0 -12px 36px rgba(0,0,0,.5);animation:sm-pop-in .14s ease;}" +
435
+ ".sm-target-option{display:flex;align-items:center;gap:10px;width:100%;border:0;border-radius:8px;background:transparent;padding:7px 9px;color:var(--sm-fg);font-family:var(--sm-font);text-align:left;cursor:pointer;}" +
436
+ ".sm-target-option:hover,.sm-target-option:focus-visible{background:rgba(255,255,255,.06);outline:none;}.sm-target-option.is-selected{background:rgba(169,194,255,.1);}" +
437
+ ".sm-target-text{display:flex;flex:1;min-width:0;flex-direction:column;gap:1px;}" +
438
+ ".sm-target-option-label{font-size:12px;font-weight:600;}" +
439
+ ".sm-target-option-detail{overflow:hidden;color:var(--sm-dim);font-size:11px;text-overflow:ellipsis;white-space:nowrap;}" +
440
+ ".sm-target-check{display:flex;flex:none;width:14px;color:var(--sm-accent);}.sm-target-check svg{width:14px;height:14px;}" +
441
+ ".sm-chat-ta{flex:1;min-width:0;min-height:30px;max-height:150px;resize:none;border:0;background:transparent;padding:5px 8px;color:var(--sm-fg);font:13px/1.5 var(--sm-font);outline:none;}" +
442
+ ".sm-chat-ta::placeholder{color:#6b6b72;}" +
443
+ ".sm-send-pill,.sm-approve{display:inline-flex;align-items:center;gap:7px;height:40px;flex-shrink:0;border:0;border-radius:999px;padding:0 17px;font:600 12.5px var(--sm-font);cursor:pointer;white-space:nowrap;transition:transform .14s ease,box-shadow .14s ease,background .14s ease,opacity .14s ease;}" +
444
+ ".sm-send-pill svg,.sm-approve svg{width:15px;height:15px;}" +
445
+ ".sm-send-pill{background:var(--sm-accent);color:#0f1220;}" +
489
446
  ".sm-send-pill:hover{transform:translateY(-1px);box-shadow:0 8px 22px rgba(169,194,255,.35);}" +
490
- ".sm-send-pill.is-off{opacity:.35;cursor:not-allowed;pointer-events:none;}" +
491
- ".sm-composer-sep{width:1px;align-self:stretch;background:var(--sm-border);}" +
492
- ".sm-approve{display:inline-flex;flex-direction:column;align-items:center;gap:1px;border:0;border-radius:12px;padding:7px 16px;" +
493
- "background:#f4f4f5;color:#0f0f0f;cursor:pointer;font-family:var(--sm-font);transition:transform .14s ease,background .14s ease,box-shadow .14s ease;white-space:nowrap;}" +
447
+ ".sm-approve{background:#f4f4f5;color:#0f0f0f;}.sm-approve svg{color:var(--sm-success);}" +
494
448
  ".sm-approve:hover{background:#fff;transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.45);}" +
495
- ".sm-approve .main{display:flex;align-items:center;gap:7px;font-weight:600;font-size:12.5px;}" +
496
- ".sm-approve .main svg{width:15px;height:15px;color:var(--sm-success);}" +
497
- ".sm-approve .sub{font-size:9.5px;font-weight:500;color:#61616a;}" +
498
- ".sm-approve.is-armed{background:#f6c454;color:#241a00;}" +
499
- ".sm-approve.is-armed .main svg{color:#241a00;}" +
500
- ".sm-approve.is-off{opacity:.45;cursor:not-allowed;pointer-events:none;}" +
501
- /* waiting state */
502
- "@keyframes sm-rotate{to{transform:rotate(360deg)}}" +
503
- ".sm-wait{display:flex;align-items:center;gap:12px;padding:7px 2px;font-size:12.5px;color:var(--sm-dim);line-height:1.5;}" +
504
- ".sm-wait b{color:var(--sm-fg);font-weight:600;}" +
505
- ".sm-spin{width:17px;height:17px;border-radius:50%;border:2px solid var(--sm-border);border-top-color:var(--sm-accent);animation:sm-rotate .9s linear infinite;flex-shrink:0;}" +
506
- /* done veil */
507
- ".sm-veil{position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:30px;" +
508
- "font-family:var(--sm-font);color:var(--sm-fg);background:radial-gradient(ellipse at center,rgba(16,16,18,.93),rgba(5,5,7,.98));" +
509
- "-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);animation:sm-veil-in .4s ease;}" +
510
- ".sm-veil .sm-logo{width:188px;height:106px;border-radius:14px;animation:sm-rise .6s cubic-bezier(.2,.7,.2,1) both;}" +
511
- ".sm-veil .word{display:flex;flex-direction:column;gap:6px;font-family:var(--sm-word);font-weight:300;font-size:36px;letter-spacing:.6em;text-align:center;animation:sm-rise .6s .06s cubic-bezier(.2,.7,.2,1) both;}" +
512
- ".sm-veil .word b{font-weight:300;}" +
513
- ".sm-veil .word .top{animation:sm-drift-a 18s ease-in-out infinite;}" +
514
- ".sm-veil .word .bot{margin-left:.3em;animation:sm-drift-b 18s ease-in-out infinite;}" +
515
- ".sm-veil .status{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:600;animation:sm-rise .6s .12s cubic-bezier(.2,.7,.2,1) both;}" +
516
- ".sm-veil .status svg{width:24px;height:24px;color:var(--sm-success);}" +
517
- ".sm-veil .sub{color:var(--sm-dim);font-size:13.5px;animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both;}" +
518
- ".sm-veil .sub.sm-pulse{animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both,sm-pulse 2s .9s ease-in-out infinite;}";
449
+ /* off: still there, so it is clear Approve comes back once the comments are sent */
450
+ ".sm-send-pill.is-off,.sm-approve.is-off{opacity:.35;cursor:not-allowed;transform:none;box-shadow:none;}";
519
451
 
520
452
  // ---------- helpers ----------
521
- function el(tag, cls, html) {
522
- var e = document.createElement(tag);
523
- if (cls) e.className = cls;
524
- if (html != null) e.innerHTML = html;
525
- return e;
526
- }
527
- function wordmarkHtml() {
528
- return '<b class="top">slide</b><b class="bot">mind</b>';
529
- }
530
- // Genuine path: the add-in's per-brand orb PNG (CFG.logoMark). Fallback: the
531
- // inline embossed slide glyph — no CLI-side orb re-creation.
532
- function logoNode(cls, idSuffix) {
533
- var box = el("div", cls);
534
- if (CFG.logoMark) {
535
- var img = el("img", "sm-logo-img");
536
- img.src = CFG.logoMark;
537
- img.alt = "slidemind";
538
- box.appendChild(img);
539
- return box;
540
- }
541
- box.appendChild(el("div", "sm-logo-svg", idSuffix ? logoWithIds(idSuffix) : LOGO_SVG));
542
- return box;
543
- }
544
453
  function clamp(v, min, max) {
545
454
  return Math.min(Math.max(v, min), max);
546
455
  }
@@ -564,16 +473,48 @@
564
473
  /* headless / detached — ignore */
565
474
  }
566
475
  }
476
+ function pctOf(v) {
477
+ return (v * 100).toFixed(2) + "%";
478
+ }
479
+ function copyOf(o) {
480
+ var d = {};
481
+ for (var k in o) d[k] = o[k];
482
+ return d;
483
+ }
484
+ function layoutOptions() {
485
+ var d = shownDesign();
486
+ return d && !d.current ? d.layouts || [] : boundLayouts;
487
+ }
488
+ function designOptions() {
489
+ return Array.isArray(CFG.designs) ? CFG.designs : [];
490
+ }
491
+ // The design the page shows: the picked one, else the deck's own (null when
492
+ // the deck is bound to none this machine has cached).
493
+ function shownDesign() {
494
+ var all = designOptions();
495
+ for (var i = 0; i < all.length; i++) {
496
+ if (S.design ? all[i].presFP === S.design : all[i].current) return all[i];
497
+ }
498
+ return null;
499
+ }
500
+ function findLayout(componentName) {
501
+ return layoutOptions().find(function (o) {
502
+ return o.componentName === componentName;
503
+ }) || null;
504
+ }
567
505
 
568
506
  // ---------- session (shared with the preview gate via gate-session.js) ------
569
507
  function saveLedger() {
570
508
  SES.saveLedger(ledger);
571
509
  }
572
- // The small round context the NEXT round restores from: the selected tab and
573
- // the unsent draft (the server nonce is added by the shared session). The
574
- // plan body is NOT in here — see BODY_KEY.
510
+ // The small round context the NEXT round restores from: the selected slide,
511
+ // the unsent draft and the shell's hand-off state (the server nonce is added
512
+ // by the shared session). The plan body is NOT in here — see BODY_KEY.
575
513
  function ctxFields() {
576
- return { round: roundNo, selectedIndex: S.selectedIndex, draft: feedbackText, working: working };
514
+ var f = { round: roundNo, selectedIndex: S.selectedIndex, draft: feedbackText, working: working };
515
+ var shell = SHELL.ctxFields();
516
+ for (var k in shell) f[k] = shell[k];
517
+ return f;
577
518
  }
578
519
  function saveCtx() {
579
520
  SES.saveCtx(ctxFields());
@@ -583,12 +524,53 @@
583
524
  SES.saveCtxSoon(ctxFields());
584
525
  }
585
526
 
527
+ // Card edits that are not sent yet, keyed by the plan version they were made
528
+ // on (CFG.subjectHash, else a hash of the served body): a reload of the same
529
+ // version brings them back; edits on an older version are offered for copying
530
+ // instead of being applied to text they were not written against. Written on
531
+ // each committed edit (blur / drop), never per keystroke.
532
+ var EDITS_KEY = "slidemind-plan-edits:" + DECK_KEY;
533
+ var editsRestored = false; // this load brought unsent edits back
534
+ var staleEdits = null; // edits made on an older version: { value }
535
+ function textHash(t) {
536
+ var h = 0x811c9dc5;
537
+ for (var i = 0; i < t.length; i++) {
538
+ h ^= t.charCodeAt(i);
539
+ h = Math.imul(h, 0x01000193) >>> 0;
540
+ }
541
+ return ("0000000" + h.toString(16)).slice(-8) + ":" + t.length;
542
+ }
543
+ function editsBase() {
544
+ return CFG.subjectHash || "body:" + textHash(baseline);
545
+ }
546
+ function saveEdits() {
547
+ if (S.value !== baseline || S.storyDraft || S.design) SES.write(EDITS_KEY, { base: editsBase(), value: S.value, storyDraft: S.storyDraft || null, design: S.design || null });
548
+ else SES.write(EDITS_KEY, null);
549
+ }
550
+ function restoreEdits() {
551
+ var saved = SES.read(EDITS_KEY, null);
552
+ if (!saved || typeof saved !== "object" || typeof saved.value !== "string") return;
553
+ if (saved.base === editsBase()) {
554
+ S.value = saved.value;
555
+ S.storyDraft = saved.storyDraft || null;
556
+ // a picked design survives a reload only while it is still on offer
557
+ S.design = designOptions().some(function (d) {
558
+ return d.presFP === saved.design && !d.current;
559
+ })
560
+ ? saved.design
561
+ : null;
562
+ editsRestored = S.value !== baseline || !!S.storyDraft || !!S.design;
563
+ } else {
564
+ staleEdits = { value: saved.value };
565
+ SES.write(EDITS_KEY, null);
566
+ }
567
+ }
568
+
586
569
  // Per-slide segment diff between the previous round's plan body and this one,
587
570
  // keyed by the slide NUMBER the `### Slide N` heading declares — never by the
588
571
  // heading's position. Real decks number non-contiguously (2,3,4,7,8,9), so a
589
- // positional key badged the wrong tab. Each chunk also STOPS at the next `## `
590
- // section, so trailing prose (`## Design language`, `## Quellen`) can't be
591
- // swept into the last slide and badge it for an unrelated edit.
572
+ // positional key badged the wrong card. Each chunk also STOPS at the next
573
+ // `## ` section, so trailing prose can't be swept into the last slide.
592
574
  function slideChunks(md) {
593
575
  var out = {};
594
576
  String(md || "")
@@ -596,8 +578,6 @@
596
578
  .forEach(function (part) {
597
579
  var m = /^###\s+Slide\s+(\d+)/.exec(part);
598
580
  if (!m) return;
599
- // Cut at the next level-2 heading (the chunk's own `###` line is at
600
- // index 0, so only a later `## ` can match).
601
581
  var stop = part.search(/\n##\s+/);
602
582
  var body = stop > 0 ? part.slice(0, stop) : part;
603
583
  out[Number(m[1])] = body.trim();
@@ -618,117 +598,112 @@
618
598
  // Runs before the first render so the surface is built in the right mode.
619
599
  function restoreSession() {
620
600
  var prevBody = SES.read(BODY_KEY, null);
621
- var st = SES.restore(CFG.ledgerSeed);
601
+ // A changed nonce while the agent still revises this page's comments is not
602
+ // the next round (SHELL.holdsRound) — nothing it was handed is answered yet.
603
+ var st = SES.restore(CFG.ledgerSeed, { hold: SHELL.holdsRound });
622
604
  ledger = st.ledger;
623
605
  roundNo = st.roundNo;
624
606
  working = st.working;
625
- if (st.newRound) {
607
+ if (st.newRound && !st.unchanged) {
626
608
  // Cosmetics only: WHICH slides moved is a content question, so it stays a
627
609
  // body diff — the round transition itself came from the server nonce.
628
610
  changedSlides = diffSlides(prevBody, baseline);
629
- bannerText = "Updated · Round " + roundNo;
611
+ bannerText = "Updated by your agent";
630
612
  }
631
- // Reviewer context survives the round (invariant 6): selection + draft.
613
+ // Same bytes, asked again: never "updated", never "worked in".
614
+ askedAgainUnchanged = !!(st.unchanged && st.hadSent);
615
+ // Reviewer context survives the round: selection + draft + hand-off state.
632
616
  if (st.ctx) {
633
617
  if (typeof st.ctx.selectedIndex === "number") S.selectedIndex = st.ctx.selectedIndex;
634
618
  if (typeof st.ctx.draft === "string") feedbackText = st.ctx.draft;
635
619
  }
620
+ arrivedAfterRevising = SHELL.restoreCtx(st.ctx, working, st.newRound);
621
+ if (!working) restoreEdits();
636
622
  saveLedger();
637
623
  saveCtx();
638
624
  // Once per page load — the served body never changes while this page lives.
639
- SES.write(BODY_KEY, baseline);
625
+ // A held load keeps the body the reviewer commented on: the "updated"
626
+ // badges of the real next round compare against THAT.
627
+ if (!st.held) SES.write(BODY_KEY, baseline);
640
628
  }
641
629
 
642
630
  // ---------- chrome (top bar) ----------
643
631
  function buildChrome() {
644
- bar = el("div", "sm-o sm-bar");
645
- bar.setAttribute("data-sm-overlay", "1");
646
-
647
- var brand = el("div", "sm-brand");
648
- brand.appendChild(logoNode("sm-logo"));
649
- brand.appendChild(el("div", "sm-word", wordmarkHtml()));
650
- bar.appendChild(brand);
651
- bar.appendChild(el("div", "sm-sep"));
652
-
653
- var meta = el("div", "sm-meta");
654
- var title = el("div", "t");
655
- title.appendChild(document.createTextNode("Plan review"));
632
+ var bar = SHELL.buildBar("Plan review", "");
656
633
  editedBadge = el("span", "sm-edited", "edited");
657
634
  editedBadge.style.display = "none";
658
- title.appendChild(editedBadge);
659
- meta.appendChild(title);
660
- hintEl = el("div", "h");
661
- meta.appendChild(hintEl);
662
- bar.appendChild(meta);
663
- bar.appendChild(el("div", "sm-grow"));
664
-
665
- document.body.appendChild(bar);
635
+ bar.titleEl.appendChild(editedBadge);
636
+ hintEl = bar.hintEl;
637
+ // the deck-wide choices sit next to the title, each named: the design and the body slides' layout
638
+ barTools = el("div", "sm-bar-tools");
639
+ bar.bar.insertBefore(barTools, bar.grow);
666
640
  }
667
641
  function refreshChrome() {
668
- if (editedBadge) editedBadge.style.display = planWasEdited() ? "" : "none";
642
+ if (editedBadge) editedBadge.style.display = planWasEdited() || S.design ? "" : "none";
669
643
  if (hintEl) {
670
644
  hintEl.textContent = working
671
- ? "The agent is revising · read on and browse the strip — commenting and editing resume with the next round"
672
- : "Shape the red thread and storyboards below · drag to reorder, rename moves, edit blocks · decide when it reads right";
645
+ ? CFG.embedded
646
+ ? "Read on and browse the slides · you can comment again when your agent asks"
647
+ : "The agent is revising · read on and browse the slides — commenting and editing resume with the next round"
648
+ : SHELL.notAskedYet()
649
+ ? "Your agent is still working on this plan · it asks for your review when it is ready"
650
+ : SHELL.alreadyApproved()
651
+ ? "Approved · comment only if something must still change"
652
+ : "Pick a slide on the left · drag to reorder, click its title to rename · edit its story, points and layout on the card";
673
653
  }
674
654
 
675
- var hasFeedback = commentText().length > 0;
676
- var canSend = (!working && !!S.storyDraft) || hasFeedback;
677
- if (sendBtn) sendBtn.classList.toggle("is-off", !canSend);
678
-
679
- if (approveBtn) {
680
- var main = approveBtn.querySelector(".main span");
681
- var sub = approveBtn.querySelector(".sub");
682
- if (working) {
683
- // Approve applies to exactly the displayed round; while the agent is
684
- // revising, that round is already superseded (server refuses it too).
685
- approveArmed = false;
686
- approveBtn.classList.remove("is-armed");
687
- approveBtn.classList.add("is-off");
688
- if (main) main.textContent = "Approve plan";
689
- if (sub) {
690
- sub.style.display = "";
691
- sub.textContent = "The agent is revising — approve the next round";
692
- }
693
- } else if (S.storyDraft) {
694
- // Approve is blocked until the staged storyline is sent or discarded.
695
- approveArmed = false;
696
- approveBtn.classList.remove("is-armed");
697
- approveBtn.classList.add("is-off");
698
- if (main) main.textContent = "Approve plan";
699
- if (sub) {
700
- sub.style.display = "";
701
- sub.textContent = "Send or discard your storyline changes first";
702
- }
703
- } else {
704
- approveBtn.classList.remove("is-off");
705
- if (approveArmed && hasFeedback) {
706
- approveBtn.classList.add("is-armed");
707
- if (main) main.textContent = "Feedback not sent — approve anyway?";
708
- if (sub) sub.style.display = "none";
709
- } else {
710
- approveArmed = false;
711
- approveBtn.classList.remove("is-armed");
712
- if (main) main.textContent = "Approve plan";
713
- if (sub) {
714
- sub.style.display = "";
715
- sub.textContent = "the agent builds the slides next";
716
- }
717
- }
655
+ // the in-flight latch: one answer at a time, a double click cannot send twice
656
+ var sending = SHELL.decisionPending() ? SHELL.state.pending : null;
657
+ var total = draftComments().length + (commentText() ? 1 : 0);
658
+ // Send appears once there is something to send — a comment typed or
659
+ // listed, or a storyline change — and stays while that answer is on its way.
660
+ if (sendBtn) {
661
+ var canSend = !working && (!!S.storyDraft || total > 0);
662
+ sendBtn.style.display = canSend || (sending && sending.decision === "comments") ? "" : "none";
663
+ sendBtn.classList.toggle("is-off", !!sending);
664
+ var n = sendBtn.querySelector(".n");
665
+ if (n) {
666
+ n.textContent =
667
+ sending && sending.decision === "comments"
668
+ ? "Sending…"
669
+ : S.storyDraft
670
+ ? "Send changes"
671
+ : "Send " + total + (total === 1 ? " comment" : " comments");
718
672
  }
719
673
  }
720
- }
721
674
 
722
- // ---------- workspace: one scrollable document surface + composer ----------
675
+ if (!approveBtn) return;
676
+ // Why Approve is off — said on hover, never as one more line on the page.
677
+ // A comment and an approval never go out together: anything typed or
678
+ // listed turns Approve off until it is sent or removed.
679
+ var why = null;
680
+ if (working) why = CFG.embedded ? "Your answer is saved. You can approve the next version when your agent asks." : "The agent is revising — approve the next round";
681
+ else if (sending) why = "Sending your answer…";
682
+ else if (S.storyDraft) why = "Send or discard your storyline changes first";
683
+ else if (SHELL.notAskedYet()) why = "Your agent asks for your approval when the plan is ready";
684
+ else if (SHELL.alreadyApproved()) why = "This plan is approved";
685
+ else if (total > 0) why = "Send or remove your " + (total === 1 ? "comment" : "comments") + " first";
686
+ approveBtn.classList.toggle("is-off", !!why);
687
+ approveBtn.setAttribute("aria-disabled", why ? "true" : "false");
688
+ approveBtn.title = why || approveTip();
689
+ var label = approveBtn.querySelector(".n");
690
+ if (label) label.textContent = sending && sending.decision === "approve" ? "Approving…" : SHELL.alreadyApproved() ? "Plan approved" : "Approve plan";
691
+ }
692
+
693
+ // What Approve does, on hover. In the workspace the click also copies the
694
+ // message the reviewer pastes to their agent — the dialog after the click is
695
+ // where that is said; the button itself stays two words (doc 21).
696
+ function approveTip() {
697
+ if (!CFG.embedded) return "Approve the plan — the agent builds the slides next";
698
+ return "Approve the plan. You then paste a message into your agent chat.";
699
+ }
700
+
701
+ // ---------- workspace: surface + composer ----------
723
702
  function buildWorkspace() {
724
703
  wsEl = el("div", "sm-o sm-ws");
725
704
  wsEl.setAttribute("data-sm-overlay", "1");
726
-
727
705
  surfaceEl = el("div", "sm-ws-surface");
728
- // Delegated inline-edit commit for Mode A rendered markdown blocks. Attached
729
- // to the persistent surface so a Mode-B re-render (which replaces the
730
- // surface's children) keeps these live. commitRenderedEdit is a no-op for
731
- // anything that isn't a stamped, editable, non-storyboard block.
706
+ // Delegated inline-edit commit for Mode A rendered markdown blocks.
732
707
  surfaceEl.addEventListener(
733
708
  "blur",
734
709
  function (e) {
@@ -742,7 +717,7 @@
742
717
  if (e.key !== "Enter" || e.shiftKey) return;
743
718
  var t = e.target;
744
719
  if (!(t instanceof HTMLElement)) return;
745
- if (t.closest(".plan-storyboard")) return;
720
+ if (t.closest(".sm-plan")) return;
746
721
  if (!t.closest("[data-md-line][data-md-kind]")) return;
747
722
  e.preventDefault();
748
723
  commitRenderedEdit(t);
@@ -751,12 +726,10 @@
751
726
  true,
752
727
  );
753
728
  wsEl.appendChild(surfaceEl);
754
- document.body.appendChild(wsEl);
755
-
756
729
  composerEl = el("div", "sm-composer");
757
730
  composerEl.setAttribute("data-sm-overlay", "1");
758
731
  wsEl.appendChild(composerEl);
759
-
732
+ document.body.appendChild(wsEl);
760
733
  renderSurface();
761
734
  renderComposer();
762
735
  }
@@ -767,7 +740,7 @@
767
740
  if (locked()) return;
768
741
  if (!(target instanceof HTMLElement)) return;
769
742
  var editable = target.closest("[data-md-line][data-md-kind]");
770
- if (!editable || editable.closest(".plan-storyboard")) return;
743
+ if (!editable || editable.closest(".sm-plan")) return;
771
744
  var next = PLAN.replaceRenderedMarkdownBlock({
772
745
  markdown: S.value,
773
746
  line: Number(editable.dataset.mdLine),
@@ -776,21 +749,21 @@
776
749
  });
777
750
  if (next !== S.value) {
778
751
  S.value = next;
779
- approveArmed = false; // any plan edit disarms a pending approve-confirm
752
+ saveEdits(); // an unsent edit survives a reload of this version
780
753
  refreshChrome();
781
754
  }
782
755
  }
783
756
 
784
757
  // ---------- surface render ----------
785
- // A plan with storyboard fences renders as the dossier; otherwise the plain
786
- // markdown renders inline-editable. Mode B re-renders in full on every
787
- // committed model change (nothing is focused at commit — edits commit on
788
- // blur / pointerup — so no caret is lost); Mode A never re-renders here.
758
+ // A plan with storyboard fences renders as rail + stage + aside; otherwise the
759
+ // plain markdown renders inline-editable. The dossier re-renders in full on
760
+ // every committed model change (edits commit on blur / pointerup, so nothing
761
+ // is focused at commit time).
789
762
  function renderSurface() {
790
763
  if (!surfaceEl) return;
764
+ surfaceLocked = locked();
791
765
  surfaceEl.innerHTML = "";
792
766
  focusAfterRender = null;
793
- if (bannerText) surfaceEl.appendChild(roundBanner());
794
767
  var segments = PLAN.splitPlanMarkdownIntoSegments(S.value);
795
768
  var hasStoryboards = segments.some(function (s) {
796
769
  return s.type === "storyboard";
@@ -803,20 +776,15 @@
803
776
  }
804
777
  }
805
778
 
806
- // Transient "Updated · Round N" banner. It states the round arrived and how
807
- // many slides changed; the per-tab badges say WHICH. Self-dismisses so it
808
- // can't be mistaken for a permanent state.
779
+ // Transient "Updated · Round N" banner. Self-dismisses so it can't be
780
+ // mistaken for a permanent state.
809
781
  function roundBanner() {
810
782
  var count = Object.keys(changedSlides).length;
811
783
  var node = el("div", "sm-round-banner");
812
784
  node.innerHTML =
813
785
  ICONS.sparkles +
814
- "<span><b>" +
815
- bannerText +
816
- "</b> " +
817
- (count
818
- ? count + (count === 1 ? " slide changed" : " slides changed") + " — marked “updated” on the strip."
819
- : "The plan was revised.") +
786
+ "<span><b>" + bannerText + "</b> " +
787
+ (count ? count + (count === 1 ? " slide changed" : " slides changed") + " — marked “updated” on the left." : "The plan was revised.") +
820
788
  "</span>";
821
789
  setTimeout(function () {
822
790
  bannerText = null;
@@ -826,7 +794,9 @@
826
794
  }
827
795
 
828
796
  function renderModeA(segments) {
797
+ var doc = el("div", "sm-doc");
829
798
  var root = el("div", "plan-md-root");
799
+ if (bannerText) root.appendChild(roundBanner());
830
800
  segments.forEach(function (seg) {
831
801
  if (seg.type !== "markdown") return;
832
802
  var html = PLAN.renderPlanMarkdown(seg.text, { lineOffset: seg.lineOffset });
@@ -835,20 +805,21 @@
835
805
  holder.innerHTML = html;
836
806
  while (holder.firstChild) root.appendChild(holder.firstChild);
837
807
  });
838
- surfaceEl.appendChild(root);
808
+ doc.appendChild(root);
809
+ surfaceEl.appendChild(doc);
839
810
  }
840
811
 
841
- // The story beats on the strip: the `## Red thread` moves — but only when
842
- // they match the boards one-to-one, so a drifted red thread (more moves than
843
- // slides) never renders phantom slide tabs — otherwise each board's key
844
- // message (read-only tabs then). Each beat carries the slide number its board
845
- // declares, which is what a "This slide" comment is anchored to (so the agent
846
- // reads the same number the reviewer saw).
812
+ // The story beats: the backbone (a body slide's `## Red thread` move, a frame
813
+ // slide's key message), else the moves — but only when they match the boards
814
+ // one-to-one, so a drifted red thread never renders phantom slides —
815
+ // otherwise each board's key message. Each beat carries the slide number its
816
+ // board declares, which is what a "This slide" comment is anchored to.
847
817
  function planBeats() {
848
818
  var boards = PLAN.splitPlanMarkdownIntoSegments(S.value).filter(function (s) {
849
819
  return s.type === "storyboard";
850
820
  });
851
- var messages = PLAN.extractRedThreadMessages(S.value);
821
+ var backbone = PLAN.parsePlanBackbone(S.value);
822
+ var messages = backbone ? backbone.messages : PLAN.extractRedThreadMessages(S.value);
852
823
  if (messages.length !== boards.length) {
853
824
  messages = boards.map(function (seg, i) {
854
825
  return (seg.data && seg.data.keyMessage) || "Slide " + ((seg.data && seg.data.slide) || i + 1);
@@ -860,7 +831,7 @@
860
831
  });
861
832
  }
862
833
 
863
- // ---------- dossier (storyboard mode) ----------
834
+ // ---------- dossier: rail | stage | aside ----------
864
835
  function renderDossier(segments) {
865
836
  var storyboardSegments = segments.filter(function (s) {
866
837
  return s.type === "storyboard";
@@ -875,77 +846,37 @@
875
846
  });
876
847
  var committed = PLAN.backboneFromMessages(beatMessages);
877
848
  var slides = S.storyDraft || committed;
878
- var backboneEditable = PLAN.parsePlanBackbone(S.value) !== null;
879
- var canEdit = !locked() && backboneEditable;
849
+ var canEdit = !locked() && PLAN.parsePlanBackbone(S.value) !== null;
880
850
 
881
851
  if (S.selectedIndex >= slides.length) S.selectedIndex = slides.length - 1;
882
852
  if (S.selectedIndex < 0) S.selectedIndex = 0;
883
853
  var activeIndex = S.selectedIndex;
884
854
 
885
- // A staged draft resolves a tab to the board of the slide it originally was;
886
- // new slides (originalIndex null) have no board yet.
887
- var activeSegment;
888
- if (S.storyDraft) {
889
- var oi = slides[activeIndex] ? slides[activeIndex].originalIndex : undefined;
890
- activeSegment = oi === null || oi === undefined ? undefined : storyboardSegments[oi];
891
- } else {
892
- activeSegment = storyboardSegments[activeIndex];
893
- }
894
-
895
- var dossier = el("div", "plan-dossier");
896
- var root = el("div", "plan-md-root plan-md-root--storyboards");
897
- dossier.appendChild(root);
898
-
899
- root.appendChild(
900
- renderStrip(slides, committed, beatMessages, storyboardData, storyboardSegments, canEdit, activeIndex),
901
- );
902
-
903
- var body = el("div", "plan-dossier-body");
904
- if (activeSegment) {
905
- if (activeSegment.data) {
906
- body.appendChild(renderCanvas(activeSegment));
907
- } else {
908
- var err = el("div", "plan-storyboard-error");
909
- err.appendChild(el("p", null, "Storyboard block could not be rendered."));
910
- var pre = el("pre");
911
- var codeNode = document.createElement("code");
912
- codeNode.textContent = activeSegment.raw || "";
913
- pre.appendChild(codeNode);
914
- err.appendChild(pre);
915
- body.appendChild(err);
916
- }
917
- } else {
918
- // A staged new slide (or a beat with no board yet): the agent builds its
919
- // storyboard once the story round is sent.
920
- var placeholder = el("div", "plan-dossier-new-slide");
921
- placeholder.innerHTML =
922
- ICONS.ruler +
923
- "<p><strong>Storyline edited.</strong> Sending asks the agent to build storyboards and content for your new and renamed slides — add comments below if you like.</p>";
924
- body.appendChild(placeholder);
855
+ // A staged draft resolves a card to the board of the slide it originally
856
+ // was; new slides (originalIndex null) have no board yet.
857
+ function segmentFor(index) {
858
+ if (!S.storyDraft) return storyboardSegments[index];
859
+ var oi = slides[index] ? slides[index].originalIndex : undefined;
860
+ return oi === null || oi === undefined ? undefined : storyboardSegments[oi];
925
861
  }
862
+ var activeSegment = segmentFor(activeIndex);
926
863
 
927
- var note = el("div", "plan-storyboard-draft-note");
928
- note.innerHTML =
929
- ICONS.ruler +
930
- "<p><strong>Draft storyboard.</strong> A rough sketch of what goes where — it guides the design agent, and the finished slides will follow your corporate design.</p>";
931
- body.appendChild(note);
932
-
933
- root.appendChild(body);
934
- surfaceEl.appendChild(dossier);
864
+ var plan = el("div", "sm-plan");
865
+ plan.appendChild(renderRail(slides, beatMessages, storyboardData, segmentFor, canEdit, activeIndex));
866
+ plan.appendChild(renderStage(activeSegment, activeIndex, slides));
867
+ surfaceEl.appendChild(plan);
935
868
  }
936
869
 
937
- // ---------- storyline tab strip ----------
938
- function renderStrip(slides, committed, committedMessages, storyboardData, storyboardSegments, canEdit, activeIndex) {
939
- var section = el("section", "plan-storyline");
940
- var head = el("div", "plan-storyline-head");
941
- var headText = el("div", "plan-storyline-head-text");
942
- headText.appendChild(el("span", "plan-storyline-label", "Red thread"));
943
- head.appendChild(headText);
944
- head.appendChild(renderDeckLayoutKnob(storyboardData));
945
- section.appendChild(head);
870
+ // ---------- the rail: one card per slide, in story order ----------
871
+ function renderRail(slides, committedMessages, storyboardData, segmentFor, canEdit, activeIndex) {
872
+ var rail = el("aside", "sm-rail");
873
+ var head = el("div", "sm-rail-head", "<span>Storyline</span>");
874
+ head.appendChild(el("span", "n", "")).textContent = slides.length + (slides.length === 1 ? " slide" : " slides");
875
+ rail.appendChild(head);
876
+ renderBarTools(storyboardData);
946
877
 
947
- var track = el("ol", "plan-storyline-track");
948
- section.appendChild(track);
878
+ var list = el("ol", "sm-rail-list");
879
+ rail.appendChild(list);
949
880
 
950
881
  var cardEls = [];
951
882
  var gapEls = [];
@@ -958,20 +889,31 @@
958
889
  else gapEls[i].removeAttribute("data-active");
959
890
  }
960
891
  }
892
+ // Gap index nearest the pointer: each card contributes its leading edge
893
+ // (the gap before it) and trailing edge (the gap after it), on whichever
894
+ // axis the rail flows.
961
895
  function nearestGap(cx, cy) {
962
896
  var best = 0;
963
897
  var bd = Infinity;
898
+ var horizontal = list.scrollWidth > list.clientWidth + 1 || getComputedStyle(list).display === "flex";
964
899
  for (var i = 0; i < slides.length; i++) {
965
900
  var card = cardEls[i];
966
901
  if (!card) continue;
967
902
  var r = card.getBoundingClientRect();
968
- var dy = cy - (r.top + r.height / 2);
969
- var before = (cx - r.left) * (cx - r.left) + dy * dy;
903
+ var before, after;
904
+ if (horizontal) {
905
+ var dy = cy - (r.top + r.height / 2);
906
+ before = (cx - r.left) * (cx - r.left) + dy * dy;
907
+ after = (cx - r.right) * (cx - r.right) + dy * dy;
908
+ } else {
909
+ var dx = cx - (r.left + r.width / 2);
910
+ before = (cy - r.top) * (cy - r.top) + dx * dx;
911
+ after = (cy - r.bottom) * (cy - r.bottom) + dx * dx;
912
+ }
970
913
  if (before < bd) {
971
914
  bd = before;
972
915
  best = i;
973
916
  }
974
- var after = (cx - r.right) * (cx - r.right) + dy * dy;
975
917
  if (after < bd) {
976
918
  bd = after;
977
919
  best = i + 1;
@@ -980,32 +922,34 @@
980
922
  return best;
981
923
  }
982
924
 
983
- // Sequence-only edits (reorder / remove) cascade to the storyboards now;
984
- // renames / inserts stage a draft the agent has to build.
925
+ // Only a RENAME stages a draft: a renamed move says the slide's job in the
926
+ // story changed, which is the agent's to re-answer. Reorders, removals and
927
+ // INSERTS apply now. An insert used to stage too, which left the new slide
928
+ // with no record at all — the stage had nothing to render, so its beat,
929
+ // points and layout could not be touched until a whole agent round had run.
985
930
  function beatCommit(next) {
986
- var sequenceOnly = next.every(function (s) {
987
- return s.originalIndex !== null && committedMessages[s.originalIndex] === s.message;
931
+ var renamed = next.some(function (s) {
932
+ return s.originalIndex !== null && committedMessages[s.originalIndex] !== s.message;
988
933
  });
989
- if (sequenceOnly) {
934
+ if (renamed) {
935
+ S.storyDraft = next;
936
+ } else {
990
937
  S.storyDraft = null;
991
938
  var applied = PLAN.applyPlanBackbone(S.value, next);
992
939
  if (applied) S.value = applied;
993
- } else {
994
- S.storyDraft = next;
995
940
  }
996
- approveArmed = false; // a storyline edit disarms a pending approve-confirm
941
+ saveEdits();
997
942
  refreshChrome();
998
943
  renderComposer();
999
944
  renderSurface();
1000
945
  }
1001
946
  function renameSlide(index, message) {
1002
947
  var cur = slides[index];
948
+ editingKey = null;
1003
949
  if (!cur || cur.message === message) {
1004
- editingKey = null;
1005
950
  renderSurface();
1006
951
  return;
1007
952
  }
1008
- editingKey = null;
1009
953
  beatCommit(
1010
954
  slides.map(function (s, i) {
1011
955
  return i === index ? { key: s.key, originalIndex: s.originalIndex, message: message } : s;
@@ -1015,11 +959,7 @@
1015
959
  function insertSlideAt(position) {
1016
960
  newSlideCounter += 1;
1017
961
  var next = slides.slice();
1018
- next.splice(position, 0, {
1019
- key: "new-" + newSlideCounter,
1020
- originalIndex: null,
1021
- message: "New slide",
1022
- });
962
+ next.splice(position, 0, { key: "new-" + newSlideCounter, originalIndex: null, message: "New slide" });
1023
963
  S.selectedIndex = position;
1024
964
  beatCommit(next);
1025
965
  }
@@ -1044,7 +984,7 @@
1044
984
  }
1045
985
 
1046
986
  function gapAt(position) {
1047
- var li = el("li", "plan-storyline-gap");
987
+ var li = el("li", "sm-rail-gap");
1048
988
  li.setAttribute("aria-hidden", canEdit ? "false" : "true");
1049
989
  if (canEdit) {
1050
990
  var btn = el("button", "plan-storyline-insert", ICONS.plus);
@@ -1061,35 +1001,34 @@
1061
1001
  }
1062
1002
 
1063
1003
  slides.forEach(function (item, index) {
1064
- track.appendChild(gapAt(index));
1065
-
1004
+ list.appendChild(gapAt(index));
1005
+ var segment = segmentFor(index);
1006
+ var data = segment && segment.data;
1066
1007
  var li = el(
1067
1008
  "li",
1068
- "plan-storyline-beat" +
1069
- (index === activeIndex ? " plan-storyline-beat--active" : "") +
1070
- (canEdit ? " plan-storyline-beat--draggable" : "") +
1071
- (editingKey === item.key ? " plan-storyline-beat--editing" : ""),
1009
+ "sm-thumb plan-beat" + (index === activeIndex ? " is-active" : "") + (editingKey === item.key ? " is-editing" : ""),
1072
1010
  );
1073
1011
  cardEls[index] = li;
1012
+ li.setAttribute("aria-current", index === activeIndex ? "true" : "false");
1074
1013
 
1075
1014
  var numeral = S.storyDraft ? index + 1 : (storyboardData[index] && storyboardData[index].slide) || index + 1;
1076
- var slideNo = el("div", "plan-storyline-beat-slide");
1077
- slideNo.setAttribute("aria-hidden", "true");
1078
- slideNo.textContent = numeral;
1079
- li.appendChild(slideNo);
1080
-
1081
- // "updated" badge: this tab's slide changed in the round that just landed.
1082
- // Keyed by the SLIDE NUMBER shown on the tab (`numeral`, straight off the
1083
- // board data) — the same source the diff keys by, so non-contiguous decks
1084
- // (2,3,4,7,8,9) badge the tab that actually moved. A staged storyline
1085
- // renumbers positionally, so badges are suppressed while one is open.
1015
+ // "updated" badge: this card's slide changed in the round that just
1016
+ // landed. Keyed by the SLIDE NUMBER (`numeral`, straight off the board
1017
+ // data) — the same source the diff keys by. A staged storyline renumbers
1018
+ // positionally, so badges are suppressed while one is open.
1086
1019
  if (!S.storyDraft && changedSlides[numeral]) {
1087
1020
  var upd = el("span", "plan-storyline-beat-updated", "updated");
1088
1021
  upd.title = "This slide changed in the latest round";
1089
1022
  li.appendChild(upd);
1090
1023
  }
1091
1024
 
1092
- var textEl = el("div", "plan-storyline-beat-text");
1025
+ li.appendChild(renderMini(data));
1026
+
1027
+ var meta = el("div", "sm-thumb-meta");
1028
+ var num = el("span", "sm-thumb-number");
1029
+ num.textContent = String(numeral).padStart(2, "0");
1030
+ meta.appendChild(num);
1031
+ var textEl = el("div", "sm-thumb-name");
1093
1032
  textEl.setAttribute("role", "textbox");
1094
1033
  textEl.textContent = item.message;
1095
1034
  var isEditing = canEdit && editingKey === item.key;
@@ -1121,28 +1060,15 @@
1121
1060
  }
1122
1061
  });
1123
1062
  }
1124
- li.appendChild(textEl);
1125
- // Layout chip: the board's layout, picked in place. A tab resolves to the
1126
- // board of the slide it originally was; staged new slides have no board.
1127
- var chipIndex = S.storyDraft ? item.originalIndex : index;
1128
- var chipSegment = chipIndex === null || chipIndex === undefined ? undefined : storyboardSegments[chipIndex];
1129
- if (chipSegment && chipSegment.data) {
1130
- var chip = renderLayoutPicker(chipSegment.data.layout, {
1131
- className: "plan-storyboard-layout-picker--beat",
1132
- ariaLabel: "Choose the layout of slide " + (index + 1),
1133
- onPick: function (componentName) {
1134
- var d = {};
1135
- for (var k in chipSegment.data) d[k] = chipSegment.data[k];
1136
- d.layout = componentName;
1137
- d.layoutSelection = "explicit";
1138
- d.layoutKey = (CFG.layouts || []).find(function(o) { return o.componentName === componentName; })?.layoutKey;
1139
- delete d.layoutReason;
1140
- commitStoryboard(chipSegment, d);
1141
- },
1142
- });
1143
- chip.setAttribute("data-no-drag", "");
1144
- li.appendChild(chip);
1145
- }
1063
+ meta.appendChild(textEl);
1064
+ li.appendChild(meta);
1065
+
1066
+ var sub = el("div", "sm-thumb-sub");
1067
+ var chosen = data && findLayout(data.layout);
1068
+ sub.textContent = data
1069
+ ? (chosen && chosen.nativeLayoutName) || data.layout || "No layout chosen"
1070
+ : "Storyboard follows once sent";
1071
+ li.appendChild(sub);
1146
1072
 
1147
1073
  if (canEdit) {
1148
1074
  var rm = el("button", "plan-storyline-beat-remove", ICONS.x);
@@ -1161,19 +1087,12 @@
1161
1087
  }
1162
1088
 
1163
1089
  // Pointer: drag ≥ threshold reorders; a click selects; a click on the
1164
- // active tab (when editable) enters rename mode.
1090
+ // active card's title (when editable) enters rename mode.
1165
1091
  li.addEventListener("pointerdown", function (e) {
1166
1092
  if (e.button !== 0) return;
1167
1093
  if (editingKey === item.key) return;
1168
1094
  if (e.target.closest && e.target.closest("[data-no-drag]")) return;
1169
- stripDrag = {
1170
- index: index,
1171
- key: item.key,
1172
- pointerId: e.pointerId,
1173
- ox: e.clientX,
1174
- oy: e.clientY,
1175
- moved: false,
1176
- };
1095
+ stripDrag = { index: index, key: item.key, pointerId: e.pointerId, ox: e.clientX, oy: e.clientY, moved: false, onTitle: e.target === textEl };
1177
1096
  try {
1178
1097
  li.setPointerCapture(e.pointerId);
1179
1098
  } catch {
@@ -1187,7 +1106,7 @@
1187
1106
  var dy = e.clientY - stripDrag.oy;
1188
1107
  if (!stripDrag.moved && Math.hypot(dx, dy) < BEAT_DRAG_THRESHOLD) return;
1189
1108
  stripDrag.moved = true;
1190
- li.classList.add("plan-storyline-beat--dragging");
1109
+ li.classList.add("is-dragging");
1191
1110
  li.style.transform = "translate(" + dx + "px," + dy + "px)";
1192
1111
  dropGap = nearestGap(e.clientX, e.clientY);
1193
1112
  paintGaps();
@@ -1203,10 +1122,11 @@
1203
1122
  var gap = dropGap;
1204
1123
  var idx = stripDrag.index;
1205
1124
  var key = stripDrag.key;
1125
+ var onTitle = stripDrag.onTitle;
1206
1126
  stripDrag = null;
1207
1127
  dropGap = null;
1208
1128
  li.style.transform = "";
1209
- li.classList.remove("plan-storyline-beat--dragging");
1129
+ li.classList.remove("is-dragging");
1210
1130
  paintGaps();
1211
1131
  if (moved) {
1212
1132
  if (gap !== null) reorderTo(idx, gap);
@@ -1217,7 +1137,7 @@
1217
1137
  saveCtx();
1218
1138
  renderSurface();
1219
1139
  renderComposer(); // the scope control names the newly selected slide
1220
- } else if (canEdit) {
1140
+ } else if (canEdit && onTitle) {
1221
1141
  editingKey = key;
1222
1142
  renderSurface();
1223
1143
  }
@@ -1228,389 +1148,381 @@
1228
1148
  stripDrag = null;
1229
1149
  dropGap = null;
1230
1150
  li.style.transform = "";
1231
- li.classList.remove("plan-storyline-beat--dragging");
1151
+ li.classList.remove("is-dragging");
1232
1152
  paintGaps();
1233
1153
  });
1234
-
1235
- track.appendChild(li);
1236
- });
1237
- track.appendChild(gapAt(slides.length));
1238
-
1239
- return section;
1240
- }
1241
-
1242
- // ---------- storyboard canvas ----------
1243
- function fittedFontSize(value, variant) {
1244
- var len = value.trim().length;
1245
- if (variant === "element") return Math.max(8.5, 12.5 - Math.max(0, len - 14) * 0.22);
1246
- return Math.max(8, 13 - Math.max(0, len - 12) * 0.3);
1247
- }
1248
- function compactSingleLine(text, variant) {
1249
- var lim = variant === "element" ? PLAN.MAX_DESIGN_ELEMENT_LENGTH : PLAN.MAX_CONTENT_LENGTH;
1250
- return text
1251
- .replace(/\u00a0/g, " ")
1252
- .replace(/\s+/g, " ")
1253
- .trim()
1254
- .slice(0, lim);
1255
- }
1256
- function notesToText(notes) {
1257
- return notes && notes.length ? notes.join("\n") : "";
1258
- }
1259
- function textToNotes(text) {
1260
- var notes = text
1261
- .replace(/\u00a0/g, " ")
1262
- .split(/\r?\n/)
1263
- .map(function (l) {
1264
- return l.trim();
1265
- })
1266
- .filter(Boolean);
1267
- return notes.length ? notes : undefined;
1268
- }
1269
- function updateBlock(data, blockId, patch) {
1270
- var blocks = data.blocks.map(function (b) {
1271
- if (b.id !== blockId) return b;
1272
- var next = {};
1273
- for (var k in b) next[k] = b[k];
1274
- for (var p in patch) next[p] = patch[p];
1275
- return next;
1154
+ list.appendChild(li);
1276
1155
  });
1277
- return withBlocks(data, blocks);
1278
- }
1279
- function withBlocks(data, blocks) {
1280
- var next = {};
1281
- for (var k in data) next[k] = data[k];
1282
- next.blocks = blocks;
1283
- return next;
1156
+ list.appendChild(gapAt(slides.length));
1157
+ if (canEdit) {
1158
+ var add = el("li");
1159
+ var addBtn = el("button", "sm-rail-add", ICONS.plus + "<span>Add slide</span>");
1160
+ addBtn.type = "button";
1161
+ addBtn.onclick = function () {
1162
+ insertSlideAt(slides.length);
1163
+ };
1164
+ add.appendChild(addBtn);
1165
+ list.appendChild(add);
1166
+ }
1167
+ // Keep the selected card in view (the preview rail does the same).
1168
+ setTimeout(function () {
1169
+ var active = cardEls[activeIndex];
1170
+ if (active && active.scrollIntoView) active.scrollIntoView({ block: "nearest", inline: "nearest" });
1171
+ }, 0);
1172
+ return rail;
1284
1173
  }
1285
1174
 
1286
- // Commit a storyboard mutation into S.value and re-render the surface. Safe to
1287
- // full-re-render because every canvas edit commits on blur / pointerup, so no
1288
- // field is focused at commit time.
1289
- function commitPlan(nextValue) {
1290
- if (locked()) return;
1291
- S.value = nextValue;
1292
- approveArmed = false; // a storyboard edit disarms a pending approve-confirm
1293
- refreshChrome();
1294
- renderSurface();
1175
+ // The rail card's picture: the render of the layout this slide sits on.
1176
+ function renderMini(data) {
1177
+ var box = el("div", "sm-thumb-preview");
1178
+ var mini = el("div", "sm-mini");
1179
+ if (!data) {
1180
+ mini.appendChild(el("div", "sm-mini-empty", "new"));
1181
+ box.appendChild(mini);
1182
+ return box;
1183
+ }
1184
+ var selected = selectedPlanLayout(data);
1185
+ if (selected && selected.previewUrl) {
1186
+ var img = el("img", "sm-mini-chrome");
1187
+ img.src = selected.previewUrl;
1188
+ img.alt = "";
1189
+ img.loading = "lazy";
1190
+ mini.appendChild(img);
1191
+ } else {
1192
+ mini.appendChild(el("div", "sm-mini-empty", ""));
1193
+ }
1194
+ // Same slot treatment as the stage: scanning the rail should say how each
1195
+ // slide is SHAPED, not just what it is called.
1196
+ mini.appendChild(renderSlotGeometry(selected, "sm-mini-slots"));
1197
+ box.appendChild(mini);
1198
+ return box;
1295
1199
  }
1296
- function commitStoryboard(segment, nextData) {
1297
- commitPlan(PLAN.replaceStoryboardSegment(S.value, segment, nextData));
1200
+
1201
+ // ---------- the stage: the slide, fitted ----------
1202
+ // The bound layout's own chrome render with its slot geometry on top, a light
1203
+ // veil pushing it back, and ONE panel floating over both: the story beat, the
1204
+ // 3-5 key points, the layout pick. That is the whole of what the gate asks a
1205
+ // human — the storyboard canvas it replaced was design input nobody reviewed.
1206
+ //
1207
+ // The slide is CONTEXT and the panel is the SUBJECT, so the panel is centred
1208
+ // and is the only thing on the stage with elevation. Pinning it to the
1209
+ // layout's content region made it read as part of the slide, which is the one
1210
+ // thing it must not be; the layout's shape is carried by its own geometry.
1211
+ var MAX_BULLETS = 5;
1212
+
1213
+ function renderStage(segment, activeIndex, slides) {
1214
+ var stage = el("section", "sm-stage");
1215
+ var fit = el("div", "sm-stage-fit");
1216
+ if (bannerText) fit.appendChild(roundBanner());
1217
+ if (segment) {
1218
+ if (segment.data) fit.appendChild(renderSlideCard(segment, segment.data));
1219
+ else {
1220
+ var err = el("div", "plan-storyboard-error");
1221
+ err.appendChild(el("p", null, "Slide plan could not be read."));
1222
+ var pre = el("pre");
1223
+ var codeNode = document.createElement("code");
1224
+ codeNode.textContent = segment.raw || "";
1225
+ pre.appendChild(codeNode);
1226
+ err.appendChild(pre);
1227
+ fit.appendChild(err);
1228
+ }
1229
+ } else {
1230
+ // A staged new slide (or a beat with no record yet): the agent fills it
1231
+ // once the story round is sent.
1232
+ var head = el("div", "sm-stage-head");
1233
+ head.appendChild(el("span", "t", "")).textContent = "Slide " + (activeIndex + 1) + " · " + ((slides[activeIndex] && slides[activeIndex].message) || "");
1234
+ fit.appendChild(head);
1235
+ var placeholder = el("div", "plan-dossier-new-slide");
1236
+ placeholder.innerHTML =
1237
+ ICONS.ruler +
1238
+ "<p><strong>Storyline edited.</strong> Sending asks the agent to draft the content for your new and renamed slides — add comments below if you like.</p>";
1239
+ fit.appendChild(placeholder);
1240
+ }
1241
+ fit.appendChild(draftNote());
1242
+ stage.appendChild(fit);
1243
+ return stage;
1298
1244
  }
1299
1245
 
1300
- function renderField(variant, value, onCommit) {
1301
- var wrap = el("div", "plan-storyboard-field");
1302
- wrap.setAttribute("data-field", variant);
1303
- var ctrl = el("div", "plan-storyboard-field-control");
1304
- ctrl.setAttribute("role", "textbox");
1305
- ctrl.setAttribute("aria-label", variant === "element" ? "Design element" : "Content");
1306
- ctrl.textContent = value;
1307
- ctrl.style.fontSize = fittedFontSize(value, variant) + "px";
1308
- if (!locked()) {
1309
- ctrl.contentEditable = "true";
1310
- ctrl.spellcheck = true;
1246
+ function renderSlideCard(segment, data) {
1247
+ var frame = el("div", "plan-stage-frame");
1248
+ var selected = selectedPlanLayout(data);
1249
+ // The slide card: the same embossed physical object the add-in uses for a
1250
+ // slide anywhere else. The clip lives HERE, not on the frame, because the
1251
+ // panel's layout menu has to escape it.
1252
+ var slide = el("div", "plan-stage-slide slide-emboss");
1253
+ if (selected && selected.previewUrl) {
1254
+ var chrome = el("img", "plan-stage-chrome");
1255
+ chrome.src = selected.previewUrl;
1256
+ chrome.alt = "";
1257
+ slide.appendChild(chrome);
1258
+ } else {
1259
+ slide.appendChild(el("div", "plan-stage-chrome plan-stage-chrome--blank"));
1311
1260
  }
1312
- // Commit exactly once per edit session. A commit re-renders the surface,
1313
- // which detaches this (still-focused) node and fires a SECOND blur; the
1314
- // `committed` guard makes that second blur a no-op so we never splice the
1315
- // stale `segment` offsets back in (double-commit → corrupted fences).
1316
- var committed = false;
1317
- function commit() {
1318
- if (committed) return;
1319
- committed = true;
1320
- var normalized = compactSingleLine(ctrl.textContent || "", variant);
1321
- ctrl.textContent = normalized;
1322
- ctrl.style.fontSize = fittedFontSize(normalized, variant) + "px";
1323
- if (normalized !== value) onCommit(normalized);
1261
+ slide.appendChild(renderSlotGeometry(selected));
1262
+ slide.appendChild(el("div", "plan-stage-veil"));
1263
+ frame.appendChild(slide);
1264
+
1265
+ var card = el("div", "plan-stage-card");
1266
+
1267
+ var beatRow = el("div", "plan-stage-row");
1268
+ beatRow.appendChild(el("span", "plan-stage-label", "Story beat"));
1269
+ beatRow.appendChild(
1270
+ renderDetailField("plan-stage-beat", "Story beat", data.keyMessage || "", false, function (next) {
1271
+ var d = copyOf(data);
1272
+ if (next) d.keyMessage = next;
1273
+ else delete d.keyMessage;
1274
+ commitStoryboard(segment, d);
1275
+ }),
1276
+ );
1277
+ card.appendChild(beatRow);
1278
+
1279
+ var bullets = data.contentNotes || data.depthNotes || [];
1280
+ function writeBullets(next) {
1281
+ var d = {};
1282
+ for (var k in data) if (k !== "contentNotes" && k !== "depthNotes") d[k] = data[k];
1283
+ if (next.length) d.contentNotes = next;
1284
+ commitStoryboard(segment, d);
1324
1285
  }
1325
- ctrl.addEventListener("input", function () {
1326
- ctrl.style.fontSize = fittedFontSize(ctrl.textContent || "", variant) + "px";
1327
- });
1328
- ctrl.addEventListener("blur", commit);
1329
- // Enter blurs and lets the blur handler be the single committer (matches the
1330
- // beat-rename pattern) — never commit directly here, or the follow-on blur
1331
- // would commit a second time against the mutated markdown.
1332
- ctrl.addEventListener("keydown", function (e) {
1333
- if (e.key !== "Enter") return;
1334
- e.preventDefault();
1335
- ctrl.blur();
1286
+ var pointsRow = el("div", "plan-stage-row");
1287
+ pointsRow.appendChild(el("span", "plan-stage-label", "Key points"));
1288
+ var points = el("div", "plan-stage-points");
1289
+ var list = el("ul", "plan-stage-bullets");
1290
+ bullets.forEach(function (bullet, index) {
1291
+ var li = el("li", "plan-stage-bullet");
1292
+ li.appendChild(
1293
+ renderDetailField("plan-stage-bullet-text", "Key points", bullet, false, function (next) {
1294
+ writeBullets(
1295
+ bullets.reduce(function (acc, b, at) {
1296
+ if (at !== index) acc.push(b);
1297
+ else if (next) acc.push(next);
1298
+ return acc;
1299
+ }, []),
1300
+ );
1301
+ }),
1302
+ );
1303
+ if (!locked()) {
1304
+ var rm = document.createElement("button");
1305
+ rm.type = "button";
1306
+ rm.className = "plan-stage-bullet-remove";
1307
+ rm.title = "Remove this point";
1308
+ rm.setAttribute("aria-label", "Remove this point");
1309
+ rm.innerHTML = ICONS.x;
1310
+ rm.addEventListener("click", function () {
1311
+ writeBullets(
1312
+ bullets.filter(function (_, at) {
1313
+ return at !== index;
1314
+ }),
1315
+ );
1316
+ });
1317
+ li.appendChild(rm);
1318
+ }
1319
+ list.appendChild(li);
1336
1320
  });
1337
- wrap.appendChild(ctrl);
1338
- return wrap;
1339
- }
1340
-
1341
- // Where the generic body-safe area lands on the frame. Blocks are planned in
1342
- // the 1600×900 stage inside BODY_SAFE_AREA; a bound layout's content zone
1343
- // (normalized 0..1) IS the real body, so the safe area is stretched onto it —
1344
- // every block reads as a fraction of the body, never as a bare stage position
1345
- // that may fall outside the template's content area. Without a zone the
1346
- // stage is the frame. {x,y} = normalized frame offset, {sx,sy} = normalized
1347
- // frame units per stage unit.
1348
- function frameMapFor(data) {
1349
- var selected = selectedPlanLayout(data);
1350
- var zone = selected && selected.canvasBounds;
1351
- // A zero-size zone would make the per-pixel scale infinite and a drag
1352
- // write NaN coordinates — treat it as no zone.
1353
- if (!zone || !(zone.w > 0) || !(zone.h > 0)) {
1354
- return { x: 0, y: 0, sx: 1 / PLAN.STORYBOARD_WIDTH, sy: 1 / PLAN.STORYBOARD_HEIGHT };
1321
+ if (pendingBullet) {
1322
+ var draftLi = el("li", "plan-stage-bullet");
1323
+ var draftField = renderDetailField("plan-stage-bullet-text", "One short point", "", false, function (next) {
1324
+ pendingBullet = false;
1325
+ if (next) writeBullets(bullets.concat([next]));
1326
+ else render();
1327
+ });
1328
+ draftLi.appendChild(draftField);
1329
+ list.appendChild(draftLi);
1330
+ focusAfterRender = draftField;
1355
1331
  }
1356
- var A = PLAN.BODY_SAFE_AREA;
1357
- var sx = zone.w / A.w;
1358
- var sy = zone.h / A.h;
1359
- return { x: zone.x - A.x * sx, y: zone.y - A.y * sy, sx: sx, sy: sy };
1360
- }
1361
- function placeBlock(node, map, x, y, w, h) {
1362
- node.style.left = (map.x + x * map.sx) * 100 + "%";
1363
- node.style.top = (map.y + y * map.sy) * 100 + "%";
1364
- node.style.width = w * map.sx * 100 + "%";
1365
- node.style.height = h * map.sy * 100 + "%";
1366
- }
1367
- // Stage units per client pixel, per axis, for a 16:9 frame `width` px wide.
1368
- function stagePerPixel(map, width) {
1369
- var height = (width * PLAN.STORYBOARD_HEIGHT) / PLAN.STORYBOARD_WIDTH;
1370
- return { x: 1 / (width * map.sx), y: 1 / (height * map.sy) };
1371
- }
1372
-
1373
- function renderBlock(frameEl, block, data, segment, canRemove) {
1374
- var node = el("div", "plan-storyboard-block" + (locked() ? "" : " plan-storyboard-block--movable"));
1375
- node.setAttribute("data-emphasis", block.emphasis || "secondary");
1376
- var map = frameMapFor(data);
1377
- placeBlock(node, map, block.x, block.y, block.w, block.h);
1378
-
1379
- function change(patch) {
1380
- commitStoryboard(segment, updateBlock(data, block.id, patch));
1332
+ points.appendChild(list);
1333
+ if (!locked() && !pendingBullet && bullets.length < MAX_BULLETS) {
1334
+ var add = document.createElement("button");
1335
+ add.type = "button";
1336
+ add.className = "plan-stage-bullet-add";
1337
+ add.innerHTML = ICONS.plus;
1338
+ add.appendChild(document.createTextNode("Add a point"));
1339
+ add.addEventListener("click", function () {
1340
+ pendingBullet = true;
1341
+ render();
1342
+ });
1343
+ points.appendChild(add);
1381
1344
  }
1345
+ pointsRow.appendChild(points);
1346
+ card.appendChild(pointsRow);
1382
1347
 
1383
- node.appendChild(
1384
- renderField("element", block.designElement, function (v) {
1385
- change({ designElement: v });
1386
- }),
1387
- );
1388
- node.appendChild(
1389
- renderField("content", block.content, function (v) {
1390
- change({ content: v });
1348
+ var usesRow = renderImageUses(data.assetUses);
1349
+ if (usesRow) card.appendChild(usesRow);
1350
+
1351
+ var foot = el("div", "plan-stage-foot");
1352
+ // Same label column as the two fields above, so the layout reads as a third
1353
+ // thing this gate lets you change rather than a caption on the panel.
1354
+ foot.appendChild(el("span", "plan-stage-label", "Change layout"));
1355
+ foot.appendChild(
1356
+ renderLayoutPicker(data.layout, {
1357
+ className: "plan-stage-layout",
1358
+ ariaLabel: "Choose the slide's layout",
1359
+ onPick: function (componentName) {
1360
+ var d = copyOf(data);
1361
+ d.layout = componentName;
1362
+ d.layoutSelection = "explicit";
1363
+ d.layoutKey = (findLayout(componentName) || {}).layoutKey;
1364
+ delete d.layoutReason;
1365
+ commitStoryboard(segment, d);
1366
+ },
1391
1367
  }),
1392
1368
  );
1369
+ card.appendChild(foot);
1370
+
1371
+ frame.appendChild(card);
1372
+ return frame;
1373
+ }
1374
+
1375
+ // ---------- the slide's image uses (read-only) ----------
1376
+ // What a reviewer signs off is not "there is an image" but WHICH image and
1377
+ // what it is for, so the row shows the file, names the obligation in plain
1378
+ // language and quotes the agent's instruction verbatim. Framing is shown only
1379
+ // when it is not the default (`contain`, centred, uncropped) — a line saying
1380
+ // the default back is noise.
1381
+ //
1382
+ // Read-only ON PURPOSE. There is deliberately no role picker (design decision
1383
+ // 3): a correction is a comment like every other change to the plan, and the
1384
+ // agent rewrites the use.
1385
+ // Keyed by fidelity AND scope, not composed from a shared stem: the add-in
1386
+ // ships the same sentences in de/fr, where the "whole slide" / "one element"
1387
+ // qualifier does not sit where English puts it. One key per case is what lets
1388
+ // both clients say the same thing to the same user.
1389
+ var USE_WORDS = {
1390
+ original: "Placed as the image itself",
1391
+ "reconstruct:slide": "Rebuilt as editable slide content — whole slide",
1392
+ "reconstruct:element": "Rebuilt as editable slide content — one element",
1393
+ reference: "Visual reference only",
1394
+ };
1393
1395
 
1394
- if (!locked() && canRemove) {
1395
- var rm = el("button", "plan-storyboard-block-remove", ICONS.x);
1396
- rm.type = "button";
1397
- rm.setAttribute("data-no-drag", "");
1398
- rm.title = "Remove container";
1399
- rm.setAttribute("aria-label", "Remove container");
1400
- rm.addEventListener("pointerdown", function (e) {
1401
- e.stopPropagation();
1402
- });
1403
- rm.addEventListener("click", function () {
1404
- if (data.blocks.length <= 1) return;
1405
- commitStoryboard(
1406
- segment,
1407
- withBlocks(
1408
- data,
1409
- data.blocks.filter(function (b) {
1410
- return b.id !== block.id;
1411
- }),
1412
- ),
1413
- );
1414
- });
1415
- node.appendChild(rm);
1396
+ function useWords(use) {
1397
+ if (use.fidelity === "reconstruct") {
1398
+ return USE_WORDS["reconstruct:" + use.reconstructionScope] || USE_WORDS["reconstruct:element"];
1416
1399
  }
1400
+ return USE_WORDS[use.fidelity] || use.fidelity;
1401
+ }
1417
1402
 
1418
- if (!locked()) {
1419
- node.title = "Drag to reposition";
1420
- wireBlockDrag(node, frameEl, block, map, change);
1421
- var rz = el("button", "plan-storyboard-block-resize");
1422
- rz.type = "button";
1423
- rz.setAttribute("data-no-drag", "");
1424
- rz.title = "Drag to resize";
1425
- rz.setAttribute("aria-label", "Drag to resize");
1426
- wireBlockResize(rz, frameEl, block, map, change);
1427
- node.appendChild(rz);
1403
+ function imageAssetById(assetId) {
1404
+ var all = (CFG && CFG.imageAssets) || [];
1405
+ for (var i = 0; i < all.length; i++) {
1406
+ if (all[i] && all[i].assetId === assetId) return all[i];
1428
1407
  }
1429
- return node;
1408
+ return null;
1430
1409
  }
1431
1410
 
1432
- function clampPos(block, x, y) {
1433
- var A = PLAN.BODY_SAFE_AREA;
1434
- return {
1435
- x: clamp(x, A.x, A.x + A.w - block.w),
1436
- y: clamp(y, A.y, A.y + A.h - block.h),
1437
- };
1438
- }
1439
- function clampSize(block, w, h) {
1440
- var A = PLAN.BODY_SAFE_AREA;
1441
- return {
1442
- w: clamp(w, PLAN.MIN_BLOCK_WIDTH, A.x + A.w - block.x),
1443
- h: clamp(h, PLAN.MIN_BLOCK_HEIGHT, A.y + A.h - block.y),
1444
- };
1445
- }
1446
- function wireBlockDrag(node, frameEl, block, map, change) {
1447
- var st = null;
1448
- node.addEventListener("pointerdown", function (e) {
1449
- if (locked() || e.button !== 0) return;
1450
- if (e.target.closest && e.target.closest(".plan-storyboard-field, [data-no-drag]")) return;
1451
- var rect = frameEl.getBoundingClientRect();
1452
- if (!rect || rect.width <= 0) return;
1453
- st = {
1454
- pointerId: e.pointerId,
1455
- ox: e.clientX,
1456
- oy: e.clientY,
1457
- sx: block.x,
1458
- sy: block.y,
1459
- scale: stagePerPixel(map, rect.width),
1460
- moved: false,
1461
- };
1462
- try {
1463
- node.setPointerCapture(e.pointerId);
1464
- } catch {
1465
- /* headless */
1466
- }
1467
- });
1468
- node.addEventListener("pointermove", function (e) {
1469
- if (!st || st.pointerId !== e.pointerId) return;
1470
- var dx = e.clientX - st.ox;
1471
- var dy = e.clientY - st.oy;
1472
- if (!st.moved && Math.hypot(dx, dy) < BLOCK_DRAG_THRESHOLD) return;
1473
- st.moved = true;
1474
- node.classList.add("plan-storyboard-block--dragging");
1475
- var p = clampPos(block, st.sx + dx * st.scale.x, st.sy + dy * st.scale.y);
1476
- placeBlock(node, map, p.x, p.y, block.w, block.h);
1477
- });
1478
- function end(e) {
1479
- if (!st || st.pointerId !== e.pointerId) return;
1480
- try {
1481
- node.releasePointerCapture(e.pointerId);
1482
- } catch {
1483
- /* ignore */
1484
- }
1485
- var s = st;
1486
- st = null;
1487
- if (!s.moved) {
1488
- node.classList.remove("plan-storyboard-block--dragging");
1489
- return;
1490
- }
1491
- var p = clampPos(block, s.sx + (e.clientX - s.ox) * s.scale.x, s.sy + (e.clientY - s.oy) * s.scale.y);
1492
- var x = Math.round(p.x);
1493
- var y = Math.round(p.y);
1494
- if (x !== block.x || y !== block.y) change({ x: x, y: y });
1495
- else node.classList.remove("plan-storyboard-block--dragging");
1411
+ function useFramingText(use) {
1412
+ // Only an original is framed, and only a non-default framing is worth a
1413
+ // line: `contain`, centred and uncropped says nothing back to the reviewer.
1414
+ // The reconstruct scope is part of the label above, not a framing fact.
1415
+ if (use.fidelity !== "original") return "";
1416
+ var bits = [];
1417
+ if (use.crop) {
1418
+ bits.push(
1419
+ "Cropped to " +
1420
+ Math.round(use.crop.w * 100) + "% × " + Math.round(use.crop.h * 100) +
1421
+ "% of the source",
1422
+ );
1496
1423
  }
1497
- node.addEventListener("pointerup", end);
1498
- node.addEventListener("pointercancel", function (e) {
1499
- if (!st || st.pointerId !== e.pointerId) return;
1500
- st = null;
1501
- node.classList.remove("plan-storyboard-block--dragging");
1502
- });
1503
- }
1504
- function wireBlockResize(handle, frameEl, block, map, change) {
1505
- var st = null;
1506
- handle.addEventListener("pointerdown", function (e) {
1507
- if (locked() || e.button !== 0) return;
1508
- e.stopPropagation();
1509
- var rect = frameEl.getBoundingClientRect();
1510
- if (!rect || rect.width <= 0) return;
1511
- st = {
1512
- pointerId: e.pointerId,
1513
- ox: e.clientX,
1514
- oy: e.clientY,
1515
- sw: block.w,
1516
- sh: block.h,
1517
- scale: stagePerPixel(map, rect.width),
1518
- moved: false,
1519
- };
1520
- try {
1521
- handle.setPointerCapture(e.pointerId);
1522
- } catch {
1523
- /* headless */
1424
+ if (use.fit === "cover") bits.push("Framed to fill (cover)");
1425
+ if (use.slot) bits.push("In the " + use.slot + " slot");
1426
+ return bits.join(" · ");
1427
+ }
1428
+
1429
+ function renderImageUses(uses) {
1430
+ if (!uses || !uses.length) return null;
1431
+ var row = el("div", "plan-stage-row");
1432
+ row.appendChild(el("span", "plan-stage-label", uses.length === 1 ? "Image" : "Images"));
1433
+ var list = el("div", "plan-stage-uses");
1434
+ uses.forEach(function (use) {
1435
+ var asset = imageAssetById(use.assetId);
1436
+ var item = el("div", "plan-stage-use");
1437
+ if (asset && asset.previewUrl) {
1438
+ var thumb = el("img", "plan-stage-use-thumb");
1439
+ thumb.src = asset.previewUrl;
1440
+ thumb.alt = "";
1441
+ item.appendChild(thumb);
1442
+ } else {
1443
+ item.appendChild(el("div", "plan-stage-use-thumb"));
1524
1444
  }
1525
- });
1526
- handle.addEventListener("pointermove", function (e) {
1527
- if (!st || st.pointerId !== e.pointerId) return;
1528
- var dx = e.clientX - st.ox;
1529
- var dy = e.clientY - st.oy;
1530
- if (!st.moved && Math.hypot(dx, dy) < BLOCK_DRAG_THRESHOLD) return;
1531
- st.moved = true;
1532
- var node = handle.parentNode;
1533
- var p = clampSize(block, st.sw + dx * st.scale.x, st.sh + dy * st.scale.y);
1534
- node.classList.add("plan-storyboard-block--dragging");
1535
- placeBlock(node, map, block.x, block.y, p.w, p.h);
1536
- });
1537
- function end(e) {
1538
- if (!st || st.pointerId !== e.pointerId) return;
1539
- try {
1540
- handle.releasePointerCapture(e.pointerId);
1541
- } catch {
1542
- /* ignore */
1445
+ var body = el("div", "plan-stage-use-body");
1446
+ var what = el("div", "plan-stage-use-what");
1447
+ what.textContent = useWords(use);
1448
+ // The file the user handed over — the only way to tell two screenshots
1449
+ // apart when both thumbnails look alike.
1450
+ var name = el("span", "plan-stage-use-name", "");
1451
+ name.textContent = " · " + ((asset && asset.displayName) || use.assetId);
1452
+ what.appendChild(name);
1453
+ body.appendChild(what);
1454
+ var instruction = el("div", "plan-stage-use-instruction");
1455
+ instruction.textContent = use.instruction || "";
1456
+ body.appendChild(instruction);
1457
+ var framing = useFramingText(use);
1458
+ if (framing) {
1459
+ var fr = el("div", "plan-stage-use-framing");
1460
+ fr.textContent = framing;
1461
+ body.appendChild(fr);
1543
1462
  }
1544
- var s = st;
1545
- st = null;
1546
- if (!s.moved) return;
1547
- var p = clampSize(block, s.sw + (e.clientX - s.ox) * s.scale.x, s.sh + (e.clientY - s.oy) * s.scale.y);
1548
- var w = Math.round(p.w);
1549
- var h = Math.round(p.h);
1550
- if (w !== block.w || h !== block.h) change({ w: w, h: h });
1463
+ item.appendChild(body);
1464
+ list.appendChild(item);
1465
+ });
1466
+ row.appendChild(list);
1467
+ return row;
1468
+ }
1469
+
1470
+ // The layout's own slots over its render: how a reviewer sees WHICH layout
1471
+ // they are on without reading its name. Read-only, pointer-through, and
1472
+ // under the veil so the panel stays the brightest thing on the stage.
1473
+ function renderSlotGeometry(layout, extraClass) {
1474
+ var slots = el("div", "plan-stage-slots" + (extraClass ? " " + extraClass : ""));
1475
+ slots.setAttribute("aria-hidden", "true");
1476
+ if (!layout) return slots;
1477
+ function place(node, rect) {
1478
+ node.style.left = rect.x * 100 + "%";
1479
+ node.style.top = rect.y * 100 + "%";
1480
+ node.style.width = rect.w * 100 + "%";
1481
+ node.style.height = rect.h * 100 + "%";
1482
+ slots.appendChild(node);
1551
1483
  }
1552
- handle.addEventListener("pointerup", end);
1553
- handle.addEventListener("pointercancel", function (e) {
1554
- if (st && st.pointerId === e.pointerId) st = null;
1484
+ (layout.placeholders || []).forEach(function (ph) {
1485
+ var box = el("div", "plan-stage-slot");
1486
+ if (ph.label) box.appendChild(el("span", "plan-stage-slot-label", "")).textContent = ph.label;
1487
+ place(box, ph);
1555
1488
  });
1489
+ (layout.contentAreas || []).forEach(function (area) {
1490
+ place(el("div", "plan-stage-zone"), area);
1491
+ });
1492
+ return slots;
1556
1493
  }
1557
1494
 
1558
- function renderDetailField(className, ariaLabel, value, multiline, onCommit) {
1559
- var field = el("div", className);
1560
- field.setAttribute("role", "textbox");
1561
- field.setAttribute("aria-label", ariaLabel);
1562
- if (multiline) field.setAttribute("aria-multiline", "true");
1563
- field.textContent = value;
1564
- if (!locked()) {
1565
- field.contentEditable = "true";
1566
- field.spellcheck = true;
1567
- }
1568
- // Commit-once guard: a commit re-renders the surface and detaches this
1569
- // focused field, firing a second blur \u2014 which must not re-splice the stale
1570
- // segment (double-commit \u2192 corrupted fences on the single-line beat field).
1571
- var committed = false;
1572
- function commit() {
1573
- if (committed) return;
1574
- committed = true;
1575
- var normalized = (field.textContent || "").replace(/\u00a0/g, " ").trim();
1576
- field.textContent = normalized;
1577
- if (normalized !== value) onCommit(normalized);
1578
- }
1579
- field.addEventListener("blur", commit);
1580
- if (!multiline) {
1581
- // Enter blurs; the blur handler is the single committer.
1582
- field.addEventListener("keydown", function (e) {
1583
- if (e.key !== "Enter") return;
1584
- e.preventDefault();
1585
- field.blur();
1586
- });
1587
- }
1588
- return field;
1495
+ function draftNote() {
1496
+ var note = el("div", "plan-storyboard-draft-note");
1497
+ note.innerHTML =
1498
+ ICONS.ruler +
1499
+ "<p><strong>Draft plan.</strong> The finished slides follow your corporate design.</p>";
1500
+ return note;
1589
1501
  }
1590
1502
 
1591
- // Layout picker: current choice (chrome thumbnail + name) plus a dropdown of
1592
- // the bundle's layouts (CFG.layouts; empty → read-only). Used per slide in
1593
- // the details aside and once for the deck-level knob; `opts.onPick` owns the
1594
- // commit. All labels/names go through textContent — bundle strings never
1595
- // reach innerHTML.
1596
- // One short line per option instead of the codegen's geometry dump: the
1597
- // count is what a human picks on; the exact rectangles are drawn on the
1598
- // storyboard frame once the layout is chosen.
1599
1503
  function contentAreaSummary(option) {
1600
1504
  var n = option.contentAreas ? option.contentAreas.length : 0;
1601
1505
  if (!n && option.canvas) n = (option.canvas.match(/(?:^|;)\s*\d+\./g) || []).length || 1;
1602
1506
  if (!n) return "Chrome only";
1603
1507
  return n === 1 ? "1 content area" : n + " content areas";
1604
1508
  }
1509
+ function usesSummary(option) {
1510
+ var u = option.selection;
1511
+ if (!u) return "";
1512
+ if (u.currentCount) return u.currentCount + " uses in this presentation";
1513
+ if (u.usage && u.usage.count) return u.usage.count + " observed uses";
1514
+ return "";
1515
+ }
1605
1516
 
1517
+ // ---------- the compact deck-level layout knob (rail head) ----------
1518
+ // Dropdown of the bundle's layouts: one pick binds EVERY body board's
1519
+ // `layout` in one commit; a mixed deck reads "Mixed". Read-only without a
1520
+ // bundle. Bundle strings only ever reach textContent.
1606
1521
  function renderLayoutPicker(value, opts) {
1607
- var options = Array.isArray(CFG.layouts) ? CFG.layouts : [];
1608
- if (opts.bodyOnly) options = options.filter(function(o) { return o.canvasBounds || o.contentAreas?.length; });
1522
+ var options = layoutOptions();
1523
+ if (opts.bodyOnly) options = options.filter(function (o) { return o.canvasBounds || (o.contentAreas && o.contentAreas.length); });
1609
1524
  var root = el("div", "plan-storyboard-layout-picker" + (opts.className ? " " + opts.className : ""));
1610
- var selected = null;
1611
- for (var i = 0; i < options.length; i++) {
1612
- if (options[i].componentName === value) selected = options[i];
1613
- }
1525
+ var selected = findLayout(value);
1614
1526
  var canPick = !locked() && options.length > 0;
1615
1527
 
1616
1528
  var current = document.createElement("button");
@@ -1623,51 +1535,37 @@
1623
1535
  thumb.className = "plan-storyboard-layout-thumb";
1624
1536
  thumb.src = selected.previewUrl;
1625
1537
  thumb.alt = "";
1626
- thumb.setAttribute("aria-hidden", "true");
1627
1538
  current.appendChild(thumb);
1628
1539
  }
1629
1540
  var nameSpan = el("span", "plan-storyboard-layout-name");
1630
1541
  nameSpan.textContent = (selected && selected.nativeLayoutName) || value || opts.placeholder || "No layout chosen";
1631
1542
  current.appendChild(nameSpan);
1632
- if (value && options.length > 0 && !selected) {
1633
- var badge = el("span", "plan-storyboard-layout-unknown");
1634
- badge.textContent = "Not in this brand";
1635
- current.appendChild(badge);
1636
- }
1637
1543
  if (canPick) {
1638
- var chevron = el("span", "plan-storyboard-layout-chevron", "\u25BE");
1544
+ var chevron = el("span", "plan-storyboard-layout-chevron", CHEVRON);
1639
1545
  chevron.setAttribute("aria-hidden", "true");
1640
1546
  current.appendChild(chevron);
1641
1547
  }
1642
1548
  root.appendChild(current);
1643
-
1644
1549
  if (!canPick) return root;
1645
1550
 
1646
1551
  var menu = null;
1647
1552
  function closeMenu() {
1648
- if (menu) {
1649
- menu.remove();
1650
- menu = null;
1651
- document.removeEventListener("pointerdown", onOutside);
1652
- }
1553
+ if (!menu) return;
1554
+ menu.remove();
1555
+ menu = null;
1556
+ document.removeEventListener("pointerdown", onOutside);
1653
1557
  }
1654
1558
  function onOutside(e) {
1655
1559
  if (!root.contains(e.target)) closeMenu();
1656
1560
  }
1657
1561
  current.addEventListener("click", function () {
1658
- if (menu) {
1659
- closeMenu();
1660
- return;
1661
- }
1562
+ if (menu) return closeMenu();
1662
1563
  menu = el("div", "plan-storyboard-layout-menu");
1663
1564
  menu.setAttribute("role", "listbox");
1664
1565
  options.forEach(function (option) {
1665
1566
  var btn = document.createElement("button");
1666
1567
  btn.type = "button";
1667
- btn.className =
1668
- option.componentName === value
1669
- ? "plan-storyboard-layout-option plan-storyboard-layout-option--selected"
1670
- : "plan-storyboard-layout-option";
1568
+ btn.className = "plan-storyboard-layout-option" + (option.componentName === value ? " plan-storyboard-layout-option--selected" : "");
1671
1569
  btn.setAttribute("role", "option");
1672
1570
  btn.setAttribute("aria-selected", option.componentName === value ? "true" : "false");
1673
1571
  if (option.previewUrl) {
@@ -1675,23 +1573,14 @@
1675
1573
  t.className = "plan-storyboard-layout-thumb plan-storyboard-layout-thumb--option";
1676
1574
  t.src = option.previewUrl;
1677
1575
  t.alt = "";
1678
- t.setAttribute("aria-hidden", "true");
1679
1576
  btn.appendChild(t);
1680
1577
  }
1681
1578
  var text = el("span", "plan-storyboard-layout-option-text");
1682
1579
  var n = el("span", "plan-storyboard-layout-name");
1683
1580
  n.textContent = option.nativeLayoutName || option.componentName;
1684
- if (options.filter(function (other) { return other.nativeLayoutName === option.nativeLayoutName; }).length > 1 && option.selection?.masterName) n.textContent += " · " + option.selection.masterName;
1685
- if (option.selection) n.title = option.selection.masterName || "";
1686
1581
  text.appendChild(n);
1687
- if (option.purpose) {
1688
- var p = el("span", "plan-storyboard-layout-purpose");
1689
- p.textContent = option.purpose;
1690
- text.appendChild(p);
1691
- }
1692
1582
  var c = el("span", "plan-storyboard-layout-canvas");
1693
- c.textContent = contentAreaSummary(option);
1694
- if (option.selection) { var u = option.selection; c.textContent += u.currentCount ? " · " + u.currentCount + " uses in this presentation" : u.usage.count ? " · " + u.usage.count + " observed uses" : ""; }
1583
+ c.textContent = [contentAreaSummary(option), usesSummary(option)].filter(Boolean).join(" · ");
1695
1584
  text.appendChild(c);
1696
1585
  btn.appendChild(text);
1697
1586
  btn.addEventListener("click", function () {
@@ -1706,356 +1595,360 @@
1706
1595
  return root;
1707
1596
  }
1708
1597
 
1709
- // Deck-level layout knob: the one layout every board shares (or "Mixed"); a
1710
- // pick binds EVERY storyboard's `layout` in one commit.
1598
+ // ---------- the header's deck-wide choices: the design, then the body slides' layout ----------
1599
+ function renderBarTools(storyboardData) {
1600
+ if (!barTools) return;
1601
+ barTools.textContent = "";
1602
+ var designPicker = renderDesignPicker();
1603
+ if (designPicker) barTools.appendChild(barTool("Design", designPicker));
1604
+ barTools.appendChild(barTool("Slide Layouts", renderDeckLayoutKnob(storyboardData)));
1605
+ }
1606
+ function barTool(label, control) {
1607
+ var group = el("div", "sm-bar-tool");
1608
+ group.appendChild(el("span", "sm-bar-tool-label", "")).textContent = label;
1609
+ group.appendChild(control);
1610
+ return group;
1611
+ }
1711
1612
  function renderDeckLayoutKnob(storyboardData) {
1712
- var wrap = el("div", "plan-storyline-deck-layout");
1713
- wrap.appendChild(el("span", "plan-storyline-deck-layout-label", "Body defaults"));
1714
1613
  var seen = {};
1715
1614
  var distinct = 0;
1716
1615
  for (var i = 0; i < storyboardData.length; i++) {
1616
+ if (storyboardData[i].role === "frame") continue;
1717
1617
  var name = storyboardData[i].layout || "";
1718
1618
  if (!seen[name]) {
1719
1619
  seen[name] = true;
1720
1620
  distinct++;
1721
1621
  }
1722
1622
  }
1723
- var common = distinct === 1 && storyboardData[0] ? storyboardData[0].layout : undefined;
1724
- wrap.appendChild(
1725
- renderLayoutPicker(common || undefined, {
1726
- placeholder: distinct > 1 ? "Mixed" : undefined,
1727
- ariaLabel: "Set the default for compatible body slides",
1728
- bodyOnly: true,
1729
- onPick: function (componentName) {
1730
- commitPlan(PLAN.setStoryboardLayoutForAll(S.value, componentName, CFG.layouts || []));
1731
- },
1732
- }),
1733
- );
1734
- if ((CFG.layouts || []).some(function(o) { return o.selection?.counterpartKey; })) {
1735
- ["light", "dark"].forEach(function(tone) {
1736
- var button = el("button", "plan-storyboard-layout-current", tone === "dark" ? "Dark variants" : "Light variants");
1737
- button.type = "button";
1738
- button.addEventListener("click", function() { commitPlan(PLAN.setStoryboardStyle(S.value, tone, CFG.layouts || [])); });
1739
- wrap.appendChild(button);
1740
- });
1741
- }
1742
- return wrap;
1743
- }
1744
-
1745
- function renderDetails(data, segment) {
1746
- var aside = el("aside", "plan-storyboard-detail");
1747
- var roleSelect = document.createElement("select");
1748
- roleSelect.setAttribute("aria-label", "Slide role");
1749
- ["body", "cover", "agenda", "divider", "closing"].forEach(function(role) {
1750
- var option = document.createElement("option"); option.value = role; option.textContent = role;
1751
- option.selected = role === (data.role === "frame" ? data.frameKind : "body"); roleSelect.appendChild(option);
1752
- });
1753
- roleSelect.addEventListener("change", function() { var d = Object.assign({}, data);
1754
- d.role = roleSelect.value === "body" ? "body" : "frame";
1755
- d.frameKind = d.role === "frame" ? roleSelect.value : undefined;
1756
- commitStoryboard(segment, d);
1623
+ var common = distinct === 1 ? Object.keys(seen)[0] : undefined;
1624
+ return renderLayoutPicker(common || undefined, {
1625
+ placeholder: distinct > 1 ? "Mixed layouts" : undefined,
1626
+ ariaLabel: "Set the layout for every body slide",
1627
+ bodyOnly: true,
1628
+ onPick: function (componentName) {
1629
+ commitPlan(PLAN.setStoryboardLayoutForAll(S.value, componentName, layoutOptions()));
1630
+ },
1757
1631
  });
1758
- aside.appendChild(roleSelect);
1759
- if (data.layoutReason) aside.appendChild(el("div", "plan-storyboard-layout-purpose", data.layoutReason));
1760
- aside.appendChild(el("div", "plan-storyboard-detail-label", "Layout"));
1761
- aside.appendChild(
1762
- renderLayoutPicker(data.layout, {
1763
- onPick: function (componentName) {
1764
- var d = {};
1765
- for (var k in data) d[k] = data[k];
1766
- d.layout = componentName;
1767
- d.layoutSelection = "explicit";
1768
- d.layoutKey = (CFG.layouts || []).find(function(o) { return o.componentName === componentName; })?.layoutKey;
1769
- delete d.layoutReason;
1770
- commitStoryboard(segment, d);
1771
- },
1772
- }),
1773
- );
1774
- aside.appendChild(el("div", "plan-storyboard-detail-label", "Story beat"));
1775
- aside.appendChild(
1776
- renderDetailField(
1777
- "plan-storyboard-key-message",
1778
- "Story beat",
1779
- data.keyMessage || "",
1780
- false,
1781
- function (next) {
1782
- if (next) {
1783
- var d = {};
1784
- for (var k in data) d[k] = data[k];
1785
- d.keyMessage = next;
1786
- commitStoryboard(segment, d);
1787
- } else {
1788
- var d2 = {};
1789
- for (var k2 in data) if (k2 !== "keyMessage") d2[k2] = data[k2];
1790
- commitStoryboard(segment, d2);
1791
- }
1792
- },
1793
- ),
1794
- );
1795
- aside.appendChild(el("div", "plan-storyboard-detail-label", "Content draft"));
1796
- var currentContent = notesToText(data.contentNotes);
1797
- aside.appendChild(
1798
- renderDetailField(
1799
- "plan-storyboard-detail-text",
1800
- "Content draft",
1801
- currentContent,
1802
- true,
1803
- function (next) {
1804
- var notes = textToNotes(next);
1805
- if (notesToText(notes) === currentContent) return;
1806
- var d = {};
1807
- for (var k in data) if (k !== "contentNotes" && k !== "depthNotes") d[k] = data[k];
1808
- if (notes) d.contentNotes = notes;
1809
- commitStoryboard(segment, d);
1810
- },
1811
- ),
1812
- );
1813
- return aside;
1814
1632
  }
1815
1633
 
1816
- // The planned layout, when the storyboard names one the bundle has: its
1817
- // chrome render grounds the frame and its canvas rect draws the content/
1818
- // no-go zones. Absent both, the generic hatched bands render as before.
1819
- function selectedPlanLayout(data) {
1820
- var options = Array.isArray(CFG.layouts) ? CFG.layouts : [];
1821
- if (!data.layout) return null;
1822
- for (var i = 0; i < options.length; i++) {
1823
- if (options[i].componentName === data.layout) return options[i];
1634
+ // ---------- a dropdown menu (the comment target, the design picker) ----------
1635
+ // Drawn here rather than a native <select>, whose open list is the system's
1636
+ // own — white on this dark page. Opens on click (or an arrow key), closes on
1637
+ // a pick, an outside press, Tab or Escape; the arrow keys walk the options.
1638
+ // `render(node, item, selected)` fills an option; strings reach textContent.
1639
+ var CHEVRON = '<svg viewBox="0 0 10 6" aria-hidden="true"><path d="M1 1l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
1640
+ function attachMenu(root, trigger, spec) {
1641
+ var menu = null;
1642
+ trigger.setAttribute("aria-haspopup", "listbox");
1643
+ trigger.setAttribute("aria-expanded", "false");
1644
+ function options() {
1645
+ return menu ? Array.prototype.slice.call(menu.querySelectorAll('[role="option"]')) : [];
1824
1646
  }
1825
- return null;
1826
- }
1827
-
1828
- function pctOf(v) {
1829
- return (v * 100).toFixed(2) + "%";
1830
- }
1831
-
1832
- function renderZoneOverlay(zone, areas) {
1833
- var zones = el("div", "plan-storyboard-zones");
1834
- zones.setAttribute("aria-hidden", "true");
1835
- function wash(style) {
1836
- var d = el("div", "plan-storyboard-zone-wash");
1837
- for (var k in style) d.style[k] = style[k];
1838
- zones.appendChild(d);
1647
+ function close(refocus) {
1648
+ if (!menu) return;
1649
+ menu.remove();
1650
+ menu = null;
1651
+ trigger.setAttribute("aria-expanded", "false");
1652
+ document.removeEventListener("pointerdown", onOutside, true);
1653
+ if (refocus) trigger.focus();
1839
1654
  }
1840
- wash({ left: "0", top: "0", right: "0", height: pctOf(zone.y) });
1841
- wash({ left: "0", top: pctOf(zone.y + zone.h), right: "0", bottom: "0" });
1842
- wash({ left: "0", top: pctOf(zone.y), width: pctOf(zone.x), height: pctOf(zone.h) });
1843
- wash({
1844
- left: pctOf(zone.x + zone.w),
1845
- top: pctOf(zone.y),
1846
- right: "0",
1847
- height: pctOf(zone.h),
1848
- });
1849
- // One rectangle per writable area; the washes above cover the union bbox
1850
- // and the gutters between areas stay unmarked on purpose.
1851
- for (var i = 0; i < areas.length; i++) {
1852
- var area = areas[i];
1853
- var rect = el("div", "plan-storyboard-zone-canvas");
1854
- rect.style.left = pctOf(area.x);
1855
- rect.style.top = pctOf(area.y);
1856
- rect.style.width = pctOf(area.w);
1857
- rect.style.height = pctOf(area.h);
1858
- var chip = el("span", "plan-storyboard-zone-label");
1859
- chip.textContent = areas.length > 1 ? "Content area " + (i + 1) : "Content area";
1860
- rect.appendChild(chip);
1861
- zones.appendChild(rect);
1655
+ function onOutside(e) {
1656
+ if (!root.contains(e.target)) close(false);
1862
1657
  }
1863
- return zones;
1864
- }
1865
-
1866
- // Per-image-slot prompt fields drawn INSIDE the bound layout's image
1867
- // placeholders. The value lives structurally in `imagePrompts` (keyed by the
1868
- // slot's codegen prop name); a populated prompt is binding guidance for the
1869
- // downstream image search/generation, empty = the AI decides. Own layer
1870
- // after the blocks (paint order = DOM order), click-through except the fields.
1871
- function renderImagePrompts(segment, data, slots) {
1872
- var layer = el("div", "plan-storyboard-image-layer");
1873
- slots.forEach(function (slot) {
1874
- var value = (data.imagePrompts && data.imagePrompts[slot.key]) || "";
1875
- var box = el("div", "plan-storyboard-image-slot" + (value.trim() ? " plan-storyboard-image-slot--filled" : ""));
1876
- box.style.left = pctOf(slot.x);
1877
- box.style.top = pctOf(slot.y);
1878
- box.style.width = pctOf(slot.w);
1879
- box.style.height = pctOf(slot.h);
1880
- box.appendChild(el("span", "plan-storyboard-image-caption", "Image idea"));
1881
- var input = document.createElement("textarea");
1882
- input.className = "plan-storyboard-image-input";
1883
- input.value = value;
1884
- input.maxLength = PLAN.MAX_IMAGE_PROMPT_LENGTH;
1885
- input.placeholder = "Describe the image for this slot — leave empty to let the AI decide";
1886
- input.disabled = locked();
1887
- input.setAttribute("aria-label", "Image idea " + slot.key);
1888
- input.addEventListener("blur", function () {
1889
- var trimmed = input.value.trim().slice(0, PLAN.MAX_IMAGE_PROMPT_LENGTH);
1890
- if (trimmed === value.trim()) return;
1891
- var next = {};
1892
- for (var k in data.imagePrompts || {}) next[k] = data.imagePrompts[k];
1893
- if (trimmed) next[slot.key] = trimmed;
1894
- else delete next[slot.key];
1895
- var rest = {};
1896
- for (var k2 in data) if (k2 !== "imagePrompts") rest[k2] = data[k2];
1897
- if (Object.keys(next).length) rest.imagePrompts = next;
1898
- commitStoryboard(segment, rest);
1658
+ function open() {
1659
+ menu = el("div", spec.menuClass);
1660
+ menu.setAttribute("role", "listbox");
1661
+ spec.items.forEach(function (item) {
1662
+ var selected = !!spec.isSelected(item);
1663
+ var btn = document.createElement("button");
1664
+ btn.type = "button";
1665
+ btn.className = spec.optionClass + (selected ? " is-selected" : "");
1666
+ btn.setAttribute("role", "option");
1667
+ btn.setAttribute("aria-selected", selected ? "true" : "false");
1668
+ spec.render(btn, item, selected);
1669
+ btn.addEventListener("click", function () {
1670
+ close(true);
1671
+ spec.onPick(item);
1672
+ });
1673
+ menu.appendChild(btn);
1899
1674
  });
1900
- box.appendChild(input);
1901
- layer.appendChild(box);
1675
+ root.appendChild(menu);
1676
+ trigger.setAttribute("aria-expanded", "true");
1677
+ document.addEventListener("pointerdown", onOutside, true);
1678
+ var all = options();
1679
+ var first = menu.querySelector('[aria-selected="true"]') || all[0];
1680
+ if (first) first.focus();
1681
+ }
1682
+ trigger.addEventListener("click", function () {
1683
+ if (menu) close(true);
1684
+ else open();
1685
+ });
1686
+ root.addEventListener("keydown", function (e) {
1687
+ if (!menu) {
1688
+ if (e.target === trigger && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
1689
+ e.preventDefault();
1690
+ open();
1691
+ }
1692
+ return;
1693
+ }
1694
+ if (e.key === "Escape") {
1695
+ e.preventDefault();
1696
+ e.stopPropagation();
1697
+ close(true);
1698
+ } else if (e.key === "Tab") {
1699
+ close(false);
1700
+ } else if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Home" || e.key === "End") {
1701
+ e.preventDefault();
1702
+ var all = options();
1703
+ var at = all.indexOf(document.activeElement);
1704
+ var next = e.key === "Home" ? 0 : e.key === "End" ? all.length - 1 : e.key === "ArrowDown" ? Math.min(at + 1, all.length - 1) : Math.max(at - 1, 0);
1705
+ if (all[next]) all[next].focus();
1706
+ }
1902
1707
  });
1903
- return layer;
1904
1708
  }
1905
1709
 
1906
- function renderCanvas(segment) {
1907
- var data = segment.data;
1908
- var figure = el("figure", "plan-storyboard");
1909
- var selected = selectedPlanLayout(data);
1910
- var chromeUrl = selected && selected.previewUrl ? selected.previewUrl : null;
1911
- var zone = selected && selected.canvasBounds ? selected.canvasBounds : null;
1912
- var areas = selected && selected.contentAreas && selected.contentAreas.length ? selected.contentAreas : zone ? [zone] : [];
1913
- // A known layout WITHOUT a canvas has no plannable region — wash the whole
1914
- // frame + notice instead of presenting the generic guides as its zone.
1915
- var noCanvas = Boolean(selected) && !zone;
1916
- var showBands = !selected && !chromeUrl;
1917
-
1918
- var addBtn = null;
1919
- if (!locked()) {
1920
- addBtn = el("button", "plan-storyboard-add", ICONS.plus + "<span>Add container</span>");
1921
- addBtn.type = "button";
1922
- addBtn.title = "Add container";
1923
- addBtn.setAttribute("aria-label", "Add container");
1924
- addBtn.disabled = data.blocks.length >= PLAN.MAX_BLOCKS;
1925
- }
1926
-
1927
- var header = el("div", "plan-storyboard-header");
1928
- header.appendChild(el("span", null, "Slide " + data.slide + " body storyboard"));
1929
- var legend = el("span", "plan-storyboard-legend");
1930
- legend.appendChild(el("span", "plan-storyboard-legend-item plan-storyboard-legend-item--element", "Design element"));
1931
- legend.appendChild(el("span", "plan-storyboard-legend-item plan-storyboard-legend-item--content", "Content"));
1932
- if (!showBands && addBtn) legend.appendChild(addBtn);
1933
- header.appendChild(legend);
1934
- figure.appendChild(header);
1935
-
1936
- var layout = el("div", "plan-storyboard-layout");
1937
- var frame = el(
1938
- "div",
1939
- chromeUrl
1940
- ? "plan-storyboard-frame plan-storyboard-frame--chrome slide-emboss"
1941
- : "plan-storyboard-frame slide-emboss",
1942
- );
1943
- if (chromeUrl) {
1944
- var chromeImg = document.createElement("img");
1945
- chromeImg.className = "plan-storyboard-chrome";
1946
- chromeImg.src = chromeUrl;
1947
- chromeImg.alt = "";
1948
- chromeImg.setAttribute("aria-hidden", "true");
1949
- frame.appendChild(chromeImg);
1950
- }
1951
- // Abstract guides grid ONLY without a template: once the real layout
1952
- // render is the ground, its own artwork is the guide.
1953
- if (showBands) {
1954
- var grid = el("div", "plan-storyboard-body");
1955
- grid.setAttribute("aria-hidden", "true");
1956
- frame.appendChild(grid);
1710
+ // ---------- the design picker (rail head, above the layout default) ----------
1711
+ // Every design cached on this machine, each shown as its name and accent
1712
+ // colours. A pick swaps the layout palette and moves every slide whose layout
1713
+ // the new design lacks onto its nearest equivalent (remapStoryboardLayouts);
1714
+ // the deck is rebound only when the answer is sent. Bundle strings reach
1715
+ // textContent only, and a colour must be a plain hex to reach a style.
1716
+ function designSwatches(design) {
1717
+ var dots = el("span", "plan-design-dots");
1718
+ dots.setAttribute("aria-hidden", "true");
1719
+ (design.accents || []).forEach(function (hex) {
1720
+ if (!/^#[0-9a-f]{6}$/i.test(String(hex))) return;
1721
+ var dot = el("i", "plan-design-dot");
1722
+ dot.style.background = hex;
1723
+ dots.appendChild(dot);
1724
+ });
1725
+ return dots;
1726
+ }
1727
+ function designLabel(node, design) {
1728
+ var name = el("span", "plan-design-name");
1729
+ name.textContent = design ? design.name : "Choose a design";
1730
+ node.appendChild(name);
1731
+ if (design && design.cached === false) node.appendChild(el("span", "plan-design-get", "")).textContent = "Download";
1732
+ else if (design) node.appendChild(designSwatches(design));
1733
+ }
1734
+ // The design picker stays a dropdown while the plan can change — even when
1735
+ // the one design on offer is already picked: the rest of the menu (every
1736
+ // design a connected backend serves) loads after the page does (loadDesigns),
1737
+ // and a design this machine has not downloaded yet is fetched when picked
1738
+ // (downloadDesign), then applied like any other.
1739
+ var designPickerEl = null; // the picker on screen, swapped when its state changes
1740
+ var designBusy = null; // the design being downloaded, null otherwise
1741
+ var designsAsked = false; // the backend's part of the menu was asked for
1742
+ function renderDesignPicker() {
1743
+ var options = designOptions();
1744
+ if (!options.length && !CFG.designsUrl) return null;
1745
+ var shown = shownDesign();
1746
+ var canPick = !locked() && !designBusy;
1747
+ var root = el("div", "plan-design-picker");
1748
+ var current = document.createElement("button");
1749
+ current.type = "button";
1750
+ current.className = "plan-design-current" + (designBusy ? " is-busy" : "");
1751
+ current.setAttribute("aria-label", "Choose the deck's design");
1752
+ current.disabled = !canPick;
1753
+ if (designBusy) current.appendChild(el("span", "plan-design-name", "")).textContent = "Downloading " + designBusy.name + "…";
1754
+ else designLabel(current, shown);
1755
+ if (canPick) {
1756
+ var chevron = el("span", "plan-storyboard-layout-chevron", CHEVRON);
1757
+ chevron.setAttribute("aria-hidden", "true");
1758
+ current.appendChild(chevron);
1957
1759
  }
1958
- // Placeholder outlines go UNDER the washes: template ground, muted by the
1959
- // no-go wash instead of competing with the sketch as another line system.
1960
- if (selected && Array.isArray(selected.placeholders) && selected.placeholders.length) {
1961
- var phLayer = el("div", "plan-storyboard-zones");
1962
- phLayer.setAttribute("aria-hidden", "true");
1963
- selected.placeholders.forEach(function (ph) {
1964
- var box = el("div", "plan-storyboard-ph");
1965
- box.style.left = pctOf(ph.x);
1966
- box.style.top = pctOf(ph.y);
1967
- box.style.width = pctOf(ph.w);
1968
- box.style.height = pctOf(ph.h);
1969
- if (ph.label) {
1970
- var chip = el("span", "plan-storyboard-ph-label");
1971
- chip.textContent = ph.label;
1972
- box.appendChild(chip);
1760
+ root.appendChild(current);
1761
+ designPickerEl = root;
1762
+ if (!canPick) return root;
1763
+ attachMenu(root, current, {
1764
+ menuClass: "plan-design-menu",
1765
+ optionClass: "plan-design-option",
1766
+ items: options, // read when the menu opens: designs that arrive later are in it
1767
+ isSelected: function (design) {
1768
+ return design === shown;
1769
+ },
1770
+ render: function (node, design) {
1771
+ designLabel(node, design);
1772
+ // not on this computer yet: a quieter, smaller row under a divider
1773
+ if (design.cached === false) {
1774
+ node.classList.add("is-remote");
1775
+ var first = null;
1776
+ for (var i = 0; i < options.length; i++) {
1777
+ if (options[i].cached === false) {
1778
+ first = options[i];
1779
+ break;
1780
+ }
1781
+ }
1782
+ if (design === first) node.classList.add("is-first-remote");
1973
1783
  }
1974
- phLayer.appendChild(box);
1784
+ },
1785
+ onPick: function (design) {
1786
+ if (design.cached === false) downloadDesign(design);
1787
+ else pickDesign(design.presFP);
1788
+ },
1789
+ });
1790
+ return root;
1791
+ }
1792
+ function refreshDesignPicker() {
1793
+ var old = designPickerEl;
1794
+ if (!old || !old.parentNode) return;
1795
+ var next = renderDesignPicker();
1796
+ if (next) old.parentNode.replaceChild(next, old);
1797
+ }
1798
+ // Once per page: the designs a connected backend serves that this machine has
1799
+ // not downloaded. A backend that does not answer leaves the menu as it is.
1800
+ function loadDesigns() {
1801
+ if (!CFG.designsUrl || designsAsked) return;
1802
+ designsAsked = true;
1803
+ if (!Array.isArray(CFG.designs)) CFG.designs = [];
1804
+ fetch(CFG.designsUrl, { credentials: "same-origin" })
1805
+ .then(function (r) {
1806
+ return r.ok ? r.json() : null;
1807
+ })
1808
+ .then(function (j) {
1809
+ if (!j || !Array.isArray(j.designs)) return;
1810
+ var all = designOptions();
1811
+ var have = {};
1812
+ all.forEach(function (d) {
1813
+ have[d.presFP] = true;
1814
+ });
1815
+ var added = 0;
1816
+ j.designs.forEach(function (d) {
1817
+ if (d && typeof d.presFP === "string" && typeof d.name === "string" && !have[d.presFP]) {
1818
+ all.push({ presFP: d.presFP, name: d.name, accents: [], current: false, cached: false });
1819
+ added++;
1820
+ }
1821
+ });
1822
+ if (!added) return;
1823
+ // what this machine has first, what would still have to be downloaded last
1824
+ all.sort(function (a, b) {
1825
+ var ra = a.cached === false ? 1 : 0;
1826
+ var rb = b.cached === false ? 1 : 0;
1827
+ return ra - rb || String(a.name).localeCompare(String(b.name));
1828
+ });
1829
+ refreshDesignPicker();
1830
+ })
1831
+ .catch(function () {
1832
+ /* the menu stays at what this machine has */
1975
1833
  });
1976
- frame.appendChild(phLayer);
1977
- }
1978
- if (zone) frame.appendChild(renderZoneOverlay(zone, areas));
1979
-
1980
- var top = el("div", "plan-storyboard-reserved plan-storyboard-reserved-top");
1981
- top.appendChild(document.createTextNode("Template header/title area not planned here"));
1982
- if (addBtn) {
1983
- var add = addBtn;
1984
- add.addEventListener("click", function () {
1985
- if (data.blocks.length >= PLAN.MAX_BLOCKS) return;
1986
- var serial = data.blocks.length + 1;
1987
- while (
1988
- data.blocks.some(function (b) {
1989
- return b.id === "block-" + serial;
1834
+ }
1835
+ // A picked design this machine does not have yet: the workspace downloads it
1836
+ // (POST, carrying the gate token like the answer does), and its entry —
1837
+ // palette, layouts — replaces the bare one before it is applied.
1838
+ function downloadDesign(design) {
1839
+ if (designBusy || locked() || !CFG.designsUrl) return;
1840
+ designBusy = design;
1841
+ SHELL.clearNotice("design");
1842
+ refreshDesignPicker();
1843
+ var url = CFG.designsUrl + "/" + encodeURIComponent(design.presFP) + "?t=" + encodeURIComponent(CFG.designsGate || "");
1844
+ fetch(url, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: "{}" })
1845
+ .then(function (r) {
1846
+ return r
1847
+ .json()
1848
+ .catch(function () {
1849
+ return null;
1990
1850
  })
1991
- )
1992
- serial += 1;
1993
- var A = PLAN.BODY_SAFE_AREA;
1994
- var w = 360;
1995
- var h = 150;
1996
- var offset = (data.blocks.length % 4) * 48;
1997
- var nextBlocks = data.blocks.concat([
1998
- {
1999
- id: "block-" + serial,
2000
- x: clamp(A.x + 40 + offset, A.x, A.x + A.w - w),
2001
- y: clamp(A.y + 40 + offset, A.y, A.y + A.h - h),
2002
- w: w,
2003
- h: h,
2004
- designElement: "container",
2005
- content: "New content",
2006
- },
2007
- ]);
2008
- commitStoryboard(segment, withBlocks(data, nextBlocks));
1851
+ .then(function (j) {
1852
+ return r.ok && j && j.design && j.design.presFP === design.presFP ? j.design : null;
1853
+ });
1854
+ })
1855
+ .then(function (fresh) {
1856
+ designBusy = null;
1857
+ if (!fresh) throw new Error("no design");
1858
+ var all = designOptions();
1859
+ for (var i = 0; i < all.length; i++) if (all[i].presFP === fresh.presFP) all[i] = fresh;
1860
+ pickDesign(fresh.presFP); // re-renders the surface, this picker with it
1861
+ refreshDesignPicker(); // …or not, when the pick changed nothing
1862
+ })
1863
+ .catch(function () {
1864
+ designBusy = null;
1865
+ refreshDesignPicker();
1866
+ SHELL.showNotice("design", { tone: "warn", title: "That design could not be downloaded", text: "Check your connection, then pick it again.", dismiss: true });
2009
1867
  });
2010
- if (showBands) top.appendChild(add);
1868
+ }
1869
+ function pickDesign(presFP) {
1870
+ if (locked()) return;
1871
+ var target = null;
1872
+ designOptions().forEach(function (d) {
1873
+ if (d.presFP === presFP) target = d;
1874
+ });
1875
+ if (!target || target === shownDesign()) return;
1876
+ if (!designUndo || S.value !== designUndo.after) designUndo = { before: S.value, from: layoutOptions() };
1877
+ S.design = target.current ? null : presFP;
1878
+ var next = PLAN.remapStoryboardLayouts(designUndo.before, layoutOptions(), designUndo.from);
1879
+ designUndo.after = next;
1880
+ if (next === S.value) {
1881
+ saveEdits(); // the layouts all exist in both designs: only the pick changed
1882
+ refreshChrome();
1883
+ renderSurface();
1884
+ } else {
1885
+ commitPlan(next);
2011
1886
  }
2012
- if (showBands) frame.appendChild(top);
2013
-
1887
+ }
2014
1888
 
2015
- var canRemove = !locked() && data.blocks.length > 1;
2016
- data.blocks.forEach(function (block) {
2017
- frame.appendChild(renderBlock(frame, block, data, segment, canRemove));
2018
- });
1889
+ function commitPlan(nextValue) {
1890
+ if (locked()) return;
1891
+ S.value = nextValue;
1892
+ saveEdits();
1893
+ refreshChrome();
1894
+ renderSurface();
1895
+ }
1896
+ function commitStoryboard(segment, nextData) {
1897
+ commitPlan(PLAN.replaceStoryboardSegment(S.value, segment, nextData));
1898
+ }
2019
1899
 
2020
- if (showBands) {
2021
- var bottom = el("div", "plan-storyboard-reserved plan-storyboard-reserved-bottom", "Footer/template area not planned here");
2022
- frame.appendChild(bottom);
1900
+ function renderDetailField(className, ariaLabel, value, multiline, onCommit) {
1901
+ var field = el("div", className);
1902
+ field.setAttribute("role", "textbox");
1903
+ field.setAttribute("aria-label", ariaLabel);
1904
+ if (multiline) field.setAttribute("aria-multiline", "true");
1905
+ field.textContent = value;
1906
+ if (!locked()) {
1907
+ field.contentEditable = "true";
1908
+ field.spellcheck = true;
2023
1909
  }
2024
- var imageSlots = selected && Array.isArray(selected.placeholders)
2025
- ? selected.placeholders.filter(function (ph) { return typeof ph.key === "string"; })
2026
- : [];
2027
- if (imageSlots.length) frame.appendChild(renderImagePrompts(segment, data, imageSlots));
2028
- if (noCanvas) {
2029
- // Own top layer, click-through: the warning must never hide behind blocks.
2030
- var noticeLayer = el("div", "plan-storyboard-zone-notice-layer");
2031
- noticeLayer.setAttribute("aria-hidden", "true");
2032
- var notice = el("span", "plan-storyboard-zone-notice");
2033
- notice.textContent =
2034
- "No content area on this layout — pick a layout with a content body";
2035
- noticeLayer.appendChild(notice);
2036
- frame.appendChild(noticeLayer);
1910
+ // Commit-once guard: a commit re-renders the surface and detaches this
1911
+ // focused field, firing a second blur \u2014 which must not re-splice the stale
1912
+ // segment (double-commit \u2192 corrupted fences on the single-line beat field).
1913
+ var committed = false;
1914
+ function commit() {
1915
+ if (committed) return;
1916
+ committed = true;
1917
+ var normalized = (field.textContent || "").replace(/\u00a0/g, " ").trim();
1918
+ field.textContent = normalized;
1919
+ if (normalized !== value) onCommit(normalized);
2037
1920
  }
2038
-
2039
- layout.appendChild(frame);
2040
- layout.appendChild(renderDetails(data, segment));
2041
- figure.appendChild(layout);
2042
-
2043
- if (segment.warnings && segment.warnings.length) {
2044
- var ul = el("ul", "plan-storyboard-warnings");
2045
- segment.warnings.forEach(function (w) {
2046
- var li = document.createElement("li");
2047
- li.textContent = w;
2048
- ul.appendChild(li);
1921
+ field.addEventListener("blur", commit);
1922
+ if (!multiline) {
1923
+ // Enter blurs; the blur handler is the single committer.
1924
+ field.addEventListener("keydown", function (e) {
1925
+ if (e.key !== "Enter") return;
1926
+ e.preventDefault();
1927
+ field.blur();
2049
1928
  });
2050
- figure.appendChild(ul);
2051
1929
  }
2052
- return figure;
1930
+ return field;
1931
+ }
1932
+
1933
+ // The planned layout, when the record names one the bundle has: its chrome
1934
+ // render grounds the stage and its content region places the slide card.
1935
+ function selectedPlanLayout(data) {
1936
+ var options = layoutOptions();
1937
+ if (!data.layout) return null;
1938
+ for (var i = 0; i < options.length; i++) {
1939
+ if (options[i].componentName === data.layout) return options[i];
1940
+ }
1941
+ return null;
1942
+ }
1943
+
1944
+ function pctOf(v) {
1945
+ return (v * 100).toFixed(2) + "%";
2053
1946
  }
2054
1947
 
2055
1948
  // ---------- comment ledger ----------
2056
1949
  // The anchor a comment written right now would carry. "This slide" reads the
2057
- // SELECTED strip tab; without a storyboard strip there is nothing to anchor
2058
- // to, so everything is whole-story.
1950
+ // SELECTED rail card; without storyboards there is nothing to anchor to, so
1951
+ // everything is whole-story.
2059
1952
  function currentAnchor() {
2060
1953
  var beats = planBeats();
2061
1954
  if (!beats.length || commentScope !== "slide") {
@@ -2071,7 +1964,7 @@
2071
1964
  return "c-" + Date.now() + "-" + Math.random().toString(16).slice(2);
2072
1965
  }
2073
1966
  }
2074
- function newComment(text, forceScope) {
1967
+ function newComment(text, forceScope, status) {
2075
1968
  var a = forceScope === "story" ? { scope: "story", slideIndex: null, slideHeading: null } : currentAnchor();
2076
1969
  return {
2077
1970
  id: newId(),
@@ -2081,74 +1974,93 @@
2081
1974
  slideHeading: a.slideHeading,
2082
1975
  text: text,
2083
1976
  at: new Date().toISOString(),
2084
- status: "sent",
1977
+ status: status || "sent",
2085
1978
  };
2086
1979
  }
1980
+ // Comments queue up as drafts (Enter); ONE send carries the batch — the
1981
+ // reviewer reads the whole plan, notes everything, then answers once (every
1982
+ // answer is one paste to the agent, so one batch is one round trip).
1983
+ function draftComments() {
1984
+ return ledger.filter(function (c) {
1985
+ return c.status === "draft";
1986
+ });
1987
+ }
1988
+ function addDraft() {
1989
+ if (working) return;
1990
+ var typed = commentText();
1991
+ if (!typed) return;
1992
+ ledger.push(newComment(typed, null, "draft"));
1993
+ feedbackText = "";
1994
+ if (commentEl) commentEl.value = "";
1995
+ saveLedger();
1996
+ saveCtx();
1997
+ renderComposer();
1998
+ if (commentEl) commentEl.focus();
1999
+ }
2000
+ function deleteDraft(c) {
2001
+ var i = ledger.indexOf(c);
2002
+ if (i >= 0) ledger.splice(i, 1);
2003
+ saveLedger();
2004
+ renderComposer();
2005
+ }
2087
2006
  function anchorLabel(c) {
2088
2007
  if (c.scope !== "slide") return "Whole story";
2089
2008
  return "Slide " + (c.slideIndex == null ? "?" : c.slideIndex);
2090
2009
  }
2091
- function statusLabel(c) {
2092
- return SES.statusLabel(c);
2093
- }
2094
- // The rail: every comment of this session, newest last, with its anchor and
2095
- // where it stands. This is what replaced the single "previous round" note —
2096
- // it survives rounds and shows every comment the reviewer sent.
2097
- function ledgerRail() {
2098
- var rail = el("div", "sm-ledger");
2099
- ledger.forEach(function (c) {
2100
- var row = el("div", "sm-ledger-row");
2101
- row.appendChild(el("span", "sm-ledger-anchor", anchorLabel(c)));
2102
- var txt = el("span", "sm-ledger-text");
2103
- txt.textContent = c.text;
2104
- row.appendChild(txt);
2105
- var chip = el("span", "sm-ledger-status");
2106
- chip.setAttribute("data-state", c.status);
2107
- chip.textContent = statusLabel(c);
2108
- row.appendChild(chip);
2109
- rail.appendChild(row);
2010
+ // The comments not sent yet — the only ones the composer shows. Sent and
2011
+ // worked-in comments stay in the ledger (the next round marks them) but not
2012
+ // on screen: what the reviewer needs here is what this answer will carry.
2013
+ function draftList(drafts) {
2014
+ var list = el("ul", "sm-drafts");
2015
+ drafts.forEach(function (c) {
2016
+ var row = el("li", "sm-draft");
2017
+ row.appendChild(el("span", "sm-draft-anchor", "")).textContent = anchorLabel(c);
2018
+ var text = row.appendChild(el("span", "sm-draft-text", ""));
2019
+ text.textContent = c.text;
2020
+ text.title = c.text;
2021
+ if (!SHELL.inFlight(c.id)) {
2022
+ var del = el("button", "sm-draft-del", ICONS.close);
2023
+ del.type = "button";
2024
+ del.title = "Remove this comment";
2025
+ del.setAttribute("aria-label", "Remove this comment");
2026
+ del.onclick = function () {
2027
+ deleteDraft(c);
2028
+ };
2029
+ row.appendChild(del);
2030
+ }
2031
+ list.appendChild(row);
2110
2032
  });
2111
- return rail;
2112
- }
2113
-
2114
- // ---------- chat composer bar ----------
2115
- // Rebuilt on structural state changes (a storyline draft staged / discarded,
2116
- // a mode or selection change), NOT on every keystroke (keystrokes only call
2117
- // refreshChrome). The typed feedback survives a rebuild via feedbackText.
2033
+ return list;
2034
+ }
2035
+
2036
+ // ---------- the composer ----------
2037
+ // One row: what the comment is about, the comment, Send (only once there is
2038
+ // something to send) and Approve (off while there is). Rebuilt on structural
2039
+ // changes (a draft added / removed, a storyline staged, the selected slide),
2040
+ // NOT on every keystroke (keystrokes only call refreshChrome); the typed text
2041
+ // survives a rebuild via feedbackText.
2042
+ //
2043
+ // While the agent has the next move (WORKING) the composer is gone: the
2044
+ // answer is saved, the hand-off dialog said what to do with it, and the bar's
2045
+ // hint says commenting opens again — nothing to type into, nothing greyed out.
2118
2046
  function renderComposer() {
2119
2047
  if (!composerEl) return;
2120
2048
  composerEl.innerHTML = "";
2121
-
2049
+ composerEl.hidden = working;
2122
2050
  if (working) {
2123
- var status = el("div", "sm-wait");
2124
- status.appendChild(el("div", "sm-spin"));
2125
- var note = el("div");
2126
- note.innerHTML =
2127
- "<b>" +
2128
- (workingNote || "Comments sent.") +
2129
- "</b> The agent is revising the plan — this tab refreshes when the next " +
2130
- "round is ready. Keep reading and navigating; comment again on that round.";
2131
- status.appendChild(note);
2132
- composerEl.appendChild(status);
2133
- // The permission prompt was raised at the send click (a user gesture, the
2134
- // only context a browser shows it in); once granted, say so.
2135
- (notifyPermPromise || Promise.resolve(SES.notifyPermission())).then(function (perm) {
2136
- if (perm === "granted") {
2137
- note.innerHTML += " You'll get a notification when the next round needs you.";
2138
- }
2139
- });
2051
+ commentEl = sendBtn = approveBtn = null;
2052
+ refreshChrome();
2053
+ return;
2140
2054
  }
2141
2055
 
2142
- if (!working && S.storyDraft) {
2056
+ if (S.storyDraft) {
2143
2057
  var banner = el("div", "sm-story-banner");
2144
- banner.innerHTML =
2145
- ICONS.sparkles +
2146
- '<span class="txt"><b>Storyline edited.</b> Sending asks the agent to build storyboards and content for your new and renamed slides — add comments below if you like.</span>';
2058
+ banner.innerHTML = ICONS.sparkles + '<span class="txt"><b>Storyline changed.</b> Send it and your agent fills in the new and renamed slides.</span>';
2147
2059
  var discard = el("button", "sm-story-discard", "Discard changes");
2148
2060
  discard.type = "button";
2149
2061
  discard.onclick = function () {
2150
2062
  S.storyDraft = null;
2151
- approveArmed = false; // discarding the storyline disarms approve-confirm
2063
+ saveEdits();
2152
2064
  refreshChrome();
2153
2065
  renderComposer();
2154
2066
  renderSurface();
@@ -2157,88 +2069,92 @@
2157
2069
  composerEl.appendChild(banner);
2158
2070
  }
2159
2071
 
2160
- if (ledger.length) composerEl.appendChild(ledgerRail());
2072
+ var drafts = draftComments();
2073
+ if (drafts.length) composerEl.appendChild(draftList(drafts));
2074
+
2075
+ var row = el("div", "sm-composer-row");
2076
+ var box = el("div", "sm-chat-box");
2161
2077
 
2162
- // Scope control — "This slide" reads the selected strip tab. Hidden when
2163
- // there is no strip to anchor to (plain-markdown plans).
2078
+ // What the comment is about: the slide on stage, or the whole story. A plan
2079
+ // with no slides to anchor to is whole-story only, and shows no choice.
2164
2080
  var beats = planBeats();
2165
2081
  if (!beats.length) commentScope = "story";
2166
2082
  if (beats.length) {
2167
- var meta = el("div", "sm-composer-meta");
2168
- meta.appendChild(el("span", "sm-scope-label", "Comment on"));
2169
- var scopeBox = el("div", "sm-scope");
2170
2083
  var active = clamp(S.selectedIndex, 0, beats.length - 1);
2171
- [
2172
- { key: "slide", label: "Slide " + beats[active].slide },
2173
- { key: "story", label: "Whole story" },
2174
- ].forEach(function (opt) {
2175
- var b = el("button", "sm-scope-btn" + (commentScope === opt.key ? " is-on" : ""), opt.label);
2176
- b.type = "button";
2177
- b.onclick = function () {
2178
- commentScope = opt.key;
2179
- renderComposer();
2084
+ var targets = [
2085
+ { key: "slide", label: "Slide " + beats[active].slide, detail: beats[active].message },
2086
+ { key: "story", label: "Whole story", detail: "The storyline as a whole" },
2087
+ ];
2088
+ var wrap = el("div", "sm-target-wrap");
2089
+ var target = el("button", "sm-target");
2090
+ target.type = "button";
2091
+ target.setAttribute("aria-label", "What your comment is about");
2092
+ target.title = "What your comment is about";
2093
+ var targetLabel = el("span", "sm-target-label");
2094
+ targetLabel.textContent = targets[commentScope === "story" ? 1 : 0].label;
2095
+ target.appendChild(targetLabel);
2096
+ target.appendChild(el("span", "sm-target-chevron", CHEVRON));
2097
+ wrap.appendChild(target);
2098
+ attachMenu(wrap, target, {
2099
+ menuClass: "sm-target-menu",
2100
+ optionClass: "sm-target-option",
2101
+ items: targets,
2102
+ isSelected: function (t) {
2103
+ return t.key === commentScope;
2104
+ },
2105
+ render: function (node, t, selected) {
2106
+ var text = el("span", "sm-target-text");
2107
+ text.appendChild(el("span", "sm-target-option-label")).textContent = t.label;
2108
+ text.appendChild(el("span", "sm-target-option-detail")).textContent = t.detail;
2109
+ node.appendChild(text);
2110
+ node.appendChild(el("span", "sm-target-check", selected ? ICONS.check : ""));
2111
+ },
2112
+ onPick: function (t) {
2113
+ commentScope = t.key;
2114
+ targetLabel.textContent = t.label;
2180
2115
  if (commentEl) commentEl.focus();
2181
- };
2182
- scopeBox.appendChild(b);
2116
+ },
2183
2117
  });
2184
- meta.appendChild(scopeBox);
2185
- composerEl.appendChild(meta);
2118
+ box.appendChild(wrap);
2186
2119
  }
2187
2120
 
2188
- var row = el("div", "sm-composer-row");
2189
2121
  commentEl = el("textarea", "sm-chat-ta");
2190
2122
  commentEl.rows = 1;
2191
- commentEl.placeholder = working
2192
- ? "The agent is revising — comment on the next round."
2193
- : "What should change? Leave a note, or edit the plan directly.";
2194
- commentEl.disabled = working;
2195
2123
  commentEl.value = feedbackText;
2124
+ commentEl.placeholder = drafts.length ? "Add another comment…" : "What should change? Press Enter to add it.";
2125
+ commentEl.setAttribute("aria-label", "Your comment");
2196
2126
  commentEl.addEventListener("input", function () {
2197
2127
  feedbackText = commentEl.value;
2198
- approveArmed = false; // typing more feedback disarms a pending approve-confirm
2199
2128
  commentEl.style.height = "auto";
2200
2129
  commentEl.style.height = Math.min(commentEl.scrollHeight, 150) + "px";
2201
2130
  saveDraftSoon(); // an unsent draft survives the next round's reload
2202
2131
  refreshChrome();
2203
2132
  });
2204
2133
  commentEl.addEventListener("keydown", function (e) {
2205
- if ((e.metaKey || e.ctrlKey) && e.key === "Enter") requestChanges();
2134
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") return requestChanges();
2135
+ if (e.key === "Enter" && !e.shiftKey) {
2136
+ e.preventDefault();
2137
+ addDraft();
2138
+ }
2206
2139
  });
2207
- row.appendChild(commentEl);
2140
+ box.appendChild(commentEl);
2141
+ row.appendChild(box);
2208
2142
 
2209
- if (!working && S.storyDraft) {
2210
- // The round is sendable as-is — "send" grows the story-iteration label.
2211
- sendBtn = el("button", "sm-send-pill", ICONS.sparkles + "<span>Iterate on the story</span>");
2212
- sendBtn.type = "button";
2213
- sendBtn.title = "Iterate on the story";
2214
- } else {
2215
- sendBtn = el("button", "sm-send", ICONS.send);
2216
- sendBtn.type = "button";
2217
- sendBtn.title = working ? "The agent is revising — send on the next round" : "Send feedback";
2218
- sendBtn.setAttribute("aria-label", sendBtn.title);
2219
- sendBtn.disabled = working;
2220
- }
2143
+ sendBtn = el("button", "sm-send-pill", (S.storyDraft ? ICONS.sparkles : ICONS.send) + '<span class="n">Send</span>');
2144
+ sendBtn.type = "button";
2145
+ sendBtn.title = S.storyDraft ? "Send your storyline changes and comments" : "Send your comments (⌘/Ctrl+Enter)";
2221
2146
  sendBtn.onclick = requestChanges;
2222
2147
  row.appendChild(sendBtn);
2223
2148
 
2224
- row.appendChild(el("div", "sm-composer-sep"));
2225
-
2226
- approveBtn = el("button", "sm-approve");
2149
+ approveBtn = el("button", "sm-approve", ICONS.check + '<span class="n">Approve plan</span>');
2227
2150
  approveBtn.type = "button";
2228
- approveBtn.innerHTML =
2229
- '<span class="main">' +
2230
- ICONS.check +
2231
- "<span>Approve plan</span></span>" +
2232
- '<span class="sub">the agent builds the slides next</span>';
2233
2151
  approveBtn.onclick = function () {
2234
2152
  if (working) return; // superseded round
2153
+ if (SHELL.decisionPending()) return; // one answer at a time
2154
+ if (SHELL.alreadyApproved()) return; // already approved and nobody asks again
2155
+ if (SHELL.notAskedYet()) return; // the agent is still working on it
2235
2156
  if (S.storyDraft) return; // blocked while a storyline draft is staged
2236
- // Approving with unsent feedback in the input discards it — confirm inline.
2237
- if (commentText() && !approveArmed) {
2238
- approveArmed = true;
2239
- refreshChrome();
2240
- return;
2241
- }
2157
+ if (draftComments().length || commentText()) return; // comments go out or away first
2242
2158
  submit("approve");
2243
2159
  };
2244
2160
  row.appendChild(approveBtn);
@@ -2249,7 +2165,7 @@
2249
2165
 
2250
2166
  // ---------- working mode (the agent is revising) ----------
2251
2167
  // The tab neither veils over nor freezes: the plan stays readable and the
2252
- // strip stays navigable; the composer, the editing affordances and Approve go
2168
+ // rail stays navigable; the composer, the editing affordances and Approve go
2253
2169
  // away until the next round. Heartbeat polling waits for the agent's next
2254
2170
  // `slidemind plan` round, which reloads this same tab.
2255
2171
  function enterWorking(note) {
@@ -2264,97 +2180,85 @@
2264
2180
  }
2265
2181
 
2266
2182
  // ---------- submit ----------
2267
- // Send: resolves the gate right now with the typed comment (plus a staged
2183
+ // Send: resolves the gate right now with the listed comments (plus a staged
2268
2184
  // storyline restructure as a whole-story instruction). Off while WORKING.
2269
2185
  function requestChanges() {
2270
2186
  if (working) return;
2271
- var typed = commentText();
2187
+ if (SHELL.decisionPending()) return; // the answer on its way carries no late additions
2188
+ addDraft(); // whatever is still typed goes with the batch
2189
+ var drafts = draftComments();
2272
2190
  var batch = [];
2273
2191
  if (S.storyDraft) {
2274
2192
  var staged = PLAN.stageStoryIteration(S.value, S.storyDraft);
2275
2193
  if (!staged) return;
2276
2194
  // A storyline restructure is a whole-story instruction by construction.
2277
- batch = batch.concat([
2278
- newComment(typed ? staged.feedback + "\n\n" + typed : staged.feedback, "story"),
2279
- ]);
2195
+ batch = [newComment(staged.feedback, "story")].concat(drafts);
2280
2196
  pendingEdited = staged.editedPlanMarkdown;
2281
2197
  } else {
2282
- if (!typed) return;
2283
- batch = batch.concat([newComment(typed)]);
2198
+ if (!drafts.length) return;
2199
+ batch = drafts;
2284
2200
  pendingEdited = planWasEdited() ? S.value : null;
2285
2201
  }
2286
2202
  pendingComments = batch;
2287
2203
  // Ask for notification permission HERE — the click is a user gesture, the
2288
- // only context where the browser will actually show the prompt.
2289
- notifyPermPromise = SES.requestNotifyPermission();
2204
+ // only context where the browser will actually show the prompt. Standalone
2205
+ // only: in the workspace a permission prompt is friction on the click that
2206
+ // copies the message, and nothing waits in the background there.
2207
+ if (!CFG.embedded) SES.requestNotifyPermission();
2290
2208
  submit("comments");
2291
2209
  }
2292
- function clearDraft() {
2293
- feedbackText = "";
2294
- if (commentEl) commentEl.value = "";
2295
- saveCtx();
2296
- }
2210
+ // One answer at a time (SHELL.decide holds the in-flight latch and carries
2211
+ // the operationId + the plan version it answers). Nothing here changes until
2212
+ // the server SAVED it: decided() below is the success path.
2297
2213
  function submit(decision) {
2298
- if (submitted) return;
2299
- submitted = true;
2300
- var payload = {
2214
+ if (SHELL.decisionPending()) return;
2215
+ SHELL.decide({
2301
2216
  decision: decision,
2302
2217
  comments: decision === "comments" ? pendingComments.map(SES.wire) : [],
2303
2218
  editedPlan: decision === "comments" ? pendingEdited : planWasEdited() ? S.value : null,
2304
- };
2305
- fetch(CFG.endpoint, {
2306
- method: "POST",
2307
- headers: { "Content-Type": "application/json" },
2308
- body: JSON.stringify(payload),
2309
- })
2310
- .then(function (res) {
2311
- if (res.status === 409) {
2312
- // Another tab already decided this run — this round is stale. The
2313
- // typed text stays in the input (restored on the next round's reload)
2314
- // so it isn't lost; disarm the submit latch — leaving it armed made
2315
- // every later send a silent no-op. A staged story restructure is
2316
- // dropped deliberately: its editedPlanMarkdown was computed against
2317
- // the superseded plan and applying it to the next round would corrupt
2318
- // it (same call the add-in dialog makes on round arrival).
2319
- S.storyDraft = null;
2320
- submitted = false;
2321
- enterWorking("Round superseded.");
2322
- return;
2323
- }
2324
- if (!res.ok) throw new Error("gate refused the decision");
2325
- if (decision === "approve") {
2326
- showApproved();
2327
- return;
2328
- }
2329
- pendingComments.forEach(function (c) {
2330
- c.status = "sent";
2331
- if (ledger.indexOf(c) < 0) ledger.push(c);
2332
- });
2333
- saveLedger();
2334
- clearDraft();
2335
- // The staged restructure is now the agent's job — keeping it staged
2336
- // would show the reviewer a storyline the served plan no longer claims.
2337
- S.storyDraft = null;
2338
- enterWorking("Comments sent.");
2339
- })
2340
- .catch(function () {
2341
- submitted = false;
2342
- alert("Could not reach the slidemind plan server — is the terminal command still running?");
2343
- });
2219
+ design: S.design,
2220
+ });
2221
+ refreshChrome();
2344
2222
  }
2345
- function requestTabClose() {
2346
- setTimeout(function () {
2347
- try {
2348
- window.open("", "_self");
2349
- window.close();
2350
- } catch {
2351
- // Some browsers only allow script-opened tabs to close themselves.
2352
- }
2353
- }, 120);
2223
+ // The server saved the answer. The rows it carried are sent — read off the
2224
+ // request itself, so an answer re-sent after a reload marks the right rows —
2225
+ // and the edits that rode with it are the server's now.
2226
+ function decided(decision, body) {
2227
+ baseline = S.value;
2228
+ if (S.design) {
2229
+ // the server rebound the deck: the picked design is its own from here on
2230
+ designOptions().forEach(function (d) {
2231
+ if (d.current) d.layouts = boundLayouts;
2232
+ d.current = d.presFP === S.design;
2233
+ if (d.current) boundLayouts = d.layouts || [];
2234
+ });
2235
+ S.design = null;
2236
+ }
2237
+ designUndo = null;
2238
+ SES.write(EDITS_KEY, null);
2239
+ if (decision === "approve") {
2240
+ showApproved();
2241
+ return;
2242
+ }
2243
+ ((body && body.comments) || []).forEach(function (w) {
2244
+ var row = null;
2245
+ for (var i = 0; i < ledger.length; i++) if (ledger[i].id === w.id) row = ledger[i];
2246
+ if (row) row.status = "sent";
2247
+ else ledger.push(Object.assign({}, w, { status: "sent" })); // the generated story instruction
2248
+ });
2249
+ saveLedger();
2250
+ // The staged restructure is now the agent's job — keeping it staged
2251
+ // would show the reviewer a storyline the served plan no longer claims.
2252
+ S.storyDraft = null;
2253
+ enterWorking(CFG.embedded ? "Comments saved." : "Comments sent.");
2354
2254
  }
2255
+
2355
2256
  // The shared heartbeat poll (gate-session.js) with this gate's two endings:
2356
- // a retired tab simply says where the review went.
2257
+ // a retired tab simply says where the review went. Standalone only — in the
2258
+ // workspace page the shell's monitor watches the round for the page's whole
2259
+ // life, and the page never "ends".
2357
2260
  function startNextRoundPolling() {
2261
+ if (CFG.embedded) return;
2358
2262
  SES.startNextRoundPolling({
2359
2263
  onEnded: showSessionEnded,
2360
2264
  // A retired tab still holding an unsent draft must not discard it: the
@@ -2369,58 +2273,37 @@
2369
2273
 
2370
2274
  // ---------- done veil ----------
2371
2275
  function showVeil(statusHtml, subText, subPulse) {
2372
- if (bar) bar.style.display = "none";
2373
- if (wsEl) wsEl.style.display = "none";
2374
- var existing = document.querySelector(".sm-veil[data-sm-overlay]");
2375
- if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
2376
- var veil = el("div", "sm-o sm-veil");
2377
- veil.setAttribute("data-sm-overlay", "1");
2378
- veil.appendChild(logoNode("sm-logo", "-v"));
2379
- veil.appendChild(el("div", "word", wordmarkHtml()));
2380
- veil.appendChild(el("div", "status", statusHtml));
2381
- veil.appendChild(el("div", "sub" + (subPulse ? " sm-pulse" : ""), subText));
2382
- document.body.appendChild(veil);
2276
+ return SHELL.showVeil(statusHtml, subText, subPulse, [wsEl]);
2383
2277
  }
2278
+ // Embedded, the veil says the agent HAS the approval — only once the wire
2279
+ // proved it (delivered, or picked up later). Until then the approval is saved
2280
+ // and the strip + hand-off say what is still missing.
2384
2281
  function showApproved() {
2385
- showVeil(
2386
- ICONS.check + "<span>Plan approved</span>",
2387
- "The agent is building your slides now — you can close this tab.",
2388
- true,
2389
- );
2390
- setTimeout(requestTabClose, APPROVE_CLOSE_DELAY_MS);
2282
+ if (CFG.embedded) {
2283
+ if (SHELL.state.phase !== "approved") {
2284
+ enterWorking("Plan approved.");
2285
+ return;
2286
+ }
2287
+ // working, so a reload (the approval itself may move the round) restores
2288
+ // this state instead of a fresh "approved — nothing to do" page
2289
+ if (!working) enterWorking("Plan approved.");
2290
+ var picked = SHELL.state.pickedUpAt;
2291
+ showVeil(
2292
+ ICONS.check + "<span>Plan approved</span>",
2293
+ picked ? "Your agent picked it up at " + SHELL.hhmm(picked) + " and is building the slides now." : "Your agent has it and is building the slides now.",
2294
+ true,
2295
+ );
2296
+ return;
2297
+ }
2298
+ showVeil(ICONS.check + "<span>Plan approved</span>", "The agent is building your slides now — you can close this tab.", true);
2299
+ setTimeout(SHELL.requestTabClose, APPROVE_CLOSE_DELAY_MS);
2391
2300
  }
2392
2301
  function showSessionEnded() {
2393
2302
  showVeil("<span>Session ended</span>", "You can close this tab.");
2394
2303
  }
2395
2304
  function showSuperseded() {
2396
2305
  document.title = "Plan review moved — slidemind";
2397
- showVeil(
2398
- "<span>Continued in a new tab</span>",
2399
- "The next plan round opened in a fresh browser tab — you can close this one.",
2400
- );
2401
- }
2402
-
2403
- // ---------- assets ----------
2404
- function injectAssets() {
2405
- var style = el("style");
2406
- style.id = "sm-plan-style";
2407
- style.textContent = CSS;
2408
- document.head.appendChild(style);
2409
-
2410
- var pre1 = el("link");
2411
- pre1.rel = "preconnect";
2412
- pre1.href = "https://fonts.googleapis.com";
2413
- var pre2 = el("link");
2414
- pre2.rel = "preconnect";
2415
- pre2.href = "https://fonts.gstatic.com";
2416
- pre2.crossOrigin = "anonymous";
2417
- var fonts = el("link");
2418
- fonts.rel = "stylesheet";
2419
- fonts.href =
2420
- "https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&family=Urbanist:wght@300;400&display=swap";
2421
- document.head.appendChild(pre1);
2422
- document.head.appendChild(pre2);
2423
- document.head.appendChild(fonts);
2306
+ showVeil("<span>Continued in a new tab</span>", "The next plan round opened in a fresh browser tab — you can close this one.");
2424
2307
  }
2425
2308
 
2426
2309
  // ---------- wire up ----------
@@ -2439,19 +2322,58 @@
2439
2322
  var flagged = SES.consumeRoundFlag();
2440
2323
  restoreSession();
2441
2324
 
2442
- injectAssets();
2325
+ SHELL.injectAssets(CSS);
2443
2326
  buildChrome();
2444
2327
  buildWorkspace();
2328
+ refreshChrome();
2329
+ loadDesigns(); // the rest of the design menu, after the page is up
2445
2330
 
2446
2331
  if (working) {
2447
2332
  // A refresh of a round the reviewer already sent: keep waiting quietly.
2448
2333
  if (wsEl) wsEl.classList.add("is-waiting");
2449
2334
  document.title = WAITING_TITLE;
2450
2335
  startNextRoundPolling();
2336
+ if (CFG.embedded) SHELL.renderRevising(); // a refresh mid-revision keeps the strip
2337
+ if (CFG.embedded && SHELL.state.phase === "approved") showApproved(); // the agent has the approval
2451
2338
  } else {
2452
2339
  // Parked round: the agent is waiting on the reviewer — announce it.
2453
2340
  SES.announceNewRound(flagged);
2341
+ if (SHELL.alreadyApproved()) SHELL.showApprovedBanner("Your agent builds the slides from it. If something must still change, leave a comment.");
2342
+ if (SHELL.askedAgain()) SHELL.showApprovedBanner("Approve again to let it continue, or leave a comment if something must still change.", "Your agent asks you to confirm the plan again");
2343
+ if (SHELL.notAskedYet()) SHELL.showNotAskedBanner("Your agent is still writing the plan. Read along if you like — this page updates by itself when it asks you.");
2344
+ if (CFG.embedded && askedAgainUnchanged && CFG.asked) {
2345
+ SHELL.showAskedAgainUnchanged();
2346
+ } else if (CFG.embedded && arrivedAfterRevising && CFG.asked) {
2347
+ var n = Object.keys(changedSlides).length;
2348
+ SHELL.showArrived(n ? n + (n === 1 ? " slide changed" : " slides changed") + " — your comments were worked in. Have another look." : "Your comments were worked in. Have another look.");
2349
+ }
2350
+ if (editsRestored) {
2351
+ SHELL.showNotice("edits", { tone: "info", title: "Your unsent edits are back", text: "They are not sent yet — send or approve when you are ready.", dismiss: true });
2352
+ }
2353
+ if (staleEdits) {
2354
+ var old = staleEdits.value;
2355
+ SHELL.showNotice("edits", {
2356
+ tone: "warn",
2357
+ title: "Your agent changed the plan after your last unsent edits",
2358
+ text: "They were made on the older version, so they are not applied here. Copy them if you want to reuse a part.",
2359
+ dismiss: true,
2360
+ actions: [
2361
+ {
2362
+ label: "Copy my edited version",
2363
+ onClick: function () {
2364
+ try {
2365
+ navigator.clipboard.writeText(old);
2366
+ } catch {
2367
+ /* no clipboard in this frame */
2368
+ }
2369
+ },
2370
+ },
2371
+ ],
2372
+ });
2373
+ }
2454
2374
  }
2375
+ // The hand-over in progress, the next-load note, an answer on its way.
2376
+ SHELL.resume();
2455
2377
  }
2456
2378
 
2457
2379
  if (document.readyState === "loading") {