create-pmos 0.179.0 → 0.180.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 (51) 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-setup-test.js +190 -0
  7. package/workspace/c3-reports-test.js +30 -97
  8. package/workspace/guide/document-sheet.md +14 -0
  9. package/workspace/guide/index.md +3 -2
  10. package/workspace/guide/reports-dashboards.md +17 -28
  11. package/workspace/guide/reports-page.md +10 -5
  12. package/workspace/guide/reports-set-up.md +56 -0
  13. package/workspace/guide/reports.md +13 -1
  14. package/workspace/guide/settings.md +11 -2
  15. package/workspace/lib/doc-sync.js +28 -6
  16. package/workspace/lib/docs-cli.js +7 -11
  17. package/workspace/lib/four-panel.js +1 -1
  18. package/workspace/lib/headless.js +21 -2
  19. package/workspace/lib/report-checks.js +103 -0
  20. package/workspace/lib/report-cli.js +89 -4
  21. package/workspace/lib/report-export.js +347 -0
  22. package/workspace/lib/report-finder.js +156 -0
  23. package/workspace/lib/report-needs.js +1 -1
  24. package/workspace/lib/report-setups.js +743 -0
  25. package/workspace/lib/route-modules.js +1 -0
  26. package/workspace/lib/routes-c3-reports.js +35 -17
  27. package/workspace/lib/routes-doc-health.js +15 -1
  28. package/workspace/lib/routes-report-setups.js +368 -0
  29. package/workspace/lib/schedule-logic.js +5 -2
  30. package/workspace/package-lock.json +2 -2
  31. package/workspace/package.json +1 -1
  32. package/workspace/public/pm/c3-demo-rep2.js +15 -0
  33. package/workspace/public/pm/c3-demo-rs.js +281 -0
  34. package/workspace/public/pm/c3-demo.js +1 -1
  35. package/workspace/public/pm/c3-doc-formats.js +152 -0
  36. package/workspace/public/pm/c3-doc-sheet.js +115 -25
  37. package/workspace/public/pm/c3-quiet.js +2 -2
  38. package/workspace/public/pm/c3-report-print.css +20 -0
  39. package/workspace/public/pm/c3-report-setup.css +62 -0
  40. package/workspace/public/pm/c3-report-setup.js +260 -0
  41. package/workspace/public/pm/c3-report-store.js +208 -0
  42. package/workspace/public/pm/c3-reports.css +4 -0
  43. package/workspace/public/pm/c3-reports.js +47 -181
  44. package/workspace/public/pm/c3-settings.js +4 -3
  45. package/workspace/public/pm/c3-shell.js +15 -31
  46. package/workspace/server.js +66 -5
  47. package/workspace/skills/doc-sync.skill +17 -8
  48. package/workspace/skills/report-find.skill +75 -0
  49. package/workspace/skills/report-setup.skill +67 -0
  50. package/workspace/test-files.json +10 -0
  51. package/workspace/test-lists/report-workspace-ui.json +5 -0
@@ -40,8 +40,10 @@ import { c3ic } from './pm-work-view.js';
40
40
  import { openSheet } from './c3-sheet.js';
41
41
  import { say } from './c3-init-extras.js';
42
42
  import { c3btn } from './page-agent.js';
43
+ import { button } from './primitives.js';
43
44
  import { mountSlide } from './slide-confirm.js';
44
45
  import { parseMd, toMd, canonMd, diffMd, toDom, fromDom, wordCounts, safeHref } from './c3-md.js';
46
+ import { ensureReportCss, formatOf, formatView, outlineLabels, downloadsOf, printPdf, copyKindFor } from './c3-doc-formats.js';
45
47
 
46
48
  /* ── pure: sections, round trip, word diff ── */
47
49
 
