create-pmos 0.197.0 → 0.199.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 (40) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-report-design-real-test.js +225 -0
  7. package/workspace/c3-report-design-test.js +403 -0
  8. package/workspace/c3-report-setup-test.js +3 -2
  9. package/workspace/c3-work-test.js +1 -1
  10. package/workspace/guide/core-documents.md +1 -1
  11. package/workspace/guide/index.md +1 -0
  12. package/workspace/guide/report-design.md +104 -0
  13. package/workspace/guide/report-slides.md +3 -1
  14. package/workspace/guide/reports-set-up.md +2 -1
  15. package/workspace/lib/headless.js +24 -6
  16. package/workspace/lib/report-design.js +741 -0
  17. package/workspace/lib/report-draft.js +8 -3
  18. package/workspace/lib/report-setups.js +1 -1
  19. package/workspace/lib/route-modules.js +1 -0
  20. package/workspace/lib/routes-core-docs.js +18 -11
  21. package/workspace/lib/routes-doc-sheet.js +8 -1
  22. package/workspace/lib/routes-report-design.js +481 -0
  23. package/workspace/lib/routes-reports.js +2 -1
  24. package/workspace/package-lock.json +2 -2
  25. package/workspace/package.json +1 -1
  26. package/workspace/public/pm/c3-demo-rd.js +426 -0
  27. package/workspace/public/pm/c3-demo-rep2.js +2 -0
  28. package/workspace/public/pm/c3-demo-rs.js +5 -1
  29. package/workspace/public/pm/c3-design-studio.js +308 -0
  30. package/workspace/public/pm/c3-doc-formats.js +3 -2
  31. package/workspace/public/pm/c3-doc-sheet.js +30 -5
  32. package/workspace/public/pm/c3-initiatives.js +2 -1
  33. package/workspace/public/pm/c3-report-deck.css +139 -0
  34. package/workspace/public/pm/c3-report-deck.js +558 -0
  35. package/workspace/public/pm/c3-report-frame.css +20 -0
  36. package/workspace/public/pm/c3-report-setup.js +18 -6
  37. package/workspace/server.js +17 -5
  38. package/workspace/skills/report-design.skill +61 -0
  39. package/workspace/test-lists/report-design-backend.json +5 -0
  40. package/workspace/test-lists/report-design-studio-ui.json +5 -0
