create-pmos 0.239.0 → 0.241.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 (48) 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-structure-real-test.js +211 -0
  7. package/workspace/c3-report-structure-test.js +382 -0
  8. package/workspace/guide/briefing.md +31 -3
  9. package/workspace/guide/index.md +2 -0
  10. package/workspace/guide/report-design.md +4 -0
  11. package/workspace/guide/report-structure.md +91 -0
  12. package/workspace/guide/reports-dashboards.md +1 -1
  13. package/workspace/guide/reports-set-up.md +2 -0
  14. package/workspace/lib/charts-store.js +124 -0
  15. package/workspace/lib/headless.js +17 -3
  16. package/workspace/lib/needs-providers.js +1 -0
  17. package/workspace/lib/report-structure-needs.js +64 -0
  18. package/workspace/lib/report-structure.js +515 -0
  19. package/workspace/lib/route-modules.js +3 -1
  20. package/workspace/lib/routes-brief-page.js +56 -5
  21. package/workspace/lib/routes-mentions.js +37 -0
  22. package/workspace/lib/routes-report-setups.js +11 -6
  23. package/workspace/lib/routes-report-structure.js +619 -0
  24. package/workspace/package-lock.json +2 -2
  25. package/workspace/package.json +1 -1
  26. package/workspace/public/pm/briefing-deck.js +1 -0
  27. package/workspace/public/pm/c3-brief-mentions.js +136 -0
  28. package/workspace/public/pm/c3-brief-needs.js +23 -0
  29. package/workspace/public/pm/c3-brief-pulse.js +1 -0
  30. package/workspace/public/pm/c3-brief-summary.js +139 -0
  31. package/workspace/public/pm/c3-demo-brief-page.js +20 -4
  32. package/workspace/public/pm/c3-demo-rd.js +4 -0
  33. package/workspace/public/pm/c3-design-studio.js +7 -3
  34. package/workspace/public/pm/c3-report-setup.js +3 -0
  35. package/workspace/public/pm/c3-report-structure.css +40 -0
  36. package/workspace/public/pm/c3-report-structure.js +455 -0
  37. package/workspace/public/pm/c3-reports.css +2 -0
  38. package/workspace/public/pm/c3-reports.js +22 -3
  39. package/workspace/public/pm/c3-shell.js +34 -2
  40. package/workspace/public/pm/console3.css +12 -0
  41. package/workspace/server.js +38 -9
  42. package/workspace/skills/report-source-check.skill +45 -0
  43. package/workspace/skills/report-structure-dry.skill +51 -0
  44. package/workspace/skills/report-structure.skill +61 -0
  45. package/workspace/skills/report-template.skill +10 -0
  46. package/workspace/test-files.json +2 -0
  47. package/workspace/test-lists/report-structure-backend.json +5 -0
  48. package/workspace/test-lists/report-structure-ui.json +11 -0