@@ -179,7 +181,7 @@ export function composeSections(base, groups, intro, front) {
179
181
  /* ── the sheet ── */
180
182
 
181
183
  const NAMES = { fresh: 'Fresh', decisions: 'Matches decisions', data: 'Matches the data', complete: 'Complete', traced: 'Traced to a goal and tickets', readers: 'Readers have this version' };
182
- const STATE = { current: ['Current', 'ds-good', 'check'], stale: ['Stale', 'ds-warn', 'alert'], draft: ['Draft', 'ds-pe', 'clock'], missing: ['Missing', 'ds-bad', 'plus'] };
184
+ const STATE = { issued: ['Issued', 'ds-good', 'check'], current: ['Current', 'ds-good', 'check'], stale: ['Stale', 'ds-warn', 'alert'], draft: ['Draft', 'ds-pe', 'clock'], missing: ['Missing', 'ds-bad', 'plus'] };
183
185
  const TABS = [['read', 'eye', 'Read'], ['edit', 'edit', 'Edit'], ['ask', 'spark', 'Proposals'], ['hist', 'clock', 'Edit log'], ['health', 'shield', 'Health'], ['links', 'init', 'Relations']];
184
186
  const MODE = { proposals: 'ask', props: 'ask', log: 'hist', history: 'hist' };
185
187
  const isDemo = () => !!window.__c3demo;
@@ -190,6 +192,8 @@ const hcls = (h) => (!h || h.total === 0 ? '' : h.passed === h.total ? 'ds-good'
190
192
  const isAgentRev = (r) => !!r && (r.text_by ? r.text_by === 'agent' : byName(r.by) === 'Agent');
191
193
  const md = (text, extra) => el('div', { class: 'ds-md' + (extra ? ' ' + extra : '') }, toDom(parseMd(text), document));
192
194
  const btn = (cls, attrs, kids, onClick) => c3btn(cls, attrs, kids, onClick);
195
+ /* a console3 primitive button (the Issue press and the menu: the mockup's UI.btn) */
196
+ const pb = (variant, attrs, kids, onClick) => { const b = button({ label: '', variant, size: 'sm', onClick, attrs: attrs || {} }); b.replaceChildren(...[].concat(kids).filter((k) => k != null && k !== false)); return b; };
193
197
  const headLevel = (s) => (/^(#+)/.exec(s.prefix || '') || ['', ''])[1].length;
194
198
 
195
199
  /* ── v2 (c3-doc-sheet-v2): the copies in other tools, the outline, the graph ── */
@@ -201,16 +205,19 @@ const SIG = {
201
205
  diverged: ['Both changed', 'alert', 'ds-bad', 'Both changed since they last matched.', 'both changed'],
202
206
  unchecked: ['Not checked', 'info', '', 'No check has read this copy yet.', 'not checked'],
203
207
  unreadable: ['Cannot open', 'alert', 'ds-bad', 'The last check could not open the copy.', 'cannot open'],
208
+ none: ['Not created', 'info', '', 'No copy exists yet.', 'not created'],
204
209
  'missing-remote': ['Not found', 'alert', 'ds-bad', 'The copy is not there any more.', 'not found'],
205
210
  'missing-repo': ['Not in PMOS', 'alert', 'ds-bad', 'This file is not in the repository any more.', 'not in PMOS'] };
206
211
  const SYNCING = ['Updating', 'clock', 'ds-acc', 'A session is updating this copy.', 'updating'];
207
- const KLABEL = { google: 'Google Doc', confluence: 'Confluence' };
212
+ const KLABEL = { google: 'Google Doc', 'google-slides': 'Google Slides', 'google-sheets': 'Google Sheets', confluence: 'Confluence' };
208
213
  const sigOf = (l) => (l.syncing ? SYNCING : SIG[l.signal] || SIG.unchecked);
209
214
  const labelOf = (l) => KLABEL[l.kind] || l.label || l.kind;
210
215
  /** Which tool a pasted address names (the server checks it again): 'confluence', 'google' or null. */
211
216
  export function copyKindOf(target) {
212
217
  const t = String(target || '');
213
218
  if (/atlassian|confluence|\/wiki\/|pageId=/i.test(t)) return 'confluence';
219
+ if (/docs\.google\.com\/presentation|slides\.google|\/presentation\/d\//i.test(t)) return 'google-slides';
220
+ if (/docs\.google\.com\/spreadsheets|sheets\.google|\/spreadsheets\/d\//i.test(t)) return 'google-sheets';
214
221
  if (/google|docs\.|drive\./i.test(t)) return 'google';
215
222
  return null;
216
223
  }
@@ -244,13 +251,20 @@ const olGet = () => { try { return localStorage.getItem(OL_KEY) !== 'off'; } cat
244
251
  const olSet = (on) => { try { localStorage.setItem(OL_KEY, on ? 'on' : 'off'); } catch { /* no storage here */ } };
245
252
  const ESTATE = { ok: 'in step', drift: 'out of step', conflict: 'conflict', missing: 'missing', ahead: 'PMOS ahead' };
246
253
 
247
- export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, onChanged, generated, secExtra, library, openGraph }) {
254
+ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, onChanged, generated, secExtra, library, openGraph, report }) {
248
255
  const S = { mode: MODE[mode] || mode, prompt, text: null, err: '', revs: null, texts: {}, health: null, hAsk: false, busy: false, changed: false,
249
256
  meta: null, metaFor: null, props: null, propsErr: '', log: null, logErr: '', asking: null, closed: false, who: false, src: false, preview: {},
250
257
  pick: {}, rejFor: null, rejWhy: '', whyFor: null, whyText: '', staleWhy: null, logF: 'all', cmp: null, cmpNode: null, flashRev: null, ed: null, leave: null,
251
- copies: null, copiesErr: '', syncOpen: false, linking: false, linkUrl: '', pend: {}, checkRun: null, lib: undefined, ol: olGet(), olOpen: false, olq: '', olShut: {}, cur: null };
258
+ copies: null, copiesErr: '', syncOpen: false, menu: false, linking: false, linkUrl: '', pend: {}, checkRun: null, lib: undefined, ol: olGet(), olOpen: false, olq: '', olShut: {}, cur: null };
252
259
  const demo = isDemo();
253
260
  const base = { ws, path: doc.path };
261
+ /* a report cycle (initiative report-workspace): report = { output, state: 'draft'|'issued', name, cycle, foot, onIssue(doc), onNext(doc), onState(state) }.
262
+ * It is a document with an output format: Read draws it as slides, a grid, a page or A4 pages, an issued one is frozen, and the copy is
263
+ * created by the PM's press. Everything else in the sheet is the ordinary document sheet. */
264
+ const R = report || null;
265
+ if (R) ensureReportCss();
266
+ const frozen = () => !!R && R.state === 'issued';
267
+ const fmt = () => formatOf(R && R.output);
254
268
  let ui = null;
255
269
 
256
270
  /* ── data ── */
@@ -324,11 +338,12 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
324
338
  const goEl = (sel) => { const n = ui && ui.body.querySelector(sel); if (n) { n.scrollIntoView({ block: 'start' }); n.classList.remove('flash'); void n.offsetWidth; n.classList.add('flash'); } };
325
339
 
326
340
  function head() {
327
- const st = STATE[fresh() ? 'stale' : doc.st] || STATE.current; const h = S.health; const last = lastRev();
341
+ const st = STATE[R ? R.state : fresh() ? 'stale' : doc.st] || STATE.current; const h = S.health; const last = lastRev();
328
342
  return el('div', { class: 'chips2 ds-meta', 'data-doc-meta': '' },
329
343
  el('span', { class: 'chip2 ' + st[1] }, c3ic(st[2], 'sm'), st[0]), el('span', { class: 'chip2' }, doc.ty || 'Document'), doc.v ? el('span', { class: 'chip2 mono' }, doc.v) : null,
330
344
  h && h.total ? btn('chip2 lk ' + hcls(h), { 'data-doc-hchip': '' }, [c3ic('shield', 'sm'), h.passed + ' of ' + h.total + ' checks'], () => go('health')) : null,
331
345
  el('span', { class: 'chip2' }, c3ic('init', 'sm'), doc.iniName || doc.ini || 'Portfolio'), copyChips(),
346
+ R ? el('span', { class: 'chip2', 'data-doc-format': R.output }, c3ic(fmt().ic, 'sm'), fmt().l) : null,
332
347
  last && !generated ? btn('chip2 lk', { 'data-doc-last': '' }, [c3ic(isAgentRev(last) ? 'spark' : 'user', 'sm'), 'Last change: ', el('b', null, whoOf(last) + ','), ' ' + dayName(last.at)], () => go('hist')) : null);
333
348
  }
334
349
  function seg() {
@@ -389,10 +404,13 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
389
404
  const d = (j.docs || []).find((x) => x.path === doc.path);
390
405
  S.copies = d ? d.links || [] : []; S.copiesErr = ''; S.checkRun = j.session || j.open || null;
391
406
  };
407
+ /* a report with no copy yet still shows the copy it will make: the tool its output keeps its copy in, "not created" */
408
+ const copyList = () => { const L = S.copies || []; return R && !L.length && fmt().copy ? [{ kind: fmt().copy, signal: 'none', synthetic: true, title: doc.name }] : L; };
392
409
  const copyChips = () => {
393
410
  if (generated || S.copies == null) return null;
394
- if (!S.copies.length) return btn('chip2 lk', { 'data-doc-copychip': '', 'aria-expanded': String(S.syncOpen) }, [c3ic('link', 'sm'), 'Link a Confluence or Google Doc copy'], () => toggleCopies());
395
- return S.copies.map((l) => { const g = sigOf(l); return btn('chip2 lk ' + g[2], { 'data-doc-copychip': l.kind, 'aria-expanded': String(S.syncOpen), title: l.words || g[3] }, [c3ic(g[1], 'sm'), labelOf(l) + ': ' + g[4]], () => toggleCopies()); });
411
+ if (R && !fmt().copy) return btn('chip2 lk', { 'data-doc-copychip': 'pdf', 'aria-expanded': String(S.syncOpen) }, [c3ic('pdf', 'sm'), 'PDF: no live copy'], () => toggleCopies());
412
+ if (!copyList().length) return btn('chip2 lk', { 'data-doc-copychip': '', 'aria-expanded': String(S.syncOpen) }, [c3ic('link', 'sm'), 'Link a Confluence or Google Doc copy'], () => toggleCopies());
413
+ return copyList().map((l) => { const g = sigOf(l); return btn('chip2 lk ' + g[2], { 'data-doc-copychip': l.kind, 'aria-expanded': String(S.syncOpen), title: l.words || g[3] }, [c3ic(g[1], 'sm'), labelOf(l) + ': ' + g[4]], () => toggleCopies()); });
396
414
  };
397
415
  function toggleCopies() {
398
416
  S.syncOpen = !S.syncOpen;
@@ -401,23 +419,28 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
401
419
  }
402
420
  function copiesPanel() {
403
421
  if (!S.syncOpen || generated) return null;
404
- const L = S.copies || [];
422
+ if (R && !fmt().copy) return el('section', { class: 'ds-sync', 'aria-label': 'Copies in other tools', 'data-doc-copies': '' }, el('div', { class: 'hd' }, el('b', null, 'Copies in other tools'),
423
+ btn('ds-btn', { 'aria-label': 'Close the copies', 'data-copy-close': '' }, c3ic('x', 'sm'), () => { S.syncOpen = false; draw(); })),
424
+ el('div', { class: 'fold' }, c3ic('info', 'sm'), 'A PDF has no live copy. Download it when you issue. Nothing goes to another tool.'));
425
+ const L = R ? copyList() : (S.copies || []);
405
426
  const rows = L.map((l) => {
406
427
  const g = sigOf(l); const name = labelOf(l); const pend = S.pend[l.kind];
407
428
  const acts = [btn('ds-btn', { 'data-copy-check': l.kind, disabled: !!S.checkRun, 'aria-disabled': S.checkRun ? 'true' : null }, [c3ic(S.checkRun ? 'clock' : 'undo', 'sm'), S.checkRun ? 'Checking' : 'Check now'], () => checkNow()),
408
429
  l.url ? el('a', { class: 'ds-btn', href: l.url, target: '_blank', rel: 'noopener', 'data-copy-open': l.kind }, c3ic('out', 'sm'), 'Open in ' + name) : null];
409
430
  if (!l.syncing && (l.signal === 'behind' || l.signal === 'diverged')) acts.push(btn('ds-btn ag', { 'data-copy-take': l.kind }, [c3ic('spark', 'sm'), 'Bring their change in as a proposal'], () => takeCopy(l)));
410
431
  if (!l.syncing && l.signal === 'ahead') acts.push(pend ? btn('ds-btn', { 'data-copy-undo': l.kind }, [c3ic('undo', 'sm'), 'Undo the update'], () => undoPush(l))
411
- : btn('ds-btn pri', { 'data-copy-push': l.kind }, [c3ic('up', 'sm'), 'Update the ' + name + ' to this version'], () => pushCopy(l)));
432
+ : btn('ds-btn pri', { 'data-copy-push': l.kind }, [c3ic('up', 'sm'), R ? 'Update the ' + name + ' copy to ' + (doc.v || 'this version') : 'Update the ' + name + ' to this version'], () => pushCopy(l)));
433
+ if (R && !l.syncing && l.signal === 'none') acts.push(pend ? btn('ds-btn', { 'data-copy-undo': l.kind }, [c3ic('undo', 'sm'), 'Undo'], () => undoPush(l))
434
+ : btn('ds-btn pri', { 'data-copy-create': l.kind }, [c3ic('out', 'sm'), 'Create the ' + name + ' copy'], () => createCopy(l)));
412
435
  const changed = l.remote_modified_at ? 'changed ' + whenOf(l.remote_modified_at) + (l.remote_modified_by ? ' by ' + l.remote_modified_by : '') : '';
413
436
  return el('li', { 'data-copy': l.kind },
414
437
  el('div', { class: 'l1' }, c3ic('doc', 'sm'), el('b', null, l.title || name), el('span', { class: 'tag ' + g[2], 'data-copy-state': l.syncing ? 'syncing' : l.signal || 'unchecked' }, c3ic(g[1], 'sm'), g[0])),
415
438
  el('div', { class: 'ds-mono' }, [name, l.target, changed].filter(Boolean).join(' · ')),
416
- el('p', null, l.words || g[3]),
439
+ el('p', null, l.words || (R && l.signal === 'none' ? 'No copy exists yet. PMOS writes to ' + name + ' only when you press Create the ' + name + ' copy.' : g[3])),
417
440
  el('div', { class: 'ds-note' }, l.checked_at ? 'Last checked ' + whenOf(l.checked_at) + (l.checked_by ? ' by ' + byName(l.checked_by) : '') + '.' : 'Not checked yet.'),
418
441
  l.syncing ? el('div', { class: 'fold', role: 'status', 'data-copy-syncing': l.kind }, c3ic('clock', 'sm'), 'A session is updating ' + (l.syncing.use === 'copy' ? 'PMOS from the ' + name : 'the ' + name + ' to this version') + (l.syncing.started_at ? '. It started ' + whenOf(l.syncing.started_at) + '.' : '.')) : null,
419
442
  l.sync_failed ? el('div', { class: 'fold w', role: 'alert', 'data-copy-failed': l.kind }, c3ic('alert', 'sm'), 'The last update did not finish' + (l.sync_failed.reason ? ': ' + l.sync_failed.reason : '.')) : null,
420
- pend ? el('div', { class: 'fold', role: 'status', 'data-copy-pending': l.kind }, c3ic('clock', 'sm'), 'The update starts in five seconds. Undo stops it.') : null,
443
+ pend ? el('div', { class: 'fold', role: 'status', 'data-copy-pending': l.kind }, c3ic('clock', 'sm'), (R && l.signal === 'none' ? 'The copy is created in five seconds' : 'The update starts in five seconds') + '. Undo stops it.') : null,
421
444
  el('div', { class: 'ds-acts' }, acts));
422
445
  });
423
446
  const linkRow = S.linking ? el('div', { class: 'ds-whyadd ds-linkadd' },
@@ -425,11 +448,12 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
425
448
  btn('ds-btn pri', { 'data-copy-linkok': '' }, 'Link', () => linkCopy()), btn('ds-btn', {}, 'Cancel', () => { S.linking = false; draw(); })) : null;
426
449
  return el('section', { class: 'ds-sync', 'aria-label': 'Copies in other tools', 'data-doc-copies': '' },
427
450
  el('div', { class: 'hd' }, el('b', null, 'Copies in other tools'), el('span', { class: 'ds-acts' },
428
- btn('ds-btn', { 'data-copy-link': '' }, [c3ic('link', 'sm'), L.length ? 'Link another copy' : 'Link a copy'], () => { S.linking = true; S.linkUrl = ''; draw(); const i = ui.body.querySelector('#dcopyurl'); if (i) i.focus(); }),
451
+ R ? null : btn('ds-btn', { 'data-copy-link': '' }, [c3ic('link', 'sm'), L.length ? 'Link another copy' : 'Link a copy'], () => { S.linking = true; S.linkUrl = ''; draw(); const i = ui.body.querySelector('#dcopyurl'); if (i) i.focus(); }),
429
452
  btn('ds-btn', { 'aria-label': 'Close the copies', 'data-copy-close': '' }, c3ic('x', 'sm'), () => { S.syncOpen = false; draw(); }))),
430
453
  S.copiesErr ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.copiesErr) : null, linkRow,
431
454
  L.length ? el('ul', null, rows) : el('p', { class: 'ds-note', 'data-copy-none': '' }, 'This document has no copy in Confluence or Google Docs yet. Link one to see whether it is in step.'),
432
- el('p', { class: 'ds-note' }, 'PMOS never overwrites this document from a copy: a change in the copy comes in as a proposal you accept by section. Updating a copy starts one agent session, and you have five seconds to undo.'));
455
+ el('p', { class: 'ds-note' }, R ? 'PMOS never writes to ' + fmt().app + ' on its own. Create and Update start one agent session through your own connector, and you can undo for 5 seconds. Nothing is sent to people.'
456
+ : 'PMOS never overwrites this document from a copy: a change in the copy comes in as a proposal you accept by section. Updating a copy starts one agent session, and you have five seconds to undo.'));
433
457
  }
434
458
  async function checkNow() {
435
459
  const r = await pmPost('/api/docs/check', { ws }).catch(() => null);
@@ -459,6 +483,18 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
459
483
  }, 5000);