@@ -0,0 +1,308 @@
1
+ /* pm/c3-design-studio.js — the DESIGN STUDIO and the saved DESIGN BRIEF of a report (initiative report-design-studio; PRD sections 1 and 2;
2
+ * mockup 42-reportdeck.js, hashes #design, #design-point, #design-compare, #design-doc, #design-saved).
3
+ *
4
+ * openStudio({ ws, id, name, view, onClose }) the wide sheet: the conversation on the left (describe, add samples, turns, the PMOS lines
5
+ * that name each version), the live result on the right (the version strip, Compare two,
6
+ * go back to a version, point and say), the Deck / Document switch, This is it, Design brief
7
+ * openBrief({ ws, id, name }) the saved design in the report setup sheet: the description, the samples, the reference
8
+ * version, the rules PMOS wrote, and the cycles drafted to match it
9
+ * Entry points: "Design how it looks" on a setup row and "Refine the design" in a report document (c3-report-setup.js, c3-report-deck.js).
10
+ * A turn starts ONE report-design job on the server (POST /api/report-design/turn); the page shows it running and reads GET /api/report-design
11
+ * until the version lands. The page can be left: the job goes on. Nothing here sends anything outside the workspace. */
12
+ import { el } from './dom.js';
13
+ import { icon, pbtn, btn, chip, sect, put, lk } from './c3-ini-ui.js';
14
+ import { openSheet, closeSheet } from './c3-sheet.js';
15
+ import { toast } from './c3-toast.js';
16
+ import { RS, scheduleLabel } from './c3-report-store.js';
17
+ import { formatOf } from './c3-doc-formats.js';
18
+ import { RD, failed, splitRender, slideCard, frame, wireFrame, diffOutlines, ensureDeckCss, slotWord } from './c3-report-deck.js';
19
+
20
+ const MAX = 20 * 1024 * 1024;
21
+ const KIND_OF = (name) => { const e = String(name || '').toLowerCase().split('.').pop(); return e === 'pdf' ? 'pdf' : e === 'pptx' ? 'pptx' : e === 'docx' ? 'docx' : /^(png|jpe?g)$/.test(e) ? 'image' : ''; };
22
+ const SICON = { pdf: 'pdf', pptx: 'img', docx: 'doc', image: 'img', link: 'link', 'pmos-doc': 'doc' };
23
+ const SWORD = { pdf: 'Upload, PDF', pptx: 'Upload, .pptx', docx: 'Upload, .docx', image: 'Upload, image', link: 'Link', 'pmos-doc': 'Past PMOS document' };
24
+ const hhmm = (iso) => { const m = /T(\d{2}):(\d{2})/.exec(String(iso || '')); return m ? m[1] + ':' + m[2] : ''; };
25
+ const dayAt = (iso) => { const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})/.exec(String(iso || '')); return m ? slotWord(m[1]) + ', ' + m[2] + ':' + m[3] : String(iso || ''); };
26
+ const short = (t, n = 60) => { const s = String(t || '').replace(/\s+/g, ' ').trim(); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
27
+ const hint = (k) => (/headline|metric/.test(k) ? 'Hover a value for its detail. Click a number to see its source.' : /list/.test(k) ? 'Click a status to see where it comes from.' : 'Click any part of the preview to attach a request to it.');
28
+
29
+ /* one studio session per report setup: what the PM points at waits here until the next turn */
30
+ const LOCAL = {};
31
+ const local = (id) => LOCAL[id] || (LOCAL[id] = { view: null, cmp: false, cmpA: null, sidx: 0, point: true, markers: [], draft: null, mOpen: null, panel: false, text: '', fmt: null });
32
+
33
+ function sampleChip(s) {
34
+ const th = s.thumb && /^data:image\//.test(s.thumb) ? el('img', { class: 'rd-dks-thumb img', src: s.thumb, alt: '' }) : el('span', { class: 'rd-dks-thumb ' + (s.kind === 'pptx' || s.kind === 'image' ? 'deck' : 'page'), 'aria-hidden': 'true' }, el('i'), el('i'), el('i'));
35
+ return el('div', { class: 'rd-dks-smp', 'data-rd-sample': s.id }, th, el('div', null, el('b', null, icon(SICON[s.kind] || 'doc', 'sm'), s.name), el('span', { class: 'dim mono' }, SWORD[s.kind] || s.kind),
36
+ s.readable === false ? el('p', { class: 'rd-warn' }, 'PMOS could not read it with your tools. It is kept, but it does not shape the design.')
37
+ : el('p', null, el('span', { class: 'dim' }, 'PMOS took: '), s.took || 'PMOS reads it on the next turn.')));
38
+ }
39
+
40
+ /* ── the studio ── */
41
+ export function openStudio({ ws, id, name, view, onClose } = {}) {
42
+ ensureDeckCss();
43
+ const K = local(id); if (view != null) { K.view = view; K.cmp = false; }
44
+ const S = { D: null, setup: null, err: '', cache: {}, timer: null, closed: false, busy: false };
45
+ const body = el('div', { class: 'brief rs rd-dk rd-dks-wrap', 'data-rd-studio': id });
46
+ const ui = openSheet({ title: 'Design studio', icon: 'spark', wide: true, dw: true, cls: 'rd-wide', body, onClose: () => { S.closed = true; clearTimeout(S.timer); if (onClose) onClose(); } });
47
+
48
+ async function load(keepView) {
49
+ const r = await RD.design(ws, id);
50
+ if (!r || r.ok === false) { S.err = (r && r.error) || 'The design could not be read.'; paint(); return; }
51
+ S.D = r.design; S.setup = r.setup || { id, name };
52
+ if (!K.fmt || !S.D.versions.length) K.fmt = S.D.format || 'deck';
53
+ if (!keepView || K.view == null || !S.D.versions.some((v) => v.n === K.view)) K.view = S.D.current;
54
+ if (S.D.running) poll(); else clearTimeout(S.timer);
55
+ if (K.view != null) await render(K.view);
56
+ if (K.cmp) await render(cmpA());
57
+ paint();
58
+ }
59
+ async function render(n) {
60
+ if (n == null || S.cache[n]) return;
61
+ try { S.cache[n] = splitRender(await RD.version(ws, id, n)); } catch (e) { const style = ''; S.cache[n] = { err: (e && e.message) || 'not readable', slides: [], secs: [], style, body: '' }; }
62
+ }
63
+ function poll() { clearTimeout(S.timer); S.timer = setTimeout(async () => { if (S.closed) return; const was = S.D && S.D.current; await load(false); if (S.D && !S.D.running && S.D.current !== was) { K.markers = []; K.draft = null; scrollLog(); } }, 1500); }
64
+ const ver = (n) => (S.D.versions || []).find((v) => v.n === n);
65
+ const cmpA = () => { const b = S.D.current; const a = K.cmpA != null && K.cmpA !== b && ver(K.cmpA) ? K.cmpA : (S.D.versions.filter((v) => v.n < b).pop() || S.D.versions.find((v) => v.n !== b) || {}).n; return a; };
66
+ const scrollLog = () => requestAnimationFrame(() => { const l = body.querySelector('.rd-dks-log'); if (l) l.scrollTop = l.scrollHeight; });
67
+ const vlabel = (v) => (v ? (v.turn ? short(v.turn, 48) : v.n === 1 ? 'From your description' : 'Your turn') : '');
68
+
69
+ /* ── the conversation ── */
70
+ function msg(l) {
71
+ const you = l.who === 'you';
72
+ const sm = l.kind === 'sample' ? (S.D.samples || []).filter((s) => (l.sample ? s.id === l.sample : String(l.text || '').includes(s.name))) : [];
73
+ const v = l.version != null ? ver(l.version) : null;
74
+ return el('li', { class: 'rd-dks-msg ' + (you ? 'you' : 'pmos') + (l.kind === 'error' ? ' err' : ''), 'data-rd-log': l.kind },
75
+ el('span', { class: 'ds-av ' + (you ? 'h' : l.kind === 'error' ? 'w' : 'a') }, you ? icon('user', 'sm') : icon(l.kind === 'error' ? 'alert' : 'spark', 'sm')),
76
+ el('div', { class: 'bub' }, el('div', { class: 'who' }, you ? 'You' : 'PMOS'), l.text && !(l.kind === 'sample' && sm.length) ? el('p', null, l.text) : null, sm.map(sampleChip),
77
+ l.points && l.points.length ? chip(icon('target', 'sm'), 'Pointed at: marker ' + l.points.map((p) => p.n).join(', ') + ' on the preview') : null,
78
+ v ? lk({ class: 'rd-dks-ver', 'data-rd-ver': String(v.n), 'aria-pressed': String(K.view === v.n && !K.cmp), onclick: () => look(v.n) }, icon('img', 'sm'), 'D' + v.n + ': ' + vlabel(v)) : null));
79
+ }
80
+ function runningMsg() {
81
+ const n = (S.D.versions.length ? Math.max(...S.D.versions.map((v) => v.n)) : 0) + 1;
82
+ return el('li', { class: 'rd-dks-msg pmos', 'data-rd-running': '' }, el('span', { class: 'ds-av a' }, icon('spark', 'sm')),
83
+ el('div', { class: 'bub' }, el('div', { class: 'who' }, 'PMOS'), el('p', null, 'Making D' + n + ' from this cycle\'s data. This takes a minute or two. You can close the studio; the version lands here when the job ends.')));
84
+ }
85
+ function panel() {
86
+ if (!K.panel) return null;
87
+ const file = el('input', { type: 'file', class: 'rd-hide', accept: '.pdf,.pptx,.docx,.png,.jpg,.jpeg', 'data-rd-file': '', onChange: (e) => upload(e.target.files && e.target.files[0]) });
88
+ const link = el('input', { type: 'text', 'data-rd-link': '', placeholder: 'Paste a Google Drive or Confluence link', 'aria-label': 'Link to a sample', onKeyDown: (e) => { if (e.key === 'Enter') addLink(); } });
89
+ const docs = (RS.docs || []).filter((d) => d.setup !== id || d.state === 'issued').slice(0, 3);
90
+ return el('div', { class: 'rd-dks-panel', 'data-rd-panel': '' }, el('div', { class: 'ds-h' }, 'Add a sample'),
91
+ btn({ class: 'rd-dks-opt', 'data-rd-smp': 'file', onclick: () => file.click() }, icon('drop', 'sm'), el('span', null, 'Upload a past deck or document', el('br'), el('span', { class: 'dim rd-small' }, 'PDF, .pptx, .docx or a screenshot, up to 20 MB'))), file,
92
+ el('div', { class: 'ds-whyadd' }, link, btn({ class: 'ds-btn', 'data-rd-smp': 'link', onclick: () => addLink() }, 'Add')),
93
+ docs.map((d) => btn({ class: 'rd-dks-opt', 'data-rd-smp': 'doc', 'data-v': d.path, onclick: () => addDoc(d) }, icon('doc', 'sm'), el('span', null, 'Pick a past PMOS document', el('br'), el('span', { class: 'dim rd-small' }, d.name + ', ' + slotWord(d.slot))))),
94
+ el('div', { class: 'dim rd-small' }, 'PMOS reads a sample to learn how it looks. It does not copy its numbers. A link is read only if your tools can open it.'));
95
+ }
96
+ async function sampleDone(r) {
97
+ if (!r || !r.ok) { failed(r, 'The sample was not added'); return; }
98
+ K.panel = false; await load(true); scrollLog(); toast('Added the sample. Say what to take from it, then make the next version.');
99
+ }
100
+ function upload(f) {
101
+ if (!f) return;
102
+ const kind = KIND_OF(f.name); if (!kind) { toast({ text: 'PMOS reads PDF, .pptx, .docx, PNG or JPG files.', tone: 'error' }); return; }
103
+ if (f.size > MAX) { toast({ text: 'The file is larger than 20 MB. Add a smaller copy, or a link.', tone: 'error' }); return; }
104
+ const rd = new FileReader();
105
+ rd.onload = async () => { const data = String(rd.result || '').replace(/^data:[^,]*,/, ''); sampleDone(await RD.sample({ ws, id, name: f.name, kind, data })); };
106
+ rd.readAsDataURL(f);
107
+ }
108
+ async function addLink() { const i = body.querySelector('[data-rd-link]'); const v = i && i.value.trim(); if (!v) { if (i) i.focus(); return; } sampleDone(await RD.sample({ ws, id, link: v })); }
109
+ async function addDoc(d) { sampleDone(await RD.sample({ ws, id, doc: d.path })); }
110
+
111
+ function chat() {
112
+ const D = S.D; const first = !D.versions.length;
113
+ const ta = el('textarea', { 'data-rd-in': '', 'aria-label': first ? 'Describe the report' : 'Tell PMOS what to change',
114
+ placeholder: first ? 'Describe the report in plain words: who reads it, what they need to see first, what must stand out.' : K.fmt === 'deck' ? 'Tell PMOS what to change, for example: denser, like slide 3 of my sample' : 'Tell PMOS what to change, for example: put the chart before the summary',
115
+ onInput: (e) => { K.text = e.target.value; }, onKeyDown: (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); send(); } } });
116
+ ta.value = K.text;
117
+ return el('div', { class: 'rd-dks-chat' }, el('div', { class: 'rd-dks-desc' }, el('span', { class: 'dim mono' }, 'DESCRIBE, ADD SAMPLES, REFINE'),
118
+ (D.samples || []).length ? el('div', { class: 'chips' }, D.samples.map((s) => chip(icon(SICON[s.kind] || 'doc', 'sm'), s.name))) : null),
119
+ el('ul', { class: 'rd-dks-log', 'data-rd-chat': '' }, (D.log || []).map(msg), D.running ? runningMsg() : null,
120
+ !(D.log || []).length && !D.running ? el('li', { class: 'fold' }, icon('info', 'sm'), ' Start with who reads the report and what they need. Add a past deck or document if you have one.') : null),
121
+ panel(),
122
+ el('div', { class: 'rd-dks-in' }, ta, K.markers.length ? el('div', { class: 'chips' }, chip(icon('target', 'sm'), K.markers.length + (K.markers.length === 1 ? ' request' : ' requests') + ' on the preview go with this turn')) : null,
123
+ el('div', { class: 'ds-acts rd-between' }, btn({ class: 'ds-btn', 'data-rd-act': 'panel', 'aria-expanded': String(K.panel), onclick: () => { K.panel = !K.panel; paint(); } }, icon('plus', 'sm'), 'Add a sample'),
124
+ btn({ class: 'ds-btn ag', 'data-rd-act': 'send', disabled: !!D.running || S.busy, onclick: () => send() }, icon('spark', 'sm'), first ? 'Make the first version' : 'Make the next version')),
125
+ el('div', { class: 'dim rd-small' }, 'Each turn makes a new version. The data stays this cycle\'s; only how it looks changes.')));
126
+ }
127
+ async function send(textIn) {
128
+ const D = S.D; const i = body.querySelector('[data-rd-in]');
129
+ let text = String(textIn != null ? textIn : (i ? i.value : K.text) || '').trim();
130
+ if (!text && K.markers.length) text = 'Do the requests I pointed at.';
131
+ if (!text) { if (i) i.focus(); return; }
132
+ if (D.running) { toast('A version is being made. Wait for it to land.'); return; }
133
+ S.busy = true; paint();
134
+ let r;
135
+ if (!D.description) { r = await RD.describe({ ws, id, text, format: K.fmt }); if (!r || !r.ok) { S.busy = false; paint(); failed(r, 'The description was not saved'); return; } }
136
+ const points = K.markers.map((m) => ({ n: m.n, slide: m.slide, x: m.x, y: m.y, text: m.text }));
137
+ r = await RD.turn({ ws, id, text: D.description ? text : 'Make the first version from my description.', points });
138
+ S.busy = false;
139
+ if (!r || !r.ok) { paint(); if (r && (r.status === 409 || /run/i.test(String(r.error || '')))) toast('A version is being made. Wait for it to land.'); else failed(r, 'PMOS did not start'); return; }
140
+ /* the preview follows the version this turn makes: a fast job can land it before this load, and keepView would hold the old one */
141
+ K.text = ''; K.markers = []; K.draft = null; K.mOpen = null; K.view = null; K.cmp = false;
142
+ await load(true); scrollLog();
143
+ }
144
+
145
+ /* ── the result ── */
146
+ function look(n) { K.view = n; K.cmp = false; render(n).then(paint); }
147
+ function strip() {
148
+ const D = S.D; const cur = D.current;
149
+ return el('div', { class: 'rd-dks-vers' }, el('div', { class: 'seg', role: 'group', 'aria-label': 'Versions', 'data-rd-versions': '' }, D.versions.map((v) => btn({ 'data-rd-v': String(v.n), 'aria-pressed': String(K.view === v.n && !K.cmp), title: vlabel(v), onclick: () => look(v.n) }, 'D' + v.n, D.saved && D.saved.version === v.n ? icon('check', 'sm') : null))),
150
+ el('span', { class: 'chips2' }, K.view !== cur && !K.cmp ? btn({ class: 'ds-btn', 'data-rd-act': 'revert', onclick: () => revert(K.view) }, icon('undo', 'sm'), 'Go back to D' + K.view) : null,
151
+ D.versions.length > 1 ? btn({ class: 'ds-btn', 'data-rd-act': 'cmp', 'aria-pressed': String(K.cmp), onclick: async () => { K.cmp = !K.cmp; if (K.cmp) { K.view = cur; await render(cur); await render(cmpA()); } paint(); } }, icon('cols', 'sm'), K.cmp ? 'Stop comparing' : 'Compare two') : null));
152
+ }
153
+ async function revert(n) {
154
+ const r = await RD.revert({ ws, id, n }); if (!r || !r.ok) { failed(r, 'It did not go back'); return; }
155
+ await load(true); toast('Back to D' + n + '. The next turn builds on it. The later versions stay in the strip.');
156
+ }
157
+ function markers(key, scaleBox) {
158
+ if (!K.point) return [];
159
+ const L = K.markers.filter((m) => m.slide === key).map((m) => { const b = btn({ class: 'rd-pindot a rd-dks-mk' + (K.mOpen === m.id ? ' on' : ''), 'data-rd-mk': String(m.n), 'aria-label': 'Request ' + m.n + ': ' + m.text, onclick: () => { K.mOpen = K.mOpen === m.id ? null : m.id; paint(); } }, String(m.n)); place(b, m, scaleBox); return b; });
160
+ if (K.draft && K.draft.slide === key) { const d = el('span', { class: 'rd-pindot a on rd-dks-mk', 'aria-hidden': 'true' }, String(K.markers.length + 1)); place(d, K.draft, scaleBox); L.push(d); }
161
+ return L;
162
+ }
163
+ const place = (n, m, box) => { if (box) { n.style.setProperty('--rp-x', Math.round(box.left + box.width * m.x / 100) + 'px'); n.style.setProperty('--rp-y', Math.round(box.top + box.height * m.y / 100) + 'px'); } else { n.style.setProperty('--rp-x', m.x + '%'); n.style.setProperty('--rp-y', m.y + '%'); } };
164
+ function pointSect(key) {
165
+ const ms = K.markers.filter((m) => m.slide === key);
166
+ const draft = K.draft && K.draft.slide === key ? el('div', { class: 'ds-whyadd' }, el('input', { type: 'text', 'data-rd-mkdraft': '', placeholder: 'What should change here? For example: make this a table', 'aria-label': 'Request for this spot', onKeyDown: (e) => { if (e.key === 'Enter') saveMk(); } }),
167
+ btn({ class: 'ds-btn pri', 'data-rd-act': 'mksave', onclick: () => saveMk() }, 'Attach'), btn({ class: 'ds-btn', 'data-rd-act': 'mkcancel', onclick: () => { K.draft = null; paint(); } }, 'Cancel')) : null;
168
+ return sect({ ic: 'target', title: 'Point and say', count: ms.length, attrs: { 'data-rd-point': key },
169
+ right: el('label', { class: 'ds-tg' }, el('input', { type: 'checkbox', checked: K.point, 'data-rd-ck': 'point', onChange: (e) => { K.point = e.target.checked; K.draft = null; paint(); } }), 'On') },
170
+ ms.length ? el('ul', { class: 'ds-log rd-notes' }, ms.map((m) => el('li', { class: 'rd-note' + (K.mOpen === m.id ? ' on' : ''), 'data-rd-mkrow': String(m.n) },
171
+ btn({ class: 'rd-pindot a', 'aria-label': 'Request ' + m.n, onclick: () => { K.mOpen = K.mOpen === m.id ? null : m.id; paint(); } }, String(m.n)),
172
+ el('div', { class: 'ds-le' }, el('div', { class: 'l1' }, el('b', null, m.text), el('span', { class: 'when' }, 'Waiting')),
173
+ K.mOpen === m.id ? [el('div', { class: 'who' }, el('span', null, 'PMOS makes it in the next version, with your other requests.')), el('div', { class: 'ds-acts' }, btn({ class: 'ds-btn ag', 'data-rd-act': 'mkgo', onclick: () => send(K.text.trim() || 'Do the requests I pointed at.') }, icon('spark', 'sm'), 'Make the next version'), btn({ class: 'ds-btn', 'data-rd-act': 'mkdrop', onclick: () => { K.markers = K.markers.filter((x) => x.id !== m.id).map((x, k) => ({ ...x, n: k + 1 })); K.mOpen = null; paint(); } }, icon('trash', 'sm'), 'Remove'))] : null)))) : null,
174
+ draft, el('div', { class: 'dim rd-small' }, K.point ? 'Click any part of the preview to attach a request to it.' : 'Point and say is off.'));
175
+ }
176
+ function saveMk() {
177
+ const i = body.querySelector('[data-rd-mkdraft]'); const t = i && i.value.trim(); if (!t) { if (i) i.focus(); return; }
178
+ const m = { id: 'm' + Date.now(), n: K.markers.length + 1, slide: K.draft.slide, x: K.draft.x, y: K.draft.y, text: t };
179
+ K.markers.push(m); K.draft = null; K.mOpen = m.id; paint();
180
+ }
181
+ const pointHands = (getBox) => ({ place: (e, doc) => { if (!K.point) return false; const b = getBox(e, doc); if (!b) return false; K.draft = { slide: b.id, x: Math.round(b.x), y: Math.round(b.y) }; paint(); const i = body.querySelector('[data-rd-mkdraft]'); if (i) i.focus(); return true; } });
182
+ function preview() {
183
+ const D = S.D;
184
+ if (!D.versions.length) return [D.running ? el('div', { class: 'fold a', role: 'status' }, icon('clock', 'sm'), ' PMOS is making the first version.') : null,
185
+ el('div', { class: 'fold rd-none', 'data-rd-noversion': '' }, icon('info', 'sm'), ' No version yet. Describe the report on the left, add a sample if you have one, then press Make the first version.')];
186
+ if (K.cmp) return [strip(), compare()];
187
+ const v = ver(K.view) || ver(D.current); const R = S.cache[v.n];
188
+ const capline = el('div', { class: 'dim rd-small', 'data-rd-vcap': String(v.n) }, el('b', { class: 'rd-fg' }, 'D' + v.n), ' · ' + vlabel(v) + ' · made ' + hhmm(v.made_at) + '. Built with this cycle\'s real data.' + (v.n !== D.current ? ' You are looking at an older version.' : ''));
189
+ const run = D.running ? el('div', { class: 'fold a', role: 'status' }, icon('clock', 'sm'), ' PMOS is making the next version. This one stays on screen until it lands.') : null;
190
+ if (!R) return [strip(), capline, el('div', { class: 'fold', role: 'status' }, icon('clock', 'sm'), ' Reading D' + v.n + '.')];
191
+ if (R.err) return [strip(), capline, el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), ' D' + v.n + ' could not be read: ' + R.err + '.')];
192
+ if (R.format === 'doc') {
193
+ const card = el('div', { class: 'rd-docf rd-dks-doc' + (K.point ? ' placing' : ''), 'data-rd-sbig': 'doc' }); const layer = el('div', { class: 'rd-doclayer' });
194
+ const box = (e) => { const sec = e.target.closest && e.target.closest('section.pmos-sec'); if (!sec) return null; const r = sec.getBoundingClientRect(); return { id: sec.getAttribute('data-sec-id'), x: (e.clientX - r.left) / r.width * 100, y: (e.clientY - r.top) / r.height * 100 }; };
195
+ card.append(frame(R.style, R.body, { title: 'D' + v.n + ', the document', cls: 'pmos-doc', focusable: true, onLoad: (doc, fr) => {
196
+ const fit = () => { fr.style.setProperty('--rp-h', Math.max(200, doc.documentElement.scrollHeight) + 'px'); const L = []; for (const s of R.secs) { const n = doc.querySelector('section.pmos-sec[data-sec-id="' + s.id.replace(/"/g, '') + '"]'); if (n) L.push(...markers(s.id, n.getBoundingClientRect())); } put(layer, ...L); };
197
+ fit(); setTimeout(fit, 120); wireFrame(doc, { ...pointHands(box), more: fit });
198
+ } }), layer);
199
+ const key = K.draft ? K.draft.slide : (K.markers[0] || {}).slide || (R.secs[0] || {}).id || 'doc';
200
+ return [strip(), capline, run, card, pointSect(key)];
201
+ }
202
+ if (!R.slides.length) return [strip(), capline, el('div', { class: 'fold w' }, icon('alert', 'sm'), ' D' + v.n + ' has no slide.')];
203
+ K.sidx = Math.min(K.sidx, R.slides.length - 1); const s = R.slides[K.sidx];
204
+ const big = slideCard(R, s, { cls: 'rd-big' + (K.point ? ' placing' : ''), attrs: { 'data-rd-sbig': s.id }, focusable: true, over: markers(s.id), wire: (doc) => wireFrame(doc, pointHands((e) => ({ id: s.id, x: e.clientX / 1280 * 100, y: e.clientY / 720 * 100 }))) });
205
+ return [strip(), capline, run, thumbStrip(R, K.sidx, (i) => { K.sidx = i; K.draft = null; paint(); }),
206
+ el('div', { class: 'rd-dk-cap' }, el('b', { 'data-rd-scap': '' }, 'Slide ' + s.n + ' of ' + R.slides.length + ': ' + s.title), el('span', { class: 'dim rd-small' }, hint(s.kind))), big, pointSect(s.id)];
207
+ }
208
+ function thumbStrip(R, cur, pick) {
209
+ return el('nav', { class: 'rd-dk-strip h', 'aria-label': 'Slides', 'data-rd-sstrip': '' }, R.slides.map((s, i) => btn({ class: 'rd-dk-th', 'aria-current': String(cur === i), 'aria-label': 'Slide ' + s.n + ': ' + s.title, 'data-rd-sgo': String(i), onclick: () => pick(i) },
210
+ el('div', { inert: '' }, slideCard(R, s, { cls: 'rd-thumb' })), el('span', { class: 'rd-dk-thn' }, String(s.n)))));
211
+ }
212
+ function compare() {
213
+ const D = S.D; const b = D.current; const a = cmpA(); const RA = S.cache[a]; const RB = S.cache[b];
214
+ const seg = el('div', { class: 'rd-dk-cap' }, el('span', { class: 'chips2 rd-wrap' }, 'Compare ', el('div', { class: 'seg', role: 'group', 'aria-label': 'Compare with', 'data-rd-cmpa': '' }, D.versions.filter((v) => v.n !== b).map((v) => btn({ 'data-rd-a': String(v.n), 'aria-pressed': String(a === v.n), onclick: async () => { K.cmpA = v.n; await render(v.n); paint(); } }, 'D' + v.n))), ' with ', el('b', null, 'D' + b)));
215
+ if (!RA || !RB) return el('div', null, seg, el('div', { class: 'fold', role: 'status' }, icon('clock', 'sm'), ' Reading both versions.'));
216
+ const va = ver(a); const vb = ver(b);
217
+ const ch = diffOutlines(va.outline, vb.outline); if (vb.turn) ch.push('Your turn for D' + b + ': ' + short(vb.turn, 90)); if (va.format !== vb.format) ch.unshift('D' + a + ' is a ' + (va.format === 'deck' ? 'deck' : 'document') + ', D' + b + ' a ' + (vb.format === 'deck' ? 'deck' : 'document'));
218
+ ch.push('Same data in both: this cycle\'s draft');
219
+ const head = (n, v) => el('div', { class: 'chips2 rd-cmph' }, el('b', null, 'D' + n), el('span', { class: 'dim' }, vlabel(v)));
220
+ let cols; let tail = null;
221
+ if (RB.format === 'doc' || RA.format === 'doc') {
222
+ const docOf = (R) => (R.format === 'doc' ? el('div', { class: 'rd-docf sm' }, frame(R.style, R.body, { title: 'Version', cls: 'pmos-doc', onLoad: (doc, fr) => fr.style.setProperty('--rp-h', Math.max(200, doc.documentElement.scrollHeight) + 'px') })) : slideCard(R, R.slides[0] || { id: 'x', n: 1, title: '', html: '' }));
223
+ cols = [el('div', { class: 'rd-cmpc' }, head(a, va), docOf(RA)), el('div', { class: 'rd-cmpc' }, head(b, vb), docOf(RB))];
224
+ } else {
225
+ if (!RB.slides[K.sidx]) K.sidx = 0; const B = RB.slides[K.sidx]; const A = RA.slides.find((x) => x.id === B.id);
226
+ cols = [el('div', { class: 'rd-cmpc' }, head(a, va), A ? slideCard(RA, A) : el('div', { class: 'fold rd-none' }, icon('info', 'sm'), ' Not in D' + a + '.')), el('div', { class: 'rd-cmpc' }, head(b, vb), slideCard(RB, B))];
227
+ tail = thumbStrip(RB, K.sidx, (i) => { K.sidx = i; paint(); });
228
+ seg.append(el('span', { class: 'dim rd-small' }, 'Slide: ' + B.title + '. Pick another slide below.'));
229
+ }
230
+ return el('div', { class: 'rd-stack', 'data-rd-compare': '' }, seg, el('div', { class: 'rd-cmpg' }, ...cols),
231
+ sect({ ic: 'cols', title: 'What changed from D' + a + ' to D' + b, count: ch.length, attrs: { 'data-rd-changed': '' } }, el('ul', { class: 'chgl' }, ch.map((t) => el('li', null, icon('edit', 'sm'), el('span', null, t), el('span'))))), tail);
232
+ }
233
+
234
+ /* ── the bar ── */
235
+ async function setFmt(f) {
236
+ if (K.fmt === f) return; K.fmt = f;
237
+ if (S.D.description && S.D.format !== f) { const r = await RD.describe({ ws, id, text: S.D.description, format: f }); if (!r || !r.ok) { failed(r, 'The format was not changed'); return; } await load(true); toast('The next version is ' + (f === 'deck' ? 'a slide deck' : 'a document') + '. Make it from the conversation.'); return; }
238
+ paint();
239
+ }
240
+ async function accept() {
241
+ const n = K.view != null ? K.view : S.D.current; if (n == null) { toast('Make a version first.'); return; }
242
+ const r = await RD.accept({ ws, id, n }); if (!r || !r.ok) { failed(r, 'The design was not saved'); return; }
243
+ await load(true); toast('Saved D' + n + ' as the design of this report. PMOS drafts each cycle to match it. Nothing is sent.');
244
+ }
245
+ function paint() {
246
+ if (S.closed) return;
247
+ if (!S.D) { put(body, S.err ? el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), ' ' + S.err) : el('div', { class: 'fold', role: 'status' }, icon('clock', 'sm'), ' Reading the design.')); return; }
248
+ const D = S.D; const nm = (S.setup && S.setup.name) || name || 'Report';
249
+ const sb = body.closest('.sb'); const y = sb ? sb.scrollTop : 0; const log = body.querySelector('.rd-dks-log'); const ly = log ? log.scrollTop : null;
250
+ const focus = document.activeElement && body.contains(document.activeElement) && document.activeElement.matches('[data-rd-in],[data-rd-link]') ? document.activeElement.getAttribute('data-rd-in') != null ? '[data-rd-in]' : '[data-rd-link]' : null;
251
+ put(body, el('div', { class: 'rd-dk-bar' }, el('div', { class: 'chips2 rd-wrap' }, el('div', { class: 'seg', role: 'group', 'aria-label': 'Report type' },
252
+ btn({ 'data-rd-fmt': 'deck', 'aria-pressed': String(K.fmt === 'deck'), onclick: () => setFmt('deck') }, icon('img', 'sm'), 'Deck'), btn({ 'data-rd-fmt': 'doc', 'aria-pressed': String(K.fmt === 'doc'), onclick: () => setFmt('doc') }, icon('doc', 'sm'), 'Document')),
253
+ el('b', null, nm), D.saved ? el('span', { class: 'chip2 ds-good', 'data-rd-saved': String(D.saved.version) }, icon('check', 'sm'), 'Saved design: D' + D.saved.version) : el('span', { class: 'chip2', 'data-rd-saved': '' }, 'Not saved yet')),
254
+ el('div', { class: 'chips2' }, pbtn('primary', 'This is it', 'check', { 'data-rd-act': 'accept', disabled: !D.versions.length, onclick: () => accept() }),
255
+ D.saved ? pbtn('ghost', 'Design brief', 'doc', { 'data-rd-act': 'brief', onclick: () => openBrief({ ws, id, name: nm }) }) : null)),
256
+ el('div', { class: 'fold' }, icon('info', 'sm'), ' No template to fill in. Describe the report, add samples, then refine until it looks right. PMOS keeps what you settle on and drafts every cycle to match it. Nothing is sent.'),
257
+ el('div', { class: 'rd-dks' }, chat(), el('div', { class: 'rd-dks-out', 'data-rd-preview': '' }, ...[].concat(preview()))));
258
+ if (sb) sb.scrollTop = y; const l2 = body.querySelector('.rd-dks-log'); if (l2) l2.scrollTop = ly == null ? l2.scrollHeight : ly;
259
+ if (focus) { const n = body.querySelector(focus); if (n) n.focus(); }
260
+ }
261
+ paint(); load(false).then(scrollLog);
262
+ return { close: () => ui.close(), reload: () => load(true) };
263
+ }
264
+
265
+ /* ── the saved design: a brief, not a template ── */
266
+ export function openBrief({ ws, id, name } = {}) {
267
+ ensureDeckCss();
268
+ const body = el('div', { class: 'calbody brief rs rd-dk', 'data-rd-brief': id });
269
+ openSheet({ title: 'Report setup: ' + (name || id), icon: 'report', wide: true, cls: 'rd-wide', body });
270
+ put(body, el('div', { class: 'fold', role: 'status' }, icon('clock', 'sm'), ' Reading the design.'));
271
+ (async () => {
272
+ const r = await RD.design(ws, id);
273
+ if (!r || r.ok === false) { put(body, el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), ' The design could not be read: ' + ((r && r.error) || 'no answer') + '.')); return; }
274
+ const D = r.design; const setup = RS.setups.find((x) => x.id === id) || r.setup || {};
275
+ const nm = setup.name || name || id; const out = formatOf(setup.output);
276
+ const lh = el('div', { class: 'lh' }, el('span', { class: 'ki' }, icon('report')), el('div', { class: 'rs-min0' }, el('b', null, nm),
277
+ el('div', { class: 'dim' }, (setup.label || scheduleLabel(setup.schedule)) + '. Output: ' + (setup.output === 'google-slides' ? 'a slide deck in PMOS. A Google Slides copy is optional.' : out.l + '.'))));
278
+ if (!D.saved) {
279
+ put(body, lh, sect({ ic: 'spark', title: 'Design', attrs: { 'data-rd-design': '' } }, el('div', { class: 'fold', 'data-rd-nosaved': '' }, icon('info', 'sm'), ' No saved design yet. Describe the report and refine it in the Design studio, then press This is it.'),
280
+ el('div', { class: 'ds-acts' }, pbtn('primary', 'Design how it looks', 'spark', { 'data-rd-act': 'studio', onclick: () => openStudio({ ws, id, name: nm }) }))));
281
+ return;
282
+ }
283
+ const n = D.saved.version; let R = null; try { R = splitRender(await RD.version(ws, id, n)); } catch { R = null; }
284
+ const rule = (h, xs, attr) => (xs && xs.length ? el('div', { class: 'rd-dks-rule', 'data-rd-rule': attr }, el('b', null, h), el('ul', null, xs.map((x) => el('li', null, x)))) : null);
285
+ const ref = !R ? el('div', { class: 'fold w' }, icon('alert', 'sm'), ' The reference version could not be read.')
286
+ : R.format === 'deck' ? [el('div', { class: 'rd-dks-ref', 'data-rd-ref': String(n) }, R.slides.slice(0, 6).map((s) => el('div', { inert: '' }, slideCard(R, s, { cls: 'rd-thumb' })))), el('div', { class: 'dim rd-small' }, R.slides.length + ' slides with this cycle\'s data.' + (R.slides.length > 6 ? ' Showing the first 6.' : ''))]
287
+ : el('div', { class: 'rd-docf sm', 'data-rd-ref': String(n) }, frame(R.style, R.body, { title: 'The reference version', cls: 'pmos-doc', onLoad: (doc, fr) => fr.style.setProperty('--rp-h', Math.min(520, doc.documentElement.scrollHeight) + 'px') }));
288
+ const rules = D.rules || {};
289
+ const cycHost = el('ul', { class: 'chgl', 'data-rd-briefcycles': '' });
290
+ const docs = (RS.docs || []).filter((d) => d.setup === id).sort((a, b) => String(b.slot).localeCompare(String(a.slot)));
291
+ put(body, lh,
292
+ sect({ ic: 'spark', title: 'Design', right: el('span', { class: 'tag acc' }, 'Design D' + n), attrs: { 'data-rd-design': '' } },
293
+ el('div', { class: 'dim rd-small' }, 'Written by PMOS from your studio session. It is not a template: PMOS reads it each cycle and drafts to match it. Saved ' + dayAt(D.saved.at) + ', version D' + n + '.'),
294
+ el('h3', { class: 'ds-h' }, 'Your description'), el('blockquote', { class: 'rd-dks-q', 'data-rd-desc': '' }, D.description || 'No description.'),
295
+ el('h3', { class: 'ds-h' }, 'Your samples'), (D.samples || []).length ? D.samples.map(sampleChip) : el('div', { class: 'dim rd-small' }, 'No sample.'),
296
+ el('h3', { class: 'ds-h' }, 'Reference version · D' + n), ref,
297
+ el('h3', { class: 'ds-h' }, 'What PMOS learned'), rule('Keeps the same each cycle', rules.same, 'same'), rule('Changes each cycle', rules.changes, 'changes'), rule('Learned from your turns', rules.learned, 'learned'),
298
+ el('div', { class: 'ds-acts' }, pbtn('primary', 'Refine the design', 'spark', { 'data-rd-act': 'studio', onclick: () => openStudio({ ws, id, name: nm }) }), pbtn('outline', 'See the reference version', 'eye', { 'data-rd-act': 'refver', onclick: () => openStudio({ ws, id, name: nm, view: n }) }))),
299
+ sect({ ic: 'cal', title: 'Cycles drafted to match it', count: docs.length }, docs.length ? cycHost : el('div', { class: 'fold' }, icon('info', 'sm'), ' No cycle yet. The next one is drafted to match D' + n + '.')),
300
+ el('div', { class: 'fold' }, icon('shield', 'sm'), ' PMOS drafts each cycle. Nothing is sent: you present the deck or share the PDF yourself.'));
301
+ for (const d of docs) {
302
+ const c = await RD.cycle(ws, d.path);
303
+ const word = !c || c.ok === false || !c.designed ? el('span', { class: 'chip2' }, 'Before the design') : c.matches ? el('span', { class: 'chip2 ds-good' }, icon('check', 'sm'), 'Matches the design') : el('span', { class: 'chip2 ds-warn' }, icon('alert', 'sm'), (c.differences || []).length + ' difference' + ((c.differences || []).length === 1 ? '' : 's') + ' from the design');
304
+ cycHost.append(el('li', { 'data-rd-briefcycle': d.path }, icon('doc', 'sm'), el('span', null, slotWord(d.slot) + ' ', el('span', { class: 'dim rd-small' }, d.state === 'issued' ? 'Issued' : 'Draft')),
305
+ el('span', { class: 'chips2' }, word, pbtn('ghost', 'Open', 'out', { onclick: () => { closeSheet(); if (typeof RS.openDoc === 'function') RS.openDoc(d); } }))));
306
+ }
307
+ })();
308
+ }
@@ -17,9 +17,10 @@ export function ensureReportCss() {
17
17
  document.head.appendChild(l);
18
18
  }
