create-pmos 0.52.0 → 0.53.1

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 (229) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/mods/pmos-cockpit/README.md +13 -0
  4. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
  5. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
  6. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
  7. package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
  8. package/payload/planning/evals/eval-calibration.md +1 -1
  9. package/payload/planning/okrs/health-budgets.md +1 -1
  10. package/payload/scripts/plan-items-stage.js +271 -0
  11. package/payload/scripts/pmos-log.sh +18 -7
  12. package/payload/scripts/workflow-state.js +28 -2
  13. package/payload/state/discoveries.md +1 -1
  14. package/payload/state/log.md +1 -1
  15. package/workspace/README.md +2 -0
  16. package/workspace/REMOTE.md +24 -63
  17. package/workspace/bin/pmos.js +19 -3
  18. package/workspace/cut-stub.js +40 -0
  19. package/workspace/first-paint-audit.js +145 -0
  20. package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
  21. package/workspace/fixtures/initiatives-api/list.json +176 -0
  22. package/workspace/fixtures/initiatives-api/post-403.json +5 -0
  23. package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
  24. package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
  25. package/workspace/fixtures/initiatives-api/sheet.json +112 -0
  26. package/workspace/fixtures/report-api/draft.json +177 -0
  27. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
  28. package/workspace/fixtures/today-api/provider-rows.json +204 -0
  29. package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
  30. package/workspace/guide/board.md +81 -0
  31. package/workspace/guide/breakdown.md +154 -0
  32. package/workspace/guide/command-palette.md +75 -0
  33. package/workspace/guide/index.md +28 -17
  34. package/workspace/guide/initiatives-roadmap.md +130 -0
  35. package/workspace/guide/initiatives-screen.md +89 -0
  36. package/workspace/guide/initiatives-timeline.md +57 -0
  37. package/workspace/guide/integrated-work.md +94 -0
  38. package/workspace/guide/knowledge-graph.md +3 -68
  39. package/workspace/guide/launcher.md +51 -0
  40. package/workspace/guide/layout.md +102 -32
  41. package/workspace/guide/live-notes.md +3 -3
  42. package/workspace/guide/pm-only-acts.md +89 -0
  43. package/workspace/guide/pm-only-lines.md +61 -0
  44. package/workspace/guide/proactive-engine.md +21 -5
  45. package/workspace/guide/reports-page.md +80 -0
  46. package/workspace/guide/reports.md +73 -0
  47. package/workspace/guide/timeline.md +70 -0
  48. package/workspace/guide/today.md +85 -0
  49. package/workspace/guide/validate-with-typesafe.md +1 -1
  50. package/workspace/lib/actor.js +91 -0
  51. package/workspace/lib/board-cache.js +32 -2
  52. package/workspace/lib/board-cli.js +108 -0
  53. package/workspace/lib/board-fold.js +232 -0
  54. package/workspace/lib/breakdown-export.js +75 -0
  55. package/workspace/lib/brief.js +20 -2
  56. package/workspace/lib/capture.js +98 -0
  57. package/workspace/lib/engine/plan-items-stage.js +271 -0
  58. package/workspace/lib/engine/workflow-state.js +28 -2
  59. package/workspace/lib/factory-join.js +158 -0
  60. package/workspace/lib/goals.js +165 -0
  61. package/workspace/lib/grant-mint.js +113 -0
  62. package/workspace/lib/health-rules-factory.js +92 -0
  63. package/workspace/lib/health-rules-plan.js +164 -0
  64. package/workspace/lib/health.js +79 -0
  65. package/workspace/lib/initiative-cli.js +222 -0
  66. package/workspace/lib/initiative-display.js +183 -0
  67. package/workspace/lib/initiative-needs.js +127 -0
  68. package/workspace/lib/initiative-rows.js +210 -0
  69. package/workspace/lib/initiative-writer.js +141 -0
  70. package/workspace/lib/issues.js +206 -0
  71. package/workspace/lib/item-cli.js +195 -0
  72. package/workspace/lib/item-spans.js +137 -0
  73. package/workspace/lib/launcher-cli.js +207 -0
  74. package/workspace/lib/measure-cli.js +100 -0
  75. package/workspace/lib/measurements.js +189 -0
  76. package/workspace/lib/mobile-nav.js +57 -53
  77. package/workspace/lib/needs-providers.js +120 -0
  78. package/workspace/lib/placements.js +440 -0
  79. package/workspace/lib/plan-items.js +922 -0
  80. package/workspace/lib/plan-ledgers.js +85 -0
  81. package/workspace/lib/plan-rules.js +72 -0
  82. package/workspace/lib/plan-tree.js +380 -0
  83. package/workspace/lib/pm-guard.js +174 -0
  84. package/workspace/lib/pm-lines-needs.js +331 -0
  85. package/workspace/lib/pmos-verbs.js +69 -0
  86. package/workspace/lib/proactive-cli.js +58 -2
  87. package/workspace/lib/proactive.js +9 -6
  88. package/workspace/lib/qr.js +293 -0
  89. package/workspace/lib/remote-access.js +93 -1
  90. package/workspace/lib/report-cli.js +152 -0
  91. package/workspace/lib/report-draft.js +578 -0
  92. package/workspace/lib/report-needs.js +213 -0
  93. package/workspace/lib/route-modules.js +128 -0
  94. package/workspace/lib/routes-board.js +91 -0
  95. package/workspace/lib/routes-capture.js +52 -0
  96. package/workspace/lib/routes-initiatives.js +371 -0
  97. package/workspace/lib/routes-reports.js +163 -0
  98. package/workspace/lib/routes-timeline.js +197 -0
  99. package/workspace/lib/routes-today.js +32 -0
  100. package/workspace/lib/today-cli.js +99 -0
  101. package/workspace/lib/today.js +333 -0
  102. package/workspace/lib/triage.js +163 -0
  103. package/workspace/lib/verify-work-cli.js +81 -0
  104. package/workspace/lib/work-check-needs.js +99 -0
  105. package/workspace/package-lock.json +1 -1
  106. package/workspace/package.json +4 -5
  107. package/workspace/public/icons/apple-touch-icon.png +0 -0
  108. package/workspace/public/icons/icon-192.png +0 -0
  109. package/workspace/public/icons/icon-512.png +0 -0
  110. package/workspace/public/index.html +40 -19
  111. package/workspace/public/pm/app.js +330 -30
  112. package/workspace/public/pm/bd-board.js +113 -0
  113. package/workspace/public/pm/bd-drag.js +59 -0
  114. package/workspace/public/pm/bd-model.js +152 -0
  115. package/workspace/public/pm/bd-timeline.js +233 -0
  116. package/workspace/public/pm/bd-tree.js +244 -0
  117. package/workspace/public/pm/board-copy.js +57 -0
  118. package/workspace/public/pm/breakdown-api.js +58 -0
  119. package/workspace/public/pm/breakdown-copy.js +138 -0
  120. package/workspace/public/pm/components.css +3135 -0
  121. package/workspace/public/pm/cycle-review.js +125 -0
  122. package/workspace/public/pm/data.js +40 -187
  123. package/workspace/public/pm/dest-board.js +438 -0
  124. package/workspace/public/pm/dest-initiatives.js +134 -0
  125. package/workspace/public/pm/dest-reports.js +270 -0
  126. package/workspace/public/pm/dest-settings.js +217 -0
  127. package/workspace/public/pm/dest-today.js +182 -0
  128. package/workspace/public/pm/dialogs.js +88 -0
  129. package/workspace/public/pm/display-word.js +56 -0
  130. package/workspace/public/pm/dock.js +175 -0
  131. package/workspace/public/pm/fixture.js +8 -227
  132. package/workspace/public/pm/frame-copy.js +99 -0
  133. package/workspace/public/pm/handoff-box.js +82 -0
  134. package/workspace/public/pm/http.js +88 -0
  135. package/workspace/public/pm/icons.js +53 -0
  136. package/workspace/public/pm/initiative-list.js +218 -0
  137. package/workspace/public/pm/initiative-sheet.js +261 -0
  138. package/workspace/public/pm/initiatives-api.js +59 -0
  139. package/workspace/public/pm/initiatives-copy.js +215 -0
  140. package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
  141. package/workspace/public/pm/initiatives-timeline.js +410 -0
  142. package/workspace/public/pm/initiatives-ui.js +123 -0
  143. package/workspace/public/pm/item-sheet.js +251 -0
  144. package/workspace/public/pm/keep-card.js +204 -0
  145. package/workspace/public/pm/lifecycle.js +7 -1
  146. package/workspace/public/pm/mobile.css +49 -20
  147. package/workspace/public/pm/mobile.js +157 -29
  148. package/workspace/public/pm/modes-live.js +7 -883
  149. package/workspace/public/pm/modes.js +32 -1024
  150. package/workspace/public/pm/new-initiative-sheet.js +74 -0
  151. package/workspace/public/pm/next-step-ui.js +3 -2
  152. package/workspace/public/pm/palette-copy.js +51 -0
  153. package/workspace/public/pm/palette.js +312 -0
  154. package/workspace/public/pm/panel-breakdown.js +702 -0
  155. package/workspace/public/pm/panel-cockpit.js +63 -295
  156. package/workspace/public/pm/panel-detail.js +25 -172
  157. package/workspace/public/pm/panel-initiatives.js +1 -19
  158. package/workspace/public/pm/panel-repos.js +31 -81
  159. package/workspace/public/pm/panel-tabs.js +200 -111
  160. package/workspace/public/pm/panel-trees.js +5 -2
  161. package/workspace/public/pm/portfolio-report.js +61 -0
  162. package/workspace/public/pm/primitives.js +656 -0
  163. package/workspace/public/pm/print.css +33 -0
  164. package/workspace/public/pm/provider-cards.js +136 -0
  165. package/workspace/public/pm/registry.js +194 -0
  166. package/workspace/public/pm/release-card.js +110 -0
  167. package/workspace/public/pm/report-view.js +309 -0
  168. package/workspace/public/pm/reports-copy.js +114 -0
  169. package/workspace/public/pm/roadmap.js +271 -0
  170. package/workspace/public/pm/sig-watch.js +41 -0
  171. package/workspace/public/pm/state.js +8 -4
  172. package/workspace/public/pm/status-banner.js +13 -5
  173. package/workspace/public/pm/tab-doc.js +2 -4
  174. package/workspace/public/pm/tab-term.js +12 -2
  175. package/workspace/public/pm/tab-web.js +3 -2
  176. package/workspace/public/pm/timeline-copy.js +48 -0
  177. package/workspace/public/pm/today-cards.js +244 -0
  178. package/workspace/public/pm/today-copy.js +126 -0
  179. package/workspace/public/pm/today-goals.js +172 -0
  180. package/workspace/public/pm/tokens.css +16 -18
  181. package/workspace/public/pm/topbar.js +102 -0
  182. package/workspace/public/pm/triage-card.js +60 -0
  183. package/workspace/public/pm/undo-hold.js +103 -0
  184. package/workspace/public/pm/view-addrepo.js +2 -1
  185. package/workspace/public/pm/view-livenotes.js +3 -1
  186. package/workspace/public/pm/view-today.js +7 -162
  187. package/workspace/public/vendor/Sortable.min.js +2 -0
  188. package/workspace/public/vendor/lucide-sprite.js +79 -0
  189. package/workspace/replaced-tests.json +2877 -0
  190. package/workspace/run-tests.js +56 -5
  191. package/workspace/server.js +446 -40
  192. package/workspace/sheet-answer.js +61 -0
  193. package/workspace/skills/measure.skill +41 -0
  194. package/workspace/skills/morning-brief.skill +15 -9
  195. package/workspace/skills/report-biweekly.skill +23 -25
  196. package/workspace/skills/tracker-refresh.skill +50 -0
  197. package/workspace/test-files.json +21 -3
  198. package/workspace/test-lists/base.json +6 -0
  199. package/workspace/test-lists/run2-a.json +6 -0
  200. package/workspace/test-lists/run2-b.json +13 -0
  201. package/workspace/test-lists/run2-c.json +19 -0
  202. package/workspace/test-lists/run2-d.json +22 -0
  203. package/workspace/test-lists/run3-0.json +5 -0
  204. package/workspace/test-lists/run3-a.json +5 -0
  205. package/workspace/test-lists/run3-b.json +6 -0
  206. package/workspace/test-lists/run3-c.json +15 -0
  207. package/workspace/test-lists/run3-d.json +6 -0
  208. package/workspace/test-lists/run4-a.json +8 -0
  209. package/workspace/test-lists/run4-b.json +6 -0
  210. package/workspace/test-lists/run4-c.json +13 -0
  211. package/workspace/test-lists/run4-d.json +9 -0
  212. package/workspace/test-lists/run5-a.json +6 -0
  213. package/workspace/test-lists/run5-b.json +6 -0
  214. package/workspace/test-lists/run5-c.json +15 -0
  215. package/workspace/test-lists/workspace-launcher.json +5 -0
  216. package/workspace/test-lists/workspace-phone-app.json +5 -0
  217. package/workspace/test-run.js +27 -1
  218. package/workspace/third-party/README.md +44 -0
  219. package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
  220. package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
  221. package/workspace/design-parity.js +0 -673
  222. package/workspace/public/pm/tab-task.js +0 -86
  223. package/workspace/public/pm/view-guide.js +0 -141
  224. package/workspace/public/pm/view-knowledge.js +0 -225
  225. package/workspace/public/pm/view-memory.js +0 -122
  226. package/workspace/public/pm/view-portfolio.js +0 -153
  227. package/workspace/public/pm/view-reports.js +0 -190
  228. package/workspace/public/pm/view-scheduled.js +0 -161
  229. package/workspace/public/pm/view-skills.js +0 -216