460
484
  draw();
461
485
  }
486
+ function createCopy(l) {
487
+ const name = labelOf(l);
488
+ S.pend[l.kind] = setTimeout(async () => {
489
+ delete S.pend[l.kind];
490
+ const r = await pmPost('/api/docs/create-copy', { ws, path: doc.path, kind: l.kind }).catch(() => null);
491
+ if (!r || !r.ok) say((r && r.error) || 'The copy did not start.');
492
+ else say('Creating the ' + name + ' copy. A session writes it with your connector. Nothing is sent to people.');
493
+ await loadCopies(); drawBg();
494
+ }, 5000);
495
+ draw();
496
+ say('Creating the ' + name + ' copy in five seconds. Nothing is sent to people.', () => undoPush(l));
497
+ }
462
498
  function undoPush(l) { clearTimeout(S.pend[l.kind]); delete S.pend[l.kind]; draw(); say('Stopped. The ' + labelOf(l) + ' is not updated.'); }
463
499
  async function linkCopy() {
464
500
  const t = String(S.linkUrl || '').trim(); const kind = copyKindOf(t);
@@ -488,12 +524,12 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
488
524
  const on = S.cur === s.i;
489
525
  rows.push(el('li', { class: 'l' + lv + (on ? ' on' : ''), 'data-ol-i': String(s.i) },
490
526
  n && !qq ? btn('tw', { 'aria-label': (shut ? 'Show' : 'Hide') + ' the ' + plural(n, 'heading') + ' under ' + s.name, 'aria-expanded': String(!shut), 'data-ol-fold': String(s.i) }, c3ic('next', 'sm'), () => { S.olShut[s.i] = !shut; redrawOl(); }) : el('span', { class: 'tw' }),
491
- btn('go', { 'data-ol-go': String(s.i), 'aria-current': on ? 'location' : null }, [el('span', null, s.name),
527
+ btn('go', { 'data-ol-go': String(s.i), 'aria-current': on ? 'location' : null }, [el('span', null, it.label || s.name),
492
528
  stale.has(s.name) ? el('i', { class: 'mdot w', title: 'A source changed after this section', 'data-ol-stale': '' }) : null,
493
529
  prop.has(s.name) ? el('i', { class: 'mdot p', title: 'The agent proposes a change here', 'data-ol-prop': '' }) : null], () => jumpTo(s.i))));
494
530
  });
495
531
  return el('nav', { class: 'ds-ol', 'aria-label': 'Outline', 'data-doc-outline': '' },
496
- el('div', { class: 'hd' }, el('b', null, 'Outline'), el('span', { class: 'ds-mono' }, plural(items.length, 'heading'))),
532
+ el('div', { class: 'hd' }, el('b', null, 'Outline'), el('span', { class: 'ds-mono' }, plural(items.length, R ? fmt().word.replace(/s$/, '') : 'heading'))),
497
533
  items.length > 12 ? el('input', { type: 'text', id: 'dolq', 'data-ol-find': '', placeholder: 'Find a heading', 'aria-label': 'Find a heading', value: S.olq, onInput: (e) => { S.olq = e.target.value; redrawOl(); } }) : null,
498
534
  el('ul', null, rows.length ? rows : el('li', { class: 'none ds-note' }, 'No heading matches.')),
499
535
  stale.size || prop.size ? el('div', { class: 'key ds-note' }, stale.size ? el('span', null, el('i', { class: 'mdot w' }), 'stale') : null, prop.size ? el('span', null, el('i', { class: 'mdot p' }), 'proposal') : null) : null);
@@ -634,7 +670,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
634
670
  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.');
635
671
  const secs = splitDoc(S.text);
636
672
  if (!S.text.trim()) return el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'The document is empty.');