19
19
 
20
- /** The five outputs of a recurring report. l: the name the page shows; ic: its icon; app: the tool a copy lives in; dl: the Download menu, [label, fmt]. */
20
+ /** The five outputs of a recurring report. l: the name the page shows; ic: its icon; app: the tool a copy lives in; dl: the Download menu, [label, fmt].
21
+ * The stored value 'google-slides' is kept for compatibility; the page names it "Slide deck": the deck lives in PMOS, a Google Slides copy is optional (report-design-studio). */
21
22
  export const FORMATS = {
22
- 'google-slides': { l: 'Google Slides', ic: 'img', app: 'Google Slides', copy: 'google-slides', word: 'slides', dl: [['PDF', 'pdf'], ['PowerPoint .pptx', 'pptx']] },
23
+ 'google-slides': { l: 'Slide deck', ic: 'img', app: 'Google Slides', copy: 'google-slides', word: 'slides', dl: [['PDF', 'pdf'], ['PowerPoint .pptx', 'pptx']] },
23
24
  'google-doc': { l: 'Google Doc', ic: 'doc', app: 'Google Doc', copy: 'google', word: 'headings', dl: [['PDF', 'pdf'], ['Word .docx', 'docx']] },
24
25
  'google-sheets': { l: 'Google Sheets', ic: 'sheet', app: 'Google Sheets', copy: 'google-sheets', word: 'sheets', dl: [['Excel .xlsx', 'xlsx'], ['CSV', 'csv']] },
25
26
  confluence: { l: 'Confluence', ic: 'notes', app: 'Confluence', copy: 'confluence', word: 'headings', dl: [['PDF', 'pdf'], ['Word .docx', 'docx']] },
@@ -671,8 +671,24 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
671
671
  return el('dl', { class: 'ds-props', 'data-doc-props': '' }, fm.fields.map((f) => [el('dt', null, f.key), el('dd', null, f.value)]),
672
672
  el('div', { class: 'sub' }, c3ic('doc', 'sm'), 'Front matter', r ? [' · ', byChip(r)] : null));
673
673
  }