@@ -0,0 +1,251 @@
1
+ /* pm/item-sheet.js — the item sheet (pm-os-absorb Run 1, PRD R13).
2
+ *
3
+ * One item, every field, in the order R13 sets: state buttons, Title, Owner, Stage, After, Start and Due, Milestone,
4
+ * Blocked (with its reason), Note, Hand to agent, History. Under the state buttons, two lines show only when they apply:
5
+ * "Proposed by …" with Keep all, for an item an agent proposed; and Add evidence, for an item that reads Done with no
6
+ * evidence (it writes the same done → done move with evidence that `pmos item move … done --evidence` writes). A 420px side sheet above 760px, a bottom sheet below it, with
7
+ * a sticky row: Withdraw (held for Undo) on the left, Close on the right.
8
+ *
9
+ * Every field commits by itself and writes one line: text on Enter or blur, selects, dates and boxes on change. A press
10
+ * on a state button writes `moved` at once, except Done, which is held for Undo. When the item changes underneath — a
11
+ * poll, an agent's write, the PM's own write coming back — the sheet refreshes every field the PM is not typing in.
12
+ */
13
+
14
+ import { el } from './dom.js';
15
+ import { sheet, tabs, field, button, iconButton } from './primitives.js';
16
+ import { icon } from './icons.js';
17
+ import { COPY, STATES, ago, dayOf, ownerLabel } from './breakdown-copy.js';
18
+ import { stageLabel } from './bd-model.js';
19
+ import { handoffBox } from './handoff-box.js';
20
+
21
+ const ownerKind = (o) => (o === 'you' || o === 'agent' ? o : String(o || '').split(':')[0] || 'you');
22
+ const ownerName = (o) => { const s = String(o || ''); const i = s.indexOf(':'); return i > 0 ? s.slice(i + 1) : ''; };
23
+
24
+ /**
25
+ * openItemSheet(ctx, id, { handoff }) -> { id, refresh(), close() }
26
+ * ctx: { ws, slug, view() -> Layout, move(id,to), setFields(id,fields), flag(id,blocked,reason), withdraw(id),
27
+ * attachEvidence(id, 'kind:ref'), keep(batch), sessions() -> [], onClose(id) }
28
+ */
29
+ export function openItemSheet(ctx, id, { handoff = false } = {}) {
30
+ const L0 = ctx.view();
31
+ const it0 = L0 && L0.items[id];
32
+ if (!it0) return null;
33
+ const item = () => { const L = ctx.view(); return L && L.items[id]; };
34
+ const focused = (node) => node && document.activeElement === node;
35
+
36
+ /* 1 · state buttons */
37
+ const stateSlot = el('div', { class: 'bd-group', 'data-bd-field': 'state' });
38
+ const drawState = (it) => {
39
+ while (stateSlot.firstChild) stateSlot.removeChild(stateSlot.firstChild);
40
+ stateSlot.appendChild(tabs({
41
+ variant: 'segmented', role: 'radiogroup', full: true, ariaLabel: COPY.labels.state, value: it.state,
42
+ items: STATES.map((s) => ({ id: s, label: COPY.states[s] })),
43
+ onChange: (to) => { const cur = item(); if (cur && cur.state !== to) ctx.move(id, to); },
44
+ }));
45
+ };
46
+
47
+ /* under the state: proposed (Keep all) and no evidence (Add evidence) */
48
+ const noteSlot = el('div', { class: 'bd-group', 'data-bd-status': '' }); // part of field 1, the state
49
+ const evidence = field({
50
+ label: COPY.addEvidence, kind: 'text', placeholder: COPY.evidenceHint, attrs: { 'data-bd-input': 'evidence', spellcheck: 'false', maxlength: '220' },
51
+ onCommit: (v) => {
52
+ const t = v.trim();
53
+ if (!t || !ctx.attachEvidence) return;
54
+ // The server checks the form (commit:, pr:, file:, session:, note:). A refusal shows as a toast and leaves the
55
+ // text in the field to correct (Enter sends it again). Once it is attached the field goes, and focus goes back to
56
+ // the state buttons rather than falling to the page.
57
+ Promise.resolve(ctx.attachEvidence(id, t)).then((out) => {
58
+ const typed = evidence.control.value;
59
+ evidence.control.resetCommitted('');
60
+ if (!out || out.ok === false) { evidence.control.value = typed; return; }
61
+ const cur = item();
62
+ if (cur) drawNote(cur, true);
63
+ });
64
+ },
65
+ });
66
+ /* Drawn again only when what it says changes, so a refresh (every poll) keeps the nodes and the focus in them. While
67
+ * the evidence field has focus it stays until the write settles. When the focused control goes with the line (Keep
68
+ * all, the attached evidence), focus goes to the state buttons rather than falling to the page. */
69
+ let noteKey = null;
70
+ const drawNote = (it, force = false) => {
71
+ const L = ctx.view();
72
+ const batch = it.proposed ? ((L && L.batches) || []).find((b) => (b.items || []).includes(id)) : null;
73
+ const needsEvidence = it.state === 'done' && !!it.noEvidence;
74
+ const key = JSON.stringify([it.proposed ? [batch ? batch.byLabel : it.byLabel, dayOf(it.createdAt), !!(batch && ctx.keep)] : null, needsEvidence]);
75
+ if (key === noteKey) return;
76
+ if (!force && document.activeElement === evidence.control) return;
77
+ noteKey = key;
78
+ const had = noteSlot.contains(document.activeElement);
79
+ while (noteSlot.firstChild) noteSlot.removeChild(noteSlot.firstChild);
80
+ if (it.proposed) {
81
+ noteSlot.appendChild(el('div', { class: 'bd-proposed', 'data-bd-proposed': '' },
82
+ el('span', { class: 'bd-muted' }, COPY.proposedBy(batch ? batch.byLabel : it.byLabel, dayOf(it.createdAt))),
83
+ batch && ctx.keep ? button({ label: COPY.keepAll, variant: 'outline', size: 'sm', attrs: { 'data-bd-keep-all': '' }, onClick: () => ctx.keep(batch) }) : null));
84
+ }
85
+ if (needsEvidence) noteSlot.appendChild(evidence);
86
+ noteSlot.hidden = !noteSlot.firstChild;
87
+ if (had && !noteSlot.contains(document.activeElement)) { const b = stateSlot.querySelector('[aria-checked="true"]'); if (b) b.focus(); }
88
+ };
89
+
90
+ /* 2 · title */
91
+ const title = field({
92
+ label: COPY.labels.title, kind: 'title', value: it0.title, attrs: { 'data-bd-input': 'title', maxlength: '200' },
93
+ onCommit: (v) => { const t = v.trim(); const cur = item(); if (t && cur && t !== cur.title) ctx.setFields(id, { title: t }); else if (cur) title.control.resetCommitted(cur.title); },
94
+ });
95
+ title.setAttribute('data-bd-field', 'title');
96
+
97
+ /* 3 · owner (+ a name for a person or a tracker reference) and 4 · stage */
98
+ const ownerNameField = field({
99
+ label: COPY.labels.ownerName, kind: 'text', value: ownerName(it0.owner), attrs: { 'data-bd-input': 'owner-name', maxlength: '60' },
100
+ onCommit: (v) => {
101
+ const k = owner.control.value; const name = v.trim();
102
+ if ((k === 'person' || k === 'tracker') && name) { const o = k + ':' + name; const cur = item(); if (cur && cur.owner !== o) ctx.setFields(id, { owner: o }); }
103
+ },
104
+ });
105
+ const owner = field({
106
+ label: COPY.labels.owner, kind: 'select', value: ownerKind(it0.owner), attrs: { 'data-bd-input': 'owner' },
107
+ options: ['you', 'agent', 'person', 'tracker'].map((k) => ({ value: k, label: COPY.owners[k] })),
108
+ onCommit: (k) => {
109
+ showOwnerName(k);
110
+ if (k === 'you' || k === 'agent') { const cur = item(); if (cur && cur.owner !== k) ctx.setFields(id, { owner: k }); }
111
+ else { ownerNameField.querySelector('label').textContent = k === 'tracker' ? COPY.labels.trackerRef : COPY.labels.ownerName; ownerNameField.control.focus(); }
112
+ },
113
+ });
114
+ owner.setAttribute('data-bd-field', 'owner');
115
+ const showOwnerName = (k) => { ownerNameField.hidden = !(k === 'person' || k === 'tracker'); };
116
+ const stage = field({
117
+ label: COPY.labels.stage, kind: 'select', value: it0.stage, attrs: { 'data-bd-input': 'stage' },
118
+ options: (L0.stages || []).map((s) => ({ value: s.id, label: s.label })),
119
+ onCommit: (v) => { const cur = item(); if (cur && cur.stage !== v) ctx.setFields(id, { stage: v }); },
120
+ });
121
+ stage.setAttribute('data-bd-field', 'stage');
122
+
123
+ /* 5 · after */
124
+ const afterSlot = el('div', { class: 'bd-group', 'data-bd-field': 'after' });
125
+ const otherId = field({ label: COPY.otherItemId, kind: 'text', attrs: { 'data-bd-input': 'after-other', spellcheck: 'false' } });
126
+ const drawAfter = (it) => {
127
+ while (afterSlot.firstChild) afterSlot.removeChild(afterSlot.firstChild);
128
+ const L = ctx.view();
129
+ const after = it.after || [];
130
+ const nameOf = (x) => (L.items[x] ? L.items[x].title : x);
131
+ const chips = after.length
132
+ ? el('div', { class: 'bd-chips' }, after.map((x) => el('span', { class: 'bd-chip', 'data-after': x },
133
+ el('span', null, nameOf(x)),
134
+ iconButton({ icon: 'x', size: 'sm', ariaLabel: COPY.aria.remove(nameOf(x)), onClick: () => ctx.setFields(id, { after: after.filter((y) => y !== x) }) }))))
135
+ : el('div', { class: 'bd-muted' }, COPY.afterNone);
136
+ const choices = Object.values(L.items).filter((x) => x.id !== id && !after.includes(x.id) && !x.pending);
137
+ const add = field({
138
+ label: COPY.addPredecessor, kind: 'select', value: '', hideLabel: true, attrs: { 'data-bd-input': 'after-add' },
139
+ options: [{ value: '', label: COPY.addPredecessor + '…' }, ...choices.map((x) => ({ value: x.id, label: x.title }))],
140
+ onCommit: (v) => { if (v) ctx.setFields(id, { after: [...after, v] }); },
141
+ });
142
+ const addOther = button({
143
+ label: COPY.add, variant: 'outline', size: 'sm', attrs: { 'data-bd-after-other': '' },
144
+ onClick: () => { const v = otherId.control.value.trim(); if (v && !after.includes(v)) { otherId.control.value = ''; ctx.setFields(id, { after: [...after, v] }); } },
145
+ });
146
+ afterSlot.append(el('span', { class: 'bd-group__label' }, COPY.labels.after), chips, add,
147
+ el('div', { class: 'bd-after-add' }, el('div', { class: 'bd-after-add__field' }, otherId), addOther));
148
+ };
149
+
150
+ /* 6 · start and due, 7 · milestone, 8 · blocked */
151
+ const start = field({ label: COPY.labels.start, kind: 'date', value: it0.start || '', attrs: { 'data-bd-input': 'start' }, onCommit: (v) => ctx.setFields(id, { start: v || null }) });
152
+ const due = field({ label: COPY.labels.due, kind: 'date', value: it0.due || '', attrs: { 'data-bd-input': 'due' }, onCommit: (v) => ctx.setFields(id, { due: v || null }) });
153
+ start.setAttribute('data-bd-field', 'start'); due.setAttribute('data-bd-field', 'due');
154
+ const milestone = field({ label: COPY.labels.milestone, kind: 'check', value: !!it0.milestone, attrs: { 'data-bd-input': 'milestone' }, onCommit: (v) => ctx.setFields(id, { milestone: !!v }) });
155
+ milestone.setAttribute('data-bd-field', 'milestone');
156
+ const reason = field({
157
+ label: COPY.labels.blockedBecause, kind: 'text', value: (it0.blocked && it0.blocked.reason) || '', attrs: { 'data-bd-input': 'blocked-reason', maxlength: '200' },
158
+ onCommit: (v) => { const r = v.trim(); if (blocked.control.checked && r) ctx.flag(id, true, r); },
159
+ });
160
+ reason.setAttribute('data-bd-field', 'blocked-reason');
161
+ const blocked = field({
162
+ label: COPY.labels.blocked, kind: 'check', value: !!it0.blocked, attrs: { 'data-bd-input': 'blocked' },
163
+ onCommit: (on) => {
164
+ reason.hidden = !on;
165
+ const cur = item();
166
+ if (on) { const r = reason.control.value.trim(); if (r) ctx.flag(id, true, r); else reason.control.focus(); }
167
+ else if (cur && cur.blocked) ctx.flag(id, false);
168
+ },
169
+ });
170
+ blocked.setAttribute('data-bd-field', 'blocked');
171
+ reason.hidden = !it0.blocked;
172
+
173
+ /* 9 · note */
174
+ const note = field({ label: COPY.labels.note, kind: 'textarea', value: it0.note || '', attrs: { 'data-bd-input': 'note', maxlength: '1000' }, onCommit: (v) => ctx.setFields(id, { note: v.trim() ? v : null }) });
175
+ note.setAttribute('data-bd-field', 'note');
176
+
177
+ /* 10 · hand to agent */
178
+ const handSlot = el('div', { 'data-bd-field': 'handoff' });
179
+ const handButton = () => button({
180
+ label: COPY.handToAgent, variant: 'outline', block: true, icon: 'bot', iconEnd: 'chevron-right', attrs: { 'data-bd-hand': '' },
181
+ onClick: () => showHandoff(),
182
+ });
183
+ const showHandoff = () => {
184
+ while (handSlot.firstChild) handSlot.removeChild(handSlot.firstChild);
185
+ handSlot.appendChild(handoffBox({
186
+ ws: ctx.ws, slug: ctx.slug, item: item() || it0, sessions: ctx.sessions(),
187
+ onCancel: () => { while (handSlot.firstChild) handSlot.removeChild(handSlot.firstChild); const b = handButton(); handSlot.appendChild(b); b.focus(); },
188
+ onStarted: () => sh.close(),
189
+ }));
190
+ };
191
+ handSlot.appendChild(handButton());
192
+
193
+ /* 11 · history */
194
+ const histList = el('ol', { class: 'bd-hist', 'aria-label': COPY.labels.history });
195
+ const drawHistory = (it) => {
196
+ while (histList.firstChild) histList.removeChild(histList.firstChild);
197
+ const rows = [...(it.history || [])].reverse();
198
+ for (const h of rows) {
199
+ histList.appendChild(el('li', { class: 'bd-hist__row' },
200
+ el('span', { class: 'bd-hist__by' }, h.byLabel || ''),
201
+ el('span', { class: 'bd-hist__text' }, h.text || h.ev),
202
+ el('span', { class: 'bd-hist__when', title: h.at || '' }, ago(h.at))));
203
+ }
204
+ };
205
+ const history = el('div', { 'data-bd-field': 'history' },
206
+ el('div', { class: 'bd-hist__head' }, icon('history'), el('span', null, COPY.labels.history)), histList);
207
+
208
+ const withdraw = button({ label: COPY.labels.withdraw, variant: 'danger', attrs: { 'data-bd-withdraw': '' }, onClick: () => { sh.close(); ctx.withdraw(id); } });
209
+ const closeBtn = button({ label: COPY.labels.close, variant: 'outline', attrs: { 'data-bd-close': '' }, onClick: () => sh.close() });
210
+
211
+ const sh = sheet({
212
+ mode: 'auto', title: it0.title, sub: ctx.slug + ' · ' + stageLabel(L0, it0.stage),
213
+ attrs: { 'data-bd-sheet': id },
214
+ body: [stateSlot, noteSlot, title, el('div', { class: 'bd-grid2' }, el('div', { class: 'bd-group' }, owner, ownerNameField), stage),
215
+ afterSlot, el('div', { class: 'bd-grid2' }, start, due), el('div', { class: 'bd-checks' }, milestone, blocked), reason, note, handSlot, history],
216
+ actions: [withdraw, closeBtn],
217
+ onClose: () => { if (ctx.onClose) ctx.onClose(id); },
218
+ });
219
+ showOwnerName(ownerKind(it0.owner));
220
+
221
+ const refresh = () => {
222
+ const it = item();
223
+ if (!it) { sh.close(); return; }
224
+ const L = ctx.view();
225
+ sh.setHead(it.title, ctx.slug + ' · ' + stageLabel(L, it.stage));
226
+ const tabFocused = stateSlot.contains(document.activeElement);
227
+ drawState(it);
228
+ drawNote(it);
229
+ if (tabFocused) { const b = stateSlot.querySelector('[aria-checked="true"]'); if (b) b.focus(); }
230
+ if (!focused(title.control)) title.control.resetCommitted(it.title);
231
+ if (!focused(owner.control)) { owner.control.value = ownerKind(it.owner); showOwnerName(ownerKind(it.owner)); }
232
+ if (!focused(ownerNameField.control) && ownerName(it.owner)) ownerNameField.control.resetCommitted(ownerName(it.owner));
233
+ if (!focused(stage.control)) stage.control.value = it.stage;
234
+ if (!afterSlot.contains(document.activeElement)) drawAfter(it);
235
+ if (!focused(start.control)) start.control.value = it.start || '';
236
+ if (!focused(due.control)) due.control.value = it.due || '';
237
+ milestone.control.checked = !!it.milestone;
238
+ if (!focused(reason.control) && !focused(blocked.control)) {
239
+ blocked.control.checked = !!it.blocked;
240
+ reason.control.resetCommitted((it.blocked && it.blocked.reason) || '');
241
+ reason.hidden = !it.blocked;
242
+ }
243
+ if (!focused(note.control)) note.control.resetCommitted(it.note || '');
244
+ drawHistory(it);
245
+ };
246
+ drawState(it0); drawNote(it0); drawAfter(it0); drawHistory(it0);
247
+ if (handoff) showHandoff();
248
+ else { const b = stateSlot.querySelector('[aria-checked="true"]'); if (b) b.focus(); }
249
+ return { id, refresh, close: () => sh.close(), get open() { return sh.open; } };
250
+ }
251
+
@@ -0,0 +1,204 @@
1
+ /* pm/keep-card.js — the Keep card, and the breakdown rows of Today's needs-you list (pm-os-absorb Run 1, PRD R21/R23).
2
+ *
3
+ * An agent's items arrive PROPOSED: they show in the Tree and on the Board but count in no progress until the PM keeps
4
+ * them. One card per batch (everything one agent session proposed on one initiative) offers Keep or Withdraw all.
5
+ * Tree "Agent · sess-3 proposed 9 items on 5 Oct." [Keep] [Withdraw all]
6
+ * Today • <initiative> · 9 items · agent · sess-3 [Keep] [Withdraw all]
7
+ * The batch is named by the session label the rows already show, so two batches on one initiative read apart.
8
+ * Keep writes one `kept` line, and only from this page (the server refuses `kept` from anywhere else). Withdraw all goes
9
+ * through the Undo hold and then writes one `withdrawn` per item in one request. The card reads the batch as the server
10
+ * last sent it, so an item proposed after the read stays proposed and appears on the next refresh as a new count.
11
+ *
12
+ * Today's other two breakdown rows — an agent Done with no evidence, and a ledger line that has no effect (a forged Keep) —
13
+ * render as plain cards that open the initiative. All three come from the existing GET /api/needs.
14
+ */
15
+
16
+ import { el } from './dom.js';
17
+ import { button, toast, ensureComponentsCss } from './primitives.js';
18
+ import { COPY, dayOf } from './breakdown-copy.js';
19
+ import { postEvents, getNeeds } from './breakdown-api.js';
20
+ import { hold, isHeld } from './undo-hold.js';
21
+ import { wsOf } from './data.js';
22
+ import { state, setState, FIXTURE } from './state.js';
23
+ import { registerTodaySource } from './registry.js';
24
+ import { records } from './board-read.js';
25
+ import { COPY as TODAY } from './today-copy.js';
26
+ import { todayCard, ensureFeed, onFeed } from './today-cards.js';
27
+
28
+ export const keepKey = (ws, initiative, by) => 'batch:' + ws + ':' + initiative + ':' + by;
29
+
30
+ /**
31
+ * keepCard({ ws, initiative, initiativeTitle, batch, place: 'tree'|'today', onChanged(what), onHoldApply(layout) })
32
+ * `onChanged` runs after Keep is written, when Withdraw all is held, undone or written. `onHoldApply` is the overlay the
33
+ * hold applies to a fetched layout (the Tree passes one; Today hides the card while the hold runs).
34
+ */
35
+ export function keepCard({ ws, initiative, initiativeTitle, batch, place = 'tree', repo, cardId, onChanged, onHoldApply, onKeepApply }) {
36
+ const n = batch.count || (batch.items || []).length;
37
+ const key = keepKey(ws, initiative, batch.by);
38
+ const doKeep = async (btn) => {
39
+ btn.disabled = true;
40
+ if (onKeepApply) onKeepApply();
41
+ const out = await postEvents(ws, [{ ev: 'kept', initiative, batch: batch.by, items: batch.items }]);
42
+ if (!out || out.ok === false) { toast({ text: (out && out.error) || 'nothing was saved', tone: 'error' }); btn.disabled = false; }
43
+ else toast({ text: COPY.kept(n) });
44
+ if (onChanged) onChanged({ kept: true, ok: !!(out && out.ok !== false) });
45
+ };
46
+ const doWithdrawAll = () => {
47
+ hold({
48
+ key, ws, text: COPY.withdrewN(n),
49
+ events: (batch.items || []).map((id) => ({ ev: 'withdrawn', item: id })),
50
+ apply: onHoldApply || null,
51
+ onDone: () => { if (onChanged) onChanged({ withdrawn: true }); },
52
+ onUndo: () => { if (onChanged) onChanged({ undone: true }); },
53
+ });
54
+ if (onChanged) onChanged({ held: true });
55
+ };
56
+ const attrs = { 'data-bd-keep-card': place, 'data-batch': batch.by, 'data-initiative': initiative, 'data-count': String(n) };
57
+ if (place === 'today') {
58
+ // R12: a Today card has ONE button. Keep is it; Withdraw all is in the ⋯ menu and is said on the card.
59
+ return todayCard({
60
+ kind: 5, id: cardId || key, repo, ws, initiative, cause: (initiativeTitle || initiative) + ' · ' + COPY.keepToday(n, batch.byLabel),
61
+ evidence: TODAY.keepHint, act: { label: COPY.keep }, actAttrs: { 'data-bd-keep': '' },
62
+ menuExtra: [{ label: TODAY.withdrawAll, onSelect: doWithdrawAll }], attrs: { ...attrs, 'data-today-row': 'keep' },
63
+ onAct: (e) => doKeep(e.currentTarget),
64
+ });
65
+ }
66
+ const keep = button({ label: COPY.keep, variant: 'outline', size: 'sm', attrs: { 'data-bd-keep': '' }, onClick: () => doKeep(keep) });
67
+ const withdrawAll = button({ label: COPY.withdrawAll, variant: 'ghost', size: 'sm', attrs: { 'data-bd-withdraw-all': '' }, onClick: doWithdrawAll });
68
+ const text = [el('span', null, COPY.keepTree(n, dayOf(batch.firstAt || batch.lastAt), batch.byLabel))];
69
+ return el('div', { class: 'bd-keep', ...attrs }, el('div', { class: 'bd-keep__text' }, text), el('div', { class: 'bd-keep__actions' }, keep, withdrawAll));
70
+ }
71
+
72
+ /* ── Today ─────────────────────────────────────────────────────────────────────────────────────────────────────────── */
73
+
74
+ const KINDS = ['keep', 'evidence', 'plan-flag'];
75
+ const cache = new Map(); // ws -> { rows, loading, stale }
76
+ let rerender = null;
77
+ let hooked = false;
78
+
79
+ function load(ws) {
80
+ const c = cache.get(ws) || { rows: [], loading: false, stale: true };
81
+ cache.set(ws, c);
82
+ // A change that arrives while a read is in flight is not dropped: that read may have started before the change, so
83
+ // it is read once more when the read ends.
84
+ if (c.loading) { c.again = true; return; }
85
+ c.loading = true; c.again = false;
86
+ getNeeds(ws).then((out) => {
87
+ c.loading = false; c.stale = false;
88
+ c.rows = out && Array.isArray(out.needs) ? out.needs.filter((r) => r && KINDS.includes(r.kind) && r.run_id !== 'plan-items:unreadable') : [];
89
+ if (c.again) load(ws);
90
+ if (rerender) rerender(); else setState({});
91
+ });
92
+ }
93
+
94
+ function hook() {
95
+ if (hooked || typeof window === 'undefined') return;
96
+ hooked = true;
97
+ window.addEventListener('pmos:ledgers', (e) => {
98
+ const changed = (e && e.detail && e.detail.changed) || [];
99
+ let any = false;
100
+ for (const ws of changed) { const c = cache.get(ws); if (c) { c.stale = true; any = true; load(ws); } }
101
+ if (any && rerender) rerender();
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Read the breakdown rows of every repository once, so the nav's Today count includes them before Today has rendered.
107
+ * The nav calls it on each render. It starts a request only for a workspace that is not in the cache yet, and only after
108
+ * the first sessions poll has answered (`state.sessions` is set), so the page's startup makes no extra request. After
109
+ * that, a workspace is read again only on a `pmos:ledgers` change, as for Today. Fixture mode reads nothing.
110
+ */
111
+ export function primePlanNeeds(repos) {
112
+ if (FIXTURE || !Array.isArray(state.sessions)) return;
113
+ hook();
114
+ for (const repo of repos || []) {
115
+ const ws = wsOf(repo);
116
+ if (ws && !cache.has(ws)) load(ws);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * How many breakdown rows need the PM, over every workspace read so far (Keep batches not held for Undo, evidence and
122
+ * check rows). The nav's Today count adds it, so the badge and Today's header count the same rows. Reads the cache that
123
+ * primePlanNeeds() and Today fill; it fetches nothing itself. 0 in fixture mode.
124
+ */
125
+ function planCount(repos) {
126
+ let n = 0;
127
+ for (const repo of repos || []) {
128
+ const ws = wsOf(repo);
129
+ if (!ws) continue;
130
+ primePlanNeeds([repo]);
131
+ ensureFeed(ws);
132
+ for (const row of (cache.get(ws) || { rows: [] }).rows) {
133
+ if (row.kind === 'keep' && row.batch && isHeld(keepKey(ws, row.initiative_id, row.batch.by))) continue;
134
+ n++;
135
+ }
136
+ }
137
+ return n;
138
+ }
139
+
140
+ export function planNeedsCount() {
141
+ let n = 0;
142
+ for (const [ws, c] of cache) {
143
+ for (const row of c.rows || []) {
144
+ if (row.kind === 'keep' && row.batch && isHeld(keepKey(ws, row.initiative_id, row.batch.by))) continue;
145
+ n++;
146
+ }
147
+ }
148
+ return n;
149
+ }
150
+
151
+ /**
152
+ * The breakdown cards for Today's needs-you column: Keep cards, then evidence and check cards, for the given
153
+ * repositories. `open(repo, slug)` opens the initiative; `titleOf(slug)` names it; `onRender` is called when new rows
154
+ * arrive. Reads /api/needs once per workspace and again when that workspace's ledger changes. Fixture mode reads nothing.
155
+ * Every card is a Today card (today-cards.js): a cause, its evidence, one button and the ⋯ menu, with the id the server
156
+ * ranks by (`<ws>|<run id>`).
157
+ */
158
+ export function planNeedsCards({ repos, fixture, onRender, open, titleOf }) {
159
+ if (fixture) return [];
160
+ rerender = onRender || rerender;
161
+ hook();
162
+ const out = [];
163
+ for (const repo of repos || []) {
164
+ const ws = wsOf(repo);
165
+ if (!ws) continue;
166
+ ensureFeed(ws);
167
+ const c = cache.get(ws);
168
+ if (!c || (c.stale && !c.loading)) load(ws);
169
+ for (const row of (c && c.rows) || []) {
170
+ const slug = row.initiative_id;
171
+ const cardId = ws + '|' + row.run_id;
172
+ if (row.kind === 'keep' && row.batch) {
173
+ if (isHeld(keepKey(ws, slug, row.batch.by))) continue;
174
+ out.push(keepCard({
175
+ ws, repo, cardId, initiative: slug, initiativeTitle: titleOf ? titleOf(slug) : slug, batch: row.batch, place: 'today',
176
+ onChanged: () => { const cc = cache.get(ws); if (cc) { cc.stale = true; load(ws); } if (rerender) rerender(); },
177
+ }));
178
+ } else {
179
+ const evidence = row.kind === 'evidence';
180
+ out.push(todayCard({
181
+ kind: evidence ? 4 : 8, id: cardId, repo, ws, initiative: slug, cause: row.question,
182
+ evidence: [titleOf ? titleOf(slug) : slug, evidence ? COPY.needs.evidence : COPY.needs.flag, COPY.derived].filter(Boolean).join(' · '),
183
+ act: { label: COPY.needs.open }, onAct: () => { if (open) open(repo, slug); },
184
+ attrs: { 'data-bd-need': row.kind, 'data-item': row.item || null, 'data-today-row': row.kind },
185
+ }));
186
+ }
187
+ }
188
+ }
189
+ if (out.length) ensureComponentsCss();
190
+ return out;
191
+ }
192
+
193
+ /* The Today source: the breakdown's rows as cards of kinds 4, 5 and 8, counted once with every other source. */
194
+ if (!FIXTURE) {
195
+ onFeed((ws) => { const c = cache.get(ws); if (c) { c.stale = true; load(ws); } });
196
+ registerTodaySource({
197
+ id: 'plan-rows', kind: null, rank: 30,
198
+ count: (ctx) => planCount(ctx.repos),
199
+ cards: (ctx) => planNeedsCards({
200
+ repos: ctx.repos, fixture: false, onRender: () => (ctx.setState ? ctx.setState({}) : setState({})),
201
+ open: (r, slug) => (ctx.open ? ctx.open(r, slug) : null), titleOf: (slug) => (records()[slug] && records()[slug].tag) || slug,
202
+ }),
203
+ });
204
+ }
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { el } from './dom.js';
10
10
  import { state, setState, FIXTURE } from './state.js';
11
+ import { askText } from './dialogs.js';
11
12
 
12
13
  let dataMod = null;
13
14
  const dataReady = import('./data.js').then((m) => { dataMod = m; }).catch(() => { dataMod = null; });
@@ -29,7 +30,12 @@ function refresh(product) {
29
30
 
30
31
  async function apply(product, slug, to) {
31
32
  const word = to === 'active' ? 'Reopen' : to === 'closed' ? 'Close' : to === 'on-hold' ? 'Put on hold' : 'Cancel';
32
- const reason = to === 'active' ? '' : (typeof window !== 'undefined' && window.prompt ? window.prompt(word + ' “' + slug + '” — why? (one line, kept in the record)', '') : '');
33
+ // The two buttons never share a word: the back-out says Back and the act says what it does (Cancel initiative).
34
+ const act = to === 'cancelled' ? 'Cancel initiative' : word;
35
+ const reason = to === 'active' ? '' : await askText({
36
+ title: word + ' “' + slug + '”', label: 'Why? One line, kept in the record.',
37
+ confirmLabel: act, cancelLabel: 'Back', danger: to === 'closed' || to === 'cancelled',
38
+ });
33
39
  if (reason === null) return; // the PM backed out
34
40
  const out = await dataMod.setLifecycle(product, slug, to, String(reason).trim());
35
41
  if (!out || out.ok === false) { setState({ lcNotice: { slug, text: (out && out.error) || 'not recorded' } }); return; }
@@ -85,9 +85,9 @@
85
85
  gap: 10px;
86
86
  height: 52px;
87
87
  padding: 0 14px;
88
- background: oklch(0.1 0.004 228.8);
89
- color: oklch(0.987 0.002 197.1);
90
- border-bottom: 1px solid oklch(1 0 0 / 10%);
88
+ background: var(--sidebar);
89
+ color: var(--foreground);
90
+ border-bottom: 1px solid var(--border);
91
91
  }
92
92
  /* 44px minimums on every control: the phone's targets are thumbs, not a pointer. */
93
93
  .mob-back {
@@ -98,17 +98,17 @@
98
98
  min-width: 44px;
99
99
  min-height: 44px;
100
100
  margin-left: -6px;
101
- color: oklch(0.85 0.01 214);
101
+ color: var(--muted-foreground);
102
102
  font-size: 14px;
103
103
  }
104
104
  .mob-brand {
105
105
  flex: none;
106
106
  display: grid;
107
107
  place-items: center;
108
- width: 26px;
109
- height: 26px;
110
- background: oklch(0.488 0.243 264.376);
111
- font-size: 14px;
108
+ width: auto;
109
+ font-family: var(--mono);
110
+ font-size: 12px;
111
+ font-weight: 600;
112
112
  line-height: 1;
113
113
  }
114
114
  .mob-title {
@@ -117,7 +117,7 @@
117
117
  text-align: center;
118
118
  font-family: var(--mono);
119
119
  font-size: 12px;
120
- color: oklch(0.85 0.01 214);
120
+ color: var(--muted-foreground);
121
121
  overflow: hidden;
122
122
  text-overflow: ellipsis;
123
123
  white-space: nowrap;
@@ -128,17 +128,40 @@
128
128
  place-items: center;
129
129
  min-width: 44px;
130
130
  min-height: 44px;
131
- color: oklch(0.85 0.01 214);
131
+ color: var(--muted-foreground);
132
132
  }
133
133
  .mob-pal { font-family: var(--mono); font-size: 12px; }
134
- .mob-theme { font-size: 15px; }
135
- .mob-back:active, .mob-pal:active, .mob-theme:active { color: oklch(0.987 0.002 197.1); }
134
+ .mob-back:active, .mob-pal:active, .mob-theme:active { color: var(--foreground); }
135
+
136
+ /* The frame's bar controls (pm-os-absorb Run 3, slice 3A): Repository, which opens the repository menu, and
137
+ * Sync (only while the current repository is not in sync), and Settings. Words, at the bar's own ink; 44px targets like
138
+ * every control here. A button does not inherit the font, so Back says it too. */
139
+ .mob-back { font-family: var(--sans); }
140
+ .mob-repo, .mob-set, .mob-sync {
141
+ flex: none;
142
+ display: flex;
143
+ align-items: center;
144
+ min-width: 44px;
145
+ min-height: 44px;
146
+ padding: 0 6px;
147
+ color: inherit;
148
+ font-family: var(--sans);
149
+ font-size: 12px;
150
+ }
151
+ .mob-repo {
152
+ max-width: 128px;
153
+ overflow: hidden;
154
+ text-overflow: ellipsis;
155
+ white-space: nowrap;
156
+ font-weight: 600;
157
+ }
158
+ .mob-set[aria-current="page"] { text-decoration: underline; }
136
159
 
137
160
  /* ── the bottom block ────────────────────────────────────────────────────────────────────── */
138
161
 
139
162
  .mobmenu {
140
- background: oklch(0.1 0.004 228.8);
141
- border-top: 1px solid oklch(1 0 0 / 10%);
163
+ background: var(--sidebar);
164
+ border-top: 1px solid var(--border);
142
165
  /* viewport-fit=cover is on, so the home indicator's strip is ours to pad past. */
143
166
  padding-bottom: env(safe-area-inset-bottom, 0px);
144
167
  }
@@ -153,7 +176,7 @@
153
176
  overflow-x: auto;
154
177
  -webkit-overflow-scrolling: touch;
155
178
  background: var(--muted);
156
- border-bottom: 1px solid oklch(1 0 0 / 10%);
179
+ border-bottom: 1px solid var(--border);
157
180
  }
158
181
  .mobmenu[data-view="terminal"] .ctrlbar { display: flex; }
159
182
  .ctrlbar button {
@@ -170,9 +193,9 @@
170
193
  }
171
194
  .ctrlbar button:active { background: var(--accent); }
172
195
  .ctrlbar button[aria-pressed="true"] {
173
- border-color: var(--brand);
174
- color: var(--brand);
175
- background: var(--brand-wash);
196
+ border-color: var(--foreground);
197
+ color: var(--foreground);
198
+ background: var(--accent);
176
199
  }
177
200
  .ctrlbar .lbl { font-size: 11px; letter-spacing: .04em; }
178
201
 
@@ -187,14 +210,20 @@
187
210
  min-width: 0;
188
211
  min-height: 64px;
189
212
  padding: 0 2px;
190
- color: oklch(0.6 0.02 213);
213
+ color: var(--muted-foreground);
191
214
  }
192
215
  .mobnav-b .g { font-size: 17px; line-height: 1; }
193
216
  .mobnav-b .l { font-size: 10px; line-height: 1; }
194
- .mobnav-b[aria-current="page"] { color: oklch(0.987 0.002 197.1); }
217
+ .mobnav-b[aria-current="page"] { color: var(--foreground); }
218
+ /* The tabs are words (the frame's five tabs); the Today tab carries the count of what needs the PM. */
219
+ .mobnav-b { font-family: var(--sans); }
220
+ .mobnav-b .l { font-size: 12px; }
221
+ .mobnav-b[aria-current="page"] .l { text-decoration: underline; text-underline-offset: 4px; }
222
+ .mobnav-n { font-family: var(--mono); font-size: 11px; line-height: 1; }
195
223
  }
196
224
 
197
225
  /* `hidden` wins over every display above, in both directions of the breakpoint. Back is ABSENT at a
198
226
  * destination root rather than present-and-inert: a control that looks available and does nothing is
199
227
  * the thing this workspace refuses to ship. */
200
228
  .mob[hidden], .mobmenu[hidden], .mob-back[hidden], .mob-brand[hidden] { display: none !important; }
229
+ .mob-repo[hidden], .mob-set[hidden], .mob-sync[hidden], .mob-theme[hidden], .mobnav-n[hidden] { display: none !important; }