637
- const items = outlineOf(secs); const olOn = items.length >= 2 && !S.src;
673
+ const items = R ? outlineLabels(secs, R.output) : outlineOf(secs); const olOn = items.length >= 2 && !S.src;
638
674
  if (S.cur == null && items.length) S.cur = items[0].s.i;
639
675
  const curName = (items.find((x) => x.s.i === S.cur) || items[0] || { s: { name: '' } }).s.name;
640
676
  const olCtl = olOn ? el('span', { class: 'ds-acts' },
@@ -652,23 +688,24 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
652
688
  const meta = S.meta || {};
653
689
  const pins = (name) => openProps().filter((p) => p.sections.some((c) => c.name === name && c.kind !== 'added'));
654
690
  const flags = {}; failing().forEach((c) => { if (c.section && c.flag) flags[c.section] = c.flag; });
655
- const cells = secs.map((s) => {
691
+ const secCell = (s, inner, label) => {
656
692
  const fm = s.prefix === '' ? parseFront(s.text) : null;
657
693
  const body = fm ? fm.rest.trim() : s.text;
658
694
  if (!s.prefix && !body) return null;
659
695
  const r = meta[s.name]; const P = pins(s.name);
660
696
  const shown = P.find((p) => S.preview[p.id]);
661
697
  const ch = shown && shown.sections.find((c) => c.name === s.name);
662
- const content = ch ? md(diffMd(ch.stale ? s.text : ch.before, ch.after), 'ds-body') : body ? md(body, 'ds-body') : headLevel(s) === 1 ? null : el('p', { class: 'ds-note' }, 'This section is empty.');
698
+ const content = ch ? md(diffMd(ch.stale ? s.text : ch.before, ch.after), 'ds-body') : inner ? inner : body ? md(body, 'ds-body') : headLevel(s) === 1 ? null : el('p', { class: 'ds-note' }, 'This section is empty.');
663
699
  const lvl = Math.min(3, Math.max(2, headLevel(s)));
664
700
  return el('div', { class: 'ds-sec ' + (r ? (isAgentRev(r) ? 'a' : 'h') : '') + (ch ? ' prev' : ''), 'data-dsec': s.name, 'data-dsec-i': String(s.i) },
665
- s.prefix ? el('div', { class: 'ds-sec-h' }, headLevel(s) === 1 ? el('div', { class: 'ds-md' }, el('h1', null, s.name)) : el('h' + lvl, null, s.name), generated ? null : byChip(r)) : null,
701
+ s.prefix ? el('div', { class: 'ds-sec-h' }, label ? el('h3', null, label) : headLevel(s) === 1 ? el('div', { class: 'ds-md' }, el('h1', null, s.name)) : el('h' + lvl, null, s.name), generated ? null : byChip(r)) : null,
666
702
  P.map((p) => { const c = p.sections.find((x) => x.name === s.name); return el('div', { class: 'ds-pin', 'data-doc-pin': p.id }, el('span', null, c3ic('spark', 'sm'), (c.stale ? 'A proposal was drafted against older text here' : 'The agent proposes a change here') + ': “' + short(p.prompt) + '”'),
667
703
  el('span', { class: 'ds-acts' }, btn('ds-btn', { 'data-doc-preview': p.id }, S.preview[p.id] ? 'Show current' : 'Show change', () => { S.preview[p.id] = !S.preview[p.id]; draw(); }),
668
704
  btn('ds-btn ag', {}, 'Review', () => { S.openProp = p.id; go('ask'); }))); }),
669
705
  flags[s.name] ? el('div', { class: 'fold w', role: 'note' }, c3ic('alert', 'sm'), flags[s.name]) : null,
670
706
  content, secExtra ? secExtra(s.name) : null);
671
- });
707
+ };
708
+ 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));
672
709
  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 },