674
+ /* a designed report cycle (initiative report-design-studio): c3-report-deck.js draws the deck or the interactive document in place of the
675
+ format view, and reaches back through this api (the Edit tab at a section, the Markdown source, the copies, the download, the edit log) */
676
+ const deckApi = { redraw: () => drawBg(), setSrc: (v) => { S.src = !!v; draw(); }, toggleCopies: () => toggleCopies(), download: (fmt) => download({ fmt }), editAt: (name) => editAt(name), logChanged: () => { S.log = null; }, go: (m) => go(m) };
677
+ function editAt(name) {
678
+ if (frozen()) { say('Issued reports are frozen. Start the next cycle to change the words.'); return; }
679
+ go('edit');
680
+ setTimeout(() => {
681
+ const k = S.ed ? S.ed.base.findIndex((x) => x.name === name) : -1;
682
+ const h = k >= 0 && ui ? ui.body.querySelector('[data-sid="' + k + '"]') : null;
683
+ if (!h) return;
684
+ h.scrollIntoView({ block: 'start' }); h.setAttribute('data-doc-editat', '');
685
+ try { const r = document.createRange(); r.selectNodeContents(h); r.collapse(false); const sel = getSelection(); sel.removeAllRanges(); sel.addRange(r); const ed = ui.body.querySelector('[data-doc-editor]'); if (ed) ed.focus({ preventScroll: true }); } catch { /* the caret stays where it is */ }
686
+ }, 30);
687
+ }
674
688
  function readBody() {
675
689
  if (S.text == null) return S.err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.err) : el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the document.');
