create-pmos 0.163.0 → 0.165.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.
@@ -0,0 +1,133 @@
1
+ /* pm/c3-demo-focus.js — DEMO answers for the Meeting focus page (initiative c3-meeting-focus) on /c3.html?demo=1.
2
+ * c3-demo.js passes GET /api/meeting-focus, GET /api/meeting-focus/ask and the two POSTs (topic, ask) here. The content is the approved
3
+ * mockup's (version 7): the Partner API rate-limit review at 18:24 in, with its six topics (one added from the talk), the gist and its four
4
+ * context chips, the room's last lines, three PMOS notes and two it is holding, the captured items and a Google Slides deck.
5
+ * Whatever meeting the page asks for, the demo answers with this one. A correction (set, add, park) and a question change the answer
6
+ * for the rest of the page's life; nothing is saved and nothing reaches the server. Real mode never loads this file. */
7
+
8
+ const DAY = '2026-10-08';
9
+ const at = (hm) => Date.parse(DAY + 'T' + hm + ':00');
10
+ const START = at('11:00');
11
+ const NOW = at('11:18') + 24000;
12
+ const min = (ms) => Math.round(((ms - START) / 60000) * 10) / 10;
13
+
14
+ const EVENT = {
15
+ id: 'e2@' + DAY, title: 'Partner API rate-limit review', at: DAY + 'T11:00:00', end: DAY + 'T12:00:00',
16
+ attendees: ['Ana Putri', 'Chen Wei', 'Lina Kusuma', 'You'], initiative: 'IDPF-07',
17
+ purpose: 'Choose how the Partner API protects itself from bursts: soft or hard limits.',
18
+ links: [
19
+ { label: 'Rate limits for the Partner API', url: 'docs.google.com/presentation/d/1RateLimitsPartnerApiDemo/edit' },
20
+ { label: 'Partner API rate limits: options paper', url: 'pmos-demo.atlassian.net/wiki/spaces/IDPF/pages/7/Partner+API+rate+limits+options+paper' },
21
+ ],
22
+ };
23
+ const BRIEF = {
24
+ initiative: { id: 'IDPF-07', title: 'Partner API rate limits', status: 'on track' },
25
+ gist: ['For Partner API rate limits, which is on track.', 'The decision to choose the rate-limit approach is due today.'],
26
+ matters: [{ tone: 'bad', tag: 'Due today', text: 'Choose the rate-limit approach before the 11:00 review', meta: 'Partner API working group', ref: { type: 'decision', id: 'd2' } }],
27
+ last: { id: 'n-demo-last', title: 'Partner API rate-limit review', at: '2026-10-08T11:00:00', private: false, overview: 'On 8 Oct Chen promised the p99 numbers per partner.',
28
+ decisions: [], actions: [{ text: 'Send per-partner numbers to Lina', owner: 'Chen Wei', due: 'Tue 13 Oct' }] },
29
+ earlier: 1, purpose: EVENT.purpose, prep: 'Your one-pager is still open.', links: EVENT.links, refs: [], sources: ['the calendar', 'the IDPF-07 plan'],
30
+ };
31
+
32
+ /* the topics, in the order the page shows them: id, text, where it came from, when it came up and who raised it */
33
+ const BASE = [
34
+ { id: 'context-and-goal', text: 'Context and goal', src: 'steps' },
35
+ { id: 'options-soft-vs-hard-limits', text: 'Options: soft vs hard limits', src: 'steps' },
36
+ { id: 'a-temporary-higher-tier-during-the-grace-window', text: 'A temporary higher tier during the grace window', src: 'added', at: at('11:15'), by: 'Lina' },
37
+ { id: 'partner-impact-top-20-traffic', text: 'Partner impact: top 20 traffic', src: 'steps' },
38
+ { id: 'partner-comms-and-timing', text: 'Partner comms and timing', src: 'steps' },
39
+ { id: 'decide-and-next-steps', text: 'Decide and next steps', src: 'steps' },
40
+ ];
41
+ const ids = BASE.map((t) => t.id);
42
+ /* the stretches of talk: topic id, from, to (null while open), in epoch ms */
43
+ const fresh = () => ({
44
+ topics: BASE.map((t) => ({ ...t })), parked: new Set(), current: ids[1],
45
+ runs: [[ids[0], at('11:00'), at('11:07')], [ids[1], at('11:07'), at('11:15')], [ids[2], at('11:15'), at('11:17')], [ids[1], at('11:17'), null]],
46
+ asks: [], extra: [], seq: 0,
47
+ });
48
+ let S = fresh();
49
+
50
+ const NOTES = [
51
+ { id: 'n0', at: at('11:15'), cat: 'Topics', heard: '"Could they buy a higher tier just for the grace window?" Lina, 11:15', body: 'Added to Topics: A temporary higher tier during the grace window. The room came back to your options at 11:17.',
52
+ why: 'A new thread came up. I added it to Topics so it is not lost.', src: 'what was said since 11:15', show: 'now' },
53
+ { id: 'n1', at: at('11:16'), cat: 'Fact check', heard: '"Are we even allowed to throttle them?" Ana, 11:16', body: 'Yes. The partner agreement v3, section 4.2, allows throttling with 30 days written notice.',
54
+ why: 'You answered from memory. This is the clause, in case anyone asks for it.', src: 'Partner agreement v3 (Drive)', show: 'now',
55
+ acts: [{ label: 'Open section 4.2', url: 'drive.google.com/file/d/1PartnerAgreementV3Demo/view' }] },
56
+ { id: 'n2', at: at('11:18'), cat: 'Answer', heard: '"How many partners would hit a hard limit today?" Lina, 11:17', body: '3 of the top 20 partners went over 1,000 req/s last week: Tokopay (2,400 peak), Kirimin (1,600) and Bayarku (1,100).',
57
+ why: 'Lina asked the room and nobody had the number. You will want it before you reply.', src: "Chen's traffic analysis, page 3", show: 'now',
58
+ acts: [{ label: 'Open the analysis', ref: { type: 'report', id: 'p6' } }] },
59
+ { id: 'n3', at: at('11:18'), cat: 'Decision', heard: '', body: 'Ana and Chen lean to soft limits now, then hard limits after 30 days. That is your recommendation on slide 7.',
60
+ why: '', src: 'what was said since 11:12', show: 'hold', when: 'when the room gets to the decision' },
61
+ { id: 'n4', at: at('11:18'), cat: 'Time', heard: '', body: "41 min left and 3 topics have not come up: partner impact, partner comms, and the decision. Lina's question already pulled in Chen's numbers, so partner impact can be short.",
62
+ why: '', src: 'the topics and the time left', show: 'hold', when: 'if 2 topics are left with under 15 min' },
63
+ ];
64
+ const CHIPS = [
65
+ { label: 'Tokopay, Kirimin, Bayarku', sub: 'partners over the limit',
66
+ detail: 'Tokopay peaked at 2,400 req/s (Standard plan, account owner Lina Kusuma). Kirimin peaked at 1,600 req/s (Standard, Lina Kusuma). Bayarku peaked at 1,100 req/s (Plus, Eko Prasetyo).', from: "Chen's traffic analysis, page 3, and the partner list in the CRM" },
67
+ { label: 'Partner agreement 4.2', sub: 'allows throttling with notice',
68
+ detail: '4.2 Fair use. The Provider may limit request rates that exceed the published tier, after giving the Partner at least thirty (30) days written notice.', from: 'Drive: Legal / Partners / Partner agreement v3.pdf, page 6' },
69
+ { label: 'Option B', sub: 'slide 5 of your deck', url: 'docs.google.com/presentation/d/1RateLimitsPartnerApiDemo/edit#slide=id.p5' },
70
+ { label: 'Rate-limit decision', sub: 'due today', ref: { type: 'decision', id: 'd2' } },
71
+ ];
72
+ const TAIL = [
73
+ ['You', '11:14', 'So Option B: hard limits, with a 30 day grace window.'], ['Lina', '11:15', 'Could they buy a higher tier just for the grace window?'],
74
+ ['Ana', '11:16', 'Are we even allowed to throttle them?'], ['You', '11:16', 'Yes, with notice. That is the grace window on this slide.'],
75
+ ['Lina', '11:17', 'How many partners would hit a hard limit today?'], ['Chen', '11:18', 'Three, I think. I can pull the exact numbers.'],
76
+ ].map(([who, hm, text]) => ({ who, at: DAY + 'T' + hm + ':00', t: at(hm) - START, text, source: who === 'You' ? 'mic' : 'meeting' }));
77
+
78
+ function view() {
79
+ const spent = (id) => S.runs.filter((r) => r[0] === id).reduce((s, r) => s + ((r[2] == null ? NOW : r[2]) - r[1]), 0) / 60000;
80
+ const topics = S.topics.map((t) => ({ id: t.id, text: t.text, src: t.src, ...(t.at ? { at: t.at } : {}), ...(t.by ? { by: t.by } : {}),
81
+ state: S.current === t.id ? 'now' : S.parked.has(t.id) ? 'parked' : spent(t.id) > 0 ? 'discussed' : 'notyet', minutes: Math.round(spent(t.id)) }));
82
+ const notes = NOTES.map((n) => ({ ...n })).concat(S.extra);
83
+ return {
84
+ ok: true, now_ms: NOW, date: DAY, event: EVENT, brief: BRIEF,
85
+ note: { id: 'n-demo-focus', title: EVENT.title, state: 'recording', recorded_from: DAY + 'T11:00:00', lines: 42, sessions: [{ from: DAY + 'T11:00:00', to: null }] },
86
+ topics, current: S.current, runs: S.runs.map((r) => ({ topic: r[0], from_min: min(r[1]), to_min: r[2] == null ? null : min(r[2]) })),
87
+ tail: TAIL, captured: { decisions: [], actions: [{ text: 'Send per-partner numbers to Lina', owner: 'Chen Wei', due: 'Tue 13 Oct' }], questions: ['Can a partner buy a temporary higher tier during the grace window?'].map((text) => ({ text })) },
88
+ gist: 'The room is weighing hard limits against the 3 partners who would break today. Ana and Chen lean to soft limits first; Lina worries about telling partners.',
89
+ chips: CHIPS, notes, asks: S.asks.map((a) => ({ ...a })),
90
+ companion: { on: true, running: false, started: false, reason: 'too soon', last_run_at: NOW - 20000, error: null },
91
+ };
92
+ }
93
+
94
+ /** GET /api/meeting-focus and GET /api/meeting-focus/ask?id= */
95
+ export function focusGet(path, url) {
96
+ if (path === '/api/meeting-focus/ask') {
97
+ const q = new URL(url, location.origin).searchParams;
98
+ const a = S.asks.find((x) => x.id === q.get('id'));
99
+ return a ? { ok: true, id: a.id, state: 'answered' } : { ok: false, status: 404, error: 'no such question' };
100
+ }
101
+ return view();
102
+ }
103
+
104
+ /** POST /api/meeting-focus/topic and POST /api/meeting-focus/ask: they change the demo's answer and nothing else. */
105
+ export function focusPost(path, body) {
106
+ let b = {};
107
+ try { b = body ? JSON.parse(body) : {}; } catch { b = {}; }
108
+ const text = String(b.topic || '').replace(/\s+/g, ' ').trim().slice(0, 120);
109
+ if (path === '/api/meeting-focus/ask') {
110
+ const q = String(b.question || '').replace(/\s+/g, ' ').trim().slice(0, 1000);
111
+ if (!q) return { ok: false, error: 'Type your question.' };
112
+ const id = 'fa-demo' + (++S.seq);
113
+ S.asks.push({ id, at: NOW, question: q, state: 'answered' });
114
+ S.extra.push({ id: 'na' + S.seq, at: NOW, cat: 'Answer', heard: q, body: 'IDPF-07 has 3 open risks. The newest is partner comms, flagged by Lina on Thu 8 Oct.', why: 'You asked.', src: 'IDPF-07 risks', show: 'now', ask: id });
115
+ return { ok: true, id, state: 'working' };
116
+ }
117
+ if (path !== '/api/meeting-focus/topic') return { ok: true };
118
+ if (!text) return { ok: false, error: 'Name the topic.' };
119
+ const key = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60);
120
+ let row = S.topics.find((t) => t.id === key || t.text.toLowerCase() === text.toLowerCase());
121
+ if (!row) { row = { id: key, text, src: 'pm', at: NOW, by: 'you' }; S.topics.push(row); }
122
+ const closeOpen = () => { const r = S.runs.find((x) => x[2] == null); if (r) r[2] = NOW; };
123
+ if (b.action === 'set') { closeOpen(); S.parked.delete(row.id); S.current = row.id; S.runs.push([row.id, NOW, null]); }
124
+ else if (b.action === 'park') {
125
+ S.parked.add(row.id);
126
+ if (S.current === row.id) {
127
+ closeOpen(); S.current = null;
128
+ const next = S.topics.find((t) => t.id !== row.id && !S.parked.has(t.id) && !S.runs.some((r) => r[0] === t.id));
129
+ if (next) { S.current = next.id; S.runs.push([next.id, NOW, null]); }
130
+ }
131
+ }
132
+ return { ok: true };
133
+ }
@@ -11,6 +11,7 @@
11
11
  import { el } from './dom.js';