673
710
  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'))));
674
711
  const main = [generated ? el('div', { class: 'fold', role: 'note', 'data-doc-generated': '' }, c3ic('info', 'sm'), generated.why) : null, staleBanner(), propsBlock(secs, false), cells, adds];
@@ -679,7 +716,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
679
716
 
680
717
  /* ── Edit ── */
681
718
  function startEdit() {
682
- if (generated || S.text == null) { S.ed = null; return; }
719
+ if (generated || frozen() || S.text == null) { S.ed = null; return; }
683
720
  S.ed = { text: S.text, base: splitDoc(S.text), front: {}, kind: 'rich', why: '', whyBad: false, node: null, ta: null, range: null, link: false };
684
721
  }
685
722
  function edNode() {
@@ -777,6 +814,8 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
777
814
  }
778
815
  function editBody() {
779
816
  if (generated) return el('div', { class: 'fold w', role: 'note', 'data-doc-nostore': 'edit' }, c3ic('lock', 'sm'), generated.why);
817
+ if (frozen()) return el('div', { class: 'ds', 'data-doc-frozen': '' }, el('div', { class: 'fold w', role: 'status' }, c3ic('lock', 'sm'), 'Issued reports are frozen. Start the next cycle to change it.'),
818
+ el('div', { class: 'ds-acts' }, btn('ds-btn pri', { 'data-doc-next': '' }, [c3ic('plus', 'sm'), 'Start the next cycle'], () => nextCycle())));
780
819
  if (S.text == null) return readBody();
781
820
  if (!S.ed) startEdit();
782
821
  const E = S.ed;
@@ -815,6 +854,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
815
854
 
816
855
  /* ── Proposals ── */
817
856
  function askBox() {
857
+ if (frozen()) return el('div', { class: 'fold w', role: 'status', 'data-doc-frozen': '' }, c3ic('lock', 'sm'), 'Issued reports are frozen. Start the next cycle to ask for a change.');
818
858
  const A = S.asking;
819
859
  const ta = el('textarea', { id: 'dpr', placeholder: 'Tell the agent what to change, for example: add a one-paragraph summary at the top', 'aria-label': 'Request to the agent', 'data-doc-prompt': '', onInput: (e) => { S.prompt = e.target.value; } }, S.prompt);
820
860
  const sug = [...new Set(failing().map((c) => c.fix).filter(Boolean))];
@@ -876,8 +916,8 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
876
916
  md(c.kind === 'added' ? diffMd(null, c.after) : diffMd(c.stale ? c.current || '' : c.before, c.after)));
877
917
  });
878
918
  const host = el('div', { class: 'ds-slide', 'data-doc-slide': p.id });
