create-pmos 0.527.0 → 0.529.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 (58) hide show
  1. package/package.json +57 -29
  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/adler-moments-test.js +4 -3
  7. package/workspace/briefing-premium-calendar-3-test.js +11 -9
  8. package/workspace/briefing-premium-calendar-test.js +1 -1
  9. package/workspace/changelog.json +56 -0
  10. package/workspace/ev-everywhere-test.js +2 -6
  11. package/workspace/fixtures/premium-viewer-fixtures.js +4 -2
  12. package/workspace/guide/home.md +7 -6
  13. package/workspace/guide/idle-lab.md +1 -1
  14. package/workspace/guide/notes.md +19 -7
  15. package/workspace/guide/reports-set-up.md +4 -1
  16. package/workspace/lib/bg-queue.js +10 -3
  17. package/workspace/lib/meeting-replay.js +11 -2
  18. package/workspace/lib/meeting-suggest.js +105 -0
  19. package/workspace/lib/meeting-terms.js +57 -17
  20. package/workspace/lib/okr-goals.js +23 -3
  21. package/workspace/lib/report-chat.js +3 -3
  22. package/workspace/lib/routes-lab.js +44 -2
  23. package/workspace/lib/routes-meeting-points.js +27 -3
  24. package/workspace/lib/routes-morning-goals.js +5 -1
  25. package/workspace/lib/routes-report-flow.js +35 -5
  26. package/workspace/news-viewers-test.js +2 -1
  27. package/workspace/package-lock.json +2 -2
  28. package/workspace/package.json +1 -1
  29. package/workspace/premium-vw-meet-test.js +17 -11
  30. package/workspace/product-lane-test.js +1 -1
  31. package/workspace/program-charter-test.js +2 -2
  32. package/workspace/public/pm/bd-ini-panel.js +1 -1
  33. package/workspace/public/pm/bd-span.js +10 -7
  34. package/workspace/public/pm/cozy-board.css +1 -0
  35. package/workspace/public/pm/cozy-lab-ins.js +13 -4
  36. package/workspace/public/pm/cozy-search.js +10 -17
  37. package/workspace/public/pm/cozy-settings.js +8 -2
  38. package/workspace/public/pm/cozy-today.css +30 -14
  39. package/workspace/public/pm/cozy-vw-mtg.css +122 -24
  40. package/workspace/public/pm/good-morning.js +30 -17
  41. package/workspace/public/pm/home-app.js +11 -1
  42. package/workspace/public/pm/home-data.js +4 -1
  43. package/workspace/public/pm/home-day.js +12 -5
  44. package/workspace/public/pm/premium-board.js +7 -5
  45. package/workspace/public/pm/premium-initiative-head.js +9 -1
  46. package/workspace/public/pm/premium-insights.js +5 -3
  47. package/workspace/public/pm/premium-meeting.js +2 -1
  48. package/workspace/public/pm/premium-mtg-replay.js +149 -73
  49. package/workspace/public/pm/premium-report-read.js +11 -4
  50. package/workspace/public/pm/premium-report-tab.js +33 -4
  51. package/workspace/public/pm/premium-vw-mtg.js +176 -53
  52. package/workspace/public/pm/program-model.js +2 -1
  53. package/workspace/public/pm/report-chat.js +1 -0
  54. package/workspace/public/pm/report-flow.js +7 -3
  55. package/workspace/public/premium-board.html +4 -4
  56. package/workspace/server.js +1 -1
  57. package/workspace/task-milestones-test.js +2 -2
  58. package/workspace/timeline-holds-test.js +3 -3
@@ -60,7 +60,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
60
60
  const s1 = await sum(p);
61
61
  /* batch 25 item 11 (MvC): the summary first in the left column, the kept points with Confirm N in their header, the evidence, one strip, the docked timeline */
62
62
  ok('S1 the summary: the overview and the meta line, no primary (Confirm sits in the kept header)', /opens next to the chat/.test(s1.text) && !s1.pri, JSON.stringify(s1));
63
- ok('S1 the left column starts with the summary, then the kept points: "N kept" and Confirm N; the evidence and the docked timeline', await p.evaluate(() => { const l = document.querySelector('#czViewer .vzr-l'); return !!l && l.firstElementChild.classList.contains('czs-sum') && /^\d+ kept$/.test(l.querySelector('.vzr-ph h3').textContent) && /^Confirm \d+$/.test(l.querySelector('[data-a="confirmall"]').textContent) && !!document.querySelector('#czViewer .vzr-ev') && !!document.querySelector('#czViewer .vzp-card .vzr-dock.vzp-pl'); }));
63
+ ok('S1 the left column starts with the summary, then the kept points: "N kept" and Confirm N; one player (MpD1), no dock', await p.evaluate(() => { const l = document.querySelector('#czViewer .vzr-l'); return !!l && l.firstElementChild.classList.contains('czs-sum') && /^\d+ kept$/.test(l.querySelector('.vzr-ph h3').textContent) && /^Confirm \d+$/.test(l.querySelector('[data-a="confirmall"]').textContent) && !!document.querySelector('#czViewer .vzp-card .vzr-np') && !document.querySelector('#czViewer .vzr-dock'); }));
64
64
  const chips = await p.evaluate(() => [...document.querySelectorAll('#czViewer .vzr-fold [data-a="strip"]')].map((x) => x.dataset.k).join());
65
65
  ok('S1 one strip: Left out, Next steps, Since the last one, Facts, Names and terms, Prep and material, Transcript; no tabs, no checkbox, no filter', chips === 'left,steps,since,facts,names,prep,tr' && !(await p.$('#czViewer [role="tablist"]')) && !(await p.$('#czViewer .vzr [role="checkbox"]')) && !(await p.$('#czViewer [data-a="pf"]')), chips);
66
66
  ok('S1 the Viewer opens wide for the two columns', await p.evaluate(() => document.getElementById('czViewer').offsetWidth >= 900 && getComputedStyle(document.querySelector('#czViewer .vzr-cols')).gridTemplateColumns.split(' ').length === 2));
@@ -80,7 +80,9 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
80
80
  const strip = (k) => press(p, `.vzr-fold [data-a="strip"][data-k="${k}"]`);
81
81
  await strip('since'); const since = await body(p); await strip('facts'); const facts = await body(p);
82
82
  ok('S3 Since the last one and Facts mentioned, Matches and Not checked', /Raised again/.test(since) && /Matches/.test(facts) && /Not checked/.test(facts), facts.slice(0, 600));
83
- ok('S3 the evidence has the screen captures, with no Waiting for design marker', await until(() => p.evaluate(() => document.querySelectorAll('#czViewer .vzp-card .vzp-th').length > 0 && !document.querySelector('#czViewer [data-cz-placeholder]'))));
83
+ ok('S3 MpD4: the captures row is hidden until its icon is pressed', !(await p.$('#czViewer .vzp-card .vzp-th')));
84
+ await press(p, '.vzr-np:not(.vzr-ph1) [data-a="caps"]');
85
+ ok('S3 the captures icon shows the screen captures, with no Waiting for design marker', await until(() => p.evaluate(() => document.querySelectorAll('#czViewer .vzp-card .vzp-th').length > 0 && !document.querySelector('#czViewer [data-cz-placeholder]'))));
84
86
  await press(p, '.vzp-card .vzp-th[data-a="seekto"]');