@@ -364,6 +364,7 @@ function draw(S) {
364
364
  if (bare && (!d || !d.ok || d.scope !== S.ctx.scope || !(d.cards || []).length)) { S.root.replaceChildren(); S.root.hidden = true; S.flip = null; return; }
365
365
  if (bare) S.root.hidden = false;
366
366
  const kids = bare ? [] : [stance(S)];
367
+ if (!bare && S.ctx && S.ctx.afterStance) kids.push(S.ctx.afterStance); // a node the page keeps under the greeting (console3: The brief)
367
368
  if (!d) kids.push(el('p', { class: 'dim mono', style: small, role: 'status' }, 'Reading the repository'));
368
369
  else if (!d.ok) kids.push(el('p', { class: 'dim mono', style: small, role: 'status', 'data-bf': 'error' }, 'The briefing could not be read: ' + d.error));
369
370
  else {
@@ -0,0 +1,136 @@
1
+ /* pm/c3-brief-mentions.js — the Briefing's "Mentions" (initiative c3-brief-summary-mentions).
2
+ *
3
+ * Everything today's brief marks as a mention of the PM (Slack, Jira, Confluence, mail), from GET /api/brief-page `mentions`
4
+ * (routes-brief-page.js mentionsOf). Quiet by default: one line that names who waits on a reply. "Show all" opens the list, with a tab for
5
+ * each source that has a mention. A row: the person, the quoted text, the source and place, the time, "needs your reply" or "for your
6
+ * information", the initiative. Its actions:
7
+ * Open in <source> the mention's own https link, in a new tab (no link, no button)
8
+ * Add to Work POST /api/pm-items/add { line: "Reply to <who> (<source>) by today" } (a reply) or "Read <who>'s note (<source>)"
9
+ * Mark read POST /api/mentions/read { id, read: true }; Undo sends read: false
10
+ * A mention that is a worth-asking item (in_needs) is not listed: Needs you shows it (its row is need_key), and that row carries a "mentioned you" tag
11
+ * (tagNeeds, called after every Needs you redraw). Nothing here replies, reacts or posts in Slack, Jira, Confluence or mail. */
12
+ import { el } from './dom.js';
13
+ import { c3ic } from './pm-work-view.js';
14
+ import { pmPost } from './http.js';
15
+ import { toast } from './c3-toast.js';
16
+ import { onBriefPage, loadBriefPage } from './c3-brief-data.js';
17
+ import { button, tabs, SEL, ensureComponentsCss } from './primitives.js';
18
+ import { ensureSprite } from './icons.js';
19
+ import { srcName, waitingMentions } from './c3-brief-summary.js';
20
+
21
+ const p2 = (n) => String(n).padStart(2, '0');
22
+ const first = (s) => String(s || '').trim().split(/\s+/)[0];
23
+ const ICON = { slack: 'reply', jira: 'org', confluence: 'doc', mail: 'mail' };
24
+ const ORDER = ['slack', 'jira', 'confluence', 'mail'];
25
+ const post = (p, b) => pmPost(p, b).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
26
+
27
+ /** When, said the way the Briefing says times: "09:15" today, "yesterday 17:05" before. */
28
+ function whenOf(iso, now = new Date()) {
29
+ if (!iso) return null;
30
+ const d = new Date(iso); const hm = p2(d.getHours()) + ':' + p2(d.getMinutes());
31
+ const day = (x) => x.getFullYear() * 400 + x.getMonth() * 32 + x.getDate();
32
+ if (day(d) === day(now)) return hm;
33
+ const y = new Date(now); y.setDate(y.getDate() - 1);
34
+ return day(d) === day(y) ? 'yesterday ' + hm : d.getDate() + '/' + (d.getMonth() + 1) + ' ' + hm;
35
+ }
36
+
37
+ /** The closed line: who waits on a reply, then how many are for information. Pure. */
38
+ export function waitingLine(shown) {
39
+ const w = waitingMentions(shown); const rest = shown.length - w.length;
40
+ if (!w.length) return shown.length + (shown.length === 1 ? ' mention, for your information.' : ' mentions, all for your information.');
41
+ const names = w.map((m) => (m.who ? first(m.who) : 'Someone') + ' in ' + srcName(m.source));
42
+ const said = names.length < 2 ? names[0] : names.slice(0, -1).join(', ') + ' and ' + names[names.length - 1];
43
+ return said + (w.length === 1 ? ' is' : ' are') + ' waiting on your reply.' + (rest ? ' ' + rest + (rest === 1 ? ' more is' : ' more are') + ' for your information.' : '');
44
+ }
45
+
46
+ export function mentionsSection(S) {
47
+ ensureComponentsCss(); ensureSprite();
48
+ const st = { open: false, tab: 'all', local: new Map(), list: null };
49
+ const count = el('span', { class: 'bdg g' }, '0');
50
+ const right = el('span', { class: 'chips2' });
51
+ const body = el('div', { 'data-mentions-body': '' });
52
+ const box = el('section', { class: 'card sect', 'data-c3pg': 'mentions', 'aria-label': 'Mentions' },
53
+ el('div', { class: 'sh2' }, el('h2', null, c3ic('reply', 'sm'), 'Mentions ', count), right), body);
54
+ box.hidden = true;
55
+
56
+ const all = () => (st.list || []).map((m) => (st.local.has(m.id) ? { ...m, read: st.local.get(m.id) } : m));
57
+ const shown = () => all().filter((m) => !m.in_needs && !m.read);
58
+
59
+ const markRead = (m, read) => post('/api/mentions/read', { id: m.id, read }).then((r) => {
60
+ if (!r || r.ok === false) { toast('That did not go through' + (r && r.error ? ': ' + r.error : '.')); return false; }
61
+ st.local.set(m.id, read); draw(); if (S.briefRedraw) S.briefRedraw(); return true;
62
+ });
63
+ const addToWork = (m) => {
64
+ const who = m.who ? first(m.who) : 'the sender';
65
+ const line = m.reply ? 'Reply to ' + who + ' (' + srcName(m.source) + ') by today' : 'Read ' + who + '\'s note (' + srcName(m.source) + ')';
66
+ return post('/api/pm-items/add', { line }).then((r) => {
67
+ if (!r || r.ok === false) { toast('That did not go through' + (r && r.error ? ': ' + r.error : '.')); return; }
68
+ toast('Added to Work: ' + line.replace(/ by today$/, ''));
69
+ if (S.workChanged) S.workChanged();
70
+ });
71
+ };
72
+
73
+ const rowOf = (m) => {
74
+ const when = whenOf(m.at);
75
+ const chips = [m.reply ? el('span', { class: 'tag warn', 'data-mention-reply': '' }, 'needs your reply') : el('span', { class: 'tag', 'data-mention-fyi': '' }, 'for your information'),
76
+ el('span', { class: 'chip2', 'data-mention-where': '' }, srcName(m.source) + (m.where ? ' · ' + m.where : '')),
77
+ when ? el('span', { class: 'chip2' }, c3ic('clock', 'sm'), when) : null,
78
+ m.initiative ? el('span', { class: 'chip2' }, c3ic('init', 'sm'), m.initiative) : null];
79
+ const acts = el('div', { class: 'mn-acts' },
80
+ m.link ? el('a', { class: 'chip2 mn-open', href: m.link, target: '_blank', rel: 'noopener noreferrer', 'data-mention-open': m.id }, c3ic('out', 'sm'), 'Open in ' + srcName(m.source)) : null,
81
+ button({ label: 'Add to Work', variant: 'ghost', size: 'sm', icon: 'plus', attrs: { 'data-mention-work': m.id }, onClick: () => addToWork(m) }),
82
+ button({ label: 'Mark read', variant: 'ghost', size: 'sm', icon: 'check', attrs: { 'data-mention-read': m.id },
83
+ onClick: () => markRead(m, true).then((done) => { if (done) toast('Marked read', () => markRead(m, false)); }) }));
84
+ return el('li', { class: 'nrow mn-row', 'data-mention': m.id, 'data-source': m.source },
85
+ el('span', { class: 'ki' + (m.reply ? ' hot' : '') }, c3ic(ICON[m.source] || 'reply')),
86
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, m.who || srcName(m.source)), el('div', { class: 'mn-q' }, '"' + m.text + '"'),
87
+ el('div', { class: 'chips' }, ...chips.filter(Boolean)), acts),
88
+ el('span'));
89
+ };
90
+
91
+ const toggle = button({ label: 'Show all', variant: 'ghost', size: 'sm', attrs: { 'data-mentions-toggle': '', 'aria-expanded': 'false' }, onClick: () => { st.open = !st.open; draw(); } });
92
+ function draw() {
93
+ if (!st.list) { box.hidden = st.list === undefined; body.replaceChildren(el('p', { class: 'dim c3s2', 'data-mentions-state': 'none' }, 'PMOS has no brief for today, so no mentions were read.')); count.textContent = '0'; right.replaceChildren(); return; }
94
+ box.hidden = false;
95
+ const list = shown(); const wait = waitingMentions(list);
96
+ count.textContent = String(list.length);
97
+ toggle.querySelector(SEL.btnLabel).textContent = st.open ? 'Hide' : 'Show all';
98
+ toggle.setAttribute('aria-expanded', String(st.open));
99
+ right.replaceChildren(...(wait.length ? [el('span', { class: 'tag warn', 'data-mentions-wait': String(wait.length) }, wait.length + (wait.length === 1 ? ' needs a reply' : ' need a reply'))] : []), ...(list.length ? [toggle] : []));
100
+ if (!list.length) { body.replaceChildren(el('p', { class: 'dim c3s2', 'data-mentions-state': 'empty' }, 'No one has mentioned you today.')); return; }
101
+ if (!st.open) { body.replaceChildren(el('p', { class: 'mn-line', 'data-mentions-line': '' }, waitingLine(list))); return; }
102
+ const srcs = ORDER.filter((k) => list.some((m) => m.source === k)).concat([...new Set(list.map((m) => m.source))].filter((k) => !ORDER.includes(k)));
103
+ if (st.tab !== 'all' && !srcs.includes(st.tab)) st.tab = 'all';
104
+ const bar = tabs({ ariaLabel: 'Mentions by source', value: st.tab, attrs: { class: 'mn-tabs', 'data-mentions-tabs': '' },
105
+ items: ['all', ...srcs].map((k) => ({ id: k, label: k === 'all' ? 'All' : srcName(k), count: k === 'all' ? list.length : list.filter((m) => m.source === k).length })),
106
+ onChange: (k) => { // the list draws again; keep the focus on the chosen tab
107
+ const had = bar.contains(document.activeElement); st.tab = k; draw();
108
+ const now = had && body.querySelector('[data-mentions-tabs] [data-tab="' + k + '"]'); if (now) now.focus();
109
+ } });
110
+ const rows = list.filter((m) => st.tab === 'all' || m.source === st.tab).sort((a, b) => (b.reply - a.reply) || ((b.at ? Date.parse(b.at) : 0) - (a.at ? Date.parse(a.at) : 0)));
111
+ body.replaceChildren(bar, el('ul', { class: 'list', role: 'list', 'data-mentions-list': '' }, ...rows.map(rowOf)));
112
+ }
113
+
114
+ /* Needs you shows a worth-asking mention as its need_key row: tag it, first among its chips so the quiet layout keeps it */
115
+ S.tagNeeds = (host) => {
116
+ const by = new Map(all().filter((m) => m.in_needs && m.need_key).map((m) => [m.need_key, m]));
117
+ if (!by.size) return;
118
+ (host || document).querySelectorAll('[data-needs-row]').forEach((row) => {
119
+ const m = by.get(row.dataset.needsRow); const chips = row.querySelector('.nb > .chips');
120
+ if (!m || !chips || chips.querySelector('[data-mentioned-you]')) return;
121
+ chips.prepend(el('span', { class: 'tag', 'data-mentioned-you': '', title: (m.who ? m.who + ' mentioned you' : 'You were mentioned') + ' in ' + srcName(m.source) }, 'mentioned you'));
122
+ });
123
+ };
124
+
125
+ st.list = undefined;
126
+ onBriefPage((j) => {
127
+ st.list = j && j.ok !== false ? (Array.isArray(j.mentions) ? j.mentions : null) : null;
128
+ st.local.clear();
129
+ draw();
130
+ if (S.tagNeeds) S.tagNeeds(S.needsBox);
131
+ if (S.briefRedraw) S.briefRedraw();
132
+ });
133
+ S.mentionsNow = () => (st.list === undefined ? null : st.list ? all() : null);
134
+ S.reloadMentions = () => loadBriefPage();
135
+ return box;
136
+ }
@@ -133,6 +133,23 @@ export function needsSection(S) {
133
133
  const f = list.querySelector('.fold');
134
134
  if (!n && !f) list.replaceChildren(nothing());
135
135
  else if (n && f && list.children.length === 1 && f.textContent.startsWith('Nothing needs')) f.remove();
136
+ if (S.tagNeeds) S.tagNeeds(box); // Mentions (c3-brief-mentions.js): the "mentioned you" tag on a worth-asking row
137
+ if (S.briefRedraw) S.briefRedraw(); // The brief (c3-brief-summary.js) says what this section holds
138
+ };
139
+ /* What The brief reads of this section: the rows on the page now, with what each row's data says. null until both reads came back. */
140
+ const meta = new Map();
141
+ S.needsSnapshot = () => {
142
+ if (!last || !page || page.ok === false) return null;
143
+ const rowsIn = (host) => [...host.querySelectorAll('.nrow[data-needs-row]')];
144
+ const decisions = [...rowsIn(list), ...rowsIn(extra)].map((r) => {
145
+ const k = r.dataset.needsRow; const m = meta.get(k) || {}; const tt = r.querySelector('.tt');
146
+ return { key: k, headline: tt ? tt.textContent : '', time: m.time || null, decide: !!m.decide, overdue: !!m.overdue, days_late: m.days_late || 0 };
147
+ });
148
+ const own0 = rowsIn(own).map((r) => {
149
+ const k = r.dataset.needsRow; const d = (page.needs || []).find((x) => x.key === k) || {}; const tt = r.querySelector('.tt');
150
+ return { key: k, headline: tt ? tt.textContent : (d.headline || ''), kind: r.dataset.kind || d.kind, days_late: d.days_late || 0 };
151
+ });
152
+ return { decisions, own: own0, agentN: agent.children.length };
136
153
  };