879
- if (n) mountSlide(host, { label: 'Slide to accept ' + n + ' of ' + p.sections.length, hash: p.hash, onConfirm: (h) => acceptProp(p, h) });
880
- else host.append(el('div', { class: 'slt-why', role: 'note' }, c3ic('lock', 'sm'), anyStale && p.sections.every((c) => c.stale) ? 'Every section changed since this draft. Ask the agent again.' : 'Tick at least one section to accept.'));
919
+ if (n && !frozen()) mountSlide(host, { label: 'Slide to accept ' + n + ' of ' + p.sections.length, hash: p.hash, onConfirm: (h) => acceptProp(p, h) });
920
+ else host.append(el('div', { class: 'slt-why', role: 'note' }, c3ic('lock', 'sm'), frozen() ? 'Issued reports are frozen. Start the next cycle to accept a change.' : anyStale && p.sections.every((c) => c.stale) ? 'Every section changed since this draft. Ask the agent again.' : 'Tick at least one section to accept.'));
881
921
  const rej = S.rejFor === p.id
882
922
  ? el('div', { class: 'ds-whyadd' }, el('input', { type: 'text', id: 'drejwhy', 'data-doc-rejwhy': '', placeholder: 'Why are you rejecting it? Kept in the edit log.', 'aria-label': 'Why reject', value: S.rejWhy, onInput: (e) => { S.rejWhy = e.target.value; } }),
883
923
  btn('ds-btn pri', { 'data-doc-rejok': p.id }, 'Reject', () => rejectProp(p)), btn('ds-btn', {}, 'Cancel', () => { S.rejFor = null; draw(); }))
@@ -1013,15 +1053,65 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
1013
1053
  el('p', { class: 'ds-note' }, 'Health answers one question: can I trust this document without rereading it? The checks run again every time the text changes.'));
1014
1054
  }
1015
1055
 
1056
+ /* ── a report cycle: Issue, the menu (Download, Start the next cycle) ── */
1057
+ const toolsNode = R ? el('span', { class: 'rs-tools' }) : null;
1058
+ const slugOf = (t) => String(t || 'report').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
1059
+ function menuRow() {
1060
+ const items = downloadsOf(R.output);
1061
+ return el('div', { class: 'chips2 rs-menu', role: 'menu', 'aria-label': 'Document actions', 'data-doc-menu': '' }, el('span', { class: 'dim mono ds-mono' }, 'DOWNLOAD'),
1062
+ items.map((it) => pb('outline', { 'data-doc-download': it.fmt, role: 'menuitem' }, [c3ic('drop', 'sm'), it.label], () => download(it))),
1063
+ frozen() ? pb('ghost', { 'data-doc-next': '', role: 'menuitem' }, [c3ic('plus', 'sm'), 'Start the next cycle'], () => nextCycle()) : null);
1064
+ }
1065
+ async function download(it) {
1066
+ S.menu = false; draw();
1067
+ const title = R.name || doc.name; const secs = S.text != null ? splitDoc(S.text) : [];
1068
+ if (it.fmt === 'pdf') {
1069
+ if (!printPdf({ output: R.output, name: title, cycle: R.cycle, secs })) say('The browser blocked the print window. Allow pop-ups for this page, then press Download again.');
1070
+ else say('The print view is open. Choose Save as PDF. Nothing is uploaded.');
1071
+ return;
1072
+ }
1073
+ try {
1074
+ const r = await fetch('/api/report-export?' + q({ ws, path: doc.path, fmt: it.fmt }));
1075
+ if (!r.ok) { let m = ''; try { m = (await r.json()).error; } catch { /* not JSON */ } throw new Error(m || 'the workspace did not make the file'); }
1076
+ const blob = await r.blob();
1077
+ const cd = /filename="?([^";]+)"?/.exec(r.headers.get('content-disposition') || '');
1078
+ const name = cd ? cd[1] : slugOf(title + ' ' + (R.cycle || '')) + '.' + it.fmt;
1079
+ const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; document.body.appendChild(a); a.click(); a.remove();
1080
+ setTimeout(() => URL.revokeObjectURL(a.href), 4000);
1081
+ say('Downloaded ' + name + '. Nothing went to another tool.');
1082
+ } catch (e) { say('The file was not made: ' + ((e && e.message) || 'no answer') + '.'); }
1083
+ }
1084
+ async function issue() {
1085
+ /* Issue is the PM's act on the existing route (routes-reports.js, requirePmPress). It freezes the bytes the PM read:
1086
+ the sha comes from the file as served, and the page refuses when that text is not the text on screen. */
1087
+ const m = /reports\/([a-z][a-z0-9-]{0,39})\/(\d{4}-\d{2}-\d{2})\.md$/.exec(doc.path || '');
1088
+ if (!m) { say('This document is not a report cycle, so it cannot be issued.'); return; }
1089
+ const cur = await pmGet('/api/report/draft?' + q({ ws, cadence: m[1], slot: m[2] })).catch(() => null);
1090
+ if (!cur || !cur.ok || !cur.exists || !cur.sha) { say((cur && cur.error) || 'The report was not issued: the file could not be read.'); return; }
1091
+ if (S.text != null && cur.markdown != null && cur.markdown !== S.text) { await reloadAll(); say('The file changed since you opened it. Read it again, then press Issue.'); return; }
1092
+ const r = await pmPost('/api/report/issue', { ws, cadence: m[1], slot: m[2], sha: cur.sha }).catch(() => null);
1093
+ if (!r || !r.ok) { say((r && r.error) || 'The report was not issued.'); return; }
1094
+ R.state = 'issued'; doc.st = 'issued'; S.changed = true; S.menu = false; if (S.mode === 'edit') { S.ed = null; S.mode = 'read'; }
1095
+ if (R.onIssue) R.onIssue(doc);
1096
+ await reloadAll(); say('Issued and frozen. PMOS sends nothing; share it yourself.');
1097
+ }
1098
+ 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.'); }
1099
+ function drawTools() {
1100
+ if (!toolsNode) return;
1101
+ toolsNode.replaceChildren(...[R.state === 'draft' ? pb('outline', { 'data-doc-issue': '' }, [c3ic('check', 'sm'), 'Issue'], () => issue()) : null,
1102
+ 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));
1103
+ }
1104
+
1016
1105
  /* a load that finished in the background must not redraw under a hand that is typing */
1017
1106
  const drawBg = () => { const a = document.activeElement; if (ui && a && ui.body.contains(a) && a.closest('input,textarea,[contenteditable=true]')) { refreshTop(); return; } draw(); };