85
87
  ok('S3 a thumbnail moves the replay to its time', await until(() => p.evaluate(() => /^03:10/.test((document.querySelector('#czViewer [data-clk="pos"]') || {}).textContent || ''))), await p.evaluate(() => (document.querySelector('#czViewer [data-clk="pos"]') || {}).textContent));
86
88
  await strip('tr');
@@ -98,9 +100,9 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
98
100
 
99
101
  /* S5 a correction, and what PMOS learns (XvAReplayEdit) */
100
102
  const pid = await p.evaluate(() => { const r = document.querySelectorAll('#czViewer .vzr-row [data-a="psel"]')[1]; r.click(); return r.dataset.id; });
101
- ok('S5 pressing a row selects it and the evidence follows it: its time, its quote', await until(() => p.evaluate((id) => !!document.querySelector(`#czViewer .vzr-row.sel [data-a="psel"][data-id="${id}"]`) && !!document.querySelector(`#czViewer .vzr-ev [data-a="pedit"][data-id="${id}"]`), pid)), pid);
102
- await press(p, '.vzr-ev [data-a="pedit"]');
103
- ok('S5 Fix in the evidence opens the editor in place: the kinds, the words, what PMOS will remember', !!pid && await until(() => p.evaluate(() => !!document.querySelector('#czViewer .vzp-ed [data-a="etext"]') && document.querySelectorAll('#czViewer .vzp-ed [data-a="ekind"]').length === 3 && document.querySelector('#czViewer .vzp-ed [data-a="esave"]').disabled)), pid);
103
+ ok('S5 pressing a row selects it; the selected row has Fix and Not right', await until(() => p.evaluate((id) => !!document.querySelector(`#czViewer .vzr-row.sel [data-a="psel"][data-id="${id}"]`) && !!document.querySelector(`#czViewer .vzr-row.sel [data-a="pedit"][data-id="${id}"]`) && !!document.querySelector(`#czViewer .vzr-row.sel [data-a="pwrong"][data-id="${id}"]`), pid)), pid);
104
+ await press(p, '.vzr-row.sel [data-a="pedit"]');
105
+ ok('S5 Fix on the row opens the editor in place: the kinds, the words, what PMOS will remember', !!pid && await until(() => p.evaluate(() => !!document.querySelector('#czViewer .vzp-ed [data-a="etext"]') && document.querySelectorAll('#czViewer .vzp-dt .vzp-ed [data-a="ekind"]').length === 3 && document.querySelector('#czViewer .vzp-ed [data-a="esave"]').disabled)), pid);
104
106
  const kNow = await p.evaluate(() => document.querySelector('#czViewer .vzp-ed [data-a="ekind"][aria-checked="true"]').dataset.k);
105
107
  const kNew = kNow === 'question' ? 'action' : 'question';
106
108
  await press(p, `.vzp-ed [data-a="ekind"][data-k="${kNew}"]`);
@@ -111,12 +113,16 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
111
113
  ok('S5 the page says Fixed and what it will remember, with Undo', await until(async () => /Fixed\./.test(await body(p)) && !!(await p.$('#czViewer .vz-msg [data-a="msgundo"]'))), (await body(p)).slice(0, 300));
112
114
  await press(p, '.vz-msg [data-a="msgundo"]');