690
+ const deck = R && R.deck && !S.src ? R.deck(deckApi) : null;
691
+ if (deck) return el('div', { class: 'ds' }, staleBanner(), deck);
676
692
  const secs = splitDoc(S.text);
677
693
  if (!S.text.trim()) return el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'The document is empty.');
678
694
  const items = R ? outlineLabels(secs, R.output) : outlineOf(secs); const olOn = items.length >= 2 && !S.src;
@@ -713,7 +729,8 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
713
729
  const cells = R && !S.src ? [formatView({ output: R.output, name: R.name || doc.name, cycle: R.cycle, foot: R.foot, secs, md: (t) => md(t, 'ds-body'), wrap: (s, inner, label) => secCell(s, inner, label) })] : secs.map((s) => secCell(s));
714
730
  const adds = openProps().flatMap((p) => p.sections.filter((c) => c.kind === 'added' && S.preview[p.id]).map((c) => el('div', { class: 'ds-sec a prev', 'data-dsec-new': c.name },
715
731
  el('div', { class: 'ds-pin' }, el('span', null, c3ic('spark', 'sm'), 'The proposal adds a section: ' + c.name)), el('div', { class: 'ds-sec-h' }, el('h2', null, c.name)), md(diffMd(null, c.after), 'ds-body'))));