1018
- const top = () => el('div', { class: 'ds-top' }, head(), seg(), S.mode === 'edit' && S.ed ? toolbar() : null,
1107
+ const top = () => el('div', { class: 'ds-top' }, head(), seg(), R && S.menu ? menuRow() : null, S.mode === 'edit' && S.ed ? toolbar() : null,
1019
1108
  S.leave ? el('div', { class: 'fold w', role: 'alert', 'data-doc-leave': '' }, c3ic('alert', 'sm'), 'You have unsaved changes in ' + plural(dirty().length, 'section') + '. ',
1020
1109
  btn('ds-btn', {}, 'Keep editing', () => { S.leave = null; drawEdit(); }), btn('ds-btn', { 'data-doc-discard': '' }, 'Discard and leave', () => { const m = S.leave; S.ed = null; S.leave = null; go(m); })) : null);
1021
1110
  function refreshTop() { const t = ui && ui.body.querySelector('.ds-top'); if (t) t.replaceWith(top()); }
1022
1111
  function draw() {
1023
1112
  if (!ui) return;
1024
1113
  if (S.mode === 'edit' && S.ed && S.ed.kind === 'rich' && S.ed.node) keepRange();
1114
+ drawTools();
1025
1115
  const body = S.mode === 'read' ? readBody() : S.mode === 'edit' ? editBody() : S.mode === 'ask' ? propsBody() : S.mode === 'hist' ? logBody() : S.mode === 'health' ? healthBody() : linksBody();
1026
1116
  const keep = ui.body.querySelector('textarea:focus,input[type=text]:focus');
1027
1117
  const had = keep ? { id: keep.id, s: keep.selectionStart } : null;
@@ -1036,7 +1126,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
1036
1126
  }
1037
1127
 
1038
1128
  document.addEventListener('selectionchange', keepRange);
1039
- ui = openSheet({ title: doc.name, icon: 'doc', dw: true, body: el('div', { class: 'dpn' }), onClose: () => { S.closed = true; document.removeEventListener('selectionchange', keepRange); if (S.changed && onChanged) onChanged(); } });
1129
+ ui = openSheet({ title: doc.name, icon: 'doc', dw: true, actions: toolsNode, body: el('div', { class: 'dpn' }), onClose: () => { S.closed = true; document.removeEventListener('selectionchange', keepRange); if (S.changed && onChanged) onChanged(); } });
1040
1130
  S.open = (d) => { ui.close(); if (S.openOther) S.openOther(d); else openDocSheet({ ws, doc: d, library, openGraph }); };
1041
1131
  ui.setOpener = (fn) => { S.openOther = fn; };
1042
1132
  draw();
@@ -8,7 +8,7 @@
8
8
  * - the portfolio: the headline joins the breadcrumb, an empty triage card is not shown, the timeline folds;
9
9
  * - empty chips ("No owner", "0 epics", ...) are not drawn;
10
10
  * - Calendar: the layer chips sit behind one Layers button, the Detail card shows once a meeting is picked;
11
- * - Work: the lenses sit beside Add work; Reports: Share and Edit dashboard join the tab row.
11
+ * - Work: the lenses sit beside Add work; Reports: Send this as a report and Edit dashboard join the tab row.
12
12
  * Desktop only: below 601px the page keeps today's layout.
13
13
  * The views draw their own markup; this module only moves and hides it, so every press still goes to the view's own handler.
14
14
  * It runs again, debounced, whenever the views redraw (one MutationObserver on the root). */
@@ -144,7 +144,7 @@ function apply(root) {
144
144
  const add = w.querySelector(':scope > .c3add'), bar = w.querySelector(':scope > .calbar'), l = bar && bar.querySelector('[data-pm-lenses]');
145
145
  if (add && l && l.parentElement === bar) { for (const o of add.querySelectorAll('[data-pm-lenses]')) o.remove(); add.appendChild(l); bar.classList.add('qt-moved'); }
146
146
  }
147
- // Reports: Share and Edit dashboard on the tab row.
147
+ // Reports: Send this as a report and Edit dashboard on the tab row.
148
148
  for (const r of root.querySelectorAll('[data-rp3]')) {
149
149
  const scope = r.querySelector(':scope > .calbar .rp3-scope'), acts = r.querySelector('.rp3-head > .chips2');
150
150
  if (scope && acts) { for (const o of scope.querySelectorAll('.qt-acts')) o.remove(); acts.classList.add('qt-acts'); scope.appendChild(acts); }
@@ -0,0 +1,20 @@
1
+ /* The printable report (Download > PDF): its own window, printed by the browser. Colours are the slide tokens of
2
+ tokens.css (linked first), so the paper reads the same in either theme. Slides are 16:9 landscape cards (.rp-slide),
3
+ everything else is A4. */
4
+ @page { size: A4; margin: 16mm; }
5
+ .rp-print { margin: 0; font-family: var(--sans); font-size: 14px; line-height: 1.5; color: var(--slide-fg); background: var(--slide-bg); }
6
+ .rp-print h1 { font-size: 20px; margin: 0 0 4mm; }
7
+ .rp-print h2 { font-size: 16px; margin: 6mm 0 2mm; color: var(--slide-brand); }
8
+ .rp-print p, .rp-print ul { margin: 0 0 3mm; }
9
+ .rp-print table { border-collapse: collapse; width: 100%; margin: 0 0 3mm; }
10
+ .rp-print th, .rp-print td { border: 1px solid var(--slide-border); padding: 2mm 3mm; text-align: left; font-size: 12px; }
11
+ .rp-print th { background: var(--slide-secondary); }
12
+ .rp-print .rp-print-m { color: var(--slide-muted); }
13
+ .rp-print-slides { margin: 0; }
14
+ @page slides { size: 297mm 167mm; margin: 0; }
15
+ .rp-slide { page: slides; page-break-after: always; width: 297mm; height: 167mm; box-sizing: border-box; padding: 14mm; font-size: 20px; }
16
+ .rp-slide h2 { margin-top: 0; font-size: 24px; }
17
+ .rp-slide li { margin-bottom: 2mm; }
18
+ .rp-slide table { font-size: 16px; }
19
+ .rp-slide--title { background: var(--slide-brand); color: var(--slide-bg); }
20
+ .rp-slide--title h2 { color: var(--slide-bg); }
@@ -0,0 +1,62 @@
1
+ /* pm/c3-report-setup.css — the Reports tab (Ask PMOS, Found in your work, Set up, Documents), the "Send this as a report" sheet, the Briefing card, the Settings
2
+ * block and the report formats of the document sheet (initiative report-workspace). The mockup's 41-reportsetup.css under real class names: .mk-rs is .rs,
3
+ * .mk-rs-* is .rs-*, .mk-kv is .rs-kv. Tokens only. Corners: 6px on a control or a card drawn as paper, 8px on a container, else 0.
4
+ * c3-report-setup.js and c3-doc-sheet.js link it once. The slide colours are the --slide-* tokens of tokens.css. */
5
+ /* in .brief a .nrow has two columns, so the row buttons sit under the text */
6
+ .c3 .rs .nrow > .acts { grid-column: 2; justify-content: flex-start; display: flex; gap: 6px; flex-wrap: wrap; }
7
+ .c3 .rs, .c3 .rs > * { min-width: 0; }
8
+ .c3 .rs .nrow .chips .chip2 { max-width: 100%; }
9
+ .c3 .rs .form { margin: 0; }
10
+ .c3 .rs .seg { max-width: 100%; overflow-x: auto; }
11
+ .c3 .rs .seg button { white-space: nowrap; }
12
+ .c3 [data-rs-finder] .nrow > .acts { grid-column: 2; justify-content: flex-start; display: flex; gap: 6px; flex-wrap: wrap; }
13
+ .c3 [data-rs-finder] .seg { max-width: 100%; overflow-x: auto; }
14
+ .c3 .rs .lh { grid-template-columns: auto minmax(0, 1fr); }
15
+ .c3 .rs .lh > .chips2 { grid-column: 1 / -1; }
16
+ .c3 .rs-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 12px; margin: 0; font-size: var(--fs-2); }
17
+ .c3 .rs-kv dt { font: var(--fs-1) var(--mono); color: var(--muted-foreground); text-transform: uppercase; letter-spacing: .06em; padding-top: 2px; }
18
+ .c3 .rs-kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
19
+ .c3 .rs-kv input[type="text"] { width: 100%; box-sizing: border-box; }
20
+ .c3 .rs-kv ol { margin: 0; padding-left: 18px; }
21
+ .c3 .rs-lbl { font: var(--fs-1) var(--mono); color: var(--muted-foreground); }
22
+ .c3 .rs-help { font-size: var(--fs-2); }
23
+ .c3 .rs-stack { display: grid; gap: var(--sp-2); }
24
+ .c3 .rs-tools { display: inline-flex; gap: 6px; align-items: center; }
25
+ .c3 .rs-menu { align-items: center; }
26
+ @media (max-width: 560px) { .c3 .rs-kv { grid-template-columns: minmax(0, 1fr); } }
27
+
28
+ /* Output formats: the document drawn as slides, a grid, a doc page or A4 pages. Paper colours are the slide tokens. */
29
+ .c3 .rs-slides { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: var(--sp-3); }
30
+ .c3 .rs-slides > .ds-sec { margin-left: 0; }
31
+ .c3 .rs-slide { aspect-ratio: 16 / 9; min-width: 0; display: grid; grid-template-rows: auto 1fr auto; gap: var(--sp-2); padding: var(--sp-3); background: var(--slide-bg); color: var(--slide-fg); border: 1px solid var(--slide-border); border-radius: 6px; font-size: var(--fs-2); overflow: hidden; }
32
+ .c3 .rs-sh { font-weight: 600; font-size: var(--fs-3); color: var(--slide-brand); overflow-wrap: anywhere; }
33
+ .c3 .rs-slide.title { background: var(--slide-brand); color: var(--slide-bg); }
34
+ .c3 .rs-slide.title .rs-sh { color: var(--slide-bg); font-size: var(--fs-4); }
35
+ .c3 .rs-slide .m, .c3 .rs-page .m { color: var(--slide-muted); margin: 0; }
36
+ .c3 .rs-slide.title .m { color: var(--slide-border); }
37
+ .c3 .rs-slide p, .c3 .rs-slide .ds-md { margin: 0; font-size: var(--fs-2); color: inherit; }
38
+ .c3 .rs-slide ul, .c3 .rs-page ul { margin: 0; padding-left: 18px; display: grid; gap: 4px; }
39
+ .c3 .rs-sf { display: flex; justify-content: space-between; font: var(--fs-1) var(--mono); color: var(--slide-foot); }
40
+ .c3 .rs-big { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
41
+ .c3 .rs-big b { display: block; font-size: var(--fs-4); color: var(--slide-primary); }
42
+ .c3 .rs-big span { color: var(--slide-muted); font-size: var(--fs-1); }
43
+ .c3 .rs-sheets { display: grid; gap: var(--sp-3); }
44
+ .c3 .rs-gridwrap { max-width: 100%; overflow-x: auto; border: 1px solid var(--slide-border); border-radius: 6px; background: var(--slide-bg); }
45
+ .c3 .rs-grid { border-collapse: collapse; min-width: 640px; width: 100%; color: var(--slide-fg); font-size: var(--fs-2); }
46
+ .c3 .rs-grid th, .c3 .rs-grid td { border: 1px solid var(--slide-border); padding: 4px 8px; text-align: left; }
47
+ .c3 .rs-grid thead th, .c3 .rs-grid tbody th { background: var(--slide-secondary); color: var(--slide-muted); font: var(--fs-1) var(--mono); text-align: center; }
48
+ .c3 .rs-grid tr.hd td { font-weight: 600; background: var(--slide-track); }
49
+ .c3 .rs-grid td.n { text-align: right; font-family: var(--mono); }
50
+ .c3 .rs-grid td.pos { color: var(--slide-done); }
51
+ .c3 .rs-grid td.neg { color: var(--slide-overdue); }
52
+ .c3 .rs-page { background: var(--slide-bg); color: var(--slide-fg); border: 1px solid var(--slide-border); border-radius: 6px; padding: var(--sp-4); display: grid; gap: var(--sp-2); align-content: start; min-width: 0; font-size: var(--fs-3); }
53
+ .c3 .rs-page h3 { margin: 0; font-size: var(--fs-5); }
54
+ .c3 .rs-page p { margin: 0; }
55
+ .c3 .rs-page .ds-md { color: inherit; }
56
+ .c3 .rs-page .ds-sec-h h3 { color: var(--slide-brand); }
57
+ .c3 .rs-pages { display: grid; gap: var(--sp-3); grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); }
58
+ .c3 .rs-page.a4 { aspect-ratio: 1 / 1.414; grid-template-rows: auto; align-content: start; position: relative; }
59
+ .c3 .rs-pn { position: absolute; bottom: var(--sp-2); right: var(--sp-3); font: var(--fs-1) var(--mono); color: var(--slide-foot); }
60
+
61
+ .c3 .rs-min0 { min-width: 0; }
62
+ .c3 .rs-kv .chip2 + .rs-help { margin-left: 6px; }