12
12
  import * as D from './c3-demo-data.js';
13
13
  import { installInit } from './c3-demo-init.js';
14
+ import { focusGet, focusPost } from './c3-demo-focus.js'; // Meeting focus: the mockup's Partner API review (GET /api/meeting-focus, its ask poll, and the topic and ask POSTs)
14
15
  import { installBoards } from './c3-demo-boards.js'; // Initiatives boards: answers every /api/boards GET and POST (installed outermost, in installDemo)
15
16
  import './c3-demo-cal-ix.js'; // Calendar and the ribbon: sets window.__c3calix (calendar-views.js reads it in demo only)
16
17
  import './c3-demo-cal3.js'; // Calendar Quarter and Year: answers GET /api/c3-strategy (chained under this file's fetch wrapper)
@@ -51,6 +52,7 @@ function getRoute(path, url) {
51
52
  switch (path) {
52
53
  case '/api/briefing': return D.briefing(url);
53
54
  case '/api/calendar-views': return withCalendarPage(D.calendarViews(url));
55
+ case '/api/meeting-focus': case '/api/meeting-focus/ask': return focusGet(path, url);
54
56
  case '/api/pm-items': return D.pmItems();
55
57
  case '/api/runs': return D.runs();
56
58
  case '/api/releases': return D.releases();
@@ -74,6 +76,7 @@ function getRoute(path, url) {
74
76
  function postRoute(path, body) {
75
77
  let b = {};
76
78
  try { b = body ? JSON.parse(body) : {}; } catch { b = {}; }
79
+ if (path.startsWith('/api/meeting-focus/')) return focusPost(path, body);
77
80
  if (path === '/api/pm-items/add') return { ok: true, item: { id: 'item-demo-new' + (++added), title: String(b.line || '').trim() || 'New item', due: null, priority: null, waiting_on: null, job: null, steps: [], created_at: D.NOW_ISO + 'Z' } };
78
81
  if (path === '/api/pm-items/remove') return { ok: true, undo_ms: 8000 };
79
82
  if (path === '/api/calendar-links') return (window.__c3links = (window.__c3links || []).concat([b])), { ok: true, link: b.kind === 'none' ? null : { kind: b.kind, ws: b.ws || null, board: b.board || null, initiative: b.initiative || null, title: b.title || null, by: 'pm:wawan' } }; // nothing is saved
@@ -0,0 +1,169 @@
1
+ /* pm/c3-focus.css — the Meeting focus page (initiative c3-meeting-focus). Scoped under .c3focus; the two side sheets (Topics, Chip) are
2
+ * mounted at the body, so their own pieces are scoped under the section that carries a data-fc="...-sheet" mark. Tokens only. */
3
+ .c3focus { position: fixed; inset: 0; z-index: 50; overflow: auto; background: var(--background); color: var(--foreground); }
4
+ .c3focus .fc-page { display: grid; gap: var(--sp-2); max-width: 1480px; margin: 0 auto; padding: var(--sp-3) var(--sp-4) var(--sp-6, 32px); box-sizing: border-box; min-width: 0; }
5
+ .c3focus .fc-page > * { min-width: 0; }
6
+ .c3focus [hidden] { display: none !important; }
7
+
8
+ /* header: who and when, the clock, REC, then the bar of the talk across the full width */
9
+ .c3focus .fc-top { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 6px var(--sp-4); align-items: center; padding: var(--sp-2) var(--sp-3); }
10
+ .c3focus .fc-id { display: flex; gap: var(--sp-3); align-items: flex-start; min-width: 0; }
11
+ .c3focus .fc-tt { min-width: 0; display: grid; gap: 4px; }
12
+ .c3focus .fc-tt h1 { margin: 0; font-size: var(--fs-4); font-weight: 600; line-height: 1.2; overflow-wrap: anywhere; }
13
+ .c3focus .fc-tt .chips2 { flex-wrap: wrap; gap: 6px; font-size: var(--fs-1); display: flex; align-items: center; }
14
+ .c3focus .fc-sub { font-size: var(--fs-2); }
15
+ .c3focus .fc-clock { display: flex; gap: var(--sp-4); align-items: flex-end; flex-wrap: wrap; }
16
+ .c3focus .fc-clock > div { display: grid; justify-items: start; }
17
+ .c3focus .fc-clock b { font-size: var(--fs-5); font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
18
+ .c3focus .fc-clock .dim { font-size: var(--fs-1); text-transform: uppercase; letter-spacing: .06em; }
19
+ .c3focus .fc-clock .fc-end { align-self: center; justify-items: start; }
20
+ .c3focus .fc-acts { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
21
+ .c3focus .fc-recd { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-1); }
22
+ .c3focus .fc-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--destructive); animation: fcpulse 1.6s ease-in-out infinite; }
23
+ .c3focus .fc-dot.off { background: var(--muted-foreground); animation: none; opacity: .6; }
24
+ @keyframes fcpulse { 50% { opacity: .3; } }
25
+ @media (prefers-reduced-motion: reduce) { .c3focus .fc-dot, .c3focus .fc-listen .fc-dot { animation: none; } }
26
+ .c3focus .fc-track { grid-column: 1 / -1; position: relative; }
27
+ .c3focus .fc-runs { position: relative; height: 8px; border-radius: 6px; background: var(--muted); overflow: hidden; }
28
+ .c3focus .fc-runs i { position: absolute; top: 0; bottom: 0; left: var(--fc-left, 0%); width: var(--fc-width, 0%); display: flex; align-items: center; justify-content: center; border-right: 2px solid var(--card); box-sizing: border-box; font: 500 10px var(--mono); font-style: normal; color: var(--foreground); background: color-mix(in oklab, var(--acc) 30%, var(--muted)); overflow: hidden; }
29
+ .c3focus .fc-runs i.c { background: var(--acc); color: var(--background); }
30
+ .c3focus .fc-nowm { position: absolute; top: -3px; width: 2px; height: 14px; margin-left: -1px; left: var(--fc-left, 0%); background: var(--foreground); border-radius: 0; }
31
+ .c3focus .fc-stale { grid-column: 1 / -1; margin: 0; font-size: var(--fs-1); color: var(--warning); }
32
+ .c3focus .tag.fc-warn, .c3focus .fc-warn { color: var(--warning); }
33
+ .c3focus .tag.fc-bad { color: var(--destructive); }
34
+ .c3focus .tag.fc-acc { color: var(--acc); }
35
+ .c3focus .tag { display: inline-flex; align-items: center; gap: 4px; }
36
+
37
+ /* the now line */
38
+ .c3focus .fc-nowl { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; padding: 0 var(--sp-1); min-height: 28px; }
39
+ .c3focus .fc-nowt { display: inline-flex; align-items: center; gap: 8px; min-width: 0; font-size: var(--fs-3); flex-wrap: wrap; }
40
+ .c3focus .fc-nowt .ki { width: 22px; height: 22px; }
41
+ .c3focus .fc-nowt .mono { font-size: var(--fs-2); font-variant-numeric: tabular-nums; }
42
+ .c3focus .fc-grow { flex: 1 1 auto; }
43
+
44
+ /* what is being said, with its context chips */
45
+ .c3focus .fc-gist { display: grid; gap: 8px; padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: 8px; background: var(--card); }
46
+ .c3focus .fc-gist p { margin: 0; display: flex; gap: 8px; align-items: flex-start; font-size: var(--fs-3); line-height: 1.5; }
47
+ .c3focus .fc-gist p .ic { flex: 0 0 auto; margin-top: 4px; color: var(--acc); }
48
+ .c3focus .fc-gl { font-size: var(--fs-1); text-transform: uppercase; letter-spacing: .06em; margin-right: 6px; }
49
+ .c3focus .fc-cxs { display: flex; gap: 6px; flex-wrap: wrap; }
50
+ .c3focus .fc-cx { display: inline-grid; justify-content: stretch; height: auto; min-height: 0; white-space: normal; gap: 1px; text-align: left; padding: 4px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--background); color: var(--foreground); font: inherit; cursor: pointer; max-width: 100%; }
51
+ @media (hover: hover) { .c3focus .fc-cx:hover { border-color: var(--acc); } }
52
+ .c3focus .fc-cx:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
53
+ .c3focus .fc-cx b { font-size: var(--fs-2); font-weight: 600; overflow-wrap: anywhere; }
54
+ .c3focus .fc-cx span { font-size: var(--fs-1); color: var(--muted-foreground); }
55
+
56
+ /* stage and conversation */
57
+ .c3focus .fc-main { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 380px); gap: var(--sp-3); align-items: start; }
58
+ .c3focus .fc-s { display: grid; gap: var(--sp-3); min-width: 0; }
59
+ .c3focus .fc-stagewrap, .c3focus .fc-capwrap { min-width: 0; }
60
+ .c3focus .fc-stagewrap:empty, .c3focus .fc-capwrap:empty { display: none; }
61
+ .c3focus .fc-stage .sh2 { gap: 8px; }
62
+ .c3focus .fc-stage .sh2 h2 { text-transform: none; letter-spacing: 0; font-size: var(--fs-3); font-weight: 600; color: var(--foreground); }
63
+ .c3focus .fc-frame { margin-top: var(--sp-2); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--muted); aspect-ratio: 16 / 9; width: 100%; }
64
+ .c3focus .fc-frame iframe { display: block; width: 100%; height: 100%; border: 0; background: var(--card); }
65
+ .c3focus .fc-strip { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: var(--sp-2) 0 0; }
66
+ .c3focus .fc-src { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-1); margin: 0; }
67
+ .c3focus .fc-card { display: flex; gap: var(--sp-3); align-items: center; margin-top: var(--sp-2); padding: var(--sp-3); border: 1px solid var(--border); border-radius: 8px; background: var(--card); flex-wrap: wrap; }
68
+ .c3focus .fc-card .nb { display: grid; gap: 2px; flex: 1 1 200px; min-width: 0; overflow-wrap: anywhere; }
69
+ .c3focus .fc-note { margin: var(--sp-2) 0 0; font-size: var(--fs-1); line-height: 1.5; }
70
+ .c3focus .fc-capg { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3); }
71
+ .c3focus .fc-col { min-width: 0; }
72
+ .c3focus .fc-lbl { display: flex; align-items: center; gap: 6px; font-size: var(--fs-1); padding: var(--sp-2) 0 4px; }
73
+ .c3focus .fc-w { position: sticky; top: var(--sp-3); min-width: 0; }
74
+
75
+ /* the conversation: a faint ticker of the room, PMOS notes below it, an ask line at the foot */
76
+ .c3focus .fc-cv { display: grid; grid-template-rows: auto auto minmax(0, 1fr) auto; max-height: min(640px, calc(100vh - 140px)); min-height: 280px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); overflow: hidden; }
77
+ .c3focus .fc-cv-h { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); font-size: var(--fs-2); }
78
+ .c3focus .fc-cv-h .dim { font-size: var(--fs-1); }
79
+ .c3focus .fc-tk { display: grid; gap: 2px; padding: 8px 12px; border-bottom: 1px solid var(--border); }
80
+ .c3focus .fc-tk:empty { display: none; }
81
+ .c3focus .fc-tk p { margin: 0; font-size: var(--fs-1); color: var(--muted-foreground); opacity: .75; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
82
+ .c3focus .fc-tk p:last-child { opacity: 1; }
83
+ .c3focus .fc-tk .mono { margin-right: 4px; }
84
+ .c3focus .fc-s2 { list-style: none; margin: 0; padding: 10px 12px; display: grid; gap: 6px; overflow: auto; align-content: start; min-height: 0; }
85
+ .c3focus .fc-a { position: relative; display: grid; gap: 4px; margin: 0; padding: 0; }
86
+ .c3focus .fc-a:not(.shut) { padding: 10px 12px; border-radius: 8px; background: color-mix(in srgb, var(--acc) 7%, transparent); }
87
+ .c3focus .fc-who { display: flex; align-items: center; gap: 6px; font-size: var(--fs-1); color: var(--muted-foreground); }
88
+ .c3focus .fc-who b { color: var(--acc); font-weight: 600; }
89
+ .c3focus .fc-avdot { display: inline-grid; place-items: center; width: 16px; height: 16px; border-radius: 50%; background: color-mix(in srgb, var(--acc) 14%, transparent); color: var(--acc); flex: 0 0 auto; }
90
+ .c3focus .fc-avdot .ic { width: 10px; height: 10px; }
91
+ .c3focus .fc-cat::before { content: "· "; }
92
+ .c3focus .fc-t { margin-left: auto; }
93
+ .c3focus .fc-a.new .fc-t { margin-right: 22px; }
94
+ .c3focus .fc-re { font-size: var(--fs-1); color: var(--muted-foreground); font-style: italic; }
95
+ .c3focus .fc-b { margin: 0; font-size: var(--fs-2); line-height: 1.5; }
96
+ .c3focus .fc-acts2 { display: flex; gap: 6px; flex-wrap: wrap; }
97
+ .c3focus .fc-acts2 button { font-size: var(--fs-2); }
98
+ .c3focus .fc-meta { display: grid; gap: 2px; font-size: var(--fs-1); line-height: 1.45; }
99
+ .c3focus .fc-meta b { color: var(--foreground); font-weight: 600; margin-right: 4px; }
100
+ .c3focus .fc-x { position: absolute; top: 6px; right: 6px; width: 22px; min-height: 22px; height: 22px; padding: 0; display: grid; place-items: center; border: 0; background: none; color: var(--muted-foreground); opacity: .6; cursor: pointer; border-radius: 6px; }
101
+ @media (hover: hover) { .c3focus .fc-a.new:hover .fc-x { opacity: 1; } }
102
+ .c3focus .fc-x:focus-visible { opacity: 1; }
103
+ .c3focus .fc-sb { display: block; width: 100%; height: auto; min-height: 0; min-width: 0; padding: 4px 6px; border: 0; border-radius: 6px; background: none; color: inherit; font: inherit; text-align: left; white-space: normal; cursor: pointer; }
104
+ .c3focus .fc-sb > span { display: block; overflow: visible; }
105
+ @media (hover: hover) { .c3focus .fc-sb:hover { background: var(--muted); } }
106
+ .c3focus .fc-sb:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
107
+ .c3focus .fc-1 { display: flex; align-items: center; gap: 6px; font-size: var(--fs-2); color: var(--muted-foreground); min-width: 0; }
108
+ .c3focus .fc-1t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
109
+ .c3focus .fc-r { display: grid; gap: 2px; justify-items: end; }
110
+ .c3focus .fc-r p { margin: 0; padding: 6px 10px; border-radius: 8px; background: none; border: 1px dashed var(--border); font-size: var(--fs-1); line-height: 1.45; max-width: 92%; overflow-wrap: anywhere; }
111
+ .c3focus .fc-wait, .c3focus .fc-err { font-size: var(--fs-1); }
112
+ .c3focus .fc-err { color: var(--destructive); }
113
+ .c3focus .fc-heldli { list-style: none; }
114
+ .c3focus .fc-held { font-size: var(--fs-1); color: var(--muted-foreground); }
115
+ .c3focus .fc-held summary { cursor: pointer; display: flex; align-items: center; gap: 6px; list-style: none; padding: 4px 6px; border-radius: 6px; }
116
+ .c3focus .fc-held summary::-webkit-details-marker { display: none; }
117
+ .c3focus .fc-held summary:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
118
+ .c3focus .fc-held ul { list-style: none; margin: 6px 0 0; padding: 0; display: grid; gap: 4px; }
119
+ .c3focus .fc-pl { width: 100%; height: auto; min-height: 0; border: 0; background: none; text-align: left; font: inherit; display: flex; justify-content: flex-start; gap: 6px; align-items: baseline; flex-wrap: wrap; white-space: normal; cursor: pointer; padding: 4px 6px; border-radius: 6px; font-size: var(--fs-1); color: var(--foreground); }
120
+ .c3focus .fc-pl > span { display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap; overflow: visible; }
121
+ @media (hover: hover) { .c3focus .fc-pl:hover { background: var(--muted); } }
122
+ .c3focus .fc-hw { font-size: var(--fs-1); }
123
+ .c3focus .fc-listen { list-style: none; display: flex; align-items: center; gap: 6px; font-size: var(--fs-1); padding: 4px 6px; }
124
+ .c3focus .fc-listen .fc-dot { background: var(--acc); }
125
+ .c3focus .fc-listen .fc-dot.off { background: var(--muted-foreground); }
126
+ .c3focus .fc-in { display: flex; gap: 6px; align-items: center; padding: 8px 10px; border-top: 1px solid var(--border); }
127
+ .c3focus .fc-in input { flex: 1; min-width: 0; height: 32px; padding: 0 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--background); color: var(--foreground); font: inherit; font-size: var(--fs-2); }
128
+ .c3focus .fc-in input:focus-visible { outline: 2px solid var(--acc); outline-offset: 1px; }
129
+
130
+ /* pieces inside the side sheets (they mount at the body, outside .c3focus) */
131
+ [data-fc="topics-sheet"] .fc-ai.done .tt { color: var(--muted-foreground); }
132
+ [data-fc="topics-sheet"] .fc-ai.cur { background: color-mix(in oklab, var(--acc) 7%, transparent); border-radius: 8px; padding-inline: 8px; }
133
+ [data-fc="topics-sheet"] .fc-ai .chips { flex-wrap: wrap; }
134
+ [data-fc="topics-sheet"] .fc-ai .tag { white-space: normal; display: inline-flex; align-items: center; gap: 4px; }
135
+ [data-fc="topics-sheet"] .tag.fc-acc { color: var(--acc); }
136
+ [data-fc="topics-sheet"] .tag.fc-warn { color: var(--warning); }
137
+ [data-fc="topics-sheet"] .fc-cd { display: flex; gap: 8px; align-items: baseline; margin-top: var(--sp-2); }
138
+ [data-fc="topics-sheet"] .fc-cd b { font-size: var(--fs-5); font-weight: 500; font-variant-numeric: tabular-nums; }
139
+ [data-fc="topics-sheet"] .fc-cd span { font-size: var(--fs-1); }
140
+ [data-fc="topics-sheet"] .fc-src { display: flex; align-items: center; gap: 4px; margin: var(--sp-2) 0 0; font-size: var(--fs-1); line-height: 1.5; }
141
+ [data-fc="topics-sheet"] .fc-pick { display: grid; gap: 6px; margin-top: var(--sp-2); padding: var(--sp-2); border: 1px solid var(--border); border-radius: 8px; background: var(--card); }
142
+ [data-fc="topics-sheet"] .fc-pick .chip2 { justify-content: flex-start; text-align: left; font-size: var(--fs-2); }
143
+ [data-fc="topics-sheet"] .iacts { display: flex; gap: 8px; flex-wrap: wrap; margin-top: var(--sp-2); }
144
+ .fc-addf { display: flex; gap: 8px; align-items: center; margin: var(--sp-2) 0; }
145
+ .fc-addf input { flex: 1; min-width: 0; height: 32px; padding: 0 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--background); color: var(--foreground); font: inherit; font-size: var(--fs-2); }
146
+ [data-fc="captured-sheet"] .fc-capg { display: grid; gap: var(--sp-3); }
147
+ [data-fc="captured-sheet"] .fc-lbl { display: flex; align-items: center; gap: 6px; font-size: var(--fs-1); padding: var(--sp-2) 0 4px; }
148
+ [data-fc="captured-sheet"] .fc-note { margin: var(--sp-2) 0 0; font-size: var(--fs-1); }
149
+ [data-fc="captured-sheet"] .tag.fc-bad { color: var(--destructive); }
150
+ [data-fc="chip-sheet"] .fc-p { margin: 0; font-size: var(--fs-3); line-height: 1.6; }
151
+ [data-fc="chip-sheet"] .fc-note { margin: var(--sp-2) 0 0; font-size: var(--fs-1); }
152
+
153
+ /* narrower screens: one column, the conversation after the band */
154
+ @media (max-width: 1100px) { .c3focus .fc-main { grid-template-columns: minmax(0, 1fr) minmax(280px, 340px); } }
155
+ @media (max-width: 900px) {
156
+ .c3focus .fc-page { padding: var(--sp-2) 16px var(--sp-5, 24px); }
157
+ .c3focus .fc-main { grid-template-columns: minmax(0, 1fr); }
158
+ .c3focus .fc-w { position: static; }
159
+ .c3focus .fc-cv { max-height: 520px; }
160
+ .c3focus .fc-capg { grid-template-columns: minmax(0, 1fr); }
161
+ }
162
+ @media (max-width: 760px) {
163
+ .c3focus .fc-top { grid-template-columns: minmax(0, 1fr); }
164
+ .c3focus .fc-id { flex-direction: column; gap: var(--sp-2); }
165
+ }
166
+
167
+ .c3focus .fc-cx > span { display: grid; gap: 1px; overflow: visible; text-align: left; }
168
+ .c3focus .fc-cx > span > b { font-weight: 600; }
169
+ .c3.fc-open { container-type: normal; }