113
115
  ok('S5 Undo forgets the lesson (POST /api/meeting-points/forget)', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/forget' && x.body.lesson === 'l0123456789')), JSON.stringify(A.S.posts.map((x) => x.path)));
114
- /* MvF: a word heard wrong, selected in the quote, fixed in every place; Undo forgets it */
115
- await p.evaluate(() => { const q = document.querySelector('#czViewer .vzr-ev .vzr-q'); const tn = [...q.childNodes].find((x) => x.nodeType === 3 && /print/.test(x.textContent)) || q.firstChild; const i = tn.textContent.indexOf('print'); const r = document.createRange(); r.setStart(tn, i); r.setEnd(tn, i + 5); const s = getSelection(); s.removeAllRanges(); s.addRange(r); q.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); });
116
- ok('S5 a selected word becomes a field with ✓', await until(() => p.evaluate(() => { const f = document.querySelector('#czViewer .vzr-fe'); return !!f && f.dataset.w === 'print' && document.activeElement === f.querySelector('input'); })));
117
- await p.fill('#czViewer .vzr-fe input', 'Print'); await p.press('#czViewer .vzr-fe input', 'Enter');
118
- ok('S5 Enter posts the word fix and it shows fixed, with Undo', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/term' && x.body.heard === 'print' && x.body.means === 'Print')) && await until(async () => !!(await p.$('#czViewer .vzr-fx')) && /Fixed/.test(await body(p)) && !!(await p.$('#czViewer .vz-msg [data-a="msgundo"]'))));
119
- await press(p, '.vz-msg [data-a="msgundo"]');
116
+ /* MpW1, MpW2 (batch 27 item 12): words heard wrong, selected in Next steps, fixed in every place; the ✦ ask waits; Undo forgets it */
117
+ await strip('steps');
118
+ await until(() => p.evaluate(() => [...document.querySelectorAll('#czViewer .vzm-step .vzm-grow')].some((x) => /print/.test(x.textContent))));
119
+ await p.evaluate(() => { const g = [...document.querySelectorAll('#czViewer .vzm-step .vzm-grow')].find((x) => /print/.test(x.textContent)); const w = document.createTreeWalker(g, NodeFilter.SHOW_TEXT); let tn; while ((tn = w.nextNode()) && !/print/.test(tn.textContent)); const i = tn.textContent.indexOf('print'); const r = document.createRange(); r.setStart(tn, i); r.setEnd(tn, i + 5); const s = getSelection(); s.removeAllRanges(); s.addRange(r); g.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); });
120
+ ok('S5 selected words open the popup: Heard as, the fix field with ✓, the places; no ✦ line without a real match', await until(() => p.evaluate(() => { const f = document.querySelector('.vzr-wpop'); return !!f && f.dataset.w === 'print' && /Heard as “print”/.test(f.textContent) && !!f.querySelector('[data-a="wfok"]') && /in this meeting/.test(f.textContent) && !f.querySelector('.vzr-sug'); })));
121
+ await p.fill('.vzr-wpop [data-a="wfin"]', 'Print'); await p.press('.vzr-wpop [data-a="wfin"]', 'Enter');
122
+ ok('S5 Enter posts the word fix: it shows fixed, a toast with Undo, and Names and terms asks to remember it', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/term' && x.body.heard === 'print' && x.body.means === 'Print'))
123
+ && await until(async () => !!(await p.$('#czViewer .vzr-fx')) && !!(await p.$('#czViewer .vzr-toast [data-a="msgundo"]')) && !!(await p.$('#czViewer [data-a="wkeep"][data-v="1"]'))));
124
+ ok('S5 remembering waits for the ✓: nothing was posted to keep yet', !A.S.posts.some((x) => x.path === '/api/meeting-points/keep'));
125
+ await press(p, '.vzr-toast [data-a="msgundo"]');
120
126
  ok('S5 Undo forgets the word fix', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/forget' && x.body.lesson === 't0123456789')) && await until(async () => !(await p.$('#czViewer .vzr-fx'))));
121
127
  await press(p, '[data-a="confirmall"]');
122
128
  ok('S5 Confirm N in the kept header confirms what is left to confirm', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points' && x.body.verdict === 'confirmed' && x.body.ids.length > 0)), JSON.stringify(A.S.posts.filter((x) => x.path === '/api/meeting-points')));
@@ -122,7 +122,7 @@ function model() {
122
122
  a && a.tm.length === 3 && a.tmc.length === 1 && /^tg-tmc dn|^tg-tmc/.test(a.tmc[0]) && a.tmc[0].endsWith('|2') && !a.tml.length, JSON.stringify(a));
123
123
  ok('L the same ◇: late is orange, the Evaluate task IS the end ◇ (said once), a row\'s only milestone IS its end (no Deliver, hotfix 2)',
124
124
  a && /^tg-tm late\|Design review · Passkey sign-in · Oct 16 · late$/.test(a.tm[0]) && a.tm.some((x) => /KPI evaluated · Scarlet/.test(x)) && !a.tm.some((x) => /Deliver · Passkey/.test(x)) && !a.tm.some((x) => /Committed/.test(x)), JSON.stringify(a && a.tm));
125
- ok('H a single ◇\'s hover names the milestone and its initiative', a && a.tm.some((x) => x.endsWith('|Feature delivered · Scarlet — ST What’s The News · Jan 29')), JSON.stringify(a && a.tm));
125
+ ok('H a single ◇\'s hover names the milestone and its initiative', a && a.tm.some((x) => /\|Feature delivered · Scarlet — ST What’s The News · Jan 29(?: ’\d\d)?$/.test(x)), JSON.stringify(a && a.tm));
126
126
  ok('L the other product\'s lane: Mar 2 and Mar 6 (Recovery\'s end) merge; Email OTP has no milestone, so its end is Deliver Jun 30', b && b.tmc.length === 1 && b.tm.length === 1 && /Deliver · Email OTP · Jun 30/.test(b.tm[0]), JSON.stringify(b));
127
127
  const lg = await p.evaluate(() => (document.querySelector('.tg-lgd') || {}).textContent || '');
128
128
  ok('L the legend gets no new entry', !/lane|product/i.test(lg), lg);
@@ -134,10 +134,10 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
134
134
  await p.click('#czViewer [data-pc="pick"][data-m="pilot-live"]');
135
135
  const mn = await until(() => p.evaluate(() => { const m = document.querySelector('#czViewer .pc-menu'); return m ? { hd: [...m.querySelectorAll('.pc-mhd')].map((x) => x.textContent), items: [...m.querySelectorAll('.pc-mi')].map((x) => x.querySelector('.t').textContent + (x.querySelector('.r') ? ' ' + x.querySelector('.r').textContent : '') + (x.getAttribute('aria-checked') === 'true' ? ' ✓' : '')) } : null; }));
136
136
  ok('P the picker lists its initiatives per product with their ◆ Deliver days, the chosen ones ticked, then "None: I tick it myself"', !!mn && mn.hd.join('|') === 'Done when these pass ◆ Deliver|IDPF|Microsites'
137
- && mn.items.includes('Descope feature parity Nov 12 ✓') && mn.items.includes('Microsite sign-in on CIAM Nov 14 ✓') && mn.items.includes('Legacy IdP sunset Feb 20') && mn.items[mn.items.length - 1] === 'None: I tick it myself', JSON.stringify(mn));
137
+ && mn.items.includes('Descope feature parity Nov 12 ✓') && mn.items.includes('Microsite sign-in on CIAM Nov 14 ✓') && mn.items.some((x) => /^Legacy IdP sunset Feb 20(?: ’\d\d)?$/.test(x)) && mn.items[mn.items.length - 1] === 'None: I tick it myself', JSON.stringify(mn));
138
138
  await p.click('#czViewer .pc-menu [data-ini="legacy-idp-sunset"]');
139
139
  const wv = await until(() => p.evaluate(() => { const w = document.querySelector('#czViewer .pc-lnw:has([data-m="pilot-live"]) .pc-warn'); return w ? w.textContent : null; }));
140
- ok('P picking an initiative whose ◆ Deliver is later than the milestone warns in amber, and Save stays', wv === 'Legacy IdP sunset is set to deliver on Feb 20, after this date.' && (await head(p)).save, String(wv));
140
+ ok('P picking an initiative whose ◆ Deliver is later than the milestone warns in amber, and Save stays', /^Legacy IdP sunset is set to deliver on Feb 20(?: ’\d\d)?, after this date\.$/.test(String(wv)) && (await head(p)).save, String(wv));
141
141
  await shot(p, 'MsB3Pick-built');
142
142
  await p.click('#czViewer .pc-menu [data-ini="legacy-idp-sunset"]');
143
143
  await p.keyboard.press('Escape');
@@ -175,7 +175,7 @@ function dashFor(r) {
175
175
  function iniBody(r) {
176
176
  const h = dashFor(r);
177
177
  if (h && h.shows()) return h.html(() => '');
178
- const md = (v) => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][+v.slice(5, 7) - 1] + ' ' + +v.slice(8, 10);
178
+ const md = (v) => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][+v.slice(5, 7) - 1] + ' ' + +v.slice(8, 10) + (+v.slice(0, 4) !== new Date().getFullYear() ? ' ’' + v.slice(2, 4) : '');
179
179
  const self = !!((O.M() || {}).board || {}).self, M = O.M() || {};
180
180
  // what left the ⋯ for the thing it changes (batch 16 item 5c): the workflow chip, the dashed +, the stage chip, the dates chip, Jira's ↻
181
181
  return viewHtml(viewOf(r), { add: !!(r.id && O.addTask), adding: adding === r.key, addHint: adding === r.key && addDates ? md(addDates.start) + ' to ' + md(addDates.due) : '', openTask: !!O.openTask, quick: !!(O.setTask && O.setDates), hold: !!O.hold, tail: VW.czSections(boardSections(r), { key: 'board' }),
@@ -51,7 +51,10 @@ import { stageName } from './stage-chip.js'; // W1: the one stage
51
51
 
52
52
  const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
53
53
  const day = (v) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(v || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; };
54
- export const md = (d) => MON[d.getMonth()] + ' ' + d.getDate();
54
+ /** Month and day only, for the axis ticks: the axis says the year once on its own (tickOf). */
55
+ const mdn = (d) => MON[d.getMonth()] + ' ' + d.getDate();
56
+ /** A date in words: "Nov 3", and the short year when it is not this year ("Jun 10 ’27", workspace grill W4). */
57
+ export const md = (d, now = new Date()) => mdn(d) + (d.getFullYear() !== now.getFullYear() ? ' ’' + String(d.getFullYear()).slice(2) : '');
55
58
  /** A month and the short year: "Nov ’26" (2 yr, 5 yr and the edge tabs). */
56
59
  export const my = (d) => MON[d.getMonth()] + ' ’' + String(d.getFullYear()).slice(2);
57
60
  const addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
@@ -72,7 +75,7 @@ const colAdd = (u, d, n) => (u === 'w' ? addDays(d, 7 * n) : u === 'm' ? addMont
72
75
  /** A column's head: its label, the part under it that carries no year, and its year (the first column on screen shows it, bd-scroll.js). */
73
76
  function tickOf(u, d) {
74
77
  const y = d.getFullYear(), q = Math.floor(d.getMonth() / 3) + 1, yy = '’' + String(y).slice(2);
75
- if (u === 'w') return { at: d, label: md(d), sub: d.getMonth() === 0 && d.getDate() <= 7 ? String(y) : '', base: '', y };
78
+ if (u === 'w') return { at: d, label: mdn(d), sub: d.getMonth() === 0 && d.getDate() <= 7 ? String(y) : '', base: '', y };
76
79
  if (u === 'm') { const qs = d.getMonth() % 3 === 0 ? 'Q' + q : ''; return { at: d, label: MON[d.getMonth()], sub: qs ? qs + (q === 1 ? ' ' + yy : '') : '', base: qs, y }; }
77
80
  if (u === 'q') return { at: d, label: 'Q' + q, sub: q === 1 ? yy : '', base: '', y };
78
81
  return { at: d, label: String(y), sub: 'H1 · H2', base: 'H1 · H2', y, plain: true };
@@ -88,7 +91,7 @@ export function windowOf(span, now, phone, sprint, dates) {
88
91
  const a = sprint && day(sprint.start), b = sprint && day(sprint.end);
89
92
  const from = a && b && b > a ? a : mon, to = a && b && b > a ? addDays(b, 1) : addDays(mon, 14);
90
93
  const n = Math.round((to - from) / 86400000), step = phone ? Math.max(2, Math.ceil(n / 4)) : n > 16 ? 2 : 1;
91
- const ticks = []; for (let i = 0; i < n; i += step) { const d = addDays(from, i); ticks.push({ at: d, label: md(d), sub: '', base: '', y: d.getFullYear() }); }
94
+ const ticks = []; for (let i = 0; i < n; i += step) { const d = addDays(from, i); ticks.push({ at: d, label: mdn(d), sub: '', base: '', y: d.getFullYear() }); }
92
95
  return { from, to, ticks, cols: ticks.length, fixed: true };
93
96
  }
94
97
  const S = SCALE[span] || SCALE.quarter, u = S.unit, cols = phone ? S.ph : S.cols;
@@ -197,7 +200,7 @@ function extentOf(r, now) {
197
200
  }
198
201
 
199
202
  /** A date as the calm timeline says it (batch 13 item 4): "Nov 3", and the year only when it is not this year ("Jun 10 ’27"). */
200
- export const dy = (d, now) => md(d) + (d.getFullYear() !== now.getFullYear() ? ' ’' + String(d.getFullYear()).slice(2) : '');
203
+ export const dy = (d, now) => mdn(d) + (d.getFullYear() !== now.getFullYear() ? ' ’' + String(d.getFullYear()).slice(2) : '');
201
204
  /** What lands at a row's end: its end milestone's name, else the ◆ stage of its own workflow the end stands for, else '' (no labelled ◇). */
202
205
  const landsOf = (r, et) => (et ? et.name : endStage(r));
203
206
  /** The one label at a bar's end (XcalmA1Desk): what lands, then when; tone '' | 'risk' | 'late' | 'q' colours the date. At the right edge, with
@@ -205,7 +208,7 @@ const landsOf = (r, et) => (et ? et.name : endStage(r));
205
208
  function endLabel(at, name, d, tone, now, pxPct, hover, slip) {
206
209
  const date = dy(d, now), est = (name.length + date.length + 1) * 6.6 + 26;
207
210
  const flip = pxPct && (100 - at) * pxPct < est;
208
- const tip = name + ' · ' + md(d) + (d.getFullYear() !== now.getFullYear() ? ', ' + d.getFullYear() : '') + (hover && hover.length ? '. ' + hover.join('. ') : '');
211
+ const tip = name + ' · ' + mdn(d) + (d.getFullYear() !== now.getFullYear() ? ', ' + d.getFullYear() : '') + (hover && hover.length ? '. ' + hover.join('. ') : '');
209
212
  return `<span class="tg-endl${flip ? ' l' : ''}" style="left:${P(at)}" title="${esc(tip)}"><span class="nm">${esc(name)}</span> <b${tone ? ` class="${tone}"` : ''}>${esc(date)}</b>${slip || ''}</span>`;
210
213
  }
211
214
  /** A slip's words for the hover (PvaAC4): "Planned May 20 → Jun 10 (+3 wk)". */
@@ -278,7 +281,7 @@ function draftBar(r, X, phone, fmt, now, pxPct) {
278
281
 
279
282
  const MIN_PX = 8; // a hold or a gap narrower than this closes up (XtiA4Long, XtiAGrill 12)
280
283
  /** A hold's dates: "Oct 12 to Mar 1, 2027" (the year when it ends in another one). */
281
- export const holdRange = (h) => { const a = day(h.from), b = day(h.to || h.back); return md(a) + (b ? ' to ' + md(b) + (b.getFullYear() !== a.getFullYear() ? ', ' + b.getFullYear() : '') : ''); };
284
+ export const holdRange = (h) => { const a = day(h.from), b = day(h.to || h.back); return md(a) + (b ? ' to ' + mdn(b) + (b.getFullYear() !== a.getFullYear() ? ', ' + b.getFullYear() : '') : ''); };
282
285
  /** A hold's words for its ⏸ button and its menu (XtiAGrill 18): "On hold, Oct 12 to Mar 1, 2027, waits on the SMS vendor contract". */
283
286
  export function holdWords(x, now) {
284
287
  const h = x.h, why = h.reason ? ', ' + h.reason.replace(/\.$/, '').replace(/^./, (c) => c.toLowerCase()) : '';
@@ -443,7 +446,7 @@ function offDots(rows, now) {
443
446
  function rollUp(rows, sp, X, W, o, ms, pxPct, now) {
444
447
  if (!sp || sp.hi <= W.from || sp.lo >= W.to) return '';
445
448
  const L = laneMilestones(rows, now, ms.reach);
446
- const end = o.phone ? '' : `<span class="tg-endl" style="left:${P(X(sp.hi))}" title="${esc('Last end ' + md(sp.hi) + ', ' + sp.hi.getFullYear())}"><b>${esc(my(sp.hi))}</b></span>`;
449
+ const end = o.phone ? '' : `<span class="tg-endl" style="left:${P(X(sp.hi))}" title="${esc('Last end ' + mdn(sp.hi) + ', ' + sp.hi.getFullYear())}"><b>${esc(my(sp.hi))}</b></span>`;
447
450
  return progLine({ rows }, sp, X, now) + laneMarks(L, X, o.phone, pxPct, ms) + end;
448
451
  }
449
452
  /** The product's head row on the timeline: the name cell (tile, name, goal dots, count when folded, the fold) and the lane. Open or folded it
@@ -118,6 +118,7 @@ body.bd-page main.bd-main { position: relative; max-width: none; margin: 16px; p
118
118
  /* the calmer top bar (batch 24 item 7, canvas BbB): one pill for what needs a look, the legend behind (i), Jira as Adler's face */
119
119
  .bd-chip.bd-need { background: var(--cz-well); border-color: transparent; gap: 10px; padding: 0 10px 0 12px; font-weight: var(--fw-strong); color: var(--cz-ink); }
120
120
  .bd-chip.bd-need > span { display: inline-flex; align-items: center; gap: 5px; }
121
+ .bd-chip.bd-need > .bd-nsep { color: var(--cz-ink3); font-weight: var(--fw-regular); margin: 0 -4px; }
121
122
  .bd-chip.bd-need.on { background: var(--cz-bluebg); color: var(--cz-blueink); }
122
123
  .bd-needm { min-width: 220px; }
123
124
  .bd-needm .bd-mlbl, .bd-lgdm .bd-mlbl { font: var(--fw-strong) var(--fs-badge)/var(--lh-badge) var(--cz-sans); color: var(--cz-ink3); padding: 6px 10px 4px; letter-spacing: .5px; text-transform: uppercase; white-space: nowrap; }
@@ -33,16 +33,25 @@ function pillOf(r) {
33
33
  if (r.ini) return `<a class="lab-pill y" href="${esc(S.H.iniHref(r.ini))}">✦ on initiative</a>`;
34
34
  return '';
35
35
  }
36
- function recRow(r) {
36
+ /** When a job ran, short: "19:37" today, "Tue 14:10" this week (Wawan, 2026-11-04: the rows said what ran but not when). */
37
+ export function whenOf(at, now = Date.now()) {
38
+ const d = new Date(at); if (Number.isNaN(d.getTime())) return '';
39
+ const hm = String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
40
+ return new Date(now).toDateString() === d.toDateString() ? hm : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()] + ' ' + hm;
41
+ }
42
+ /** One row of the record. A background job says what Claude did and what came out (routes-lab.js others); its pill shows only when a result
43
+ * waits on the initiative, and says so. */
44
+ export function recRow(r, now = Date.now()) {
37
45
  const open = r.compare ? ib(`data-lab-cmp="${esc(r.id)}" data-lab-board="${esc(r.board || '')}"`, 'Open in the compare view', LC.I.open) : '';
38
- const pill = r.kind === 'job' ? (r.ini ? `<a class="lab-pill" href="${esc(S.H.iniHref(r.ini))}">initiative</a>` : '') : pillOf(r);
39
- return `<div class="lab-row"><span class="lab-ico">${SVG(ICON[r.kind] || ICON.job, 18)}</span><span class="w"><b>${esc(r.title)}</b><small>${esc(r.meta || '')}</small></span>${pill}${open}</div>`;
46
+ const pill = r.kind === 'job' ? (r.ini && S.H ? `<a class="lab-pill" href="${esc(S.H.iniHref(r.ini))}">See what it found</a>` : '') : pillOf(r);
47
+ const meta = r.kind === 'job' ? [r.meta, whenOf(r.at, now)].filter(Boolean).join(' · ') : r.meta || '';
48
+ return `<div class="lab-row"><span class="lab-ico">${SVG(ICON[r.kind] || ICON.job, 18)}</span><span class="w"><b>${esc(r.title)}</b><small>${esc(meta)}</small></span>${pill}${open}</div>`;
40
49
  }
41
50
  function html(L) {
42
51
  const k = L.kept;
43
52
  const keptCard = k ? `<div class="lab-card"><small>You kept</small><b class="lab-big">${k.kept} of ${k.filed}</b><small>${[k.proto[1] ? 'prototypes ' + k.proto[0] + ' of ' + k.proto[1] : '', k.scan[1] ? 'scans ' + k.scan[0] + ' of ' + k.scan[1] : ''].filter(Boolean).join(' · ')}. Claude learns from each reason you give.</small></div>` : '';
44
53
  const next = L.next.length ? `<div class="lab-card"><b>Next time you step away</b>${L.next.map((x) => `<div class="lab-row"><span class="lab-dot open"></span><span class="w">${esc(x.title)}${x.why ? `<small>${esc(x.why)}</small>` : ''}</span></div>`).join('')}<small>Claude picks these by itself. To skip one, say so in chat.</small></div>` : '';
45
- const rec = L.record.length ? `<div class="lab-card lab-rec"><div class="lab-bar"><b>What Claude did in the background</b><small>this week, every job</small></div>${L.record.map(recRow).join('')}${L.more ? `<small>and ${L.more} more</small>` : ''}</div>` : '';
54
+ const rec = L.record.length ? `<div class="lab-card lab-rec"><div class="lab-bar"><b>What Claude did in the background</b><small>this week, every job</small></div>${L.record.map((r) => recRow(r)).join('')}${L.more ? `<small>and ${L.more} more</small>` : ''}</div>` : '';
46
55
  return `<h2 class="ins-h3" id="hLab">Lab</h2>`
47
56
  + (L.say ? `<p class="lab-say"><span aria-hidden="true">✦</span> ${esc(L.say)}</p>` : '')
48
57
  + L.loose.map(looseHtml).join('')
@@ -1,7 +1,7 @@
1
1
  /* pm/cozy-search.js — Search (initiative premium-uncovered-features, E1; canvas EvSearch, and EvPhone on the right).
2
2
  *
3
3
  * One palette over the page: the query, the kind chips with their counts (All, Initiatives, Documents, Slides, Reports, Artifacts, Meetings)
4
- * and the worktree picker ("main + 3 worktrees"); the results grouped (Initiatives · Documents, slides and reports · Artifacts and designs ·
4
+ * (no worktree chip since workspace grill W4); the results grouped (Initiatives · Documents, slides and reports · Artifacts and designs ·
5
5
  * Meetings), and last "Ask Claude about …", which starts a chat in Home. ↑ ↓ move, ↵ opens, Esc closes. On a phone it is the whole screen.
6
6
  * openSearch({ go?, q? }) go: the page's own opener ({ ini } | { ses } | { doc }), used before a page change
7
7
  * closeSearch()
@@ -10,18 +10,16 @@
10
10
  * any other file read-only from its worktree (GET /api/find/file). Nothing here writes. */
11
11
  import { esc, svg, ICON, wsParam, wsQ, skelRows, cantRead, failedItem } from './cozy-evui.js';
12
12
  const ARROW = { up: 'M12 19V5M6 11l6-6 6 6', down: 'M12 5v14M6 13l6 6 6-6', enter: 'M20 5v7a3 3 0 0 1-3 3H5M9 11l-4 4 4 4' }; // the footer's keys, drawn (R28-13: no glyph as a control)
13
- import { SEARCH_KINDS, PHONE_KINDS, searchGroups, resultSub, resultTag, wtChip } from './cozy-evmodel.js';
13
+ import { SEARCH_KINDS, PHONE_KINDS, searchGroups, resultSub, resultTag } from './cozy-evmodel.js';
14
14
  import { open as vwOpen, current as vwCurrent } from './cozy-viewer.js';
15
15
  import { nameOf as sessionName } from './home-data.js';
16
16
  import { adler } from './cozy-adler.js'; // session names A: the PM's name wins, then Claude's
17
17
 
18
18
  const PHONE = 760;
19
19
  const isPhone = () => innerWidth <= PHONE;
20
- const S = { el: null, q: '', kind: 'all', at: 0, rows: [], res: null, busy: false, err: '', wt: null, wtOpen: false, back: null, go: null, seq: 0, boards: null, sessions: null, t: 0 };
20
+ const S = { el: null, q: '', kind: 'all', at: 0, rows: [], res: null, busy: false, err: '', wt: null, back: null, go: null, seq: 0, boards: null, sessions: null, t: 0 };
21
21
  const KIND_ICON = { initiative: ICON.flag, document: ICON.doc, slides: ICON.slides, report: ICON.report, artifact: ICON.design, file: ICON.file, meeting: ICON.meet, session: ICON.session };
22
22
  async function getj(p) { try { const r = await fetch(p, { credentials: 'same-origin' }); const j = await r.json().catch(() => null); return j || { ok: false, error: 'the server did not answer' }; } catch { return { ok: false, error: 'the server did not answer' }; } }
23
- const WT_KEY = 'pmos.search.wt';
24
- const wtPick = () => { try { const v = JSON.parse(localStorage.getItem(WT_KEY) || 'null'); return Array.isArray(v) ? v : null; } catch { return null; } };
25
23
 
26
24
  function mount() {
27
25
  if (S.el && S.el.isConnected) return S.el;
@@ -31,7 +29,7 @@ function mount() {
31
29
  + `<div class="ev-sp-in">${svg(ICON.search, 20)}<label for="evQ" class="ev-vh">Search PMOS</label><input id="evQ" type="text" autocomplete="off" spellcheck="false" placeholder="Search initiatives, documents, slides, reports, artifacts and meetings" role="combobox" aria-expanded="true" aria-controls="evRes" aria-autocomplete="list"><span class="ev-kbd">Esc</span><button type="button" class="ev-ib x" data-ev="close" aria-label="Close search">${svg(ICON.close, 18)}</button></div>`
32
30
  + '<div class="ev-sp-chips" id="evChips" role="group" aria-label="Kind"></div>'
33
31
  + '<div class="ev-sp-list" id="evRes" role="listbox" aria-label="Results"></div>'
34
- + `<div class="ev-sp-ft"><span><span class="ev-kbd" role="img" aria-label="Arrow up">${svg(ARROW.up, 12)}</span> <span class="ev-kbd" role="img" aria-label="Arrow down">${svg(ARROW.down, 12)}</span> move</span><span><span class="ev-kbd" role="img" aria-label="Enter">${svg(ARROW.enter, 12)}</span> open</span><span class="ev-r">Searches the repo and every worktree. Opens in the Viewer when it can.</span></div>`
32
+ + `<div class="ev-sp-ft"><span><span class="ev-kbd" role="img" aria-label="Arrow up">${svg(ARROW.up, 12)}</span> <span class="ev-kbd" role="img" aria-label="Arrow down">${svg(ARROW.down, 12)}</span> move</span><span><span class="ev-kbd" role="img" aria-label="Enter">${svg(ARROW.enter, 12)}</span> open</span><span class="ev-r">Searches your work. Opens in the Viewer when it can.</span></div>`
35
33
  + '</div>';
36
34
  document.body.append(...d.childNodes);
37
35
  S.el = document.getElementById('evSearch');
@@ -42,13 +40,11 @@ function mount() {
42
40
  if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); const n = S.rows.length; if (n) { S.at = (S.at + (e.key === 'ArrowDown' ? 1 : n - 1)) % n; drawList(); } }
43
41
  else if (e.key === 'Enter') { e.preventDefault(); const r = S.rows[S.at]; if (r) go(r); }
44
42
  });
45
- S.el.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); if (S.wtOpen) { S.wtOpen = false; drawChips(); return; } closeSearch(); } });
43
+ S.el.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeSearch(); } });
46
44
  S.el.addEventListener('click', (e) => {
47
45
  const b = e.target.closest('button, input[type="checkbox"]'); if (!b) return;
48
46
  if (b.dataset.ev === 'close') { closeSearch(); return; }
49
47
  if (b.dataset.evKind) { S.kind = b.dataset.evKind; S.at = 0; draw(); q.focus(); return; }
50
- if ('evWt' in b.dataset) { S.wtOpen = !S.wtOpen; drawChips(); return; }
51
- if (b.dataset.evWtId) { const all = (S.res && S.res.worktrees || []).filter((w) => !w.main).map((w) => w.id); const cur = new Set(S.wt || all); if (b.checked) cur.add(b.dataset.evWtId); else cur.delete(b.dataset.evWtId); S.wt = [...cur]; try { localStorage.setItem(WT_KEY, JSON.stringify(S.wt)); } catch { /* no storage */ } run(); return; }
52
48
  if (b.dataset.evI != null) { const r = S.rows[+b.dataset.evI]; if (r) go(r); }
53
49
  });
54
50
  return S.el;
@@ -59,7 +55,7 @@ export function openSearch(o = {}) {
59
55
  S.go = typeof o.go === 'function' ? o.go : null;
60
56
  if (!el.hidden) { el.querySelector('#evQ').focus(); return el; }
61
57
  const ae = document.activeElement; S.back = ae && ae !== document.body ? ae : null;
62
- S.wt = wtPick(); S.wtOpen = false; S.kind = 'all'; S.at = 0;
58
+ S.wt = null; S.kind = 'all'; S.at = 0; // every worktree: with no chip there is no pick to keep (workspace grill W4)
63
59
  if (o.q != null) S.q = String(o.q);
64
60
  el.hidden = false; document.getElementById('evSpScrim').hidden = false; el.setAttribute('data-esc-layer', '');
65
61
  const q = el.querySelector('#evQ'); q.value = S.q; q.focus(); q.select();
@@ -104,12 +100,9 @@ const countsOf = (rows) => { const c = { all: rows.length }; for (const r of row
104
100
 
105
101
  function drawChips() {
106
102
  const box = S.el && S.el.querySelector('#evChips'); if (!box) return;
107
- const rows = allRows(), c = countsOf(rows), K = isPhone() ? PHONE_KINDS : SEARCH_KINDS, wts = (S.res && S.res.worktrees) || [];
108
- const on = (w) => !S.wt || S.wt.includes(w.id);
109
- const shown = wts.map((w) => ({ ...w, on: w.main || on(w) }));
110
- box.innerHTML = K.map(([k, l]) => `<button type="button" class="ev-chip${S.kind === k ? ' on' : ''}" data-ev-kind="${k}" aria-pressed="${S.kind === k}">${esc(l)}${!isPhone() && S.q.trim().length >= 2 ? ' ' + (c[k] || 0) : ''}</button>`).join('')
111
- + `<button type="button" class="ev-chip gh ev-wt" data-ev-wt aria-expanded="${S.wtOpen}" aria-haspopup="true" title="Which worktrees Search reads">${svg(ICON.branch, 14)}${esc(wtChip(shown))}</button>`
112
- + `<div class="ev-wtm" role="group" aria-label="Worktrees"${S.wtOpen ? '' : ' hidden'}>${shown.map((w) => `<label><input type="checkbox" ${w.main ? 'checked disabled' : `data-ev-wt-id="${esc(w.id)}"${w.on ? ' checked' : ''}`}><span>${esc(w.main ? 'main' : w.name)}</span><small>${w.main ? 'always' : w.error ? esc(w.error) : esc(w.id)}</small></label>`).join('')}</div>`;
103
+ const rows = allRows(), c = countsOf(rows), K = isPhone() ? PHONE_KINDS : SEARCH_KINDS;
104
+ // no worktree chip and no "main" chip (workspace grill W4): Search reads every worktree, and the footer says "Searches your work"
105
+ box.innerHTML = K.map(([k, l]) => `<button type="button" class="ev-chip${S.kind === k ? ' on' : ''}" data-ev-kind="${k}" aria-pressed="${S.kind === k}">${esc(l)}${!isPhone() && S.q.trim().length >= 2 ? ' ' + (c[k] || 0) : ''}</button>`).join('');
113
106
  }
114
107
  function rowHtml(r, i) {
115
108
  const tag = resultTag(r), sub = resultSub(r);
@@ -130,7 +123,7 @@ function drawList() {
130
123
  if (ask) S.rows.push(ask);
131
124
  S.at = Math.min(S.at, Math.max(0, S.rows.length - 1));
132
125
  let h = '', i = 0;
133
- if (q.length < 2) h = '<p class="ev-sp-empty">Type two letters or more. Search reads the initiatives, the documents, slides and reports, the artifacts and designs, and the meetings, in the repository and every worktree.</p>';
126
+ if (q.length < 2) h = '<p class="ev-sp-empty">Type two letters or more. Search reads the initiatives, the documents, slides and reports, the artifacts and designs, and the meetings.</p>';
134
127
  else if (S.err) h = cantRead({ what: 'Search', why: S.err, sources: false });
135
128
  else if (S.busy && !groups.length) h = skelRows(3);
136
129
  // nothing found: Adler with his magnifier (canvas AdU4); the words stay the same
@@ -74,7 +74,10 @@ function general() {
74
74
  const theme = (() => { const t = store.get('pmos.c3.theme'); return t === 'light' || t === 'dark' ? t : 'auto'; })();
75
75
  const vw = store.get(VIEWER_KEY) === 'popup' ? 'popup' : 'column';
76
76
  const P = T.prods, on = P ? P.filter((x) => x.on) : null;
77
- const pname = !P ? 'Reading' : !P.length ? 'No product' : on.length === 1 ? on[0].name : on.length + ' of ' + P.length + ' products';
77
+ // the product the pages read (workspace grill W4): the page's own ?ws= first, then the pick; "No product" only when none is registered
78
+ const pageWs = (() => { try { return new URLSearchParams(location.search).get('ws') || ''; } catch { return ''; } })();
79
+ const wsRow = pageWs ? (P || []).find((x) => x.ws === pageWs) || (T.ws || []).find((x) => x.id === pageWs) : null;
80
+ const pname = wsRow ? wsRow.name || wsRow.label || pageWs : !P ? 'Reading' : !P.length ? 'No product' : on.length === 1 ? on[0].name : !on.length ? (P.find((x) => x.primary) || P[0]).name : on.length + ' of ' + P.length + ' products';
78
81
  const we = T.set && Array.isArray(T.set.settings) ? T.set.settings.find((x) => x.key === 'time.workEnd') : null, end = we ? Number(we.value) : 18;
79
82
  const hours = []; for (let h = 13; h <= 23; h++) hours.push(h);
80
83
  const upd = ready() ? `<div class="ev-card ev-upd">${adFace('needs-you')}<span class="ev-pill y">Update</span><p><b>PMOS ${esc(latest())} is ready.</b> <span class="ev-mut">${esc(firstLine())}</span></p><button type="button" class="ev-btn sm" data-ev-go="updates">What's new</button><button type="button" class="ev-btn sm dark" data-ev-upd="update">${appBehind() ? 'Restart to update' : 'Update'}</button></div>` : '';
@@ -508,7 +511,10 @@ async function load() {
508
511
  T.conn = p && p.connectors && Array.isArray(p.connectors.list) ? p.connectors.list : [];
509
512
  T.set = s && s.ok ? s : null;
510
513
  if (T.set) await AD.loadPrefs(async () => T.set);
511
- T.prods = b && b.ok && b.model && b.model.products ? b.model.products.available || [] : [];
514
+ // the registered products with their ticks: the pick's own read, the one Home, Board and Intelligence share (workspace grill W4: the
515
+ // briefing's model can hold none on a page opened for one product, which read "No product")
516
+ const pk = await import('./cozy-pick.js').then((PP) => PP.readPick()).catch(() => null);
517
+ T.prods = pk ? pk.products : b && b.ok && b.model && b.model.products ? b.model.products.available || [] : [];
512
518
  if (!w || !Array.isArray(w.workspaces)) T.msg = 'The repositories could not be read.'; else if (!p || !p.ok) T.msg = 'The connectors could not be read.';
513
519
  draw();
514
520
  await Promise.all(T.ws.map(async (x) => { const r = await pmGet('/api/kit/status?ws=' + encodeURIComponent(x.id)); if (r && r.ok) T.st[x.id] = r; }));
@@ -579,7 +579,7 @@ body.cz-home .hm-hd.hv-bar > .hv-hi { flex: 1 0 auto; max-width: 100%; }
579
579
  .gm-lnk { border: 0; background: none; padding: 0; min-height: 32px; font: inherit; font-size: var(--fs-label); font-weight: var(--fw-strong); color: var(--cz-blueink); cursor: pointer; text-align: left; }
580
580
  .gm-lnk:focus-visible, .gm button:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; border-radius: 6px; }
581
581
  /* the quarter strip: the verdict, one stacked bar with the PM's line as a black tick, the legend, By product */
582
- .gm-q { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(260px, 1.3fr) auto; gap: 20px; align-items: center; background: var(--cz-card); border-radius: 18px; padding: 16px 18px; }
582
+ .gm-q { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(260px, 1.3fr) auto; gap: 24px; align-items: center; background: var(--cz-well); border-radius: 18px; padding: 16px 20px; }
583
583
  .gm-qt { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
584
584
  .gm-qt b { font-size: var(--fs-panel); font-weight: var(--fw-strong); line-height: 1.3; color: var(--cz-ink); }
585
585
  .gm-qt b .o { color: var(--cz-late); }
@@ -600,17 +600,32 @@ body.cz-home .hm-hd.hv-bar > .hv-hi { flex: 1 0 auto; max-width: 100%; }
600
600
  .gm-leg span { display: inline-flex; align-items: center; gap: 5px; }
601
601
  .gm-leg i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
602
602
  /* two columns: the reading column and the time rail */
603
- .gm-x { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 340px); grid-template-areas: "a rail" "b rail"; gap: 18px 28px; align-items: start; }
603
+ .gm-x { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 420px); grid-template-areas: "a rail" "b rail"; gap: 28px 48px; align-items: start; }
604
604
  .gm-x.solo { grid-template-columns: minmax(0, 1fr); grid-template-areas: "a" "b"; }
605
- .gm-a { grid-area: a; display: flex; flex-direction: column; gap: 18px; min-width: 0; }
605
+ .gm-a { grid-area: a; display: flex; flex-direction: column; gap: 28px; min-width: 0; }
606
606
  .gm-b { grid-area: b; min-width: 0; }
607
- .gm-rail { grid-area: rail; display: flex; flex-direction: column; gap: 14px; background: var(--cz-card); border-radius: 18px; padding: 14px 16px; }
608
- /* Adler's lead: one face, a headline at 18, the "Done for you" line */
609
- .gm-lead .yd-open { background: none; padding: 0; }
610
- .gm-lead .yd-open.concerned { background: var(--cz-latebg); padding: 14px; }
611
- .gm-lead .yd-ht b { font-size: var(--fs-panel); line-height: 1.3; }
607
+ .gm-rail { grid-area: rail; display: flex; flex-direction: column; gap: 16px; background: var(--cz-well); border-radius: 20px; padding: 20px 20px 16px; }
608
+ .gm-rh .gm-lnk { margin-left: auto; min-height: 0; }
609
+ /* Adler's lead (frames G1–G4 lead2): his face at 44 beside a 26 headline and a 15 line; no banner, no tinted box, in every mood */
610
+ .gm-lead .yd-open, .gm-lead .yd-open.concerned { background: none; padding: 0; }
611
+ .gm-lead .yd-hd { align-items: flex-start; gap: 16px; }
612
+ .gm-lead .yd-face { width: 44px; height: 44px; }
613
+ .gm-lead .yd-face svg { width: 44px; height: 44px; }
614
+ .gm-lead .yd-ht b { font-size: var(--fs-display); line-height: 1.25; letter-spacing: -.01em; }
615
+ .gm-lead .yd-ht em { font-size: var(--fs-body); color: var(--cz-ink2); line-height: 1.5; margin-top: 4px; }
616
+ .gm-lead .yd-open { position: relative; display: flex; flex-direction: column; gap: 10px; }
617
+ .gm-lead .yd-hd { padding-right: 56px; }
618
+ .gm-lead .yd-sync:not(.yd-miss) { position: absolute; top: 0; right: 0; } /* read the source again: beside the headline, not a row of its own */
619
+ .gm-lead .yd-miss { margin-left: 60px; background: none; padding: 0; }
620
+ .gm-did { margin-left: 60px; }
612
621
  .gm-did { display: flex; align-items: flex-start; gap: 6px; font-size: var(--fs-label); color: var(--cz-ink2); line-height: 1.4; }
613
622
  .gm-did svg { flex: none; color: var(--cz-ok); margin-top: 1px; }
623
+ .gm-src { align-items: center; color: var(--cz-ink3); }
624
+ .gm-src span { flex: 1; min-width: 0; }
625
+ .gm-src .yd-alt { width: 32px; height: 32px; }
626
+ .gm-src .yd-alt svg { color: var(--cz-ink2); margin: 0; }
627
+ /* say it once: "N drafted for you" and "N can wait" open their fold; until pressed the fold is not drawn a second time below */
628
+ .gm-wrap .czs-sec:is([data-czs-id="drafts"], [data-czs-id="wait"], [data-czs-id="more"]):not(.gm-on) { display: none; }
614
629
  .gm-sec { display: flex; flex-direction: column; }
615
630
  /* Your call, said once */
616
631
  .gm-call { display: flex; align-items: center; gap: 12px; background: var(--cz-card); border-radius: 18px; padding: 14px 14px 14px 16px; border-left: 3px solid var(--cz-late); }
@@ -650,18 +665,19 @@ body.cz-home .hm-hd.hv-bar > .hv-hi { flex: 1 0 auto; max-width: 100%; }
650
665
  .gm-rh span { font-size: var(--fs-label); color: var(--cz-ink3); }
651
666
  .gm-ag { display: flex; flex-direction: column; gap: 6px; }
652
667
  .gm-ar { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 8px; align-items: start; }
653
- .gm-tm { font-size: var(--fs-label); font-weight: var(--fw-strong); color: var(--cz-ink3); padding-top: 9px; font-variant-numeric: tabular-nums; }
654
- .gm-ab { display: flex; flex-direction: column; gap: 2px; min-height: 44px; padding: 8px 10px; border: 0; border-left: 3px solid var(--cz-hair); border-radius: 10px; background: var(--cz-well); text-align: left; font: inherit; color: inherit; cursor: pointer; }
668
+ .gm-tm { font-size: var(--fs-label); color: var(--cz-ink3); padding-top: 9px; text-align: right; font-variant-numeric: tabular-nums; }
669
+ .gm-ab { display: flex; flex-direction: column; gap: 3px; min-height: 44px; padding: 9px 12px; border: 0; border-left: 3px solid var(--cz-hair); border-radius: 12px; background: var(--cz-card); text-align: left; font: inherit; color: inherit; cursor: pointer; }
655
670
  .gm-ab b { font-size: var(--fs-body); font-weight: var(--fw-strong); line-height: 1.3; color: var(--cz-ink); }
656
671
  .gm-ab em { display: inline-flex; align-items: center; gap: 4px; font-style: normal; font-size: var(--fs-label); color: var(--cz-ink3); line-height: 1.35; }
657
- .gm-ab.mt { border-left-color: var(--cz-blue); }
672
+ .gm-ab.mt { border-left-color: var(--cz-blueink); }
658
673
  .gm-ab.key { border-left-color: var(--cz-late); }
659
- .gm-ab.sp { background: var(--cz-bluebg); border-left-color: var(--cz-blueink); }
660
- .gm-ab.opt { background: none; border-left-style: dashed; }
674
+ .gm-ab.sp { background: var(--cz-bluebg); border: 1.5px dashed color-mix(in srgb, var(--cz-blue) 55%, transparent); }
675
+ .gm-ab.opt { background: none; border: 1px solid var(--cz-hair); }
661
676
  .gm-ab.past { opacity: .55; }
662
677
  .gm-nl { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; font-size: var(--fs-label); color: var(--cz-ink2); }
663
678
  .gm-nl div::before { content: '· '; }
664
- .gm-gap { font-size: var(--fs-label); color: var(--cz-ink3); padding: 4px 10px; border-left: 3px dotted var(--cz-hair); }
679
+ .gm-gap { font-size: var(--fs-label); color: var(--cz-ink3); padding: 2px 12px; margin-left: 6px; border-left: 2px dotted var(--cz-hair); }
680
+ .gm-ad { padding: 8px 12px; border-radius: 12px; border: 1px solid var(--cz-hair); font-size: var(--fs-body); font-weight: var(--fw-strong); color: var(--cz-ink2); }
665
681
  .gm-now { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 8px; align-items: center; }
666
682
  .gm-now span { font-size: var(--fs-label); font-weight: var(--fw-strong); color: var(--cz-late); font-variant-numeric: tabular-nums; }
667
683
  .gm-now i { height: 2px; background: var(--cz-late); border-radius: 1px; }