716
- const main = [generated ? el('div', { class: 'fold', role: 'note', 'data-doc-generated': '' }, c3ic('info', 'sm'), generated.why) : null, staleBanner(), propsBlock(secs, false), cells, adds];
732
+ const copyNote = R && R.copyNotice ? el('div', { class: 'fold', 'data-rd-copynote': '' }, c3ic('info', 'sm'), ' The deck now lives in PMOS. The Google Slides copy of this report is optional. ', btn('chip2 lk', {}, 'Copies in other tools', () => toggleCopies())) : null;
733
+ const main = [generated ? el('div', { class: 'fold', role: 'note', 'data-doc-generated': '' }, c3ic('info', 'sm'), generated.why) : null, copyNote, staleBanner(), propsBlock(secs, false), cells, adds];
717
734
  return el('div', { class: 'ds' + (S.who ? ' who' : '') }, bar, legend,
718
735
  olOn ? el('div', { class: 'ds-rlay' + (S.ol ? '' : ' nool') + (S.olOpen ? ' olopen' : '') }, outlineNav(items), el('div', { class: 'ds-rmain' }, main)) : el('div', { class: 'ds-rmain' }, main));
719
736
  }
@@ -962,6 +979,13 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
962
979
  }
963
980
 
964
981
  /* ── Edit log ── */
982
+ /* notes on the slide and the cycle's design decisions (report-design-studio): the server appends them to the document's log */
983
+ const DESIGN_T = { keep: 'Kept a difference from the design for this cycle', adopt: 'Made a difference part of the design', match: 'Matched the design' };
984
+ function pinTitle(e) {
985
+ const on = e.slide ? ' (' + e.slide + ')' : '';
986
+ if (e.op === 'add') return (e.by === 'agent' ? 'PMOS proposed a note on the slide' : 'Note pinned on the slide') + on;
987
+ return ({ edit: 'Changed a note on the slide', remove: 'Removed a note on the slide', keep: 'Kept the note PMOS proposed', dismiss: 'Dismissed the note PMOS proposed' }[e.op] || 'Note on the slide') + on;
988
+ }
965
989
  function logEntries() {
966
990
  const L = S.log || { revisions: [], events: [] };
967
991
  const out = [];
@@ -971,7 +995,8 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
971
995
  out.push({ kind: 'rev', at: r.at, r, ag, outside, people: !ag, title: (r.v ? r.v + ' · ' : 'Revision ' + r.rev + ' · ') + title });
972
996
  }
973
997
  for (const e of L.events) {
974
- const t = { proposed: 'The agent drafted a proposal', revised: 'The agent revised a proposal', rejected: 'Proposal rejected', asked: 'You asked the agent', 'ask-failed': 'The agent did not draft the change', marked: 'Marked as still current' }[e.kind] || e.kind;
998
+ const t = e.kind === 'pin' ? pinTitle(e) : e.kind === 'design' ? DESIGN_T[e.choice] || 'A decision on the design'
999
+ : { proposed: 'The agent drafted a proposal', revised: 'The agent revised a proposal', rejected: 'Proposal rejected', asked: 'You asked the agent', 'ask-failed': 'The agent did not draft the change', marked: 'Marked as still current' }[e.kind] || e.kind;
975
1000
  out.push({ kind: e.kind, at: e.at, e, ag: e.by === 'agent' || e.kind === 'proposed' || e.kind === 'revised' || e.kind === 'ask-failed', outside: false, people: e.by === 'pm' || e.kind === 'rejected' || e.kind === 'asked' || e.kind === 'marked', title: t });
976
1001
  }
977
1002
  return out.sort((a, b) => String(b.at).localeCompare(String(a.at)));
@@ -995,7 +1020,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
995
1020
  else if (noReason) why = el('p', { class: 'why none' }, c3ic('alert', 'sm'), ' No reason recorded: this change was made outside PMOS. ', btn('chip2 lk ds-pe', { 'data-doc-whyadd': String(r.rev) }, 'Add a reason', () => { S.whyFor = r.rev; S.whyText = ''; draw(); const i = ui.body.querySelector('#daddwhy'); if (i) i.focus(); }));
996
1021
  else if (r.why) why = el('p', { class: 'why' }, r.why);
997
1022
  } else {
998
- const t = e.kind === 'asked' ? '“' + e.prompt + '”' : e.kind === 'proposed' || e.kind === 'revised' ? 'For: “' + (e.prompt || '') + '”' : e.why || '';
1023
+ const t = e.kind === 'pin' ? (e.text ? '“' + e.text + '” Note on the slide: the words on the slide did not change.' : 'Note on the slide.') : e.kind === 'design' ? (e.what || e.diff || '') : e.kind === 'asked' ? '“' + e.prompt + '”' : e.kind === 'proposed' || e.kind === 'revised' ? 'For: “' + (e.prompt || '') + '”' : e.why || '';
999
1024
  if (t) why = el('p', { class: 'why' }, t);
1000
1025
  }
1001
1026
  const tags = [];
@@ -1103,7 +1128,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
1103
1128
  function nextCycle() { S.menu = false; if (R.onNext) R.onNext(doc, ui); else say('The next cycle starts on its schedule. PMOS drafts it then.'); }