137
154
  const ctx = { changed: recount };
138
155
 
@@ -157,6 +174,12 @@ export function needsSection(S) {
157
174
  const g = JSON.stringify([key, inc, !!(b && b.kept_out), S.pinNeed || '']);
158
175
  if (g === sig) return; sig = g;
159
176
  alertHost.replaceChildren(...(inc ? [el('div', { class: 'incident', role: 'status', 'data-c3pg': 'incident' }, c3ic('alert', 'lg'), el('div', null, el('b', null, 'Card payments failing since 07:40'), el('div', { class: 'dim c3s3' }, '12% of checkouts. Sam on call. Only deadlines shown.')))] : []));
177
+ meta.clear();
178
+ /* a decision only the PM can make: an engine card, or an assigned item marked Decide; an assigned item past its date is overdue */
179
+ (needs || decide.map((c, i) => fromCard({ key: c.key || String(i), card: c }, i))).forEach((n) => {
180
+ const late = /^(\d+) days? overdue$/.exec(String(n.dueText || ''));
181
+ meta.set(n.key, { time: n.time || null, decide: !!n.view || /^decide$/i.test(String(n.kind || '')), overdue: n.kind === 'Overdue', days_late: late ? Number(late[1]) : 0 });
182
+ });
160
183
  if (needs) {
161
184
  sumState.quick = needs.filter((n) => n.quick).length;
162
185
  list.replaceChildren(...needs.slice(0, 5).map((n, i) => needRow(n, i, ctx)));
@@ -84,6 +84,7 @@ export function pulse(S) {
84
84
  rows = rows.filter(shown);
85
85
  if (!rows.length) { body.replaceChildren(note('No initiative has a promised date or a Jira epic read yet, so there is nothing to show.'), ...(noPlan ? [el('p', { class: 'dim mono c3s2' }, noPlan + ' initiatives have no plan date.')] : [])); return; }
86
86
  const pr = rows.map(pulseRow);
87
+ S.pulseRows = pr; if (S.briefRedraw) S.briefRedraw(); // The brief (c3-brief-summary.js) names the rows off track or at risk
87
88
  const need = pr.filter((r) => r.cls === 'bad' || r.cls === 'warn').length;
88
89
  attn.textContent = need + ' need attention'; foot.hidden = false;
89
90
  const by = new Map(); pr.forEach((r) => { const k = r.word.replace(/ \+.*/, ''); const o = by.get(k) || { n: 0, cls: r.cls }; o.n += 1; by.set(k, o); });
@@ -0,0 +1,139 @@
1
+ /* pm/c3-brief-summary.js — "The brief": one paragraph under the greeting that says the day at a glance (initiative c3-brief-summary-mentions).
2
+ *
3
+ * composeBrief(input) → { parts, sources } PURE. parts is a list of { t } text and { t, go: { kind, key } } links, in reading order:
4
+ * 1. "The day turns on one call": the first decision only the PM can make; without one, "First up": the top row
5
+ * ("Still open from today" in Reflect)
6
+ * 2. how many things need the PM, how many are overdue, and the oldest
7
+ * 3. who waits on a reply to a mention
8
+ * 4. how many meetings are left and which one still has a to-do before it ("You held N meetings today." in Reflect)
9
+ * 5. which initiatives are off track or at risk
10
+ * A sentence whose read has not come back (null input) is left out. Nothing is invented: every number and title
11
+ * comes from a row the Briefing shows below.
12
+ * summarySection(S) → the card. It composes again when a read comes back, when Needs you changes (S.needsChanged) and when the part of
13
+ * the day changes (S.briefLens). A link goes to its row on the page through S.briefGo({ kind, key }).
14
+ *
15
+ * input: { lens, decisions: [{ key, headline, time, decide, overdue, days_late }] | null (the rows above the PM's own in Needs you; decide: a
16
+ * decision only the PM can make, overdue: an assigned item past its date),
17
+ * own: [{ key, headline, kind, days_late }] | null, agentN,
18
+ * mentions: [mention] | null, agenda: [meeting] | null, pulse: [{ id, title, cls, word }] | null }
19
+ * No model call and no write. */
20
+ import { el } from './dom.js';
21
+ import { c3ic } from './pm-work-view.js';
22
+
23
+ const p2 = (n) => String(n).padStart(2, '0');
24
+ const hm = (iso) => { const d = new Date(iso); return p2(d.getHours()) + ':' + p2(d.getMinutes()); };
25
+ const plural = (n, w) => n + ' ' + w + (n === 1 ? '' : 's');
26
+ const first = (s) => String(s || '').trim().split(/\s+/)[0];
27
+ const SRC = { slack: 'Slack', jira: 'Jira', confluence: 'Confluence', mail: 'Mail', figma: 'Figma', github: 'GitHub', drive: 'Drive', calendar: 'Calendar', wispr: 'Wispr Flow', website: 'Website', derived: 'PMOS' };
28
+ export const srcName = (s) => SRC[s] || String(s || 'a source');
29
+ /** a list said as a person says it: "a", "a and b", "a, b and c" */
30
+ const andList = (xs) => (xs.length < 2 ? xs.join('') : xs.slice(0, -1).join(', ') + ' and ' + xs[xs.length - 1]);
31
+
32
+ /** The mentions that wait on the PM's reply: not read, not already a Needs you row. */
33
+ export const waitingMentions = (ms) => (ms || []).filter((m) => m && m.reply && !m.read && !m.in_needs);
34
+
35
+ export function composeBrief(input) {
36
+ const x = input || {};
37
+ const reflect = x.lens === 'Reflect';
38
+ const out = []; const sources = [];
39
+ const t = (s) => out.push({ t: s });
40
+ const link = (s, kind, key) => out.push({ t: s, go: { kind, key } });
41
+
42
+ // 1 and 2: Needs you
43
+ if (x.decisions && x.own) {
44
+ sources.push('Needs you');
45
+ const dec = x.decisions.find((d) => d.decide);
46
+ const d0 = dec || x.own[0] || x.decisions[0]; // a decision only the PM can make, else the top row of Needs you
47
+ if (d0) {
48
+ t(reflect ? 'Still open from today: ' : dec ? 'The day turns on one call: ' : 'First up: '); link(d0.headline, 'need', d0.key);
49
+ t((d0.time ? (/overdue/.test(d0.time) ? ', ' + d0.time : ', due ' + d0.time) : d0.days_late > 0 ? ', ' + plural(d0.days_late, 'day') + ' late' : '') + '. ');
50
+ }
51
+ const agentN = Number(x.agentN) || 0;
52
+ const total = x.decisions.length + x.own.length + agentN;
53
+ const late = [...x.own.filter((r) => r.kind === 'overdue'), ...x.decisions.filter((d) => d.overdue)];
54
+ if (!total) t('Nothing needs you right now. ');
55
+ else {
56
+ const parts = [plural(x.decisions.length, 'decision'), x.own.length + ' of your own'];
57
+ if (agentN) parts.push(agentN + ' from the agent');
58
+ t(plural(total, 'thing') + (total === 1 ? ' needs' : ' need') + ' you (' + parts.join(', ') + ')');
59
+ if (late.length) {
60
+ const o = late.slice().sort((a, b) => (b.days_late || 0) - (a.days_late || 0))[0];
61
+ t(', and ' + late.length + (late.length === 1 ? ' is' : ' are') + ' overdue, the oldest ');
62
+ link(o.headline, 'need', o.key);
63
+ t(o.days_late > 0 ? ' (' + plural(o.days_late, 'day') + ' late)' : '');
64
+ }
65
+ t('. ');
66
+ }
67
+ }
68
+ // 3: mentions that wait on a reply
69
+ if (x.mentions) {
70
+ sources.push('Mentions');
71
+ const w = waitingMentions(x.mentions);
72
+ if (w.length) {
73
+ const who = w.map((m) => (m.who ? first(m.who) + ' in ' : 'someone in ') + srcName(m.source));
74
+ if (w.length === 1) t(who[0].charAt(0).toUpperCase() + who[0].slice(1) + ' is waiting on your reply to a mention. ');
75
+ else t(w.length + ' people are waiting on your reply to a mention (' + andList(who) + '). ');
76
+ }
77
+ }
78
+ // 4: the meetings
79
+ if (x.agenda) {
80
+ sources.push('Calendar');
81
+ const all = x.agenda; const left = all.filter((m) => m.state !== 'past');
82
+ if (reflect) t(all.length ? 'You held ' + plural(all.length - left.length, 'meeting') + ' today' + (left.length ? ', and ' + left.length + ' ' + (left.length === 1 ? 'is' : 'are') + ' still ahead' : '') + '. ' : 'You had no meetings today. ');
83
+ else if (!all.length) t('No meetings today. ');
84
+ else if (!left.length) t('No meetings are left today. ');
85
+ else {
86
+ const todo = left.filter((m) => (m.before || []).length);
87
+ t(plural(left.length, 'meeting') + (left.length === 1 ? ' is' : ' are') + ' left today');
88
+ if (!todo.length) t(', and nothing is left to do before ' + (left.length === 1 ? 'it' : 'them') + '. ');
89
+ else {
90
+ t(todo.length === left.length && left.length > 1 ? '; every one has a to-do before it, starting with ' : '; ');
91
+ link(todo[0].title, 'meet', todo[0].id);
92
+ t(' at ' + hm(todo[0].start) + (todo.length === left.length && left.length > 1 ? '. ' : ' still has a to-do before it. '));
93
+ }
94
+ }
95
+ }
96
+ // 5: the initiatives
97
+ if (x.pulse && x.pulse.length) {
98
+ sources.push('Initiatives');
99
+ const bad = x.pulse.filter((r) => r.cls === 'bad'); const warn = x.pulse.filter((r) => r.cls === 'warn'); const ok = x.pulse.filter((r) => r.cls === 'ok').length;
100
+ const flagged = [...bad, ...warn];
101
+ if (!flagged.length) t(ok ? (ok === 1 ? 'The one initiative with a plan is on track.' : 'All ' + ok + ' initiatives with a plan are on track.') : '');
102
+ else {
103
+ t('Across the portfolio, ');
104
+ flagged.slice(0, 3).forEach((r, i) => {
105
+ if (i) t(i === Math.min(flagged.length, 3) - 1 ? ' and ' : ', ');
106
+ link(r.title, 'init', r.id);
107
+ t(' is ' + String(r.word || (r.cls === 'bad' ? 'off track' : 'at risk')).toLowerCase());
108
+ });
109
+ if (flagged.length > 3) t(', and ' + (flagged.length - 3) + ' more need attention');
110
+ t(ok ? '; ' + ok + (ok === 1 ? ' is' : ' are') + ' on track.' : '.');
111
+ }
112
+ }
113
+ // tidy: one space between sentences, none at the end
114
+ const parts = out.filter((p) => p.t);
115
+ if (parts.length) parts[parts.length - 1] = { ...parts[parts.length - 1], t: parts[parts.length - 1].t.replace(/\s+$/, '') };
116
+ return { parts, sources };
117
+ }
118
+
119
+ export function summarySection(S) {
120
+ const when = el('span', { class: 'mono dim brief-when', 'data-brief-when': '' });
121
+ const body = el('p', { class: 'brief-sum', 'data-brief-sum': '' }, 'Reading the day.');
122
+ const box = el('section', { class: 'card sect sum-card', 'data-c3pg': 'summary', 'aria-label': 'The brief' },
123
+ el('div', { class: 'sh2' }, el('h2', null, c3ic('spark', 'sm'), 'The brief'), when), body);
124
+ let queued = false;
125
+ const draw = () => {
126
+ queued = false;
127
+ const input = S.briefInput ? S.briefInput() : {};
128
+ const { parts, sources } = composeBrief(input);
129
+ if (!parts.length) { body.replaceChildren(sources.length ? 'Nothing to say yet: no decision, item, meeting or initiative needs you.' : 'Reading the day.'); when.textContent = ''; return; }
130
+ body.replaceChildren(...parts.map((p) => (p.go
131
+ ? el('a', { class: 'brief-lk', href: '#', 'data-brief-go': p.go.kind + ':' + p.go.key, onclick: (e) => { e.preventDefault(); if (S.briefGo) S.briefGo(p.go); } }, p.t)
132
+ : document.createTextNode(p.t))));
133
+ const d = new Date();
134
+ when.textContent = 'As of ' + p2(d.getHours()) + ':' + p2(d.getMinutes()) + ' · ' + sources.join(', ');
135
+ };
136
+ S.briefRedraw = () => { if (queued) return; queued = true; Promise.resolve().then(draw); };
137
+ S.briefRedraw();
138
+ return box;
139
+ }
@@ -2,8 +2,10 @@
2
2
  * GET /api/brief-page (lib/routes-brief-page.js), for the demo day (Thu 8 Oct 2026, 08:12), with the demo's own people and initiatives.
3
3
  * Today is the five meetings of the demo calendar (c3-demo-data.js MEET). Needs you gets two items that are overdue, one due today, and two outside-PMOS items
4
4
  * (one with evidence: a meeting with Dara is booked today). Decisions are the demo briefing's own needs (c3-demo-data.js NEEDS), not repeated here.
5
- * Writes: the pm-items routes the rows call (done, notyet, remove, reopen, undo) are answered in memory, and every one is kept in window.__c3demoWrites
6
- * ({ path, body }) so a test can see what the page asked to write. Real mode never loads this file. */
5
+ * Mentions (initiative c3-brief-summary-mentions): five mentions of the demo morning; Sam's Slack message is also the Needs you row i15, so it is not
6
+ * listed and that row carries the "mentioned you" tag.
7
+ * Writes: the pm-items routes the rows call (done, notyet, remove, reopen, undo, add) and POST /api/mentions/read are answered in memory, and every one
8
+ * is kept in window.__c3demoWrites ({ path, body }) so a test can see what the page asked to write. Real mode never loads this file. */
7
9
  const iso = (h, m) => new Date(2026, 9, 8, h, m).toISOString();
8
10
  const TODAY = [
9
11
  { id: 'm1', title: 'Standup', start: iso(9, 30), end: iso(10, 0), people: ['Dara', 'Lee', 'Sam'], initiative: 'Checkout v2', prepared: [], before: [] },
@@ -25,7 +27,14 @@ const NEEDS = [
25
27
  evidence: { text: 'a meeting with Dara is booked Thu 14:00', event_id: 'm4' } },
26
28
  ];
27
29
 
28
- let gone = {};
30
+ const MENTIONS = [
31
+ { id: 'd3m000000001', source: 'slack', who: 'Priya Nair', where: '#checkout-v2', text: 'Can you confirm the revised date before the 10:30 update? Leadership will ask.', at: iso(7, 58), reply: true, link: 'https://localhost/demo/slack/checkout-v2', initiative: 'Checkout v2', in_needs: false, read: false },
32
+ { id: 'd3m000000002', source: 'jira', who: 'Dara Okafor', where: 'CHK-214 Flow 4 estimate', text: 'Which of the four flows do we cut if we hold 24 Oct? I need this before sprint planning.', at: iso(8, 4), reply: true, initiative: 'Checkout v2', in_needs: false, read: false },
33
+ { id: 'd3m000000003', source: 'confluence', who: 'Marta Silva', where: 'Returns interview guide', text: 'I moved the support questions to the end, no change to the scope.', at: iso(7, 20), reply: false, initiative: 'Returns experience', in_needs: false, read: false },
34
+ { id: 'd3m000000004', source: 'mail', who: 'Lee Park', where: 'Pricing page feedback', text: 'Copying you so the copy notes are in one thread.', at: new Date(2026, 9, 7, 17, 5).toISOString(), reply: false, in_needs: false, read: false },
35
+ { id: 'd3m000000005', source: 'slack', who: 'Sam', where: '#search', text: 'Ok to ship the logging change today?', at: iso(7, 45), reply: true, in_needs: true, need_key: 'i15', read: false },
36
+ ];
37
+ let gone = {}; const readMarks = {};
29
38
  export function installBriefPage() {
30
39
  if (window.__c3demoBriefPage) return; window.__c3demoBriefPage = true;
31
40
  window.__c3demoWrites = [];
@@ -37,7 +46,14 @@ export function installBriefPage() {
37
46
  const u = new URL(raw, location.origin);
38
47
  const method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
39
48
  if (u.origin === location.origin) {
40
- if (method === 'GET' && u.pathname === '/api/brief-page') return Promise.resolve(json({ ok: true, date: '2026-10-08', me: 'Wawan', today: TODAY.map((m) => ({ ...m, minutes: (Date.parse(m.end) - Date.parse(m.start)) / 60000, state: Date.parse(m.end) <= Date.now() ? 'past' : 'later' })).map((m, i, a) => ({ ...m, state: m.state === 'past' ? 'past' : (a.findIndex((x) => x.state !== 'past' && Date.parse(x.start) > Date.now()) === i ? 'next' : 'later') })), needs: NEEDS.filter((n) => !gone[n.id]) }));
49
+ if (method === 'GET' && u.pathname === '/api/brief-page') return Promise.resolve(json({ ok: true, date: '2026-10-08', me: 'Wawan', today: TODAY.map((m) => ({ ...m, minutes: (Date.parse(m.end) - Date.parse(m.start)) / 60000, state: Date.parse(m.end) <= Date.now() ? 'past' : 'later' })).map((m, i, a) => ({ ...m, state: m.state === 'past' ? 'past' : (a.findIndex((x) => x.state !== 'past' && Date.parse(x.start) > Date.now()) === i ? 'next' : 'later') })), needs: NEEDS.filter((n) => !gone[n.id]), mentions: MENTIONS.map((x) => ({ ...x, read: !!readMarks[x.id] })) }));
50
+ if (method !== 'GET' && (u.pathname === '/api/mentions/read' || u.pathname === '/api/pm-items/add')) {
51
+ let b = {}; try { b = init && init.body ? JSON.parse(init.body) : {}; } catch { b = {}; }
52
+ window.__c3demoWrites.push({ path: u.pathname, body: b });
53
+ if (u.pathname === '/api/pm-items/add') return prev(input, init); // recorded, then answered by the demo's own Work store
54
+ readMarks[b.id] = b.read === true;
55
+ return Promise.resolve(json({ ok: true }));
56
+ }
41
57
  const m = /^\/api\/pm-items\/(done|notyet|remove|reopen|undo)$/.exec(u.pathname);
42
58
  if (method !== 'GET' && m) {
43
59
  let b = {}; try { b = init && init.body ? JSON.parse(init.body) : {}; } catch { b = {}; }
@@ -407,6 +407,10 @@ function press(k, b) {
407
407
  /** The routes. Returns a Response, or undefined for a route this file does not answer. */
408
408
  export function rdRoute(method, u, body) {
409
409
  const p = u.pathname; const qp = u.searchParams;
410
+ /* the Structure step (initiative report-structure): demo mode holds no structure and no data chart, and saves nothing */
411
+ if (p === '/api/report-structure' && method === 'GET') { const id = qp.get('id'); const s = ctx.setups().find((x) => x.id === id); return s ? json({ ok: true, setup: { id, name: s.name, cadence: s.schedule && s.schedule.cadence }, structure: { running: false, job: null, current: null, saved: null, versions: [], log: [], plan: null, dry: null, checks: {}, errors: [] } }) : json({ ok: false, error: 'no such report' }, 404); }
412
+ if (p === '/api/charts' && method === 'GET') return json({ ok: true, charts: [] });
413
+ if (p.startsWith('/api/report-structure') && method === 'POST') return json({ ok: false, error: 'Demo data: nothing is saved' });
410
414
  if (!p.startsWith('/api/report-design') && !p.startsWith('/api/report-notes')) return undefined;
411
415
  if (method === 'GET') {
412
416
  if (p === '/api/report-design') { const id = qp.get('id'); if (!ctx.setups().some((x) => x.id === id)) return json({ ok: false, error: 'no such report' }, 404); return json(designView(id)); }
@@ -16,6 +16,7 @@ import { toast } from './c3-toast.js';
16
16
  import { RS, scheduleLabel } from './c3-report-store.js';
17
17
  import { formatOf } from './c3-doc-formats.js';
18
18
  import { RD, failed, splitRender, slideCard, frame, wireFrame, diffOutlines, ensureDeckCss, slotWord } from './c3-report-deck.js';
19
+ import { openStructure, structureSection } from './c3-report-structure.js';
19
20
 
20
21
  const MAX = 20 * 1024 * 1024;
21
22
  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' : ''; };
@@ -304,7 +305,9 @@ export function openStudio({ ws, id, name, view, onClose } = {}) {
304
305
  const sb = body.closest('.sb'); const y = sb ? sb.scrollTop : 0; const log = body.querySelector('.rd-dks-log'); const ly = log ? log.scrollTop : null;
305
306
  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;
306
307
  S.dks = S.dks || el('div', { class: 'rd-dks' }); S.out = S.out || el('div', { class: 'rd-dks-out', 'data-rd-preview': '' });
307
- keep(body, el('div', { class: 'rd-dk-bar' }, el('div', { class: 'chips2 rd-wrap' }, el('div', { class: 'seg', role: 'group', 'aria-label': 'Report type' },
308
+ keep(body, el('div', { class: 'rd-dk-bar' }, el('div', { class: 'chips2 rd-wrap' }, el('div', { class: 'seg', role: 'group', 'aria-label': 'Studio step', 'data-rd-steps': '' },
309
+ btn({ 'data-rd-step': 'structure', 'aria-pressed': 'false', onclick: () => openStructure({ ws, id, name: nm }) }, icon('todo', 'sm'), '1 Structure'), btn({ 'data-rd-step': 'design', 'aria-pressed': 'true' }, icon('img', 'sm'), '2 Design')),
310
+ el('div', { class: 'seg', role: 'group', 'aria-label': 'Report type' },
308
311
  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')),
309
312
  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')),
310
313
  el('div', { class: 'chips2' }, pbtn('primary', 'This is it', 'check', { 'data-rd-act': 'accept', disabled: !D.versions.length, onclick: () => accept() }),
@@ -332,8 +335,9 @@ export function openBrief({ ws, id, name } = {}) {
332
335
  const nm = setup.name || name || id; const out = formatOf(setup.output);
333
336
  const lh = el('div', { class: 'lh' }, el('span', { class: 'ki' }, icon('report')), el('div', { class: 'rs-min0' }, el('b', null, nm),
334
337
  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 + '.'))));
338
+ const SS = await structureSection({ ws, id, name: nm }); // the saved structure (step 1), above the design
335
339
  if (!D.saved) {
336
- 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.'),
340
+ put(body, lh, SS, 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.'),
337
341
  el('div', { class: 'ds-acts' }, pbtn('primary', 'Design how it looks', 'spark', { 'data-rd-act': 'studio', onclick: () => openStudio({ ws, id, name: nm }) }))));
338
342
  return;
339
343
  }
@@ -345,7 +349,7 @@ export function openBrief({ ws, id, name } = {}) {
345
349
  const rules = D.rules || {};
346
350
  const cycHost = el('ul', { class: 'chgl', 'data-rd-briefcycles': '' });
347
351
  const docs = (RS.docs || []).filter((d) => d.setup === id).sort((a, b) => String(b.slot).localeCompare(String(a.slot)));
348
- put(body, lh,
352
+ put(body, lh, SS,
349
353
  sect({ ic: 'spark', title: 'Design', right: el('span', { class: 'tag acc' }, 'Design D' + n), attrs: { 'data-rd-design': '' } },
350
354
  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 + '.'),
351
355
  el('h3', { class: 'ds-h' }, 'Your description'), el('blockquote', { class: 'rd-dks-q', 'data-rd-desc': '' }, D.description || 'No description.'),
@@ -16,6 +16,7 @@ import { toast } from './c3-toast.js';
16
16
  import { FORMATS, OUTPUTS, formatOf, ensureReportCss } from './c3-doc-formats.js';
17
17
  import { createDeck } from './c3-report-deck.js';
18
18
  import { openStudio, openBrief } from './c3-design-studio.js';
19
+ import { openStructure } from './c3-report-structure.js';
19
20
  import { RS, onReports, loadReports, askPmos, changeIt, cancelAsk, setupFrom, draftNow, dismissLater, pauseResume, fromDashboard, goTo, evidenceKind, normProposal, scheduleLabel, nextDraftDate, dayText, docsOf, outChip, copyChip, srcChip } from './c3-report-store.js';
20
21
 
21
22
  const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
@@ -118,6 +119,7 @@ function askCard(a, redo) {
118
119
  return sect({ ic: 'spark', title: 'PMOS answer', attrs, right: confTag('') }, el('div', { class: 'rs-stack' }, head,
119
120
  lbl('WHAT PMOS FOUND'), ...foundList(P), lbl('PROPOSED REPORT'), proposed(key, P, redo), lbl('LEARNED FROM'), ...[].concat(learnedBlock(P)), lbl('FIXES IT WILL APPLY'), fixesList(P), ...(cb ? [].concat(cb) : []),
120
121
  el('div', { class: 'chips2' }, pbtn('primary', 'Set up', 'check', { 'data-rs-act': 'setup', onclick: () => setupFrom({ ask: a.id, proposal: P }) }),
122
+ pbtn('outline', 'Shape it with PMOS', 'todo', { 'data-rs-act': 'shape', title: 'Set the report up, then decide with PMOS what it answers, where each number comes from and how it is worked out', onclick: async () => { const r = await setupFrom({ ask: a.id, proposal: P }); if (r && r.ok && r.setup) openStructure({ ws: RS.ws, id: r.setup.id, name: r.setup.name }); } }),
121
123
  pbtn('outline', 'Change', 'edit', { 'data-rs-act': 'change', onclick: () => { RS.chg[key] = { ...st, open: true }; redo(); } }),
122
124
  pbtn('ghost', 'Cancel', 'x', { 'data-rs-act': 'cancel-ask', onclick: async () => { const r = await cancelAsk(a.id); if (r && r.ok) toast('Cancelled. Nothing was set up.'); } })),
123
125
  help('PMOS reads your past copies to learn the sections. It drafts only; you issue.')));
@@ -232,6 +234,7 @@ export function mountReportSetup(host, ctx) {
232
234
  open ? cycRows(s) : null),
233
235
  el('div', { class: 'acts' }, cy.length || paused || s.source !== 'work' ? pbtn('outline', 'Open', 'out', { 'data-rs-act': 'open-reg', onclick: () => openSetup(s) })
234
236
  : pbtn('outline', 'Draft it now', 'spark', { 'data-rs-act': 'draft-now', onclick: () => openSetup(s) }),
237
+ s.source === 'work' ? pbtn('ghost', 'Shape the report', 'todo', { 'data-rs-act': 'shape', onclick: () => openStructure({ ws: RS.ws, id: s.id, name: s.name }) }) : null,
235
238
  s.source === 'work' ? pbtn('ghost', 'Design how it looks', 'spark', { 'data-rs-act': 'design', onclick: () => openStudio({ ws: RS.ws, id: s.id, name: s.name }) }) : null,
236
239
  pbtn('ghost', 'All cycles (' + n + ')', 'fold', { 'data-rs-act': 'cyc', 'aria-expanded': String(open), onclick: () => { if (RS.cyc.has(s.id)) RS.cyc.delete(s.id); else RS.cyc.add(s.id); drawSetups(); } }),
237
240
  pbtn('ghost', paused ? 'Resume' : 'Pause', paused ? 'play' : 'pause', { 'data-rs-act': 'pause', onclick: () => pauseResume(s, !paused) })));
@@ -0,0 +1,40 @@
1
+ /* pm/c3-report-structure.css — the Structure step of the report studio (c3-report-structure.js; mockup 43-reportstructure.css).
2
+ The conversation reuses the Design studio's .rd-dks-* rules (c3-report-deck.css). Tokens only; font sizes are the --fs-* scale. */
3
+ .c3 .rs-st { display: grid; gap: var(--sp-3); min-width: 0; }
4
+ .c3 .rs-st-bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; }
5
+ .c3 .rs-st-bar .seg button { display: inline-flex; gap: 4px; align-items: center; }
6
+ .c3 .rs-st .rd-dks-out > .card.sect { margin: 0; }
7
+ .c3 .rs-st-q { display: grid; gap: 4px; }
8
+ .c3 .rs-st-q b { font-size: var(--fs-2); font-weight: 500; }
9
+ .c3 .rs-st-small { font-size: var(--fs-2); }
10
+ .c3 .rs-st-tiny { font-size: var(--fs-1); }
11
+ .c3 .rs-st-fg { color: var(--foreground); }
12
+ .c3 .rs-st-field { display: grid; gap: 2px; font-size: var(--fs-2); }
13
+ .c3 .rs-st-field input { width: 100%; box-sizing: border-box; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--card); color: var(--foreground); font: var(--fs-3)/1.4 var(--sans); }
14
+ .c3 .rs-st-form { display: grid; gap: 8px; padding: 8px 10px; margin: 0 0 4px; border: 1px solid var(--border); border-radius: 8px; background: var(--background); }
15
+ .c3 .rs-st-tw { overflow-x: auto; }
16
+ .c3 .rs-st-tbl { min-width: 640px; font-size: var(--fs-2); }
17
+ .c3 .rs-st-tbl td { vertical-align: top; }
18
+ .c3 .rs-st-row.on td { background: var(--muted); }
19
+ .c3 .rs-st-tbl .tt { text-align: left; }
20
+ .c3 .rs-st-det > td { border-top: 0; padding-top: 0; }
21
+ .c3 .rs-st-detb { display: grid; gap: 10px; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--background); }
22
+ .c3 .rs-st-detb p, .c3 .rs-st-opt p, .c3 .rs-st-p { margin: 2px 0 0; font-size: var(--fs-3); line-height: 1.5; }
23
+ .c3 .rs-st-detb .dim.mono, .c3 .rs-st-opt .dim.mono { font-size: var(--fs-1); }
24
+ .c3 .rs-st-code { font-size: var(--fs-2); overflow-wrap: anywhere; }
25
+ .c3 .rs-st-ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
26
+ .c3 .rs-st-ol > li { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 8px; align-items: start; }
27
+ .c3 .rs-st-ol .chips { margin-top: 4px; }
28
+ .c3 .rs-st-n { display: grid; place-items: center; width: 24px; height: 24px; border: 1px solid var(--border); border-radius: 6px; font-size: var(--fs-1); color: var(--muted-foreground); }
29
+ .c3 .rs-st-mini { margin: 0; padding-left: 20px; display: grid; gap: 2px; font-size: var(--fs-2); }
30
+ .c3 .rs-st-opts { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-2); }
31
+ .c3 .rs-st-opt { display: grid; gap: 10px; align-content: start; margin: 0; }
32
+ .c3 .rs-st-chk > li { grid-template-columns: 16px minmax(0, 1fr) auto; }
33
+ .c3 .rs-st-saved { display: grid; gap: var(--sp-3); }
34
+ .c3 .rs-st .rd-dks-panel { display: flex; flex-direction: column; max-height: 42vh; overflow: auto; }
35
+ .c3 .rs-st .rd-dks-panel > * { flex: none; }
36
+ @media (max-width: 760px) {
37
+ .c3 .rs-st .rd-dks { grid-template-columns: minmax(0, 1fr); }
38
+ .c3 .rs-st .rd-dks-chat { position: static; max-height: none; }
39
+ .c3 .rs-st .rd-dks-log { max-height: 420px; }
40
+ }