1104
1129
  function drawTools() {
1105
1130
  if (!toolsNode) return;
1106
- toolsNode.replaceChildren(...[R.state === 'draft' ? pb('outline', { 'data-doc-issue': '' }, [c3ic('check', 'sm'), 'Issue'], () => issue()) : null,
1131
+ toolsNode.replaceChildren(...[...(R.tools ? R.tools() : []), R.state === 'draft' ? pb('outline', { 'data-doc-issue': '' }, [c3ic('check', 'sm'), 'Issue'], () => issue()) : null,
1107
1132
  btn('ib sm', { 'aria-label': 'Download and more', title: 'Download and more', 'aria-expanded': String(S.menu), 'data-doc-more': '' }, c3ic('more', 'sm'), () => { S.menu = !S.menu; draw(); })].filter(Boolean));
1108
1133
  }
1109
1134
 
@@ -1122,7 +1147,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
1122
1147
  const had = keep ? { id: keep.id, s: keep.selectionStart } : null;
1123
1148
  const above = docAbove.map((fn) => { try { return fn({ ws, doc, text: S.text, key: S }); } catch { return null; } });
1124
1149
  ui.body.replaceChildren(el('div', { class: 'dpn ds' }, top(), above, copiesPanel(), body));
1125
- const sh = ui.body.closest('.sheet'); if (sh) sh.classList.toggle('dwx', S.mode === 'read' && S.ol && !!ui.body.querySelector('.ds-ol'));
1150
+ const sh = ui.body.closest('.sheet'); if (sh) sh.classList.toggle('dwx', S.mode === 'read' && ((S.ol && !!ui.body.querySelector('.ds-ol')) || !!ui.body.querySelector('[data-rd-deck]')));
1126
1151
  const sb = ui.body.closest('.sb');
1127
1152
  if (sb && !sb.__dsTrack) { sb.__dsTrack = true; sb.addEventListener('scroll', trackCur, { passive: true }); }
1128
1153
  if (S.mode === 'read') { placeOl(); trackCur(); }
@@ -166,7 +166,8 @@ export function mountInitiatives(pane, opts) {
166
166
  const openBoard = (id, tab, filter) => go({ view: 'board', board: id, tab: tab || 'summary', filter: filter || {} });
167
167
  const refresh = () => { P.boards = null; P.ini = null; dropItems(); draw(); };
168
168
  // The Briefing's Horizon scan card opens a board's Triage or settings (c3-scan.js goBoard). A page that is not built yet reads the address instead.
169
- if (typeof window !== 'undefined') { if (window.__iniGo) window.removeEventListener('pmos:ini-go', window.__iniGo); window.__iniGo = (e) => { const d = (e && e.detail) || {}; if (d.loc != null && root.isConnected) go(parseLoc(d.loc)); else if (d.board && root.isConnected) openBoard(d.board, d.tab || 'summary'); }; window.addEventListener('pmos:ini-go', window.__iniGo); }
169
+ // A report deck's live initiative link sends { id } (c3-report-deck.js openInitiative): the initiative opens on the board in view, else the first board.
170
+ if (typeof window !== 'undefined') { if (window.__iniGo) window.removeEventListener('pmos:ini-go', window.__iniGo); window.__iniGo = (e) => { const d = (e && e.detail) || {}; if (!root.isConnected) return; if (d.id) { const b = d.board || (P.loc && P.loc.board) || (P.boards && P.boards[0] && P.boards[0].id); if (b) go({ view: 'ini', board: b, id: d.id, tab: d.tab || 'overview' }); return; } if (d.loc != null) go(parseLoc(d.loc)); else if (d.board) openBoard(d.board, d.tab || 'summary'); }; window.addEventListener('pmos:ini-go', window.__iniGo); }
170
171
  const redraw = () => draw();
171
172
 
172
173
  async function loadBoards() {
@@ -0,0 +1,139 @@
1
+ /* pm/c3-report-deck.css — the deck view, Present, the notes, the Design studio and the design brief (initiative report-design-studio). The mockup's
2
+ 42-reportdeck.css under real class names: .mk-dk is .rd-dk, .mk-dks is .rd-dks, the mockup's .mk-btn/.mk-log/.mk-sync/.mk-stale are the document
3
+ sheet's .ds-btn/.ds-log/.ds-sync/.ds-stale. The slide is a render in a sandboxed frame (.rd-card > iframe), scaled with --rp-scale.
4
+ Tokens only. Corners: 6px on a control, 8px on a container, 50% only on a dot. c3-report-deck.js links it once. */
5
+ .c3 .sheet.rd-wide { width: min(1320px, calc(100% - 24px)); max-width: none; }
6
+ .c3 .rd-dk { display: grid; gap: var(--sp-3); min-width: 0; }
7
+ .c3 .rd-dk > * { min-width: 0; }
8
+ .c3 .rd-wrap { flex-wrap: wrap; min-width: 0; }
9
+ .c3 .rd-small { font-size: var(--fs-2); }
10
+ .c3 .rd-m0 { margin: 0; }
11
+ .c3 .rd-fg { color: var(--foreground); }
12
+ .c3 .rd-hide { display: none; }
13
+ .c3 .rd-stack { display: grid; gap: 6px; min-width: 0; }
14
+ .c3 .rd-between { justify-content: space-between; }
15
+ .c3 .rd-warn { color: var(--warning); }
16
+ .c3 .rd-on { color: var(--acc); border-color: var(--acc); }
17
+ .c3 .rd-dk-bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; }
18
+ .c3 .rd-dk-bar .seg { max-width: 100%; overflow-x: auto; }
19
+ .c3 .rd-dk-bar .seg button { white-space: nowrap; }
20
+ .c3 .rd-dk-caps { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
21
+ .c3 .rd-dk-caps .dim { font-size: var(--fs-1); margin-right: 4px; }
22
+ .c3 .rd-dk-diff p { margin: 0; }
23
+
24
+ /* a slide: the render in a sandboxed frame, 1280 x 720, scaled to the card's width */
25
+ .c3 .rd-card { position: relative; width: 100%; aspect-ratio: 16 / 9; overflow: hidden; background: var(--slide-bg); box-shadow: 0 0 0 1px var(--border); border-radius: 6px; }
26
+ .c3 .rd-card > .rd-if { position: absolute; top: 0; left: 0; width: 1280px; height: 720px; border: 0; transform: scale(var(--rp-scale, 0.5)); transform-origin: top left; background: var(--slide-bg); }
27
+ .c3 .rd-thumb > .rd-if { pointer-events: none; }
28
+ .c3 .rd-big.placing, .c3 .rd-docf.placing { box-shadow: 0 0 0 2px var(--acc); }
29
+ .c3 .rd-big.placing > .rd-if, .c3 .rd-docf.placing > .rd-if { cursor: crosshair; }
30
+ .c3 .rd-docf { position: relative; border: 1px solid var(--border); border-radius: 8px; background: var(--slide-bg); overflow: hidden; }
31
+ .c3 .rd-docf > .rd-if { display: block; width: 100%; height: var(--rp-h, 480px); border: 0; background: var(--slide-bg); }
32
+ .c3 .rd-docf.sm { max-height: 520px; overflow: auto; }
33
+ .c3 .rd-doclayer { position: absolute; inset: 0; pointer-events: none; }
34
+ .c3 .rd-doclayer > * { pointer-events: auto; }
35
+ .c3 .rd-tip { position: absolute; z-index: 4; left: var(--rp-x, 0); top: var(--rp-y, 0); transform: translate(-50%, calc(-100% - 6px)); max-width: 280px; padding: 4px 8px; border-radius: 6px; background: var(--foreground); color: var(--background); font: 500 var(--fs-2) var(--sans); pointer-events: none; white-space: normal; }
36
+ .c3 .rd-tip[hidden] { display: none; }
37
+
38
+ /* the deck: filmstrip, current slide, notes */
39
+ .c3 .rd-dk-main { display: grid; grid-template-columns: 124px minmax(0, 1fr); gap: var(--sp-3); align-items: start; }
40
+ .c3 .rd-dk-strip { display: grid; gap: 8px; align-content: start; max-height: calc(100vh - 230px); overflow: auto; position: sticky; top: 0; padding: 3px; }
41
+ .c3 .rd-dk-strip.h { grid-auto-flow: column; grid-auto-columns: 120px; max-height: none; position: static; overflow-x: auto; overflow-y: hidden; }
42
+ .c3 .rd-dk-th { position: relative; display: block; width: 100%; height: auto; min-height: 0; padding: 0; border: 0; border-radius: 6px; background: none; cursor: pointer; }
43
+ .c3 .rd-dk-th[aria-current="true"] { outline: 2px solid var(--acc); outline-offset: 2px; }
44
+ .c3 .rd-dk-thn { position: absolute; left: 4px; bottom: 4px; font: var(--fs-1) var(--mono); background: var(--card); color: var(--foreground); border-radius: 6px; padding: 0 4px; }
45
+ .c3 .rd-dk-thp { position: absolute; right: 4px; top: 4px; display: inline-flex; gap: 2px; align-items: center; font: var(--fs-1) var(--mono); background: var(--acc); color: var(--background); border-radius: 6px; padding: 0 4px; }
46
+ .c3 .rd-dk-thp .ic { width: 10px; height: 10px; }
47
+ .c3 .rd-dk-stage { display: grid; gap: var(--sp-2); min-width: 0; }
48
+ .c3 .rd-dk-cap { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; }
49
+ .c3 .rd-dk-under { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: var(--sp-3); align-items: start; }
50
+ .c3 .rd-dk-ncol { display: grid; gap: var(--sp-3); min-width: 0; }
51
+ .c3 .rd-spk { min-height: 96px; resize: vertical; width: 100%; box-sizing: border-box; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--foreground); font: var(--fs-3)/1.5 var(--sans); }
52
+ .c3 .rd-carry { display: grid; gap: 6px; padding: 8px 10px; border: 1px dashed var(--border); border-radius: 8px; color: var(--muted-foreground); }
53
+ .c3 .rd-carry p { margin: 0; font-size: var(--fs-3); font-style: italic; }
54
+ /* pins: a numbered dot on the slide; yours blue, PMOS's purple and dashed while it is a proposal */
55
+ .c3 .rd-pindot { position: absolute; z-index: 2; left: var(--rp-x, 50%); top: var(--rp-y, 50%); transform: translate(-50%, -50%); width: 26px; height: 26px; min-width: 0; min-height: 0; padding: 0; border-radius: 50%; display: grid; place-items: center; place-content: center; font: 600 var(--fs-1) var(--mono); color: var(--background); background: var(--acc); border: 2px solid var(--slide-bg); box-shadow: 0 2px 6px color-mix(in oklab, var(--slide-fg) 30%, transparent); cursor: pointer; }
56
+ .c3 .rd-pindot.a { background: var(--agent); }
57
+ .c3 .rd-pindot.prop { border-style: dashed; }
58
+ .c3 .rd-pindot.on { box-shadow: 0 0 0 4px color-mix(in oklab, var(--acc) 35%, transparent); }
59
+ .c3 .rd-pindot.a.on { box-shadow: 0 0 0 4px color-mix(in oklab, var(--agent) 35%, transparent); }
60
+ .c3 .rd-notes .rd-pindot { position: static; transform: none; border-color: var(--card); box-shadow: none; }
61
+ .c3 .rd-notes > li.rd-note { grid-template-columns: 28px minmax(0, 1fr); padding: 6px; margin: 0 -6px; border-radius: 8px; }
62
+ .c3 .rd-notes > li.rd-note::before { display: none; }
63
+ .c3 .rd-notes > li.rd-note.on { background: var(--muted); }
64
+ .c3 .rd-clip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted-foreground); }
65
+ .c3 .rd-cmpg { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: var(--sp-3); }
66
+ .c3 .rd-cmph { flex-wrap: wrap; margin-bottom: 6px; }
67
+ .c3 .rd-cmpc { min-width: 0; }
68
+ .c3 .rd-none { min-height: 120px; align-content: center; }
69
+
70
+ /* Present: the audience view is the product's .rp-deck; the presenter view lays cards on the same stage */
71
+ .c3 .rd-ovhost { position: fixed; inset: 0; z-index: 60; }
72
+ .c3 .rp-deck.rd-ov { z-index: 60; display: flex; flex-direction: column; gap: 0; }
73
+ .c3 .rd-ovframe { overflow: hidden; }
74
+ .c3 .rd-ovframe > .rd-if { position: absolute; top: 0; left: 0; width: 1280px; height: 720px; border: 0; transform: scale(var(--rp-scale, 1)); transform-origin: top left; background: var(--slide-bg); }
75
+ .c3 .rd-pg { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 16px; padding: 16px; overflow: auto; align-items: start; }
76
+ .c3 .rd-pmain, .c3 .rd-pside { display: grid; gap: 12px; min-width: 0; }
77
+ .c3 .rd-pp { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--foreground); }
78
+ .c3 .rd-plab { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; }
79
+ .c3 .rd-ph { display: flex; gap: 6px; align-items: center; font: 500 var(--fs-1) var(--mono); color: var(--muted-foreground); text-transform: uppercase; letter-spacing: .06em; }
80
+ .c3 .rd-pnote { margin: 0; font-size: var(--fs-4); line-height: 1.5; }
81
+ .c3 .rd-ptime { grid-template-columns: 1fr 1fr; }
82
+ .c3 .rd-timer { font: 600 var(--fs-6) var(--mono); }
83
+ .c3 .rd-jump { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; max-height: 260px; overflow: auto; }
84
+ .c3 .rd-jump button { display: flex; gap: 8px; align-items: center; justify-content: flex-start; width: 100%; height: auto; text-align: left; background: none; border: 0; padding: 5px 8px; border-radius: 6px; font-size: var(--fs-2); }
85
+ .c3 .rd-jump button[aria-current="true"] { background: color-mix(in oklab, var(--acc) 14%, transparent); color: var(--acc); font-weight: 500; }
86
+ .c3 .rd-jump .mono { color: var(--muted-foreground); font-size: var(--fs-1); }
87
+ .c3 .rd-ov .rp-deck__bar { flex-wrap: wrap; }
88
+ @media (hover: hover) {
89
+ .c3 .rd-jump button:hover { background: var(--muted); }
90
+ .c3 .rd-dks-opt:hover { background: var(--muted); }
91
+ }
92
+
93
+ /* the Design studio: the conversation left, the live result right */
94
+ .c3 .rd-dks { display: grid; grid-template-columns: minmax(300px, 380px) minmax(0, 1fr); gap: var(--sp-4); align-items: start; }
95
+ .c3 .rd-dks-chat { display: grid; gap: var(--sp-2); position: sticky; top: 0; max-height: calc(100vh - 220px); grid-template-rows: auto minmax(0, 1fr) auto auto; min-width: 0; }
96
+ .c3 .rd-dks-desc { display: grid; gap: 6px; }
97
+ .c3 .rd-dks-desc .dim { font-size: var(--fs-1); }
98
+ .c3 .rd-dks-log { list-style: none; margin: 0; padding: 0 4px 0 0; display: grid; gap: 12px; align-content: start; overflow: auto; min-height: 120px; }
99
+ .c3 .rd-dks-msg { display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 8px; align-items: start; }
100
+ .c3 .rd-dks-msg .bub { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; justify-items: start; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); min-width: 0; }
101
+ .c3 .rd-dks-msg.you .bub { background: color-mix(in oklab, var(--acc) 7%, var(--card)); border-color: color-mix(in oklab, var(--acc) 25%, var(--border)); }
102
+ .c3 .rd-dks-msg.err .bub { border-color: color-mix(in oklab, var(--warning) 50%, var(--border)); }
103
+ .c3 .rd-dks-msg .who { font: 500 var(--fs-1) var(--mono); color: var(--muted-foreground); text-transform: uppercase; letter-spacing: .06em; }
104
+ .c3 .rd-dks-msg p { margin: 0; font-size: var(--fs-3); line-height: 1.5; overflow-wrap: anywhere; }
105
+ .c3 .rd-dks-ver { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
106
+ .c3 .rd-dks-ver[aria-pressed="true"] { color: var(--acc); border-color: var(--acc); }
107
+ .c3 .rd-dks-smp { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 10px; align-items: start; width: 100%; box-sizing: border-box; padding: 8px; border: 1px solid var(--border); border-radius: 8px; background: var(--background); }
108
+ .c3 .rd-dks-smp b { display: flex; gap: 6px; align-items: center; font-size: var(--fs-2); overflow-wrap: anywhere; }
109
+ .c3 .rd-dks-smp .mono { font-size: var(--fs-1); }
110
+ .c3 .rd-dks-smp p { margin: 2px 0 0; font-size: var(--fs-2); line-height: 1.4; }
111
+ .c3 .rd-dks-thumb { display: grid; gap: 3px; align-content: start; width: 56px; aspect-ratio: 16 / 9; padding: 4px; box-sizing: border-box; border: 1px solid var(--slide-border); border-radius: 6px; background: var(--slide-bg); }
112
+ .c3 .rd-dks-thumb.img { padding: 0; object-fit: cover; }
113
+ .c3 .rd-dks-thumb i { display: block; height: 3px; background: var(--slide-track); }
114
+ .c3 .rd-dks-thumb.deck i:first-child { height: 6px; background: var(--slide-brand); }
115
+ .c3 .rd-dks-thumb.deck i:nth-child(2) { width: 70%; }
116
+ .c3 .rd-dks-thumb.page { aspect-ratio: 3 / 4; width: 40px; justify-self: center; }
117
+ .c3 .rd-dks-thumb.page i:first-child { width: 60%; background: var(--slide-fg); }
118
+ .c3 .rd-dks-panel { display: grid; gap: 8px; padding: 10px; border: 1px dashed var(--border); border-radius: 8px; }
119
+ .c3 .rd-dks-opt { display: flex; gap: 10px; align-items: center; justify-content: flex-start; width: 100%; height: auto; text-align: left; white-space: normal; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--foreground); font: inherit; font-size: var(--fs-3); cursor: pointer; }
120
+ .c3 .rd-dks-in { display: grid; gap: 8px; padding-top: var(--sp-2); border-top: 1px solid var(--border); }
121
+ .c3 .rd-dks-in textarea { min-height: 64px; resize: vertical; width: 100%; box-sizing: border-box; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--foreground); font: var(--fs-3)/1.5 var(--sans); }
122
+ .c3 .rd-dks-out { display: grid; gap: var(--sp-2); min-width: 0; }
123
+ .c3 .rd-dks-vers { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; }
124
+ .c3 .rd-dks-vers .seg button { display: inline-flex; gap: 4px; align-items: center; }
125
+ .c3 .rd-dks-mk { z-index: 3; }
126
+ /* the saved design: a brief, not a template */
127
+ .c3 .rd-dks-q { margin: 0; padding: 8px 12px; border-left: 3px solid var(--acc); background: var(--muted); border-radius: 0; font-size: var(--fs-3); line-height: 1.5; }
128
+ .c3 .rd-dks-ref { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
129
+ .c3 .rd-dks-rule { display: grid; gap: 4px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; }
130
+ .c3 .rd-dks-rule b { font-size: var(--fs-3); }
131
+ .c3 .rd-dks-rule ul { margin: 0; padding-left: 18px; font-size: var(--fs-3); line-height: 1.5; }
132
+ @media (max-width: 900px) {
133
+ .c3 .rd-dk-main { grid-template-columns: minmax(0, 1fr); }
134
+ .c3 .rd-dk-strip { grid-auto-flow: column; grid-auto-columns: 104px; max-height: none; position: static; overflow-x: auto; overflow-y: hidden; }
135
+ .c3 .rd-pg { grid-template-columns: minmax(0, 1fr); padding: 12px; }
136
+ .c3 .rd-dks { grid-template-columns: minmax(0, 1fr); }
137
+ .c3 .rd-dks-chat { position: static; max-height: none; }
138
+ .c3 .rd-dks-log { max-height: 420px; }
139
+ }