crewforth 3.0.3 → 3.1.0-rc.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 (67) hide show
  1. package/CHANGELOG.md +211 -0
  2. package/README.md +3 -3
  3. package/VERSION +1 -1
  4. package/adopt.sh +86 -34
  5. package/kit/CLAUDE.md +1 -1
  6. package/kit/README.md +1 -1
  7. package/kit/agents/crew-commit-agent.md +7 -3
  8. package/kit/agents/crew-frontend-expert.md +4 -4
  9. package/kit/agents/crew-performance-expert.md +1 -1
  10. package/kit/agents/crew-review-agent.md +1 -1
  11. package/kit/eval/doctor.sh +57 -3
  12. package/kit/eval/golden-routing.txt +13 -0
  13. package/kit/eval/lib/git-bash.sh +11 -2
  14. package/kit/eval/parser-conformance.sh +1 -0
  15. package/kit/eval/routing-eval.sh +7 -3
  16. package/kit/eval/smoke-test.sh +2545 -214
  17. package/kit/hooks/commit-msg +1 -1
  18. package/kit/hooks/context-usage.sh +7 -1
  19. package/kit/hooks/guard-bash.sh +1608 -103
  20. package/kit/hooks/guard-commit-scan.sh +138 -19
  21. package/kit/hooks/guard-powershell.sh +89 -15
  22. package/kit/hooks/guard-write.sh +98 -22
  23. package/kit/hooks/no-bash-guard.ps1 +71 -0
  24. package/kit/hooks/pre-commit +5 -2
  25. package/kit/hooks/prompt-approval.sh +484 -0
  26. package/kit/hooks/route-hint.sh +13 -5
  27. package/kit/hooks/session-rehydrate.sh +18 -0
  28. package/kit/hooks/skill-trust.sh +17 -2
  29. package/kit/owned-blobs.tsv +43 -0
  30. package/kit/settings.json +20 -4
  31. package/kit/skills/a11y/SKILL.md +1 -0
  32. package/kit/skills/crew-doctor/SKILL.md +2 -2
  33. package/kit/skills/frontend/SKILL.md +3 -3
  34. package/kit/skills/frontend-flutter/SKILL.md +94 -0
  35. package/kit/skills/frontend-flutter/references/adaptive-and-accessible.md +55 -0
  36. package/kit/skills/frontend-flutter/references/platform-async-release.md +59 -0
  37. package/kit/skills/frontend-flutter/references/rebuilds-and-performance.md +36 -0
  38. package/kit/studio/server/hooks/studio-gate.sh +35 -1
  39. package/kit/studio/server/index.js +14 -4
  40. package/kit/studio/server/lib/graph.js +64 -3
  41. package/kit/studio/server/lib/kit.js +16 -2
  42. package/kit/studio/server/lib/permissions.js +77 -2
  43. package/kit/studio/server/lib/projects.js +60 -7
  44. package/kit/studio/server/lib/session.js +16 -2
  45. package/kit/studio/web/app.js +1765 -573
  46. package/kit/studio/web/approvals.js +124 -0
  47. package/kit/studio/web/canvas.js +932 -885
  48. package/kit/studio/web/chat.js +322 -145
  49. package/kit/studio/web/convo.js +174 -0
  50. package/kit/studio/web/dock.js +221 -0
  51. package/kit/studio/web/graph-plan.js +457 -0
  52. package/kit/studio/web/icons/builtin.svg +1 -0
  53. package/kit/studio/web/index.html +116 -50
  54. package/kit/studio/web/inspect.js +110 -0
  55. package/kit/studio/web/list-plan.js +112 -0
  56. package/kit/studio/web/list.js +157 -0
  57. package/kit/studio/web/liveness.js +66 -0
  58. package/kit/studio/web/nav.js +237 -0
  59. package/kit/studio/web/newsession.js +235 -0
  60. package/kit/studio/web/press.js +65 -0
  61. package/kit/studio/web/style.css +1281 -1014
  62. package/kit/studio/web/theme.js +43 -0
  63. package/kit/studio/web/timeline-plan.js +291 -0
  64. package/kit/studio/web/timeline.js +400 -0
  65. package/kit/studio/web/tokens.css +131 -0
  66. package/kit/studio/web/toolbar-fit.js +35 -0
  67. package/package.json +5 -4
@@ -1,81 +1,56 @@
1
- /* The panel's palette, light and dark. */
1
+ /* Colour, type, space and radius come from tokens.css, which is generated from
2
+ the design system; this file holds no colour literal of its own. What the
3
+ design draws between two tokens is mixed from them here, by name, so a tone
4
+ exists in one place and follows both themes. */
2
5
  :root {
3
- --bg: #f7f8fa;
4
- --surface: #ffffff;
5
- --surface-2: #eef0f4;
6
- --line: #d9dde5;
7
- --ink: #1a1d24;
8
- --ink-dim: #5c6472;
9
- --ink-faint: #8d95a3;
10
-
11
- --busy: #26c6e6;
12
- --waiting: #f2a65a;
13
- --idle: #9aa7cc;
14
- --good: #35c874;
15
- --bad: #ff8a4d;
16
- --accent: #5b8cff;
17
-
18
- /* Edge tones. A branch that did not finish on its own terms is the one
19
- thing on this canvas that must not be read as "some other agent colour",
20
- so it gets a hue of its own rather than the node's.
21
-
22
- The four weights are tokens and not literals because the same stroke sits
23
- on white here and on near-black in the dark theme: 0.3 that reads as
24
- "finished, out of the way" on #0d1017 reads as "barely drawn" on #f7f8fa. */
25
- --cv-fail: #d92d3f;
26
- --cv-edge-live: 1;
27
- --cv-edge-wake: 0.52;
28
- --cv-edge-done: 0.42;
29
- --cv-edge-quiet: 0.24;
30
-
31
- --radius: 10px;
32
- --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
33
- --sans: ui-sans-serif, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
34
- }
35
-
36
- :root:not([data-theme="light"]) {
37
- --bg: #0d1017;
38
- --surface: #141821;
39
- --surface-2: #1b202b;
40
- --line: #262c39;
41
- --ink: #e6e9f0;
42
- --ink-dim: #9aa7cc;
43
- --ink-faint: #5c6472;
44
-
45
- --cv-fail: #ff6b7a;
6
+ /* The dot grid behind the graph: quieter than a hairline, still on the page. */
7
+ --grid-dot: color-mix(in srgb, var(--line) 65%, var(--ground));
8
+ /* Depth under a lifted card. Navy rather than black, so it belongs to the
9
+ ground in both themes. */
10
+ --shade: color-mix(in srgb, var(--brand-navy) 45%, transparent);
11
+ --shade-soft: color-mix(in srgb, var(--brand-navy) 20%, transparent);
12
+
13
+ /* A branch that did not finish on its own terms is the one thing on this
14
+ canvas that must not be read as "some other agent colour". */
15
+ --cv-fail: var(--status-fail);
16
+
17
+ /* Edge weights. Tokens and not literals because the same stroke sits on
18
+ navy here and on near-white in the light theme: 0.3 that reads as
19
+ "finished, out of the way" on the dark ground reads as "barely drawn" on
20
+ the light one. The three blocks mirror the three in tokens.css. */
46
21
  --cv-edge-live: 0.95;
47
22
  --cv-edge-wake: 0.44;
48
- --cv-edge-done: 0.3;
49
- --cv-edge-quiet: 0.16;
23
+
24
+ /* The frame, in the spec's measures. */
25
+ --bar-h: 56px;
26
+ --nav-w: 272px;
27
+ --rail-w: 56px;
28
+ --toolbar-h: 48px;
29
+ --inspector-w: 344px;
50
30
  }
51
31
 
52
32
  @media (prefers-color-scheme: light) {
53
33
  :root:not([data-theme="dark"]) {
54
- --bg: #f7f8fa;
55
- --surface: #ffffff;
56
- --surface-2: #eef0f4;
57
- --line: #d9dde5;
58
- --ink: #1a1d24;
59
- --ink-dim: #5c6472;
60
- --ink-faint: #8d95a3;
61
-
62
- --cv-fail: #d92d3f;
63
34
  --cv-edge-live: 1;
64
35
  --cv-edge-wake: 0.52;
65
- --cv-edge-done: 0.42;
66
- --cv-edge-quiet: 0.24;
67
36
  }
68
37
  }
69
38
 
39
+ :root[data-theme="light"] {
40
+ --cv-edge-live: 1;
41
+ --cv-edge-wake: 0.52;
42
+ }
43
+
70
44
  * { box-sizing: border-box; }
71
45
 
72
46
  body {
73
47
  margin: 0;
74
- background: var(--bg);
48
+ background: var(--ground);
75
49
  color: var(--ink);
76
- font-family: var(--sans);
77
- font-size: 14px;
50
+ font-family: var(--font-sans);
51
+ font-size: var(--type-body-sm-size);
78
52
  line-height: 1.5;
53
+ -webkit-font-smoothing: antialiased;
79
54
  /* Fixed height, not min-height: the canvas sizes itself to its box, so a
80
55
  body that can grow past the window puts the bottom of the graph below the
81
56
  fold while the fit maths still believes everything is visible. */
@@ -85,64 +60,143 @@ body {
85
60
  flex-direction: column;
86
61
  }
87
62
 
88
- /* ---- top bar ---- */
63
+ /* One focus ring everywhere: 2px of the accent, 2px off the control. A control
64
+ that draws its own focus state says so next to its rule. */
65
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
66
+
67
+ /* ---- top bar ----
68
+ Left to right: the mark, where you are, what the session's agents are doing,
69
+ whether the panel is being fed, and the controls. */
89
70
  .bar {
71
+ flex: none;
72
+ height: var(--bar-h);
90
73
  display: flex;
91
74
  align-items: center;
92
- justify-content: space-between;
93
- gap: 16px;
94
- padding: 12px 20px;
75
+ gap: var(--space-4);
76
+ padding: 0 var(--space-4);
95
77
  border-bottom: 1px solid var(--line);
96
- background: var(--surface);
78
+ background: var(--ground);
79
+ min-width: 0;
97
80
  }
81
+ .bar-sep { flex: none; width: 1px; height: 24px; background: var(--line); }
82
+ .bar-fill { flex: 1; min-width: 0; }
98
83
 
99
- .brand { display: flex; align-items: center; gap: 10px; }
100
-
84
+ .brand { display: flex; align-items: center; gap: 10px; flex: none; color: inherit; text-decoration: none; border-radius: var(--radius-sm); }
101
85
  .brand h1 {
102
86
  margin: 0;
103
87
  font-size: 15px;
104
- font-weight: 600;
105
- letter-spacing: 0.01em;
88
+ font-weight: 700;
89
+ letter-spacing: -0.2px;
90
+ white-space: nowrap;
106
91
  }
107
92
 
108
- /* Crewforth's own mark, from assets/mark.svg: three chevrons (›››), drawn as
109
- strokes. Two palettes: the light ones from the logo read on the dark panel,
110
- the darker ones from mark.svg read on the light. Same geometry either way. */
111
- .brand-mark { width: 19px; height: 19px; flex: none; }
93
+ /* Crewforth's own mark: three chevrons, rear to lead, each in its own token.
94
+ The lead one is the accent in both themes. Never recoloured, never reordered. */
95
+ .brand-mark { width: 24px; height: 24px; flex: none; }
112
96
  .brand-mark polyline { fill: none; stroke-linecap: round; stroke-linejoin: round; }
113
- .brand-mark .bar-1 { stroke: #C7CBFB; }
114
- .brand-mark .bar-2 { stroke: #9AA3F7; }
115
- .brand-mark .bar-3 { stroke: #7C3AED; }
116
-
117
- :root:not([data-theme="light"]) .brand-mark .bar-1 { stroke: #E5E7FB; }
118
- :root:not([data-theme="light"]) .brand-mark .bar-2 { stroke: #B9BEF9; }
119
- :root:not([data-theme="light"]) .brand-mark .bar-3 { stroke: #A78BFA; }
120
-
121
- @media (prefers-color-scheme: dark) {
122
- :root:not([data-theme="light"]) .brand-mark .bar-1 { stroke: #E5E7FB; }
123
- :root:not([data-theme="light"]) .brand-mark .bar-2 { stroke: #B9BEF9; }
124
- :root:not([data-theme="light"]) .brand-mark .bar-3 { stroke: #A78BFA; }
97
+ .brand-mark .bar-1 { stroke: var(--chevron-1); }
98
+ .brand-mark .bar-2 { stroke: var(--chevron-2); }
99
+ .brand-mark .bar-3 { stroke: var(--chevron-3); }
100
+
101
+ /* The product name carries the weight; "Studio" steps back beside it. */
102
+ .brand-accent { color: var(--ink-dim); font-weight: 600; }
103
+
104
+ /* Project, then session. The project is a control (it opens that project's
105
+ list); the session is where you already are, so it is only a name. */
106
+ /* It gives way first, down to a floor; past that the summary folds (app.js, fitBar). */
107
+ .crumb { display: flex; align-items: center; gap: 6px; flex: 0 1 auto; min-width: 120px; overflow: hidden; font-size: 13px; }
108
+ .crumb-project {
109
+ flex: 0 1 auto; min-width: 0; max-width: 220px;
110
+ background: none; border: 0; padding: 2px 4px; margin: 0 -4px; border-radius: var(--radius-sm);
111
+ font: inherit; color: var(--ink-dim); cursor: pointer;
112
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
125
113
  }
126
- :root[data-theme="dark"] .brand-mark .bar-1 { stroke: #E5E7FB; }
127
- :root[data-theme="dark"] .brand-mark .bar-2 { stroke: #B9BEF9; }
128
- :root[data-theme="dark"] .brand-mark .bar-3 { stroke: #A78BFA; }
129
-
130
- /* Two-tone, the way the wordmark does it. */
131
- .brand-accent { color: #A78BFA; }
132
- :root[data-theme="light"] .brand-accent { color: #7C3AED; }
133
-
134
- .bar-right { display: flex; align-items: center; gap: 12px; }
135
-
136
- .pulse {
137
- font-family: var(--mono);
138
- font-size: 11px;
139
- color: var(--ink-faint);
140
- letter-spacing: 0.06em;
141
- text-transform: uppercase;
114
+ .crumb-project:hover { color: var(--ink); }
115
+ .crumb-sep { width: 12px; height: 12px; flex: none; color: var(--ink-faint); }
116
+ .crumb-session { flex: 0 1 auto; min-width: 40px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
117
+
118
+ /* ---- shared small parts ---- */
119
+
120
+ /* One-colour stroke icons, in the colour of the text beside them. */
121
+ .ic {
122
+ width: 16px; height: 16px; flex: none;
123
+ fill: none; stroke: currentColor; stroke-width: 1.6;
124
+ stroke-linecap: round; stroke-linejoin: round;
125
+ }
126
+
127
+ /* A status dot. The tone is one of the status tokens; with none it is a hollow
128
+ ring — something that is over, or a state nobody gave a colour to. It never
129
+ stands alone: the word is always beside it. */
130
+ .dot { width: 8px; height: 8px; border-radius: var(--radius-pill); flex: none; display: inline-block; }
131
+ .dot[data-tone="busy"] { background: var(--status-busy); }
132
+ .dot[data-tone="waiting"] { background: var(--status-waiting); }
133
+ .dot[data-tone="idle"] { background: var(--status-idle); }
134
+ .dot[data-tone="good"] { background: var(--status-good); }
135
+ .dot[data-tone="bad"] { background: var(--status-bad); }
136
+ .dot[data-tone="fail"] { background: var(--status-fail); }
137
+ .dot[data-tone="none"] { background: transparent; box-shadow: inset 0 0 0 1.5px var(--ink-dim); }
138
+
139
+ .pill {
140
+ display: inline-flex; align-items: center; gap: 6px;
141
+ height: 24px; padding: 0 var(--space-2);
142
+ border-radius: var(--radius-sm);
143
+ background: var(--surface-raised);
144
+ border: 1px solid transparent;
145
+ font-family: inherit; font-size: 12px; font-weight: 600; color: var(--ink);
146
+ white-space: nowrap; flex: none;
147
+ }
148
+
149
+ .badge {
150
+ display: inline-flex; align-items: center; flex: none;
151
+ height: 20px; padding: 0 6px;
152
+ border-radius: var(--radius-pill);
153
+ border: 1px solid var(--line);
154
+ background: none;
155
+ font-family: var(--font-mono); font-size: 11px; color: var(--ink-dim);
156
+ white-space: nowrap;
142
157
  }
143
- .pulse.on { color: var(--good); }
144
- .pulse.off { color: var(--bad); }
158
+ .badge[hidden] { display: none; }
145
159
 
160
+ .btn {
161
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
162
+ height: 36px; padding: 0 14px; flex: none;
163
+ border-radius: var(--radius-md);
164
+ border: 1px solid var(--line);
165
+ background: var(--surface);
166
+ color: var(--ink);
167
+ font-family: inherit; font-size: 13px; font-weight: 600;
168
+ white-space: nowrap; cursor: pointer;
169
+ }
170
+ .btn:hover { background: var(--surface-raised); }
171
+ .btn:disabled { opacity: 0.6; cursor: default; }
172
+ .btn[hidden] { display: none; }
173
+ .btn.ghost { background: transparent; }
174
+ .btn.ghost:hover { background: var(--surface-raised); }
175
+ .btn.icon { width: 36px; padding: 0; color: var(--ink-dim); }
176
+ .btn.icon:hover { color: var(--ink); }
177
+ .btn.sm { height: 30px; padding: 0 10px; font-size: 12px; }
178
+ .btn.icon.sm { width: 30px; padding: 0; }
179
+ /* The one filled control on the page. */
180
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
181
+ .btn.primary:hover { background: color-mix(in srgb, var(--accent) 88%, var(--ink)); }
182
+ .btn.on { color: var(--studio-accent); border-color: color-mix(in srgb, var(--studio-accent) 45%, transparent); }
183
+
184
+ .seg {
185
+ display: flex; flex: none; padding: 3px; gap: 2px;
186
+ background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md);
187
+ }
188
+ .seg-btn {
189
+ flex: 1; height: 28px; padding: 0 var(--space-2);
190
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
191
+ border: 0; border-radius: 7px; background: none;
192
+ font-family: inherit; font-size: 13px; font-weight: 600; color: var(--ink-dim);
193
+ cursor: pointer; white-space: nowrap;
194
+ }
195
+ .seg-btn:hover { color: var(--ink); }
196
+ .seg-btn[aria-selected="true"] { background: var(--surface-raised); color: var(--ink); }
197
+ .seg-btn .badge { height: 18px; }
198
+
199
+ /* The small bordered button the inspector still uses for copy and widen. */
146
200
  .ghost {
147
201
  background: transparent;
148
202
  border: 1px solid var(--line);
@@ -150,88 +204,49 @@ body {
150
204
  border-radius: 8px;
151
205
  padding: 4px 10px;
152
206
  cursor: pointer;
207
+ font-family: inherit;
153
208
  font-size: 13px;
154
209
  }
155
210
  .ghost:hover { color: var(--ink); border-color: var(--ink-faint); }
156
211
 
157
- /* ---- layout ---- */
158
- .meta { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); }
159
-
160
- /* ---- fleet ---- */
161
- .fleet { display: grid; gap: 1px; background: var(--line); }
162
-
163
- .session {
164
- display: grid;
165
- grid-template-columns: auto 1fr auto;
166
- gap: 14px;
167
- align-items: center;
168
- padding: 14px 16px;
169
- background: var(--surface);
170
- }
212
+ /* ---- summary chips ----
213
+ Each chip is a count and a filter. Pressed, its frame is the selection
214
+ colour and the canvas steps every other agent back. */
215
+ .chips { display: flex; align-items: center; gap: 6px; flex: none; }
216
+ .chip { cursor: pointer; }
217
+ .chip:hover { border-color: var(--line); }
218
+ .chip[aria-pressed="true"] { border-color: var(--studio-accent); box-shadow: 0 0 0 1px var(--studio-accent); }
219
+ .chip-compact { display: none; gap: 5px; }
220
+ .chip-compact .ic { width: 12px; height: 12px; color: var(--ink-dim); }
221
+ .bar[data-compact] .chip, .bar[data-compact] .note-pill { display: none; }
222
+ .bar[data-compact] .chip-compact { display: inline-flex; }
223
+ /* Not a status and not a filter: a number that has to stay in view. */
224
+ .note-pill { color: var(--status-waiting); background: color-mix(in srgb, var(--status-waiting) 12%, transparent); }
225
+
226
+ /* ---- live indicator ---- */
227
+ .live { display: inline-flex; align-items: center; gap: 6px; flex: none; font-size: 12px; color: var(--ink-dim); white-space: nowrap; }
228
+ .live strong { color: var(--ink); font-weight: 600; }
229
+ .live[data-state="stale"] strong { color: var(--status-waiting); }
230
+ .live[data-state="offline"] strong { color: var(--status-fail); }
171
231
 
172
- .ring {
173
- width: 12px; height: 12px;
174
- border-radius: 50%;
175
- border: 2px solid var(--idle);
176
- flex: none;
177
- }
178
- .ring[data-status="busy"] {
179
- border-color: var(--busy);
180
- background: color-mix(in srgb, var(--busy) 30%, transparent);
181
- animation: breathe 1.6s ease-in-out infinite;
182
- }
183
- .ring[data-status="waiting"] {
184
- border-color: var(--waiting);
185
- border-style: dashed;
186
- }
187
- .ring[data-status="idle"] { border-color: var(--idle); }
232
+ /* ---- layout ---- */
233
+ .meta { font-family: var(--font-mono); font-size: 11px; color: var(--ink-faint); }
188
234
 
189
235
  @keyframes breathe {
190
- 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--busy) 45%, transparent); }
191
- 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--busy) 0%, transparent); }
192
- }
193
- @media (prefers-reduced-motion: reduce) {
194
- .ring[data-status="busy"] { animation: none; }
195
- }
196
-
197
- .who { min-width: 0; }
198
- .who .name { font-weight: 600; font-size: 14px; }
199
- .who .path {
200
- font-family: var(--mono);
201
- font-size: 12px;
202
- color: var(--ink-dim);
203
- white-space: nowrap;
204
- overflow: hidden;
205
- text-overflow: ellipsis;
206
- }
207
- .who .waiting-for {
208
- font-size: 12px;
209
- color: var(--waiting);
210
- margin-top: 3px;
236
+ 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-busy) 45%, transparent); }
237
+ 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--status-busy) 0%, transparent); }
211
238
  }
212
239
 
213
- .stat { text-align: right; font-family: var(--mono); font-size: 11px; color: var(--ink-faint); white-space: nowrap; }
214
- .stat .status { display: block; font-size: 12px; color: var(--ink-dim); text-transform: uppercase; letter-spacing: 0.05em; }
215
-
216
240
  /* ---- honest empty / unmeasured states ---- */
217
241
  .note {
218
- padding: 28px 20px;
242
+ padding: var(--space-6) var(--space-3);
219
243
  text-align: center;
244
+ font-size: 13px;
220
245
  color: var(--ink-dim);
221
- background: var(--surface);
222
246
  }
223
247
  .note strong { display: block; color: var(--ink); margin-bottom: 4px; font-weight: 600; }
224
- .note .why { font-family: var(--mono); font-size: 12px; color: var(--ink-faint); margin-top: 6px; }
225
- .note.unmeasured strong { color: var(--waiting); }
226
-
227
- .foot {
228
- padding: 10px 20px;
229
- border-top: 1px solid var(--line);
230
- font-family: var(--mono);
231
- font-size: 11px;
232
- color: var(--ink-faint);
233
- background: var(--surface);
234
- }
248
+ .note .why { font-family: var(--font-mono); font-size: 12px; color: var(--ink-faint); margin-top: 6px; }
249
+ .note.unmeasured strong { color: var(--status-waiting); }
235
250
 
236
251
  /* ============================================================== shell === */
237
252
 
@@ -239,24 +254,29 @@ body {
239
254
  position: relative;
240
255
  flex: 1; min-height: 0;
241
256
  display: grid;
242
- /* Five columns: the two dividers are real columns, so dragging one moves a
243
- boundary rather than overlaying it. The conversation sits on the right,
244
- beside the graph rather than under it — a reply is read next to the work
245
- it is about, not below the fold. */
246
- grid-template-columns: var(--side-w, 260px) 5px 1fr 5px var(--chat-w, 420px);
257
+ /* Six columns: navigator, divider, stage, inspector, divider, conversation.
258
+ The two dividers are real columns, so dragging one moves a boundary rather
259
+ than overlaying it. The inspector's column is `auto`: it is as wide as a
260
+ docked inspector and nothing at all when the inspector floats or is closed.
261
+ The conversation sits on the right, beside the graph rather than under it
262
+ — a reply is read next to the work it is about, not below the fold. */
263
+ grid-template-columns: var(--side-w, var(--nav-w)) 5px minmax(0, 1fr) auto 5px var(--chat-w, 480px);
247
264
  }
248
265
  /* Placed explicitly, never by auto-flow. A `display: none` child occupies no
249
- grid cell, so hiding the sidebar slid every remaining column one to the left:
250
- the graph was handed the 22px rail slot and the conversation took the rest. */
251
- .shell > .side { grid-column: 1; }
252
- .shell > #resizer { grid-column: 2; }
253
- .shell > .stage { grid-column: 3; }
254
- .shell > #chat-split { grid-column: 4; }
255
- .shell > .chat { grid-column: 5; }
256
-
257
- .shell.no-chat { grid-template-columns: var(--side-w, 260px) 5px 1fr 0 0; }
258
- .shell.no-side { grid-template-columns: 0 0 1fr 5px var(--chat-w, 420px); }
259
- .shell.no-side.no-chat { grid-template-columns: 0 0 1fr 0 0; }
266
+ grid cell, so hiding the navigator slid every remaining column one to the
267
+ left: the graph was handed the rail's slot and the conversation took the rest. */
268
+ .shell > .side { grid-column: 1; }
269
+ .shell > #resizer { grid-column: 2; }
270
+ .shell > .stage { grid-column: 3; }
271
+ .shell > #chat-split { grid-column: 5; }
272
+ .shell > .chat,
273
+ .shell > .newpanel { grid-column: 6; }
274
+
275
+ /* Collapsed, the navigator leaves its rail behind, so the graph does not sit
276
+ under the control that brings it back. */
277
+ .shell.no-chat { grid-template-columns: var(--side-w, var(--nav-w)) 5px minmax(0, 1fr) auto 0 0; }
278
+ .shell.no-side { grid-template-columns: var(--rail-w) 0 minmax(0, 1fr) auto 5px var(--chat-w, 480px); }
279
+ .shell.no-side.no-chat { grid-template-columns: var(--rail-w) 0 minmax(0, 1fr) auto 0 0; }
260
280
  .shell.no-side > .side, .shell.no-side > #resizer { display: none; }
261
281
 
262
282
  .resizer {
@@ -268,144 +288,592 @@ body {
268
288
  /* A 5px target is hard to hit; the hit area is wider than the line. */
269
289
  content: ''; position: absolute; inset: 0 -4px; z-index: 5;
270
290
  }
271
- .resizer:hover, .resizer:focus-visible, .resizer.dragging { background: var(--accent); outline: none; }
291
+ .resizer:hover, .resizer.dragging { background: var(--studio-accent); }
272
292
  body.resizing { cursor: col-resize; user-select: none; }
273
293
  body.resizing .cv-root { pointer-events: none; }
274
294
 
295
+ /* ---- navigator ---- */
275
296
  .side {
276
297
  border-right: 1px solid var(--line);
277
298
  background: var(--surface);
278
299
  display: flex;
279
300
  flex-direction: column;
301
+ gap: var(--space-3);
302
+ padding: var(--space-4) var(--space-3) var(--space-3);
280
303
  min-height: 0;
304
+ min-width: 0;
281
305
  overflow: hidden;
282
306
  }
283
- .side-block { border-bottom: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0; }
284
- .side-block.grow { flex: 1; border-bottom: none; }
285
- .side-head {
286
- display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
287
- padding: 10px 14px; flex: none;
288
- }
289
- .side-head h2 {
290
- margin: 0; font-size: 11px; font-weight: 600; letter-spacing: 0.08em;
291
- text-transform: uppercase; color: var(--ink-faint);
307
+ .nav-top { display: flex; align-items: center; gap: 6px; flex: none; }
308
+ /* The input is the field, so the focus ring it gets is the field's; the icon
309
+ sits inside its left padding. */
310
+ .nav-search { position: relative; flex: 1; min-width: 0; display: block; color: var(--ink-dim); }
311
+ .nav-search .ic { position: absolute; left: 10px; top: 10px; pointer-events: none; }
312
+ .nav-search input {
313
+ width: 100%; height: 36px; padding: 0 10px 0 34px;
314
+ border: 1px solid var(--line); border-radius: var(--radius-md);
315
+ background: var(--ground);
316
+ color: var(--ink); font-family: inherit; font-size: 13px;
317
+ }
318
+ .nav-search input::placeholder { color: var(--ink-faint); }
319
+
320
+ .nav-list { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; margin: 0 -4px; padding: 0 4px; }
321
+ .nav-list[hidden] { display: none; }
322
+
323
+ .proj { display: flex; flex-direction: column; gap: var(--space-1); flex: none; }
324
+ .proj-head {
325
+ display: flex; align-items: center; gap: 6px; min-width: 0;
326
+ padding: 2px var(--space-1); border-radius: var(--radius-sm); cursor: pointer;
327
+ }
328
+ .proj-head:hover .proj-name { color: var(--ink); }
329
+ .proj-caret { width: 12px; height: 12px; color: var(--ink-faint); transition: transform 0.12s; }
330
+ .proj-head[aria-expanded="true"] .proj-caret { transform: rotate(90deg); }
331
+ .proj-name { min-width: 0; font-size: 13px; font-weight: 600; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
332
+ .proj-head.gone .proj-name { color: var(--ink-faint); text-decoration: line-through; }
333
+ .row-fill { flex: 1; min-width: 4px; }
334
+
335
+ /* The version badge. Grey when there is nothing to do; orange, and a button,
336
+ when the project is behind and a click copies the way to fix that. */
337
+ .badge-old {
338
+ color: var(--status-waiting);
339
+ border-color: color-mix(in srgb, var(--status-waiting) 32%, var(--line));
340
+ cursor: pointer;
292
341
  }
293
- .fleet, .sessions { overflow-y: auto; min-height: 0; }
294
- .fleet { max-height: 30vh; }
342
+ .badge-old:hover { background: color-mix(in srgb, var(--status-waiting) 12%, transparent); }
343
+ .badge-ahead { color: var(--studio-accent); border-color: color-mix(in srgb, var(--studio-accent) 35%, transparent); }
344
+ .badge-unknown, .badge-none { color: var(--ink-faint); }
295
345
 
296
- .session, .srow {
297
- display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: center;
298
- padding: 8px 14px; border-top: 1px solid var(--line); background: var(--surface);
346
+ .srow {
347
+ display: flex; flex-direction: column; gap: 2px; flex: none; min-width: 0;
348
+ padding: var(--space-2) 6px var(--space-2) var(--space-3);
349
+ border-radius: var(--radius-md); border: 1px solid transparent;
350
+ cursor: pointer;
351
+ }
352
+ .srow:hover { background: var(--surface-raised); }
353
+ .srow[aria-current="true"] { background: var(--surface-raised); border-color: var(--studio-accent); }
354
+ .srow.remote { cursor: default; }
355
+ .srow-top { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
356
+ .nm {
357
+ min-width: 0; font-size: 13px; line-height: 18px; color: var(--ink);
358
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
299
359
  }
300
- .srow { cursor: pointer; }
301
- .srow:hover { background: var(--surface-2); }
302
- .srow[aria-current="true"] { background: var(--surface-2); box-shadow: inset 3px 0 0 var(--accent); }
303
- .srow .sid { font-family: var(--mono); font-size: 12px; }
304
- .srow .sagents { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); }
305
- .srow .sbadge {
306
- font-family: var(--mono); font-size: 10px; color: var(--accent);
307
- border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
308
- border-radius: 4px; padding: 0 5px;
360
+ .srow[aria-current="true"] .nm { font-weight: 600; }
361
+ .mname { min-width: 0; font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
362
+ .sub { font-size: 12px; line-height: 16px; color: var(--ink-dim); white-space: nowrap; flex: none; }
363
+ .srow-sub { padding-left: var(--space-4); flex: initial; overflow: hidden; text-overflow: ellipsis; }
364
+ .srow.remote .srow-sub { padding-left: var(--space-6); }
365
+ .sub.bad, .nav-line.bad { color: var(--status-bad); }
366
+ mark { background: color-mix(in srgb, var(--studio-accent) 30%, transparent); color: inherit; border-radius: 2px; }
367
+
368
+ .row-more {
369
+ flex: none; width: 22px; height: 22px; padding: 0;
370
+ display: inline-flex; align-items: center; justify-content: center;
371
+ border: 0; border-radius: var(--radius-sm); background: none; color: var(--ink-faint); cursor: pointer;
372
+ }
373
+ .row-more .ic { stroke-width: 2.4; }
374
+ .row-more:hover { background: var(--ground); color: var(--ink); }
375
+ .srow-rename {
376
+ flex: 1; min-width: 0; height: 22px; padding: 0 6px;
377
+ border: 1px solid var(--line); border-radius: var(--radius-sm);
378
+ background: var(--ground); color: var(--ink); font-family: inherit; font-size: 13px;
379
+ }
380
+
381
+ .nav-line {
382
+ display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); flex: none;
383
+ padding: 0 var(--space-1) 0 var(--space-3);
384
+ font-size: 12px; color: var(--ink-faint);
385
+ }
386
+ .link { background: none; border: 0; padding: 0; font: inherit; color: var(--accent); cursor: pointer; }
387
+ .link:hover { text-decoration: underline; }
388
+
389
+ .nav-foot {
390
+ flex: none; display: flex; flex-direction: column; gap: var(--space-1);
391
+ border-top: 1px solid var(--line); padding-top: var(--space-3);
392
+ }
393
+ .nav-foot[hidden] { display: none; }
394
+ .nav-foot-label {
395
+ align-self: flex-start; background: none; border: 0; padding: 0 var(--space-1); cursor: pointer;
396
+ font-family: inherit;
397
+ font-size: 11px; line-height: var(--type-label-line); font-weight: var(--type-label-weight);
398
+ letter-spacing: var(--type-label-tracking); text-transform: uppercase; color: var(--ink-dim);
399
+ }
400
+ .nav-foot-label:hover { color: var(--ink); }
401
+
402
+ /* ---- menu and toast ---- */
403
+ .menu {
404
+ position: fixed; z-index: 60; min-width: 190px; max-width: 272px;
405
+ display: flex; flex-direction: column; gap: 2px; padding: var(--space-1);
406
+ background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md);
407
+ box-shadow: 0 12px 30px -12px var(--shade);
408
+ }
409
+ .menu[hidden] { display: none; }
410
+ .menu-item {
411
+ display: flex; align-items: center; gap: var(--space-2);
412
+ height: 30px; padding: 0 10px; text-align: left;
413
+ border: 0; border-radius: 7px; background: none;
414
+ font-family: inherit; font-size: 13px; color: var(--ink); cursor: pointer;
415
+ }
416
+ .menu-item:hover, .menu-item:focus-visible { background: var(--surface-raised); }
417
+ .menu-item.primary { color: var(--accent); font-weight: 600; }
418
+ .menu-item[aria-checked="true"] { background: var(--surface-raised); box-shadow: inset 0 0 0 1px var(--studio-accent); }
419
+ .menu-note {
420
+ padding: var(--space-1) 10px; font-size: 12px; line-height: 18px; color: var(--ink-dim);
421
+ overflow-wrap: anywhere;
309
422
  }
310
- .who .name { font-size: 13px; }
311
- .who .path { font-size: 11px; }
312
- .stat { font-size: 10px; }
313
- .stat .status { font-size: 10px; }
423
+ .menu-note + .menu-note { font-family: var(--font-mono); color: var(--ink); }
314
424
 
315
- .bar-mid { flex: 1; text-align: center; }
425
+ .toast {
426
+ position: fixed; z-index: 70; left: 50%; bottom: var(--space-6); transform: translateX(-50%);
427
+ max-width: min(560px, calc(100vw - 2 * var(--space-4)));
428
+ padding: var(--space-2) var(--space-3);
429
+ background: var(--surface-raised); border: 1px solid var(--line); border-radius: var(--radius-md);
430
+ box-shadow: 0 12px 30px -12px var(--shade);
431
+ font-size: 13px; color: var(--ink); overflow-wrap: anywhere;
432
+ }
433
+ .toast[hidden] { display: none; }
316
434
 
317
435
  /* ============================================================== stage === */
318
436
 
319
- .stage { position: relative; min-width: 0; display: flex; }
320
- .canvas { flex: 1; position: relative; overflow: hidden; background: var(--bg); }
437
+ /* The toolbar on top, the canvas under it. A column, so the canvas is exactly
438
+ the box that is left and the fit maths measures what is drawn. */
439
+ .stage { position: relative; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
440
+
441
+ .toolbar {
442
+ flex: none;
443
+ height: var(--toolbar-h);
444
+ display: flex; align-items: center; gap: var(--space-2);
445
+ padding: 0 var(--space-3);
446
+ border-bottom: 1px solid var(--line);
447
+ background: var(--ground);
448
+ }
449
+ /* Which view this is: the graph, or the same session against time. */
450
+ .tb-views { flex: none; }
451
+ .tb-views .seg-btn { display: inline-flex; align-items: center; gap: 6px; }
452
+ .tb-follow { display: inline-flex; align-items: center; gap: var(--space-2); flex: none; }
453
+ .tb-follow[hidden], .tb-range[hidden] { display: none; }
454
+ .tb-range { cursor: default; display: inline-flex; align-items: center; justify-content: center; }
455
+ /* An on/off control that reads as one. */
456
+ .switch {
457
+ width: 32px; height: 18px; padding: 0; flex: none; position: relative; cursor: pointer;
458
+ border: 0; border-radius: var(--radius-pill); background: var(--line);
459
+ }
460
+ .switch > span {
461
+ position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
462
+ border-radius: var(--radius-pill); background: var(--ink-dim);
463
+ }
464
+ .switch[aria-checked="true"] { background: var(--accent); }
465
+ .switch[aria-checked="true"] > span { left: 16px; background: var(--on-accent); }
466
+ .switch:disabled { opacity: 0.5; cursor: default; }
467
+ /* Shown only at the step below at which the toolbar has dropped what it holds. */
468
+ #tb-options { display: none; }
469
+ .toolbar-fill { flex: 1; min-width: 0; }
470
+ /* Group, Density, Show: the name of the choice in the quiet colour, the choice itself in the text colour. */
471
+ .tb-menu { color: var(--ink-dim); gap: 6px; }
472
+ .tb-menu > span { color: var(--ink); }
473
+ /* The menu's own name is one run of text: split into two flex items, the gap was added to the space between its words. */
474
+ .tb-menu > .tb-name { color: inherit; }
475
+ .tb-menu .ic { width: 12px; height: 12px; }
476
+ .tb-zoom {
477
+ width: 44px; height: 30px; padding: 0; flex: none;
478
+ background: none; border: 0; border-radius: var(--radius-sm); cursor: pointer;
479
+ font-family: var(--font-mono); font-size: 12px; color: var(--ink-dim); text-align: center;
480
+ }
481
+ .tb-zoom:hover { color: var(--ink); }
482
+ /* What a toolbar that does not hold everything gives up, a step at a time. The step is measured by the page
483
+ (toolbar-fit.js) and written to data-fit; no width is assumed here, because how wide the words are depends on
484
+ the font and on the choices the menus are showing.
485
+ 1: it keeps every control and drops the words of the ones that have an icon to stand on. */
486
+ .toolbar:is([data-fit="1"], [data-fit="2"], [data-fit="3"]) .tb-words,
487
+ .toolbar:is([data-fit="1"], [data-fit="2"], [data-fit="3"]) .toolbar-note { display: none; }
488
+ /* 2: nothing is dropped without a way to reach it: "View options" holds what these did. */
489
+ .toolbar:is([data-fit="2"], [data-fit="3"]) :is(#tb-group, #tb-density, #tb-show, #tb-expand, #tb-fold) { display: none; }
490
+ .toolbar:is([data-fit="2"], [data-fit="3"]) #tb-options { display: inline-flex; }
491
+ /* 3: the − and + stay; the figure between them goes. It is still on their labels. And "View options" is "View":
492
+ measured at 360 px, the width of a small phone, the full name ran 24 px over. */
493
+ .toolbar[data-fit="3"] #tb-zoom,
494
+ .toolbar[data-fit="3"] .tb-range,
495
+ .toolbar[data-fit="3"] .tb-follow .sub,
496
+ .toolbar[data-fit="3"] .tb-more { display: none; }
497
+ /* …and the row closes up. At 320 px it was still 17 px over with its usual gaps. */
498
+ .toolbar[data-fit="3"] { gap: var(--space-1); padding: 0 var(--space-2); }
499
+ /* What the panel has to say about the stream or the last action: a reason, in
500
+ words, where the eye already is. Empty most of the time. */
501
+ .toolbar-note {
502
+ min-width: 0; font-family: var(--font-mono); font-size: 11px; color: var(--ink-faint);
503
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
504
+ }
321
505
 
322
- /* A dot grid reads as a workspace rather than a void, and it makes panning
506
+ .canvas { flex: 1; min-height: 0; position: relative; overflow: hidden; background: var(--ground); }
507
+ .canvas[hidden] { display: none; }
508
+
509
+ /* ---------------------------------------------------------- the stage ---
510
+ One line for what is true of the whole stage, a first-run state, and the look of a picture that is old. */
511
+ .phone-head { display: none; }
512
+ .stage-note {
513
+ display: flex; align-items: center; gap: var(--space-3); flex: none; min-width: 0;
514
+ padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--line); font-size: 13px;
515
+ background: var(--surface); color: var(--ink-dim);
516
+ }
517
+ .stage-note[hidden], .stage-note [hidden] { display: none; }
518
+ .stage-note[data-kind="offline"] {
519
+ color: var(--ink); background: color-mix(in srgb, var(--status-fail) 10%, var(--surface));
520
+ border-bottom-color: color-mix(in srgb, var(--status-fail) 45%, var(--line));
521
+ }
522
+ .stage-note-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
523
+ /* The last picture stays while the server is away, and is drawn as what it is: old. */
524
+ .stage[data-offline="true"] .canvas,
525
+ .stage[data-offline="true"] .tl,
526
+ .stage[data-offline="true"] .ls,
527
+ .stage[data-offline="true"] .attention { opacity: 0.45; }
528
+ .first-run {
529
+ flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-3);
530
+ padding: var(--space-6); text-align: center; color: var(--ink-dim);
531
+ }
532
+ .first-run[hidden] { display: none; }
533
+ .first-run strong { font-size: 18px; color: var(--ink); }
534
+ .first-run p { margin: 0; max-width: 46ch; line-height: 1.5; }
535
+ .stage[data-first-run="true"] .canvas,
536
+ .stage[data-first-run="true"] .tl,
537
+ .stage[data-first-run="true"] .ls,
538
+ .stage[data-first-run="true"] .toolbar { display: none; }
539
+ /* Rows that are not there yet: the list is being read. */
540
+ .skel { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); }
541
+ .skel span { height: 44px; border-radius: var(--radius-md); background: var(--surface-raised); opacity: 0.6; }
542
+
543
+ /* ------------------------------------------------------------ the list ---
544
+ One column, in the order of what needs the reader. */
545
+ .ls { flex: 1; min-height: 0; min-width: 0; overflow-y: auto; background: var(--ground); padding: var(--space-3) var(--space-4) var(--space-6); }
546
+ .ls[hidden] { display: none; }
547
+ .ls-sec { max-width: 760px; margin: 0 auto var(--space-4); }
548
+ .ls-head {
549
+ display: flex; align-items: center; gap: var(--space-2); width: 100%; height: 36px; padding: 0 var(--space-1);
550
+ background: none; border: 0; color: var(--ink); font: inherit; text-align: left;
551
+ }
552
+ button.ls-head { cursor: pointer; }
553
+ .ls-title { font-size: 12px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-dim); }
554
+ .ls-sec[data-tone="waiting"] .ls-title { color: var(--status-waiting); }
555
+ .ls-sec[data-tone="fail"] .ls-title { color: var(--status-fail); }
556
+ .ls-row {
557
+ display: flex; align-items: center; gap: var(--space-3); width: 100%; min-height: 56px; box-sizing: border-box;
558
+ padding: var(--space-2) var(--space-3); margin-bottom: var(--space-2);
559
+ border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface);
560
+ color: var(--ink); font: inherit; text-align: left; cursor: pointer;
561
+ }
562
+ .ls-row:hover { border-color: var(--studio-accent); }
563
+ .ls-row.on { border-color: var(--studio-accent); box-shadow: 0 0 0 1px var(--studio-accent); }
564
+ .ls-row.dim { opacity: 0.35; }
565
+ .ls-row .cv-tile { flex: none; }
566
+ .ls-text { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; }
567
+ .ls-name { font-family: var(--font-mono); font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
568
+ .ls-line { font-size: 12px; color: var(--ink-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
569
+ .ls-aside { font-family: var(--font-mono); }
570
+ .ls-chev { display: inline-flex; color: var(--ink-faint); flex: none; }
571
+ .ls-chev .ic { width: 14px; height: 14px; }
572
+ /* A request: who wants what, the time left, and the three answers. */
573
+ .ls-card {
574
+ display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); margin-bottom: var(--space-2);
575
+ border-radius: var(--radius-md); border: 1px solid color-mix(in srgb, var(--status-waiting) 45%, var(--line));
576
+ background: color-mix(in srgb, var(--status-waiting) 8%, var(--surface));
577
+ }
578
+ .ls-card-head { display: flex; align-items: flex-start; gap: var(--space-2); min-width: 0; }
579
+ .ls-card-who { display: flex; flex-direction: column; min-width: 0; }
580
+ .ls-clock { font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--status-waiting); flex: none; font-variant-numeric: tabular-nums; }
581
+ .ls-cmd {
582
+ margin: 0; padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); background: var(--ground);
583
+ font-family: var(--font-mono); font-size: 12px; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere;
584
+ max-height: 120px; overflow-y: auto;
585
+ }
586
+ .ls-where { align-self: flex-start; background: none; border: 0; padding: 0; cursor: pointer; font: inherit; font-size: 12px; color: var(--accent); }
587
+ .ls-acts { display: flex; gap: var(--space-2); flex-wrap: wrap; }
588
+ .ls-acts .btn { flex: 1 1 auto; justify-content: center; }
589
+ .ls-empty { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); color: var(--ink-dim); text-align: center; }
590
+ .ls-empty strong { color: var(--ink); font-size: 15px; }
591
+ /* The inspector's two ways out: a cross beside a panel, an arrow back from a page. */
592
+ .ihead-back { display: none; }
593
+ #menu-btn { display: none; }
594
+ /* The List's first two sections are what the strip says; in that view it would say it twice. */
595
+ .stage[data-view="list"] .attention { display: none; }
596
+
597
+ /* ---------------------------------------------------------- timeline ---
598
+ A label column and a track. Rows scroll; the axis above them and the drawer below them stay. */
599
+ .tl { flex: 1; min-height: 0; min-width: 0; display: flex; flex-direction: column; background: var(--ground); --tl-label-w: 280px; }
600
+ .tl[hidden] { display: none; }
601
+ .tl-axis { display: flex; height: 32px; flex: none; border-bottom: 1px solid var(--line); }
602
+ .tl-axis[hidden], .tl-body[hidden], .tl-empty[hidden], .tl-note[hidden], .tl-drawer[hidden], .tl-now[hidden] { display: none; }
603
+ .tl-axis-label {
604
+ width: var(--tl-label-w); flex: none; box-sizing: border-box; padding: 0 var(--space-3);
605
+ display: flex; align-items: center; background: var(--surface); border-right: 1px solid var(--line);
606
+ font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-faint);
607
+ }
608
+ .tl-axis-track { flex: 1; min-width: 0; position: relative; overflow: hidden; }
609
+ .tl-tick {
610
+ position: absolute; top: 0; bottom: 0; padding-left: 6px; display: flex; align-items: center;
611
+ border-left: 1px solid var(--line); font-family: var(--font-mono); font-size: 11px; color: var(--ink-faint); white-space: nowrap;
612
+ }
613
+ .tl-body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; position: relative; }
614
+ .tl-rows { min-height: 100%; background: linear-gradient(to right, var(--surface) var(--tl-label-w), transparent var(--tl-label-w)); }
615
+ .tl-row { display: flex; height: 34px; border-bottom: 1px solid color-mix(in srgb, var(--line) 55%, transparent); }
616
+ .tl-row.tl-group { height: 30px; background: color-mix(in srgb, var(--surface) 60%, var(--ground)); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
617
+ .tl-row.tl-agent { cursor: pointer; }
618
+ .tl-row.tl-agent:hover, .tl-row.on { background: color-mix(in srgb, var(--studio-accent) 9%, transparent); }
619
+ .tl-row.on { box-shadow: inset 2px 0 0 var(--studio-accent); }
620
+ .tl-row.dim .tl-lab, .tl-bar.dim { opacity: 0.3; }
621
+ .tl-row.tl-more { height: 30px; }
622
+ .tl-lab {
623
+ width: var(--tl-label-w); flex: none; box-sizing: border-box; min-width: 0;
624
+ display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-3);
625
+ background: none; border: 0; border-right: 1px solid var(--line); color: var(--ink); font: inherit; text-align: left;
626
+ }
627
+ button.tl-lab { cursor: pointer; }
628
+ .tl-lab.indent { padding-left: var(--space-6); }
629
+ .tl-lab .cv-tile { flex: none; }
630
+ .tl-lab .sub { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
631
+ .tl-lab .sub[data-tone="fail"] { color: var(--status-fail); }
632
+ .tl-lab .sub[data-tone="wait"] { color: var(--status-waiting); }
633
+ .tl-name { flex: 0 1 auto; min-width: 0; font-family: var(--font-mono); font-size: 12px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
634
+ .tl-group .tl-name { flex-shrink: 0; max-width: 62%; }
635
+ .tl-chev { display: inline-flex; color: var(--ink-dim); }
636
+ .tl-chev .ic { width: 12px; height: 12px; }
637
+ .tl-lab[aria-expanded="true"] .tl-chev { rotate: 90deg; }
638
+ .tl-more-btn { background: none; border: 0; padding: 0; cursor: pointer; font: inherit; font-size: 12px; font-weight: 600; color: var(--accent); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
639
+ .tl-more-btn:hover { text-decoration: underline; }
640
+ .tl-track { flex: 1; min-width: 0; position: relative; overflow: hidden; }
641
+ /* A bar: where an agent was in time. Its colour is its status and nothing else. */
642
+ .tl-bar {
643
+ position: absolute; top: 11px; height: 12px; padding: 0; border: 0; border-radius: 3px; cursor: pointer;
644
+ background: var(--ink-faint);
645
+ }
646
+ .tl-group .tl-bar { top: 9px; }
647
+ .tl-bar.thin { height: 6px; border-radius: 2px; top: 9px; }
648
+ .tl-bar.thin[data-lane="1"] { top: 19px; }
649
+ .tl-group .tl-bar.thin { top: 7px; }
650
+ .tl-group .tl-bar.thin[data-lane="1"] { top: 17px; }
651
+ .tl-bar[data-tone="busy"] { background: var(--status-busy); }
652
+ .tl-bar[data-tone="done"] { background: color-mix(in srgb, var(--status-good) 55%, var(--ground)); }
653
+ .tl-bar[data-tone="fail"] { background: var(--status-fail); }
654
+ .tl-bar[data-tone="wait"] { background: var(--status-waiting); }
655
+ .tl-bar[data-tone="quiet"] { background: color-mix(in srgb, var(--ink-dim) 45%, var(--ground)); }
656
+ /* A bar that runs off the edge of what is shown is cut square there. */
657
+ .tl-bar[data-cut-left] { border-top-left-radius: 0; border-bottom-left-radius: 0; }
658
+ .tl-bar[data-cut-right] { border-top-right-radius: 0; border-bottom-right-radius: 0; }
659
+ .tl-bar.on { outline: 2px solid var(--studio-accent); outline-offset: 1px; }
660
+ .tl-x { position: absolute; top: 7px; margin-left: 3px; font-size: 14px; line-height: 20px; font-weight: 700; color: var(--status-fail); pointer-events: none; }
661
+ .tl-untimed { position: absolute; left: var(--space-3); top: 0; bottom: 0; display: flex; align-items: center; font-size: 12px; color: var(--ink-faint); }
662
+ .tl-now { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--studio-accent); pointer-events: none; z-index: 2; }
663
+ .tl-now::before {
664
+ content: attr(data-label); position: sticky; top: 2px; display: block; width: max-content; translate: calc(-100% + 2px) 0;
665
+ padding: 0 6px; height: 20px; line-height: 20px; border-radius: var(--radius-sm);
666
+ background: var(--studio-accent); color: var(--ground); font-family: var(--font-mono); font-size: 11px; font-weight: 700;
667
+ }
668
+ .tl-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); color: var(--ink-dim); text-align: center; padding: var(--space-6); }
669
+ .tl-empty strong { color: var(--ink); font-size: 15px; }
670
+ .tl-away { flex: none; display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3); border-top: 1px solid var(--line); font-size: 13px; color: var(--ink-dim); background: var(--surface); }
671
+ .tl-away[hidden] { display: none; }
672
+ .tl-note { flex: none; padding: 6px var(--space-3); border-top: 1px solid var(--line); font-size: 12px; color: var(--ink-faint); background: var(--surface); }
673
+ .tl-note[data-measured="false"] { color: var(--status-waiting); }
674
+ /* The drawer: the chosen agent, under the rows. */
675
+ .tl-drawer {
676
+ flex: none; display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-8); align-items: flex-start; max-height: 40%; overflow-y: auto;
677
+ padding: var(--space-4); border-top: 1px solid var(--line); background: var(--surface);
678
+ }
679
+ .tl-d-main { flex: 0 1 360px; min-width: 0; display: flex; flex-direction: column; gap: var(--space-2); }
680
+ .tl-d-head { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
681
+ .tl-d-name { font-family: var(--font-mono); font-size: 14px; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
682
+ .tl-d-task { font-size: 15px; font-weight: 600; line-height: 1.35; overflow-wrap: anywhere; }
683
+ .tl-d-facts { white-space: normal; }
684
+ /* The error is text to be read: it takes a line of its own before it is squeezed. With `flex: 1 1 0` and no wrap
685
+ it was the one thing that gave way, and in a 720 px window it was 26 px wide: a letter to a line. */
686
+ .tl-d-error { flex: 1 1 320px; min-width: 0; }
687
+ .tl-d-errtext {
688
+ margin: 0 0 var(--space-1); padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm);
689
+ background: var(--ground); border: 1px solid var(--line); max-height: 96px; overflow-y: auto;
690
+ font-family: var(--font-mono); font-size: 12px; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere;
691
+ color: var(--status-fail);
692
+ }
693
+ .tl-d-acts { flex: none; display: flex; align-items: center; gap: var(--space-2); margin-left: auto; }
694
+
695
+ /* ---- needs attention ----
696
+ A strip that exists only while something needs someone. Each chip goes to its
697
+ agent on the canvas. */
698
+ .attention {
699
+ flex: none; height: 44px;
700
+ display: flex; align-items: center; gap: var(--space-2);
701
+ padding: 0 var(--space-4);
702
+ border-bottom: 1px solid var(--line);
703
+ background: var(--surface);
704
+ overflow: hidden;
705
+ }
706
+ .attention[hidden] { display: none; }
707
+ .cv-label, .attention-label {
708
+ flex: none;
709
+ font-size: 11px; line-height: var(--type-label-line); font-weight: var(--type-label-weight);
710
+ letter-spacing: var(--type-label-tracking); text-transform: uppercase; color: var(--ink-dim);
711
+ }
712
+ .attention-label { margin-right: var(--space-1); }
713
+ .att {
714
+ display: inline-flex; align-items: center; gap: var(--space-2); flex: none;
715
+ height: 30px; padding: 0 10px;
716
+ border-radius: var(--radius-pill); border: 1px solid var(--line);
717
+ background: var(--surface); color: var(--ink);
718
+ font-family: inherit; font-size: 12px; white-space: nowrap; cursor: pointer;
719
+ }
720
+ .att:hover { background: var(--surface-raised); }
721
+ .att-name { font-family: var(--font-mono); }
722
+ .att-says { color: var(--ink-dim); }
723
+ .attention-hint { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
724
+
725
+ /* -------------------------------------------------------------- dock ---
726
+ The approval dock: a row under the canvas, there only while a tool call is
727
+ waiting. It is part of the stage's column, so the canvas above it stays
728
+ whole and usable; nothing is covered and nothing is modal. */
729
+ .dock {
730
+ flex: none; display: grid; align-items: center;
731
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
732
+ gap: var(--space-2) var(--space-4);
733
+ min-height: 72px; padding: var(--space-2) var(--space-4);
734
+ border-top: 1px solid color-mix(in srgb, var(--status-waiting) 45%, var(--line));
735
+ background: color-mix(in srgb, var(--status-waiting) 8%, var(--surface));
736
+ }
737
+ /* `display: grid` outranks the UA's `[hidden]`; without these the dock and its parts would stay. */
738
+ .dock[hidden], .dock [hidden] { display: none; }
739
+ .dock:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
740
+ .dock-timer { position: relative; width: 36px; height: 36px; flex: none; }
741
+ .dock-timer svg { width: 36px; height: 36px; rotate: -90deg; }
742
+ .dock-ring-track, .dock-ring { fill: none; stroke-width: 3; }
743
+ .dock-ring-track { stroke: var(--line); }
744
+ .dock-ring { stroke: var(--status-waiting); stroke-linecap: round; }
745
+ .dock-secs {
746
+ position: absolute; inset: 0; display: grid; place-items: center;
747
+ font-family: var(--font-mono); font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
748
+ }
749
+ .dock-text { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
750
+ .dock-head { display: flex; align-items: baseline; gap: var(--space-2); font-size: 13px; min-width: 0; }
751
+ .dock-word { font-weight: 600; color: var(--status-waiting); flex: none; }
752
+ .dock[data-state="denied"] .dock-word, .dock[data-state="timed-out"] .dock-word { color: var(--status-bad); }
753
+ .dock-who {
754
+ background: none; border: none; padding: 0; cursor: pointer; font: inherit; font-weight: 600; color: var(--ink);
755
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
756
+ }
757
+ .dock-who:hover { text-decoration: underline; }
758
+ .dock-tool { font-family: var(--font-mono); font-size: 12px; color: var(--ink-dim); flex: none; }
759
+ .dock-where { font-size: 12px; color: var(--ink-faint); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
760
+ /* The command: one line until it is asked for, then all of it. */
761
+ .dock-cmd {
762
+ background: none; border: none; padding: 0; cursor: pointer; text-align: left;
763
+ font-family: var(--font-mono); font-size: 12px; color: var(--ink);
764
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; max-width: 100%;
765
+ }
766
+ .dock-cmd.open { white-space: pre-wrap; overflow-wrap: anywhere; overflow-y: auto; max-height: 30vh; }
767
+ .dock-note { font-size: 12px; color: var(--ink-dim); }
768
+ .dock-nav { display: flex; align-items: center; gap: 2px; flex: none; }
769
+ .dock-count { font-size: 12px; color: var(--ink-dim); font-variant-numeric: tabular-nums; white-space: nowrap; }
770
+ .dock-acts { display: flex; gap: var(--space-2); flex: none; }
771
+ /* Two rows: what is asked, then the answers under it. app.js sets the form from the dock's own width, so a
772
+ dock squeezed between two open panels gets it too. */
773
+ .dock[data-form="stacked"] { grid-template-columns: auto minmax(0, 1fr) auto; }
774
+ .dock[data-form="stacked"] .dock-acts { grid-column: 1 / -1; justify-content: flex-end; flex-wrap: wrap; }
775
+
776
+ /* ============================================================== graph ===
777
+ A dot grid reads as a workspace rather than a void, and it makes panning
323
778
  legible — without it the canvas looks frozen while it moves. */
324
779
  .cv-root {
325
- background-image: radial-gradient(var(--line) 1px, transparent 1px);
326
- background-size: 22px 22px;
780
+ background-image: radial-gradient(var(--grid-dot) 1px, transparent 1.2px);
781
+ background-size: 20px 20px;
327
782
  cursor: grab;
328
783
  touch-action: none;
329
784
  user-select: none;
785
+
786
+ /* Two tones the design draws between tokens. A wire at rest is a hairline
787
+ that still reads on the ground; a group's well is the ground showing
788
+ through a card. */
789
+ --edge-rest: color-mix(in srgb, var(--line) 80%, var(--ink-dim));
790
+ --well: color-mix(in srgb, var(--surface) 55%, var(--ground));
330
791
  }
331
792
  .cv-root.cv-panning { cursor: grabbing; }
332
793
 
333
794
  .cv-viewport { position: absolute; inset: 0; transform-origin: 0 0; }
334
- .cv-edges { position: absolute; overflow: visible; pointer-events: none; }
795
+ .cv-edges { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
335
796
  .cv-nodes { position: absolute; inset: 0; }
336
797
 
337
798
  /* ------------------------------------------------------------- edge ---
338
- Delegation drawn as motion. The dash travels out of the parent's port and
339
- into the child's, because the question a glance has to answer is "where did
340
- the work go", not "these two are related".
341
-
342
- Every state is carried by at least two channels — weight, dash pattern,
343
- tone — and motion is only ever the third. That is what makes the
344
- reduced-motion path below a removal of speed rather than of information. */
799
+ A wire says the state of what it leads to, in the same colour that state has
800
+ everywhere else, and never in colour alone: weight and dash change with it.
801
+ Motion is only ever the third channel, and only a working wire has it. That
802
+ is what makes the reduced-motion path below a removal of speed rather than
803
+ of information. */
345
804
 
346
805
  .cv-edge {
347
806
  fill: none;
348
- stroke-width: 1.6;
807
+ stroke: var(--edge-rest);
808
+ stroke-width: 1.5;
349
809
  stroke-linecap: round;
350
810
  stroke-dasharray: none;
351
811
  stroke-dashoffset: 0;
352
- opacity: var(--cv-edge-done);
353
812
  /* One dash period. The flow animation travels exactly this far per
354
813
  iteration, so a state can change its dash pattern without putting a seam
355
814
  in the loop. */
356
815
  --dash-period: 13px;
357
816
  }
358
817
 
359
- /* Working. The brightest, heaviest, fastest line on the canvas. */
818
+ /* Working. The brightest and the only one that travels. */
360
819
  .cv-edge[data-state="live"] {
820
+ stroke: var(--status-busy);
361
821
  opacity: var(--cv-edge-live);
362
- stroke-width: 2.2;
822
+ stroke-width: 1.8;
363
823
  stroke-dasharray: 6px 7px;
364
824
  --dash-period: 13px;
365
825
  animation: cv-flow 0.9s linear infinite;
366
826
  }
367
827
 
368
- /* Spawned, nothing back yet. Same direction, half lit, and moving slowly:
828
+ /* Spawned, nothing back yet. Same colour, shorter dash, moving slowly:
369
829
  something is on this wire, and it has not produced anything. */
370
830
  .cv-edge[data-state="waking"] {
831
+ stroke: var(--status-busy);
371
832
  opacity: var(--cv-edge-wake);
372
- stroke-width: 1.6;
833
+ stroke-width: 1.5;
373
834
  stroke-dasharray: 2px 7px;
374
835
  --dash-period: 9px;
375
836
  animation: cv-flow 1.9s linear infinite;
376
837
  }
377
838
 
839
+ /* Waiting on the viewer. Dashed like work, still like a question. */
840
+ .cv-edge[data-state="waiting"] {
841
+ stroke: var(--status-waiting);
842
+ opacity: 1;
843
+ stroke-width: 1.8;
844
+ stroke-dasharray: 5px 4px;
845
+ }
846
+
378
847
  /* Finished. Solid and pulled back, so live work stands out against it. */
379
848
  .cv-edge[data-state="done"] {
380
- opacity: var(--cv-edge-done);
381
- stroke-width: 1.6;
849
+ stroke: var(--edge-rest);
850
+ opacity: 1;
851
+ stroke-width: 1.5;
382
852
  stroke-dasharray: none;
383
853
  }
384
854
 
385
855
  /* Ended, stale. Still there, saying nothing. */
386
856
  .cv-edge[data-state="quiet"] {
387
- opacity: var(--cv-edge-quiet);
388
- stroke-width: 1.4;
857
+ stroke: var(--edge-rest);
858
+ opacity: var(--cv-edge-wake);
859
+ stroke-width: 1.5;
389
860
  stroke-dasharray: 2px 6px;
390
861
  }
391
862
 
392
- /* Wrong, and said in colour rather than in a label somewhere else. The stroke
393
- itself is set to var(--cv-fail) rather than the child's palette colour. */
863
+ /* Wrong, and said in the colour failure has everywhere. */
394
864
  .cv-edge[data-state="failed"] {
865
+ stroke: var(--cv-fail);
395
866
  opacity: 1;
396
- stroke-width: 2.4;
397
- stroke-dasharray: 1px 5px;
867
+ stroke-width: 2;
868
+ stroke-dasharray: none;
398
869
  }
399
870
 
400
- /* A workflow's members are one dispatch. They share a weight here and a hue
401
- family in canvas.js, so twelve of them read as a bundle rather than as
402
- twelve independent decisions. */
403
- .cv-edge[data-group="member"] { stroke-width: 1.4; }
404
- .cv-edge[data-group="member"][data-state="live"] { stroke-width: 1.8; }
871
+ /* The way from the session to what is selected. */
872
+ .cv-edge.cv-path { stroke: var(--studio-accent); stroke-width: 2.4; opacity: 1; }
405
873
 
406
- /* Arrival. The edge draws itself from the parent toward the node that just
874
+ /* Arrival. The wire draws itself from the parent toward the card that just
407
875
  appeared. Both the pattern and the offset live in the keyframes, so with
408
- the animation switched off the edge simply renders in its final state
876
+ the animation switched off the wire simply renders in its final state
409
877
  wearing its own status dash. */
410
878
  .cv-edge.cv-drawing { animation: cv-draw 0.3s ease-out; }
411
879
 
@@ -415,370 +883,237 @@ body.resizing .cv-root { pointer-events: none; }
415
883
  to { stroke-dasharray: var(--edge-len) var(--edge-len); stroke-dashoffset: 0; }
416
884
  }
417
885
 
418
- /* ------------------------------------------------------------- node --- */
886
+ /* ------------------------------------------------------------- card ---
887
+ One frame for every kind of thing on the canvas. Its colour is the
888
+ surface's; what changes with status is the dot, its word, and — for the two
889
+ states that need someone — the frame. */
419
890
 
420
891
  .cv-node {
421
- position: absolute;
422
- width: 236px;
423
- min-height: 104px;
892
+ position: absolute; left: 0; top: 0;
424
893
  box-sizing: border-box;
894
+ display: flex; flex-direction: column; gap: var(--space-1);
895
+ padding: 9px var(--space-3);
425
896
  background: var(--surface);
426
897
  border: 1px solid var(--line);
427
- border-radius: 12px;
428
- padding: 12px 14px 12px 18px;
429
- cursor: grab;
430
- transition: box-shadow 0.15s, border-color 0.15s, opacity 0.25s, scale 0.25s;
431
- box-shadow: 0 1px 2px rgb(0 0 0 / 0.18), 0 8px 20px -12px rgb(0 0 0 / 0.4);
432
- }
433
- .cv-node:hover { border-color: color-mix(in srgb, var(--node-color) 55%, var(--line)); }
434
- .cv-node.cv-dragging { cursor: grabbing; z-index: 10; box-shadow: 0 12px 34px -10px rgb(0 0 0 / 0.55); }
435
- .cv-node.cv-selected {
436
- border-color: var(--node-color);
437
- box-shadow: 0 0 0 1px var(--node-color), 0 12px 30px -14px var(--node-color);
438
- }
439
- .cv-node.cv-born { opacity: 0; scale: 0.9; }
440
-
441
- .cv-accent {
442
- position: absolute; left: 0; top: 12px; bottom: 12px; width: 3px;
443
- border-radius: 0 3px 3px 0; background: var(--node-color);
444
- }
898
+ border-radius: var(--radius-md);
899
+ cursor: pointer;
900
+ transition: border-color 0.15s, opacity 0.25s, scale 0.25s;
901
+ }
902
+ .cv-node:hover { border-color: var(--ink-faint); }
903
+ .cv-node.cv-dragging { cursor: grabbing; z-index: 10; box-shadow: 0 12px 30px -12px var(--shade); }
904
+ .cv-node.cv-selected { border-color: var(--studio-accent); box-shadow: 0 0 0 1px var(--studio-accent); }
905
+ .cv-node.cv-born { opacity: 0; scale: 0.94; }
906
+ /* Only a card that IS one agent takes the frame of its state. A group's frame stays plain: one failed member is
907
+ said by its bar and by the member, not by reddening eleven others. */
908
+ .cv-node[data-kind="agent"][data-state="failed"] { border-color: color-mix(in srgb, var(--cv-fail) 45%, var(--line)); }
909
+ .cv-node[data-kind="agent"][data-state="waiting"] { border-color: color-mix(in srgb, var(--status-waiting) 50%, var(--line)); }
910
+ .cv-node[data-kind="agent"][data-state="quiet"] > * { opacity: 0.72; }
445
911
 
446
912
  /* A running card, legibly alive when nothing else on the canvas is moving.
447
- One pulse per card and no more: the accent bar used to breathe as well, and
448
- two rhythms on one card is the wall of throbbing this is trying to avoid.
449
-
450
913
  The ring is drawn whether or not it breathes, so "running" survives with
451
914
  motion off; breathing is the only part the animation adds. Only opacity is
452
915
  animated — the shadow itself never changes, so nothing re-rasterises. */
453
- .cv-node[data-state="live"]::after {
916
+ .cv-node[data-kind="agent"][data-state="live"]::after {
454
917
  content: '';
455
918
  position: absolute; inset: -2px;
456
- border-radius: 14px;
919
+ border-radius: 12px;
457
920
  pointer-events: none;
458
- box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--node-color) 55%, transparent);
921
+ box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--status-busy) 55%, transparent);
459
922
  animation: cv-pulse 2.4s ease-in-out infinite;
460
923
  }
461
924
  @keyframes cv-pulse { 0%, 100% { opacity: 0.9; } 50% { opacity: 0.28; } }
462
925
 
463
- /* Failed, killed, stopped — one look, and not the look of a colour some agent
464
- might legitimately own. */
465
- .cv-node[data-state="failed"] { border-color: var(--cv-fail); }
466
- .cv-node[data-state="failed"] .cv-accent { background: var(--cv-fail); }
467
- .cv-node[data-state="failed"] .cv-chip {
468
- color: var(--cv-fail);
469
- border-color: color-mix(in srgb, var(--cv-fail) 50%, transparent);
470
- }
471
-
472
- .cv-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
473
- .cv-type {
474
- font-family: var(--mono); font-size: 11px; font-weight: 600;
475
- letter-spacing: 0.06em; text-transform: uppercase; color: var(--node-color);
476
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
477
- }
478
- .cv-type.cv-unknown::after {
479
- content: '?'; margin-left: 5px; color: var(--ink-faint);
480
- border: 1px solid var(--line); border-radius: 3px; padding: 0 3px; font-size: 9px;
481
- }
482
-
483
- .cv-chip {
484
- font-family: var(--mono); font-size: 10px; letter-spacing: 0.05em;
485
- text-transform: uppercase; padding: 1px 6px; border-radius: 999px;
486
- border: 1px solid var(--line); color: var(--ink-faint); white-space: nowrap; flex: none;
487
- }
488
- .cv-chip[data-status="running"] { color: var(--busy); border-color: color-mix(in srgb, var(--busy) 45%, transparent); }
489
- .cv-chip[data-status="done"] { color: var(--good); border-color: color-mix(in srgb, var(--good) 40%, transparent); }
490
- .cv-chip[data-status="stale"] { color: var(--waiting); border-color: color-mix(in srgb, var(--waiting) 40%, transparent); }
491
- .cv-chip[data-status="starting"]{ color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
492
-
493
- .cv-desc {
494
- margin-top: 7px; font-size: 13px; line-height: 1.35; color: var(--ink);
495
- display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
496
- -webkit-box-orient: vertical; overflow: hidden;
497
- }
498
- .cv-node[data-kind="session"] .cv-desc { font-family: var(--mono); font-size: 12px; color: var(--ink-dim); }
499
-
500
- .cv-foot { margin-top: 9px; display: flex; flex-wrap: wrap; gap: 5px; }
501
- .cv-bit {
502
- font-family: var(--mono); font-size: 10px; color: var(--ink-faint);
503
- background: var(--surface-2); border-radius: 4px; padding: 1px 6px;
926
+ .cv-row { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
927
+ .cv-fill { flex: 1; min-width: 0; }
928
+ .cv-indent { padding-left: 32px; }
929
+ .cv-title { font-size: 13px; font-weight: 600; }
930
+ .cv-name {
931
+ min-width: 0; font-family: var(--font-mono); font-size: 12px; line-height: 16px; color: var(--ink);
932
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
504
933
  }
505
- .cv-bit.cv-live { color: var(--busy); background: color-mix(in srgb, var(--busy) 14%, transparent); }
506
- .cv-bit.cv-err { color: var(--bad); background: color-mix(in srgb, var(--bad) 14%, transparent); }
507
- .cv-bit.cv-branch { color: var(--ink-dim); }
508
-
509
- /* Ports sit on whichever axis the flow runs along. */
510
- .cv-port {
511
- position: absolute; width: 9px; height: 9px; border-radius: 50%;
512
- background: var(--surface); border: 2px solid var(--node-color);
934
+ .cv-task {
935
+ min-width: 0; font-size: 13px; line-height: 18px; color: var(--ink-dim);
936
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
513
937
  }
514
- [data-flow="down"] .cv-port { left: 50%; transform: translateX(-50%); }
515
- [data-flow="down"] .cv-in { top: -5px; }
516
- [data-flow="down"] .cv-out { bottom: -5px; }
517
-
518
- [data-flow="right"] .cv-port { top: 50%; transform: translateY(-50%); }
519
- [data-flow="right"] .cv-in { left: -5px; }
520
- [data-flow="right"] .cv-out { right: -5px; }
521
- .cv-node[data-kind="session"] .cv-in { display: none; }
522
-
523
- /* --------------------------------------------------------------- hud --- */
524
-
525
- .cv-hud {
526
- position: absolute; right: 14px; bottom: 14px; z-index: 20;
527
- display: flex; align-items: center; gap: 6px;
938
+ .cv-sub { font-size: 12px; line-height: 16px; color: var(--ink-dim); white-space: nowrap; flex: none; }
939
+ .cv-word { font-size: 11px; color: var(--ink-dim); white-space: nowrap; flex: none; }
940
+
941
+ /* The tile says whose agent it is. Crewforth's mark keeps its own colours; the
942
+ mark for everything else is drawn in the quiet text colour. */
943
+ .cv-tile {
944
+ width: 24px; height: 24px; flex: none; position: relative;
945
+ display: inline-flex; align-items: center; justify-content: center;
946
+ border-radius: var(--radius-sm); border: 1px solid var(--line);
947
+ background: var(--surface-raised); color: var(--ink-dim);
948
+ }
949
+ .cv-tile svg { width: 64%; height: 64%; display: block; }
950
+ .cv-tile-sm { width: 18px; height: 18px; }
951
+ /* A type nobody declared is marked as one. It is not quietly drawn as built-in. */
952
+ .cv-tile[data-source="unknown"]::after {
953
+ content: '?';
954
+ position: absolute; right: -5px; top: -5px;
955
+ width: 12px; height: 12px; border-radius: var(--radius-pill);
956
+ display: flex; align-items: center; justify-content: center;
528
957
  background: var(--surface); border: 1px solid var(--line);
529
- border-radius: 9px; padding: 4px 8px;
530
- }
531
- .cv-btn {
532
- background: transparent; border: none; color: var(--ink-dim);
533
- cursor: pointer; font-size: 14px; line-height: 1; padding: 3px 5px; border-radius: 5px;
534
- }
535
- .cv-btn:hover { color: var(--ink); background: var(--surface-2); }
536
- .cv-zoom {
537
- font-family: var(--mono); font-size: 10px; color: var(--ink-faint);
538
- min-width: 96px; text-align: right; white-space: nowrap;
958
+ font-family: var(--font-mono); font-size: 9px; line-height: 1; color: var(--ink-dim);
959
+ }
960
+
961
+ .cv-pill {
962
+ display: inline-flex; align-items: center; gap: 5px; flex: none;
963
+ height: 20px; padding: 0 6px;
964
+ border-radius: var(--radius-sm); background: var(--surface-raised);
965
+ font-size: 11px; font-weight: 600; color: var(--ink); white-space: nowrap;
966
+ }
967
+ .cv-pill[hidden] { display: none; }
968
+ .cv-node[data-kind="agent"][data-state="failed"] .cv-pill { color: var(--cv-fail); }
969
+
970
+ /* An agent at compact density: one line. */
971
+ .cv-node.cv-chip { flex-direction: row; align-items: center; gap: var(--space-2); padding: 0 10px; border-radius: 8px; }
972
+ .cv-chip .cv-name { flex: 0 1 auto; }
973
+ .cv-chip .cv-task { flex: 1 1 0; font-size: 12px; }
974
+
975
+ /* The session: where everything starts. */
976
+ .cv-session { cursor: pointer; }
977
+
978
+ /* A folded group is one card standing for several: a second card shows behind
979
+ it. Open, it is a well holding its members. */
980
+ .cv-group { gap: var(--space-2); padding: 10px var(--space-3); }
981
+ .cv-group:not(.cv-open) { box-shadow: 6px -6px 0 -1px var(--well), 6px -6px 0 0 var(--line); }
982
+ .cv-group.cv-selected:not(.cv-open) { box-shadow: 0 0 0 1px var(--studio-accent), 6px -6px 0 -1px var(--well), 6px -6px 0 0 var(--line); }
983
+ .cv-group.cv-open { background: var(--well); padding: 10px var(--space-2) var(--space-2); gap: 6px; cursor: default; }
984
+ .cv-group-head { height: 20px; cursor: pointer; }
985
+ .cv-group.cv-open .cv-group-head { padding: 0 var(--space-1); }
986
+ .cv-fold {
987
+ width: 20px; height: 20px; padding: 0; flex: none;
988
+ display: inline-flex; align-items: center; justify-content: center;
989
+ background: none; border: 0; border-radius: var(--radius-sm); color: var(--ink-dim); cursor: pointer;
990
+ }
991
+ .cv-fold .ic { width: 12px; height: 12px; }
992
+ .cv-fold:hover { color: var(--ink); background: var(--surface-raised); }
993
+
994
+ /* How the group went: one segment per status, as wide as its share. The words
995
+ are beside it when the group is folded and in its title when it is open. */
996
+ .cv-bar { display: flex; gap: 2px; height: 6px; flex: 1; min-width: 48px; }
997
+ .cv-group.cv-open > .cv-bar { height: 4px; flex: none; margin: 0 var(--space-1); }
998
+ .cv-bar span { display: block; border-radius: 2px; min-width: 2px; background: var(--ink-faint); }
999
+ .cv-bar span[data-tone="good"] { background: var(--status-good); }
1000
+ .cv-bar span[data-tone="busy"] { background: var(--status-busy); }
1001
+ .cv-bar span[data-tone="waiting"] { background: var(--status-waiting); }
1002
+ .cv-bar span[data-tone="fail"] { background: var(--status-fail); }
1003
+ .cv-bar span[data-tone="idle"] { background: var(--status-idle); }
1004
+
1005
+ /* A member inside an open group: a row with its mark, or a chip in a grid. */
1006
+ .cv-member {
1007
+ position: absolute; left: 0; top: 0; box-sizing: border-box;
1008
+ display: flex; align-items: center; gap: var(--space-2);
1009
+ padding: 0 10px;
1010
+ background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
1011
+ cursor: pointer;
539
1012
  }
540
- /* Loud on purpose: it only appears when something went wrong. */
541
- .cv-btn.cv-alarm { color: var(--cv-fail); font-family: var(--mono); font-size: 11px; }
542
- .cv-btn.cv-alarm:hover { color: var(--cv-fail); background: color-mix(in srgb, var(--cv-fail) 16%, transparent); }
543
- .cv-btn[hidden] { display: none; }
1013
+ .cv-member:hover { border-color: var(--ink-faint); }
1014
+ .cv-member.cv-selected { border-color: var(--studio-accent); box-shadow: 0 0 0 1px var(--studio-accent); }
1015
+ .cv-member[data-state="failed"] .cv-word { color: var(--cv-fail); }
1016
+ .cv-member-chip .cv-name { font-family: var(--font-sans); color: var(--ink-dim); flex: 1 1 0; }
1017
+
1018
+ /* A status the filter did not ask for, stepped back. */
1019
+ .cv-node.cv-dim, .cv-member.cv-dim { opacity: 0.22; }
1020
+ .cv-edge.cv-dim { opacity: 0.08; }
544
1021
 
545
1022
  .cv-empty {
546
1023
  position: absolute; inset: 0; display: grid; place-content: center;
547
1024
  text-align: center; color: var(--ink-dim); padding: 24px; pointer-events: none;
548
1025
  }
549
1026
  .cv-empty strong { display: block; color: var(--ink); margin-bottom: 6px; font-size: 15px; }
1027
+ /* `display: grid` outranks the UA's `[hidden] { display: none }`. */
1028
+ .cv-empty[hidden] { display: none; }
1029
+
1030
+ /* ---------------------------------------------------- map and legend ---
1031
+ Zoomed out, a card's words stop being readable. What still has to be found
1032
+ is where things are and which of them need someone, and the map keeps both:
1033
+ every box, and the two colours that mean "look here". */
1034
+ .cv-minimap {
1035
+ position: absolute; left: var(--space-6); bottom: var(--space-6); z-index: 20;
1036
+ width: 200px; height: 88px; overflow: hidden;
1037
+ background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md);
1038
+ cursor: pointer;
1039
+ }
1040
+ .cv-minimap[hidden] { display: none; }
1041
+ .cv-map-box { fill: var(--line); }
1042
+ .cv-map-box[data-tone="waiting"] { fill: var(--status-waiting); }
1043
+ .cv-map-box[data-tone="fail"] { fill: var(--status-fail); }
1044
+ .cv-map-box[data-tone="selected"] { fill: var(--studio-accent); }
1045
+ .cv-map-frame { fill: none; stroke: var(--studio-accent); stroke-width: 1.2; }
1046
+
1047
+ .cv-legend {
1048
+ position: absolute; right: var(--space-6); bottom: var(--space-6); z-index: 20;
1049
+ width: 260px; box-sizing: border-box; padding: var(--space-3);
1050
+ display: flex; flex-direction: column; gap: var(--space-2);
1051
+ background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md);
1052
+ cursor: default;
1053
+ }
1054
+ .cv-legend[hidden] { display: none; }
1055
+ .cv-legend-head { display: flex; align-items: center; justify-content: space-between; }
1056
+ .cv-legend-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-1) var(--space-2); }
1057
+ .cv-legend-item { display: flex; align-items: center; gap: var(--space-2); font-size: 12px; color: var(--ink-dim); white-space: nowrap; }
1058
+ .cv-legend-line { width: 28px; height: 0; flex: none; border-top: 1.5px solid var(--edge-rest); }
1059
+ .cv-legend-line[data-state="live"] { border-top: 1.5px dashed var(--status-busy); }
1060
+ .cv-legend-line[data-state="waiting"] { border-top: 1.5px dashed var(--status-waiting); }
1061
+ .cv-legend-line[data-state="failed"] { border-top-color: var(--cv-fail); }
1062
+ .cv-x {
1063
+ width: 20px; height: 20px; padding: 0; display: inline-flex; align-items: center; justify-content: center;
1064
+ background: none; border: 0; border-radius: var(--radius-sm); color: var(--ink-dim); cursor: pointer;
1065
+ }
1066
+ .cv-x .ic { width: 12px; height: 12px; }
1067
+ .cv-x:hover { color: var(--ink); background: var(--surface-raised); }
1068
+
1069
+ .cv-tip {
1070
+ position: absolute; z-index: 30; max-width: 300px; pointer-events: none;
1071
+ display: flex; flex-direction: column; gap: 2px;
1072
+ padding: var(--space-2) var(--space-3);
1073
+ background: var(--surface-raised); border: 1px solid var(--line); border-radius: var(--radius-md);
1074
+ box-shadow: 0 12px 30px -12px var(--shade);
1075
+ font-size: 12px; line-height: 18px; color: var(--ink);
1076
+ }
1077
+ .cv-tip[hidden] { display: none; }
1078
+ .cv-tip strong { font-family: var(--font-mono); font-weight: 600; }
1079
+
1080
+ /* The small chip the inspector's gate summary is made of. */
1081
+ .cv-bit {
1082
+ font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint);
1083
+ background: var(--surface-raised); border-radius: 4px; padding: 1px 6px;
1084
+ }
550
1085
 
551
1086
  /* --------------------------------------------------------- inspector --- */
552
1087
 
1088
+ /* Docked beside the canvas when there is room for it, floating over the canvas
1089
+ when there is not. Room means: 1280px or more, and no conversation open —
1090
+ navigator, inspector and conversation side by side leave no graph at all.
1091
+ Floating is the base rule, so a width the queries below do not name gets the
1092
+ form that never squeezes the canvas. */
553
1093
  .inspector {
554
- width: 300px; flex: none; border-left: 1px solid var(--line);
555
- background: var(--surface); overflow-y: auto; padding: 16px;
556
- }
557
- .inspector h3 {
558
- margin: 0 0 2px; font-family: var(--mono); font-size: 12px;
559
- letter-spacing: 0.06em; text-transform: uppercase;
1094
+ width: var(--inspector-w);
1095
+ min-height: 0;
1096
+ display: flex; flex-direction: column;
1097
+ border-left: 1px solid var(--line);
1098
+ background: var(--surface);
560
1099
  }
561
- .inspector .isub { color: var(--ink-dim); font-size: 13px; margin-bottom: 14px; }
1100
+ /* No grid-column while it floats: an absolutely placed grid child that names a
1101
+ column is positioned inside that column, and column 4 is empty then. */
1102
+ .shell > .inspector {
1103
+ position: absolute; z-index: 30;
1104
+ /* It stops above the approval dock: the answers are never under a panel. */
1105
+ top: var(--toolbar-h); bottom: var(--dock-h, 0px);
1106
+ right: calc(var(--chat-w, 480px) + 5px);
1107
+ box-shadow: -12px 0 24px -18px var(--shade);
1108
+ }
1109
+ .shell.no-chat > .inspector { right: 0; }
1110
+ /* `display: flex` outranks the UA's `[hidden] { display: none }`, so the hidden
1111
+ panel kept its width. Restate it explicitly. */
1112
+ .inspector[hidden] { display: none; }
1113
+
562
1114
  .inspector dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 6px 12px; }
563
1115
  .inspector dt { font-size: 11px; color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.05em; }
564
- .inspector dd { margin: 0; font-family: var(--mono); font-size: 12px; word-break: break-word; }
565
- .inspector .tools { margin-top: 16px; display: flex; flex-wrap: wrap; gap: 5px; }
566
-
567
- @media (max-width: 900px) {
568
- .shell { grid-template-columns: 1fr; }
569
- .side, .resizer { display: none; }
570
- .inspector { display: none; }
571
- }
572
- /* =================================================== three readings ===
573
- One picture, read three ways, chosen by how much of a card would actually
574
- survive on screen. Nothing moves between them: the layout metrics are
575
- identical in all three, so the shape of the graph is the constant and only
576
- what is written inside a card changes.
577
-
578
- The idea is not "hide things at low zoom" — hiding alone still leaves an
579
- unreadable title. What survives is drawn at a constant SCREEN size: a world
580
- font-size of 11px/k is 11px after the canvas transform, whatever k is.
581
- canvas.js publishes k as --k and the reading as data-lod on the root, and
582
- every card responds through the cascade, so a band change costs nothing per
583
- node and cannot move the world that decided the band.
584
-
585
- Registered so that a malformed write falls back to 1 rather than blanking
586
- 250 cards. k is clamped to MIN_K = 0.06 in canvas.js, so no divisor here is
587
- ever zero. (The selfcheck's CSS parser drops at-rules, so this registration
588
- is invisible to the gate — it is verified in a browser.) */
589
- @property --k { syntax: '<number>'; initial-value: 1; inherits: true; }
590
-
591
- /* NEAR — k >= 0.70. The card exactly as it has always been drawn, so it has
592
- no rules of its own here. */
593
-
594
- /* Blocks a smaller reading gives up fade rather than vanish, so the eye reads
595
- a substitution instead of a loss. */
596
- .cv-desc, .cv-foot, .cv-chip { transition: opacity 120ms; }
597
- /* The same element is the 14px source glyph up close and the hue disc far
598
- out; it travels there rather than being swapped. */
599
- .cv-mark { transition: transform 160ms ease, width 160ms ease, height 160ms ease; }
600
-
601
- /* MID — 0.20 <= k < 0.70, under the label budget. Short type, status chip,
602
- mark, fold caret. The description and the foot bits are dropped not because
603
- they are small but because they are DARK: the description fills the lower
604
- half of the card in unreadable grey, and forty of those lower halves is the
605
- noise floor that stops the type line reading. Removing them is what creates
606
- the contrast the type needs. */
607
- [data-lod="mid"] .cv-desc,
608
- [data-lod="mid"] .cv-foot { opacity: 0; }
609
-
610
- /* The chip keeps its channel and loses its words. Its text is a 10px absolute
611
- glyph, so at the bottom of this band it is 2px on screen — unreadable, and
612
- measured in a browser it was eating the width the type line needed: SESSION
613
- was clipping to SESSI and REVIEWER to REVIEWE. As a dot it still says the
614
- status in colour and gives 45 world px back to the name. */
615
- [data-lod="mid"] .cv-chip {
616
- font-size: 0;
617
- padding: 0;
618
- width: calc(7px / var(--k)); /* 7 SCREEN px at any k */
619
- height: calc(7px / var(--k));
620
- border-radius: 50%;
621
- background: currentColor;
622
- border-color: currentColor;
623
- }
624
-
625
- /* The real type line is taken out of the flow and redrawn counter-scaled, as
626
- the short name: the crew- prefix and the -expert / -agent suffix dropped. */
627
- [data-lod="mid"] .cv-type,
628
- [data-lod="far"] .cv-type { font-size: 0; letter-spacing: 0; }
629
- [data-lod="mid"] .cv-type::after,
630
- [data-lod="far"] .cv-type::after { content: none; }
631
- [data-lod="mid"] .cv-type::before {
632
- content: attr(data-short);
633
- font-size: calc(11px / var(--k)); /* 11 SCREEN px at any k */
634
- letter-spacing: 0.04em;
635
- transition: font-size 120ms ease;
636
- }
637
-
638
- /* MID, THE LIVE EXEMPTION. `far` below already establishes this principle —
639
- "with 250 strokes on screen the only bright ones must be the branches that
640
- are alive" — and gives the live card its name, its mark and a z-index. It
641
- was never applied to THIS band, which is the one a viewer actually sits in:
642
- measured at 60% with fourteen agents, and there the running card is
643
- distinguishable from thirteen finished ones only by the hue of a 7px disc.
644
- The panel's own README promises "which agent is running, what it is doing
645
- right now"; at mid it was keeping neither.
646
- The description is dropped at mid because forty dark lower halves are a
647
- noise floor that stops the type line reading. ONE is not a floor — it is
648
- the signal, and it is the only field that says WHICH agent. It is drawn
649
- counter-scaled like every other survivor in this block, so it clears the
650
- same 9px screen floor that set the band rather than undercutting it, and
651
- clamped to one line so it cannot reclaim the whole lower half. */
652
- [data-lod="mid"] .cv-node[data-state="live"] .cv-desc {
653
- opacity: 1;
654
- font-size: calc(11px / var(--k)); /* 11 SCREEN px at any k */
655
- -webkit-line-clamp: 1; line-clamp: 1;
656
- }
657
- /* The chip keeps its WORD here instead of collapsing to a disc. The disc
658
- exists because 10px of chip text is 2px on screen at the bottom of this
659
- band and was clipping the type line; the running card is the one place that
660
- trade is wrong, so its chip is counter-scaled instead of silenced. Every
661
- property the disc rule set has to be undone explicitly, or the word renders
662
- inside a 7px circle. */
663
- [data-lod="mid"] .cv-node[data-state="live"] .cv-chip {
664
- font-size: calc(9px / var(--k));
665
- padding: 0 calc(5px / var(--k));
666
- width: auto; height: auto;
667
- border-radius: calc(999px / var(--k));
668
- background: transparent;
669
- border-color: color-mix(in srgb, var(--busy) 45%, transparent);
670
- }
671
- /* Rides above its neighbours for the same reason it does at `far`: the words
672
- it just kept must not sit under the next card. */
673
- [data-lod="mid"] .cv-node[data-state="live"] { z-index: 5; }
674
-
675
- /* FAR — k < 0.20, or more drawn nodes than the label budget. No text is
676
- attempted for an ordinary node, because none would survive. Three things
677
- still resolve at this size and each gets a channel: POSITION (which type
678
- group, which depth — the layout already encodes it), HUE (--node-color, the
679
- one place colour is decided) and a RING (status). */
680
- [data-lod="far"] {
681
- /* Screen diameter of the disc: floored so it survives the tightest node
682
- pitch, capped so it never outgrows its cell. Divided back out of k to get
683
- the world size that draws it. */
684
- --dot-screen: clamp(5px, calc(88 * var(--k) * 1px), 16px);
685
- --dots: calc(var(--dot-screen) / var(--k));
686
- }
687
-
688
- /* The card body stops being a rectangle, because the rectangle is the wall. */
689
- [data-lod="far"] .cv-node {
690
- background: transparent;
691
- border-color: transparent;
692
- box-shadow: none;
693
- /* Pinned rather than left to min-height. A long description pushes the box
694
- past 104px, and since the disc is centred on the box rather than on the
695
- cell, cards with more text put their disc a few world px lower than their
696
- neighbours — a lattice of dots that does not quite line up. Nothing
697
- visible is clipped: what overflows here is already at opacity 0. */
698
- height: 104px;
699
- }
700
- /* Except the one the user picked, which still has to be findable. */
701
- [data-lod="far"] .cv-node.cv-selected {
702
- box-shadow: 0 0 0 calc(1.5px / var(--k)) var(--node-color);
703
- }
704
- [data-lod="far"] .cv-desc,
705
- [data-lod="far"] .cv-foot,
706
- [data-lod="far"] .cv-chip { opacity: 0; }
707
- [data-lod="far"] .cv-accent,
708
- [data-lod="far"] .cv-port,
709
- [data-lod="far"] .cv-fold { display: none; }
710
- /* The card ring is a 236x104 rectangle; the disc carries "running" instead. */
711
- [data-lod="far"] .cv-node[data-state="live"]::after { display: none; }
712
-
713
- /* .cv-mark becomes the disc, in place. */
714
- [data-lod="far"] .cv-mark {
715
- position: absolute;
716
- left: 18px; /* the padding it already sat behind */
717
- top: 12px;
718
- width: var(--dots);
719
- height: var(--dots);
720
- /* 93 and 33 carry a 14px mark from the head to the card's visual centre —
721
- (236-14)/2 - 18 and (104-14)/2 - 12 — and the half-diameter term holds the
722
- disc on that centre as it grows. */
723
- transform: translate(calc(93px + 7px - var(--dots) / 2), calc(33px + 7px - var(--dots) / 2));
724
- border-radius: 50%;
725
- background: var(--node-color);
726
- /* A screen-constant hairline around every agent circle. */
727
- box-shadow: 0 0 0 calc(1.4px / var(--k)) color-mix(in srgb, var(--ink) 34%, transparent);
728
- }
729
- /* An 11-ray burst is mush at 7px. */
730
- [data-lod="far"] .cv-mark > * { display: none; }
731
- /* Source survives as SHAPE, which reads at 7px where a glyph does not. */
732
- [data-lod="far"] .cv-node[data-source="kit"] .cv-mark { border-radius: 3px; }
733
- /* Status survives as the ring — the chip's colour, moved. */
734
- [data-lod="far"] .cv-node[data-state="live"] .cv-mark {
735
- box-shadow: 0 0 0 calc(2px / var(--k)) var(--busy);
736
- }
737
- [data-lod="far"] .cv-node[data-state="failed"] .cv-mark {
738
- box-shadow: 0 0 0 calc(2px / var(--k)) var(--cv-fail);
739
- }
740
-
741
- /* Who keeps a label out here. This is what makes the far band a picture
742
- rather than a field of anonymous dots, and it answers the question the panel
743
- exists for — which agent is running, and what is it doing — without a click.
744
- Running and failed branches keep their words whatever the budget says; the
745
- session and the workflows keep theirs because they are few and they anchor
746
- the picture.
747
-
748
- POSITIVE SELECTORS ONLY. The selfcheck's matcher treats a pseudo-class it
749
- does not model as a non-match, so a :not() rule here would resolve to "no
750
- match" in the gate and pass green for a rule the browser is running and the
751
- test never saw. */
752
- [data-lod="far"] .cv-node[data-state="live"] .cv-type::before,
753
- [data-lod="far"] .cv-node[data-state="failed"] .cv-type::before,
754
- [data-lod="far"] .cv-node[data-kind="session"] .cv-type::before,
755
- [data-lod="far"] .cv-node[data-kind="workflow"] .cv-type::before {
756
- content: attr(data-far);
757
- font-size: calc(11px / var(--k));
758
- letter-spacing: 0.04em;
759
- transition: font-size 120ms ease;
760
- }
761
- /* Centred under its own disc and allowed out of the card, because at this size
762
- the card is no longer a box — the disc is the node. */
763
- [data-lod="far"] .cv-node[data-state="live"] .cv-type,
764
- [data-lod="far"] .cv-node[data-state="failed"] .cv-type,
765
- [data-lod="far"] .cv-node[data-kind="session"] .cv-type,
766
- [data-lod="far"] .cv-node[data-kind="workflow"] .cv-type {
767
- position: absolute;
768
- left: 50%; /* = 118px, the disc's centre */
769
- top: calc(52px + var(--dots) * 0.62);
770
- transform: translateX(-50%);
771
- overflow: visible;
772
- text-overflow: clip;
773
- }
774
- /* Their words ride above the field rather than under the next card. */
775
- [data-lod="far"] .cv-node[data-state="live"],
776
- [data-lod="far"] .cv-node[data-state="failed"] { z-index: 5; }
777
-
778
- /* With 250 strokes on screen the only bright ones must be the branches that
779
- are alive and the branches that went wrong. */
780
- [data-lod="far"] .cv-edge[data-state="done"],
781
- [data-lod="far"] .cv-edge[data-state="quiet"] { opacity: var(--cv-edge-quiet); }
1116
+ .inspector dd { margin: 0; font-family: var(--font-mono); font-size: 12px; word-break: break-word; }
782
1117
 
783
1118
  /* ------------------------------------------------------- stillness ---
784
1119
  Two ways the canvas stops moving, and one set of rules for both.
@@ -795,17 +1130,8 @@ body.resizing .cv-root { pointer-events: none; }
795
1130
 
796
1131
  [data-motion="still"] .cv-edge,
797
1132
  [data-motion="still"] .cv-edge.cv-drawing,
798
- [data-motion="still"] .cv-node::after { animation: none; }
1133
+ [data-motion="still"] .cv-node[data-kind][data-state]::after { animation: none; }
799
1134
  [data-motion="still"] .cv-node.cv-born { opacity: 1; scale: 1; }
800
- /* A reading change is motion too — the mark travels to the card's centre and
801
- the label resizes. `.cv-node { transition: none }` does not reach a child;
802
- that is specificity, not the media query, so each of these carries at least
803
- the weight of the rule it cancels. */
804
- [data-motion="still"] .cv-mark,
805
- [data-motion="still"] .cv-type::before,
806
- [data-motion="still"] .cv-desc,
807
- [data-motion="still"] .cv-foot,
808
- [data-motion="still"] .cv-chip { transition: none; }
809
1135
 
810
1136
  /* A media query adds no specificity. `.cv-edge` alone loses to
811
1137
  `.cv-edge[data-state="live"]` and the flow keeps running, so each selector
@@ -813,13 +1139,8 @@ body.resizing .cv-root { pointer-events: none; }
813
1139
  @media (prefers-reduced-motion: reduce) {
814
1140
  .cv-edge[data-state],
815
1141
  .cv-edge.cv-drawing,
816
- .cv-node[data-state]::after { animation: none; }
1142
+ .cv-node[data-kind][data-state]::after { animation: none; }
817
1143
  .cv-node { transition: none; }
818
- .cv-node .cv-mark,
819
- .cv-node .cv-type::before,
820
- .cv-node .cv-desc,
821
- .cv-node .cv-foot,
822
- .cv-node .cv-chip { transition: none; }
823
1144
  /* The birth class comes off a frame later, which with no transition left is
824
1145
  a blink from invisible to placed. Land it in its final state instead. */
825
1146
  .cv-node.cv-born { opacity: 1; scale: 1; }
@@ -827,27 +1148,71 @@ body.resizing .cv-root { pointer-events: none; }
827
1148
 
828
1149
  /* ==================================================== inspector detail === */
829
1150
 
830
- .inspector { width: 340px; transition: width 0.18s; display: flex; flex-direction: column; padding: 0; }
831
- .inspector.wide { width: min(760px, 55vw); }
832
1151
 
833
- .ihead { display: flex; align-items: center; gap: 8px; padding: 14px 16px 0; }
834
- .ihead h3 { flex: 1; margin: 0; }
835
- .iwide { padding: 2px 8px; font-size: 12px; line-height: 1.2; }
836
- .inspector .isub { padding: 4px 16px 12px; margin: 0; }
1152
+ .ihead { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-3) var(--space-3) 0 var(--space-4); flex: none; }
1153
+ .ihead .cv-tile { flex: none; }
1154
+ .iname { flex: 1; min-width: 0; margin: 0; font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1155
+ .imeta { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-4) 0; flex: none; }
1156
+ .imeta .sub { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
1157
+ /* The task in full: it is the one place the whole sentence is shown. */
1158
+ .itask { padding: var(--space-3) var(--space-4); font-size: 16px; font-weight: 600; line-height: 1.35; overflow-wrap: anywhere; flex: none; max-height: 30%; overflow-y: auto; }
1159
+ .itask:empty { padding-bottom: 0; }
837
1160
 
838
- .itabs { display: flex; gap: 2px; padding: 0 12px; border-bottom: 1px solid var(--line); flex: none; }
1161
+ .itabs { display: flex; gap: var(--space-1); padding: 0 var(--space-3); border-bottom: 1px solid var(--line); flex: none; }
839
1162
  .itab {
840
1163
  background: none; border: none; cursor: pointer;
841
- font-family: var(--mono); font-size: 11px; letter-spacing: 0.05em; text-transform: uppercase;
842
- color: var(--ink-faint); padding: 8px 10px; border-bottom: 2px solid transparent; margin-bottom: -1px;
1164
+ font-family: inherit; font-size: 13px; font-weight: 500;
1165
+ color: var(--ink-dim); padding: var(--space-2) 6px; border-bottom: 2px solid transparent; margin-bottom: -1px;
843
1166
  }
844
- .itab:hover { color: var(--ink-dim); }
845
- .itab.on { color: var(--ink); border-bottom-color: var(--accent); }
1167
+ .itab:hover { color: var(--ink); }
1168
+ .itab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); }
846
1169
 
847
- .ibody { flex: 1; overflow-y: auto; padding: 14px 16px 24px; min-height: 0; }
1170
+ .ibody { flex: 1; overflow-y: auto; padding: var(--space-4); min-height: 0; }
1171
+ .ifoot { display: flex; gap: var(--space-2); padding: var(--space-3) var(--space-4); border-top: 1px solid var(--line); flex: none; }
1172
+ .ifoot .btn:first-child { flex: 1; justify-content: center; }
1173
+
1174
+ .isec { margin-top: var(--space-4); }
1175
+ .isec:first-child { margin-top: 0; }
1176
+ .isec-h {
1177
+ display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
1178
+ margin: 0 0 var(--space-2); font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
1179
+ color: var(--ink-faint);
1180
+ }
1181
+ .itiles { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); }
1182
+ .itile { display: flex; flex-direction: column; gap: 2px; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); background: var(--surface-raised); min-width: 0; }
1183
+ .itile-k { font-size: 11px; color: var(--ink-dim); }
1184
+ .itile strong { font-size: 18px; font-weight: 600; font-variant-numeric: tabular-nums; }
1185
+ .itile strong.bad { color: var(--status-bad); }
1186
+ /* Not measured is a sentence, not a number: it is set smaller and in the waiting tone, so it cannot pass for one. */
1187
+ .itile.unmeasured strong { font-size: 13px; color: var(--status-waiting); }
1188
+ .inow {
1189
+ display: flex; flex-direction: column; gap: 2px; padding: var(--space-2) var(--space-3);
1190
+ border-radius: var(--radius-md); background: var(--surface-raised);
1191
+ font-family: var(--font-mono); font-size: 12px; overflow-wrap: anywhere;
1192
+ }
1193
+ .inow-tool { color: var(--studio-accent); }
1194
+ .inow-text { color: var(--ink-dim); }
1195
+ .ichips { display: flex; flex-wrap: wrap; gap: 6px; }
1196
+ .ichip {
1197
+ height: 24px; padding: 0 var(--space-2); border-radius: var(--radius-sm); cursor: pointer;
1198
+ border: 1px solid var(--line); background: none; color: var(--ink);
1199
+ font-family: var(--font-mono); font-size: 12px;
1200
+ }
1201
+ .ichip:hover { background: var(--surface-raised); }
1202
+ .ilink { background: none; border: none; padding: 0; cursor: pointer; color: var(--accent); font: inherit; font-size: 13px; text-align: left; }
1203
+ .ilink:hover { text-decoration: underline; }
1204
+ .ibanner {
1205
+ padding: var(--space-3); margin-bottom: var(--space-4); border-radius: var(--radius-md);
1206
+ border: 1px solid color-mix(in srgb, var(--status-waiting) 45%, var(--line));
1207
+ background: color-mix(in srgb, var(--status-waiting) 8%, transparent);
1208
+ font-size: 13px; line-height: 1.45;
1209
+ }
1210
+ .ibanner p { margin: 0 0 var(--space-3); }
1211
+ .ibanner-acts { display: flex; gap: var(--space-2); flex-wrap: wrap; }
1212
+ .iallow-row { display: flex; align-items: center; gap: var(--space-2); padding: 6px 0; border-bottom: 1px solid var(--line); }
1213
+ .iallow-tool { font-family: var(--font-mono); font-size: 12px; font-weight: 600; }
1214
+ .ihint .btn { margin-top: var(--space-2); }
848
1215
  .ihint { color: var(--ink-faint); font-size: 13px; line-height: 1.5; padding: 6px 0; }
849
- .ibar { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
850
- .ibar .ghost { font-family: var(--mono); font-size: 11px; padding: 2px 8px; }
851
1216
 
852
1217
  /* ------------------------------------------------------- activity list --- */
853
1218
 
@@ -856,8 +1221,8 @@ body.resizing .cv-root { pointer-events: none; }
856
1221
  display: grid; grid-template-columns: 22px 62px 1fr; gap: 8px; align-items: baseline;
857
1222
  padding: 5px 0; border-bottom: 1px solid var(--line);
858
1223
  }
859
- .istep-n { font-family: var(--mono); font-size: 10px; color: var(--ink-faint); text-align: right; }
860
- .istep-tool { font-family: var(--mono); font-size: 11px; color: var(--accent); }
1224
+ .istep-n { font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint); text-align: right; }
1225
+ .istep-tool { font-family: var(--font-mono); font-size: 11px; color: var(--studio-accent); }
861
1226
  .istep-label { font-size: 12px; color: var(--ink-dim); overflow-wrap: anywhere; }
862
1227
  .md-narration { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--line); opacity: 0.85; }
863
1228
 
@@ -872,18 +1237,18 @@ h5.md-h, h6.md-h { font-size: 12px; color: var(--ink-dim); text-transform: upper
872
1237
  .md-list { margin: 0 0 9px; padding-left: 20px; }
873
1238
  .md-list li { margin-bottom: 3px; }
874
1239
  .md code {
875
- font-family: var(--mono); font-size: 0.9em;
876
- background: var(--surface-2); border-radius: 4px; padding: 1px 4px;
1240
+ font-family: var(--font-mono); font-size: 0.9em;
1241
+ background: var(--surface-raised); border-radius: 4px; padding: 1px 4px;
877
1242
  }
878
1243
  .md-code {
879
- background: var(--surface-2); border: 1px solid var(--line); border-radius: 8px;
1244
+ background: var(--surface-raised); border: 1px solid var(--line); border-radius: 8px;
880
1245
  padding: 10px 12px; overflow-x: auto; margin: 0 0 10px;
881
- font-family: var(--mono); font-size: 11.5px; line-height: 1.5;
1246
+ font-family: var(--font-mono); font-size: 11.5px; line-height: 1.5;
882
1247
  }
883
1248
  .md-code code { background: none; padding: 0; }
884
1249
  .md-quote {
885
1250
  margin: 0 0 10px; padding: 6px 0 6px 12px;
886
- border-left: 2px solid var(--accent); color: var(--ink-dim);
1251
+ border-left: 2px solid var(--studio-accent); color: var(--ink-dim);
887
1252
  }
888
1253
  .md-hr { border: none; border-top: 1px solid var(--line); margin: 16px 0; }
889
1254
  .md-link { color: var(--accent); }
@@ -892,11 +1257,7 @@ h5.md-h, h6.md-h { font-size: 12px; color: var(--ink-dim); text-transform: upper
892
1257
  .md-table th, .md-table td {
893
1258
  border: 1px solid var(--line); padding: 5px 9px; text-align: left; vertical-align: top;
894
1259
  }
895
- .md-table th { background: var(--surface-2); font-weight: 600; white-space: nowrap; }
896
-
897
- /* `display: flex` on .inspector outranks the UA's `[hidden] { display: none }`,
898
- so the hidden panel kept its width. Restate it explicitly. */
899
- .inspector[hidden] { display: none; }
1260
+ .md-table th { background: var(--surface-raised); font-weight: 600; white-space: nowrap; }
900
1261
 
901
1262
  /* `overflow-wrap: anywhere` keeps long paths inside the panel, but inside a
902
1263
  table it shreds every cell into one letter per line. Tables get their real
@@ -904,134 +1265,23 @@ h5.md-h, h6.md-h { font-size: 12px; color: var(--ink-dim); text-transform: upper
904
1265
  .md-table th, .md-table td { overflow-wrap: normal; min-width: 76px; }
905
1266
  .md-table td { hyphens: none; }
906
1267
 
907
- /* ================================================= project sidebar === */
908
-
909
- .side-filter { padding: 0 12px 8px; flex: none; }
910
- .side-filter input {
911
- width: 100%; box-sizing: border-box;
912
- background: var(--bg); border: 1px solid var(--line); border-radius: 7px;
913
- color: var(--ink); font-family: var(--mono); font-size: 11px; padding: 5px 8px;
914
- }
915
- .side-filter input:focus { outline: none; border-color: var(--accent); }
916
- .side-filter input::placeholder { color: var(--ink-faint); }
917
-
918
- .pgroup {
919
- display: grid; grid-template-columns: 14px 1fr auto; gap: 7px; align-items: center;
920
- padding: 7px 14px; cursor: pointer; border-top: 1px solid var(--line);
921
- background: var(--surface-2);
922
- }
923
- .pgroup:hover { background: color-mix(in srgb, var(--accent) 8%, var(--surface-2)); }
924
- .pgroup.current { box-shadow: inset 3px 0 0 var(--accent); }
925
- .pcaret { font-size: 9px; color: var(--ink-faint); }
926
- .plabel {
927
- font-size: 12px; font-weight: 600;
928
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
929
- }
930
- .pcount { display: flex; gap: 5px; align-items: center; font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
931
-
932
- .srow {
933
- display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center;
934
- padding: 6px 14px 6px 28px; border-top: 1px solid var(--line);
935
- background: var(--surface); cursor: pointer;
936
- }
937
- .srow:hover { background: var(--surface-2); }
938
- .srow[aria-current="true"] { background: var(--surface-2); box-shadow: inset 3px 0 0 var(--good); }
939
- .sname {
940
- font-size: 12px; color: var(--ink);
941
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
942
- }
943
- .srow[aria-current="true"] .sname { font-weight: 600; }
944
- .sright { display: flex; gap: 5px; align-items: center; }
945
- .sagents { font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
946
- .sbadge {
947
- font-family: var(--mono); font-size: 9px; color: var(--accent);
948
- border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
949
- border-radius: 4px; padding: 0 4px;
950
- }
951
- .ptrunc {
952
- padding: 5px 14px 5px 28px; font-family: var(--mono); font-size: 10px;
953
- color: var(--ink-faint); background: var(--surface); border-top: 1px solid var(--line);
954
- }
955
-
956
- /* ------------------------------------------------------- kit version --- */
957
-
958
- .kitbadge {
959
- font-family: var(--mono); font-size: 9px; padding: 0 4px;
960
- border-radius: 4px; border: 1px solid var(--line); color: var(--ink-faint);
961
- white-space: nowrap;
962
- }
963
- .kitbadge.ok { color: var(--good); border-color: color-mix(in srgb, var(--good) 35%, transparent); }
964
- .kitbadge.old { color: var(--waiting); border-color: color-mix(in srgb, var(--waiting) 50%, transparent);
965
- background: color-mix(in srgb, var(--waiting) 12%, transparent); }
966
- .kitbadge.ahead { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, transparent); }
967
- .kitbadge.unknown { color: var(--ink-faint); }
968
-
969
- .pgroup.gone .plabel { color: var(--ink-faint); text-decoration: line-through; }
970
-
971
- .kitline {
972
- display: block; font-family: var(--mono); font-size: 10px;
973
- color: var(--ink-faint); letter-spacing: 0.03em; margin-top: 1px;
974
- }
975
- .kitline.warn { color: var(--waiting); }
976
-
977
- .ptrunc.pmissing { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
978
- .ptrunc.pmissing .ghost { font-family: var(--mono); font-size: 10px; padding: 1px 6px; }
979
-
980
- /* ------------------------------------------------------- fold control --- */
981
-
982
- .cv-fold {
983
- background: transparent; border: 1px solid var(--line); border-radius: 5px;
984
- color: var(--ink-dim); cursor: pointer; flex: none;
985
- font-family: var(--mono); font-size: 9px; line-height: 1; padding: 2px 5px;
986
- }
987
- .cv-fold:hover { color: var(--ink); border-color: var(--node-color); }
988
- .cv-fold[hidden] { display: none; }
989
-
990
- /* A folded node is a container, so it reads as one: stacked edges behind the
991
- card stand in for the children it is holding. */
992
- .cv-node.cv-folded { box-shadow: 0 1px 2px rgb(0 0 0 / 0.18), 6px 6px 0 -2px var(--surface-2), 6px 6px 0 -1px var(--line); }
993
- .cv-node[data-kind="workflow"] { border-style: dashed; }
994
- .cv-node[data-kind="workflow"] .cv-desc { font-family: var(--mono); font-size: 11px; color: var(--ink-dim); }
995
-
996
- /* The fold control is an affordance, not decoration: when a node is holding
997
- children back it says how many, in the node's own colour. */
998
- .cv-node.cv-folded .cv-fold {
999
- color: var(--node-color);
1000
- border-color: color-mix(in srgb, var(--node-color) 45%, transparent);
1001
- background: color-mix(in srgb, var(--node-color) 12%, transparent);
1002
- font-weight: 600;
1003
- }
1004
-
1005
1268
  /* ---------------------------------------------------------- origins --- */
1006
1269
 
1007
1270
  /* Which machine a row came from, shown only once more than one is in view. */
1008
1271
  .origin {
1009
1272
  margin-left: 6px; padding: 0 4px; border-radius: 4px;
1010
- font-family: var(--mono); font-size: 9px; font-weight: 400;
1011
- color: var(--accent);
1012
- border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
1273
+ font-family: var(--font-mono); font-size: 9px; font-weight: 400;
1274
+ color: var(--studio-accent);
1275
+ border: 1px solid color-mix(in srgb, var(--studio-accent) 32%, transparent);
1013
1276
  vertical-align: 1px;
1014
1277
  }
1015
- .meta.warn { color: var(--waiting); }
1016
- .ptrunc.pdown { color: var(--bad); }
1278
+ .meta.warn { color: var(--status-waiting); }
1017
1279
 
1018
1280
  /* ================================================== conversation ====== */
1019
1281
 
1020
- .stage { position: relative; min-width: 0; display: block; }
1021
- .canvas { position: absolute; inset: 0; }
1022
-
1023
- /* The inspector floats over the canvas rather than taking a column of its own:
1024
- with the conversation on the right there is no third column to spare, and a
1025
- node's details are something you open and dismiss. */
1026
- .inspector {
1027
- position: absolute; top: 0; right: 0; bottom: 0; z-index: 30;
1028
- border-left: 1px solid var(--line);
1029
- box-shadow: -12px 0 24px -18px rgb(0 0 0 / 0.7);
1030
- }
1031
-
1032
1282
  .hsplit { height: 5px; background: var(--line); cursor: row-resize; position: relative; }
1033
1283
  .hsplit::after { content: ''; position: absolute; inset: -4px 0; }
1034
- .hsplit:hover, .hsplit:focus-visible, .hsplit.dragging { background: var(--accent); outline: none; }
1284
+ .hsplit:hover, .hsplit.dragging { background: var(--studio-accent); }
1035
1285
  .hsplit[hidden] { display: none; }
1036
1286
  body.vresizing { cursor: row-resize; user-select: none; }
1037
1287
 
@@ -1045,283 +1295,246 @@ body.vresizing { cursor: row-resize; user-select: none; }
1045
1295
  .chat[hidden] { display: none; }
1046
1296
 
1047
1297
  .chat-head {
1048
- display: flex; align-items: center; gap: 10px;
1049
- padding: 8px 14px; border-bottom: 1px solid var(--line); flex: none;
1050
- }
1051
- .chat-title {
1052
- font-size: 11px; font-weight: 600; letter-spacing: 0.08em;
1053
- text-transform: uppercase; color: var(--ink-faint);
1298
+ display: flex; align-items: center; gap: var(--space-2);
1299
+ height: var(--toolbar-h); padding: 0 var(--space-2) 0 var(--space-3);
1300
+ border-bottom: 1px solid var(--line); flex: none;
1054
1301
  }
1055
- .chat-state {
1056
- font-family: var(--mono); font-size: 10px; color: var(--ink-dim);
1057
- border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px;
1302
+
1303
+ /* The strip above a conversation: what kind of session it is, how far it has gone, and Stop. */
1304
+ .pane-head {
1305
+ display: flex; align-items: center; gap: var(--space-2);
1306
+ min-height: 44px; padding: 6px var(--space-3); border-bottom: 1px solid var(--line); flex: none; min-width: 0;
1307
+ /* On a narrow panel the strip takes a second line instead of pushing Stop out of reach. */
1308
+ flex-wrap: wrap;
1058
1309
  }
1059
- .chat-state[data-state="working"] { color: var(--busy); border-color: color-mix(in srgb, var(--busy) 45%, transparent); }
1060
- .chat-state[data-state="idle"] { color: var(--good); border-color: color-mix(in srgb, var(--good) 35%, transparent); }
1061
- .chat-state[data-state="exited"], .chat-state[data-state="failed"] { color: var(--bad); }
1062
- .chat-cost { flex: 1; font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
1063
- .chat-stop { font-family: var(--mono); font-size: 11px; padding: 2px 8px; }
1310
+ .pane-head .sub { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
1311
+ .pane-head .pane-cost { font-family: var(--font-mono); }
1312
+ .pane-ungated { color: var(--status-bad); background: color-mix(in srgb, var(--status-bad) 12%, transparent); }
1313
+ .chat-stop { flex: none; }
1314
+ .chat-stop .ic { fill: currentColor; stroke: none; }
1064
1315
 
1065
- .chat-log { flex: 1; overflow-y: auto; padding: 12px 14px; min-height: 0; }
1316
+ .chat-log { flex: 1; overflow-y: auto; padding: var(--space-4); min-height: 0; }
1066
1317
 
1067
- /* Who said it, read from which side it sits on. The role label stays, small,
1068
- because a bubble tells you the side but not the name — and once a session
1069
- spawns agents there is more than one voice in here. */
1070
- .msg { display: flex; flex-direction: column; gap: 3px; margin-bottom: 14px; }
1318
+ /* Who said it is read from the side it sits on: what the viewer wrote is a bubble on the right, what the session
1319
+ said runs the width of the panel under its name. */
1320
+ .msg { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-4); }
1071
1321
  .msg-user { align-items: flex-end; }
1072
- .msg-assistant { align-items: flex-start; }
1322
+ .msg-assistant { align-items: stretch; }
1073
1323
 
1074
- .msg-role {
1075
- font-family: var(--mono); font-size: 9px; text-transform: uppercase;
1076
- letter-spacing: 0.08em; color: var(--ink-faint); padding: 0 4px;
1077
- }
1078
- .msg-user .msg-role { color: var(--accent); }
1324
+ .msg-role { font-size: 12px; font-weight: 600; color: var(--ink-dim); }
1079
1325
 
1080
- .msg-body {
1081
- max-width: 88%;
1082
- min-width: 0;
1083
- font-size: 13px; line-height: 1.55;
1084
- border-radius: 12px;
1085
- padding: 8px 12px;
1086
- overflow-wrap: anywhere;
1087
- }
1326
+ .msg-body { min-width: 0; font-size: 14px; line-height: 1.5; overflow-wrap: anywhere; }
1088
1327
  .msg-user .msg-body {
1089
- background: color-mix(in srgb, var(--accent) 18%, var(--surface-2));
1090
- border-bottom-right-radius: 3px;
1091
- }
1092
- .msg-assistant .msg-body {
1093
- background: var(--surface-2);
1094
- border-bottom-left-radius: 3px;
1328
+ max-width: 88%; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
1329
+ background: color-mix(in srgb, var(--accent) 16%, var(--surface-raised));
1095
1330
  }
1331
+ .msg-pending .msg-body { opacity: 0.6; }
1096
1332
  .msg-text { white-space: pre-wrap; }
1097
1333
 
1098
- /* A reply carrying a table or a code block needs the room; a sentence does not. */
1099
- .msg-assistant .msg-body:has(.md-table, .md-code) { max-width: 100%; }
1100
-
1101
1334
  .msg-streaming .msg-body { color: var(--ink-dim); }
1102
- .thinking { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); }
1335
+ .thinking { font-family: var(--font-mono); font-size: 11px; color: var(--ink-faint); }
1103
1336
  .thinking::after { content: ''; animation: dots 1.2s steps(4, end) infinite; }
1104
1337
  @keyframes dots { 0% { content: ''; } 25% { content: '.'; } 50% { content: '..'; } 75% { content: '...'; } }
1105
1338
 
1106
- /* Quick replies belong to the answer above them, so they sit on its side. */
1107
- .msg-assistant .quick { justify-content: flex-start; }
1108
-
1109
- .tool-card {
1110
- display: flex; gap: 8px; align-items: baseline;
1111
- background: var(--surface-2); border-radius: 6px; padding: 4px 8px; margin: 4px 0;
1112
- }
1113
- .tool-name { font-family: var(--mono); font-size: 10px; color: var(--accent); flex: none; }
1114
- .tool-label { font-size: 12px; color: var(--ink-dim); overflow-wrap: anywhere; }
1115
-
1116
- .chat-note { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); padding: 4px 0; }
1117
- .chat-note.bad { color: var(--bad); }
1118
-
1119
- .chat-form { display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--line); flex: none; }
1339
+ /* A tool call: one line that says what ran and how it ended. Its output opens under it. */
1340
+ .tool { margin: var(--space-2) 0; }
1341
+ .tool-row {
1342
+ display: flex; align-items: baseline; gap: var(--space-2); width: 100%; min-width: 0;
1343
+ padding: 6px var(--space-2); border: 1px solid var(--line); border-radius: var(--radius-sm);
1344
+ background: none; color: var(--ink); cursor: pointer; text-align: left;
1345
+ font-family: var(--font-mono); font-size: 12px;
1346
+ }
1347
+ .tool-row:hover { background: var(--surface-raised); }
1348
+ .tool-name { color: var(--studio-accent); flex: none; }
1349
+ .tool-label { flex: 1 1 0; min-width: 0; color: var(--ink-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1350
+ .tool-sum { flex: 0 1 auto; min-width: 0; max-width: 45%; color: var(--ink-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1351
+ .tool-sum[hidden] { display: none; }
1352
+ .tool-row[data-state="ok"] .tool-sum { color: var(--status-good); }
1353
+ .tool-row[data-state="error"] .tool-sum { color: var(--status-bad); }
1354
+ .tool-row[data-state="refused"] { border-color: color-mix(in srgb, var(--status-bad) 50%, var(--line)); }
1355
+ .tool-row[data-state="refused"] .tool-sum { color: var(--status-bad); }
1356
+ .tool-out {
1357
+ margin: var(--space-1) 0 0; padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm);
1358
+ background: var(--ground); border: 1px solid var(--line);
1359
+ font-family: var(--font-mono); font-size: 12px; line-height: 1.45;
1360
+ white-space: pre-wrap; overflow-wrap: anywhere; max-height: 320px; overflow-y: auto;
1361
+ }
1362
+ .tool-out[hidden] { display: none; }
1363
+
1364
+ /* The session handing work to an agent: a card that goes to that agent on the graph. */
1365
+ .deleg {
1366
+ display: flex; align-items: center; gap: var(--space-2); width: 100%; min-width: 0;
1367
+ margin: var(--space-2) 0; padding: var(--space-2) var(--space-3);
1368
+ border: 1px solid var(--line); border-radius: var(--radius-md);
1369
+ background: var(--surface-raised); color: var(--ink); cursor: pointer; text-align: left; font: inherit;
1370
+ }
1371
+ .deleg:hover:not(:disabled) { border-color: var(--studio-accent); }
1372
+ .deleg:disabled { cursor: default; }
1373
+ .deleg .cv-tile { flex: none; }
1374
+ .deleg-text { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; }
1375
+ .deleg-type { font-family: var(--font-mono); font-size: 12px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1376
+ .deleg-task { font-size: 12px; color: var(--ink-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1377
+
1378
+ /* A request waiting in this session, said where the conversation is being read. */
1379
+ .chat-remind {
1380
+ display: flex; align-items: center; gap: var(--space-2); flex: none;
1381
+ padding: var(--space-2) var(--space-3); font-size: 13px;
1382
+ border-top: 1px solid color-mix(in srgb, var(--status-waiting) 45%, var(--line));
1383
+ background: color-mix(in srgb, var(--status-waiting) 8%, var(--surface));
1384
+ }
1385
+ .chat-remind[hidden] { display: none; }
1386
+ .chat-remind-text { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1387
+ .chat-review { background: none; border: none; padding: 0; cursor: pointer; font: inherit; font-weight: 600; color: var(--accent); flex: none; }
1388
+ .chat-review:hover { text-decoration: underline; }
1389
+
1390
+ .chat-note { font-size: 12px; line-height: 18px; color: var(--ink-faint); padding: 4px 0; overflow-wrap: anywhere; }
1391
+ .chat-note.bad { color: var(--status-bad); }
1392
+
1393
+ .chat-form { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); border-top: 1px solid var(--line); flex: none; }
1120
1394
  .chat-form[hidden] { display: none; }
1121
1395
  .chat-input {
1122
- flex: 1; resize: none; box-sizing: border-box;
1123
- background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
1124
- color: var(--ink); font-family: inherit; font-size: 13px; line-height: 1.45; padding: 7px 10px;
1396
+ resize: none; box-sizing: border-box; width: 100%;
1397
+ background: var(--ground); border: 1px solid var(--line); border-radius: var(--radius-md);
1398
+ color: var(--ink); font-family: inherit; font-size: 14px; line-height: 20px; padding: var(--space-2) var(--space-3);
1125
1399
  }
1126
- .chat-input:focus { outline: none; border-color: var(--accent); }
1127
1400
  .chat-input:disabled { opacity: 0.5; }
1128
- .chat-send {
1129
- background: var(--accent); border: none; border-radius: 8px; color: #fff;
1130
- cursor: pointer; font-size: 12px; font-weight: 600; padding: 0 16px; align-self: stretch;
1131
- }
1132
- .chat-send:hover { filter: brightness(1.1); }
1133
-
1134
- .ghost.primary {
1135
- color: var(--accent);
1136
- border-color: color-mix(in srgb, var(--accent) 45%, transparent);
1137
- font-size: 12px;
1138
- }
1139
- .ghost.primary:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent); }
1140
-
1141
- .srow.owned .sname::after {
1142
- content: 'owned'; margin-left: 6px; padding: 0 4px; border-radius: 4px;
1143
- font-family: var(--mono); font-size: 9px; color: var(--good);
1144
- border: 1px solid color-mix(in srgb, var(--good) 35%, transparent);
1145
- }
1146
-
1147
- /* `display: grid` outranks the UA's `[hidden] { display: none }`, so the empty
1148
- note kept painting over a graph that had nodes. Same trap as .inspector. */
1149
- .cv-empty[hidden] { display: none; }
1150
-
1151
- /* ------------------------------------------------------- permissions --- */
1152
-
1153
- .perm-queue { flex: none; border-top: 1px solid var(--line); padding: 10px 14px; max-height: 46vh; overflow-y: auto; }
1154
- .perm-queue[hidden] { display: none; }
1155
-
1156
- .perm {
1157
- border: 1px solid color-mix(in srgb, var(--waiting) 45%, var(--line));
1158
- background: color-mix(in srgb, var(--waiting) 7%, transparent);
1159
- border-radius: 9px; padding: 9px 11px; margin-bottom: 8px;
1160
- }
1161
- .perm-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
1162
- .perm-tool {
1163
- font-family: var(--mono); font-size: 11px; font-weight: 600;
1164
- letter-spacing: 0.05em; text-transform: uppercase; color: var(--waiting);
1165
- }
1166
- .perm-clock { font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
1167
- .perm-detail {
1168
- margin: 7px 0 0; padding: 7px 9px; border-radius: 6px;
1169
- background: var(--surface-2); font-family: var(--mono); font-size: 11.5px;
1170
- line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere;
1171
- max-height: 140px; overflow-y: auto;
1172
- }
1173
- .perm-acts { display: flex; gap: 7px; margin-top: 9px; flex-wrap: wrap; }
1174
- .perm-btn {
1175
- background: transparent; border: 1px solid var(--line); border-radius: 7px;
1176
- color: var(--ink-dim); cursor: pointer; font-size: 12px; padding: 4px 11px;
1177
- }
1178
- .perm-btn:hover:not(:disabled) { color: var(--ink); border-color: var(--ink-faint); }
1179
- .perm-btn:disabled { opacity: 0.4; cursor: default; }
1180
- .perm-btn.ok { color: var(--good); border-color: color-mix(in srgb, var(--good) 40%, transparent); }
1181
- .perm-btn.bad { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent); }
1182
-
1183
- .chat-state.ungated { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 50%, transparent); }
1401
+ .chat-form-foot { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
1402
+ .chat-form-foot .sub { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
1184
1403
 
1185
1404
  /* ------------------------------------------------------ quick replies --- */
1186
1405
 
1187
- .quick { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 8px; }
1188
- .quick-hint {
1189
- font-family: var(--mono); font-size: 10px; letter-spacing: 0.05em;
1190
- text-transform: uppercase; color: var(--ink-faint);
1191
- }
1192
- .quick-btn {
1193
- background: transparent; border: 1px solid color-mix(in srgb, var(--accent) 38%, transparent);
1194
- border-radius: 999px; color: var(--accent); cursor: pointer;
1195
- font-size: 12px; padding: 3px 12px; max-width: 100%;
1196
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1197
- }
1198
- .quick-btn:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
1406
+ .quick { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin-top: var(--space-2); }
1407
+ .quick-btn { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
1408
+ .quick-hint { flex-basis: 100%; }
1199
1409
 
1200
1410
  /* --------------------------------------------------------- session tabs --- */
1201
1411
 
1202
- .panes { flex: 1; min-height: 0; display: flex; }
1203
- .pane { flex: 1; min-height: 0; display: flex; flex-direction: column; }
1412
+ /* `min-width: 0` on both: a flex item is as wide as its longest unbreakable line unless told otherwise, and a
1413
+ conversation is full of them — a path, a command. Without it one long line widened the panel past the window. */
1414
+ .panes { flex: 1; min-height: 0; min-width: 0; display: flex; }
1415
+ .pane { flex: 1; min-height: 0; min-width: 0; display: flex; flex-direction: column; }
1204
1416
  .pane[hidden] { display: none; }
1205
1417
 
1206
1418
  .tabs { display: flex; gap: 3px; flex: 1; overflow-x: auto; align-items: center; }
1207
1419
  .tab {
1208
1420
  display: flex; align-items: center; gap: 6px;
1209
- background: transparent; border: 1px solid var(--line); border-radius: 7px;
1210
- color: var(--ink-dim); cursor: pointer; font-family: var(--mono); font-size: 11px;
1211
- padding: 3px 7px; white-space: nowrap; flex: none;
1421
+ height: 30px; background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm);
1422
+ color: var(--ink-dim); cursor: pointer; font-family: inherit; font-size: 12px; font-weight: 600;
1423
+ padding: 0 var(--space-2); white-space: nowrap; flex: none;
1212
1424
  }
1213
1425
  .tab:hover { color: var(--ink); }
1214
- .tab.on { color: var(--ink); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
1426
+ .tab[aria-selected="true"] { color: var(--ink); background: var(--surface-raised); border-color: var(--line); }
1215
1427
 
1216
- .tab-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--idle); flex: none; }
1217
- .tab-dot[data-state="working"] { background: var(--busy); animation: breathe 1.6s ease-in-out infinite; }
1218
- .tab-dot[data-state="idle"] { background: var(--good); }
1219
- .tab-dot[data-state="starting"]{ background: var(--accent); }
1220
- .tab-dot[data-state="exited"], .tab-dot[data-state="failed"] { background: var(--bad); }
1428
+ .tab-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--status-idle); flex: none; }
1429
+ .tab-dot[data-state="working"] { background: var(--status-busy); animation: breathe 1.6s ease-in-out infinite; }
1430
+ .tab-dot[data-state="idle"] { background: var(--status-good); }
1431
+ .tab-dot[data-state="starting"]{ background: var(--studio-accent); }
1432
+ .tab-dot[data-state="exited"], .tab-dot[data-state="failed"] { background: var(--status-fail); }
1221
1433
 
1222
1434
  .tab-badge {
1223
- background: var(--surface-2); border-radius: 999px; padding: 0 5px;
1435
+ background: var(--surface-raised); border-radius: 999px; padding: 0 5px;
1224
1436
  font-size: 9px; color: var(--ink-faint);
1225
1437
  }
1226
- .tab-badge.warn { background: color-mix(in srgb, var(--waiting) 22%, transparent); color: var(--waiting); }
1438
+ .tab-badge.warn { background: color-mix(in srgb, var(--status-waiting) 22%, transparent); color: var(--status-waiting); }
1227
1439
  .tab-x { color: var(--ink-faint); font-size: 13px; line-height: 1; padding: 0 1px; }
1228
- .tab-x:hover { color: var(--bad); }
1440
+ .tab-x:hover { color: var(--status-bad); }
1229
1441
 
1230
1442
  @media (prefers-reduced-motion: reduce) { .tab-dot[data-state="working"] { animation: none; } }
1231
1443
 
1232
1444
  /* -------------------------------------------------------- kit telemetry --- */
1233
1445
 
1234
1446
  .kit-sum { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 10px; }
1235
- .kit-sum .cv-bit[data-verdict="BLOCK"] { color: var(--bad); }
1236
- .kit-sum .cv-bit[data-verdict="ASK"] { color: var(--waiting); }
1237
- .kit-sum .cv-bit[data-verdict="ALLOW"] { color: var(--good); }
1447
+ .kit-sum .cv-bit[data-verdict="BLOCK"] { color: var(--status-bad); }
1448
+ .kit-sum .cv-bit[data-verdict="ASK"] { color: var(--status-waiting); }
1449
+ .kit-sum .cv-bit[data-verdict="ALLOW"] { color: var(--status-good); }
1238
1450
 
1239
1451
  .gate-row {
1240
1452
  display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: baseline;
1241
1453
  padding: 4px 0; border-bottom: 1px solid var(--line); font-size: 12px;
1242
1454
  }
1243
- .gate-when { font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
1455
+ .gate-when { font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint); }
1244
1456
  .gate-name { color: var(--ink-dim); overflow-wrap: anywhere; }
1245
- .gate-sec { font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
1457
+ .gate-sec { font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint); }
1246
1458
  .gate-verdict {
1247
- font-family: var(--mono); font-size: 9px; letter-spacing: 0.05em;
1459
+ font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.05em;
1248
1460
  border: 1px solid var(--line); border-radius: 4px; padding: 0 5px; white-space: nowrap;
1249
1461
  }
1250
- .gate-verdict[data-verdict="BLOCK"] { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent); }
1251
- .gate-verdict[data-verdict="ASK"] { color: var(--waiting); border-color: color-mix(in srgb, var(--waiting) 40%, transparent); }
1252
- .gate-verdict[data-verdict="ALLOW"] { color: var(--good); border-color: color-mix(in srgb, var(--good) 35%, transparent); }
1462
+ .gate-verdict[data-verdict="BLOCK"] { color: var(--status-bad); border-color: color-mix(in srgb, var(--status-bad) 40%, transparent); }
1463
+ .gate-verdict[data-verdict="ASK"] { color: var(--status-waiting); border-color: color-mix(in srgb, var(--status-waiting) 40%, transparent); }
1464
+ .gate-verdict[data-verdict="ALLOW"] { color: var(--status-good); border-color: color-mix(in srgb, var(--status-good) 35%, transparent); }
1253
1465
 
1254
- .inspector dd.bad { color: var(--bad); }
1255
- .ihint strong { display: block; color: var(--waiting); margin-bottom: 3px; }
1466
+ .inspector dd.bad { color: var(--status-bad); }
1467
+ .ihint strong { display: block; color: var(--status-waiting); margin-bottom: 3px; }
1256
1468
 
1257
- /* A node that holds others opens on click, so it says so before you try. */
1258
- .cv-node.cv-container { cursor: zoom-in; }
1259
- .cv-node.cv-container:not(.cv-folded) { cursor: zoom-out; }
1260
- .cv-node.cv-container.cv-folded .cv-desc::after {
1261
- content: ' — click to open';
1262
- color: var(--ink-faint);
1263
- font-size: 11px;
1264
- }
1265
-
1266
- /* --------------------------------------------------- reachable elsewhere --- */
1267
-
1268
- .reach-head { border-top: 1px solid var(--line); padding-top: 10px; }
1269
- .reach-head[hidden] { display: none; }
1270
- #reach { max-height: 24vh; }
1271
- /* Listed, not openable: the transcript is on the other machine. */
1272
- .reach-row { cursor: default; opacity: 0.92; }
1273
- .reach-row .name { font-size: 13px; }
1274
1469
 
1275
1470
  /* ------------------------------------------------ history and read-only --- */
1276
1471
 
1277
1472
  /* The seam between what was said before a fork and what is being said now. */
1278
1473
  .chat-seam {
1279
1474
  display: flex; align-items: center; gap: 10px;
1280
- margin: 14px 0; font-family: var(--mono); font-size: 10px;
1475
+ margin: 14px 0; font-family: var(--font-mono); font-size: 10px;
1281
1476
  letter-spacing: 0.05em; color: var(--ink-faint); text-transform: uppercase;
1282
1477
  }
1283
1478
  .chat-seam::before, .chat-seam::after {
1284
1479
  content: ''; flex: 1; height: 1px; background: var(--line);
1285
1480
  }
1286
1481
 
1482
+ /* The foot of a conversation Studio only reads: why there is no message box, and the two ways on. */
1287
1483
  .chat-ro {
1288
- flex: none; padding: 9px 14px; border-top: 1px solid var(--line);
1289
- font-size: 12px; line-height: 1.45; color: var(--ink-dim);
1290
- background: var(--surface-2);
1484
+ flex: none; display: flex; flex-direction: column; gap: var(--space-3);
1485
+ padding: var(--space-3); border-top: 1px solid var(--line); font-size: 13px; line-height: 1.45;
1291
1486
  }
1292
1487
  .chat-ro[hidden] { display: none; }
1293
- .chat-ro strong { color: var(--accent); font-weight: 600; }
1294
-
1295
- .chat-state[data-state="observed"] { color: var(--ink-dim); }
1296
-
1297
- /* --------------------------------------------------- panel controls --- */
1298
-
1299
- .ghost.pin { padding: 1px 6px; font-size: 11px; line-height: 1.4; }
1300
- .side-head { position: relative; }
1301
- .side-head h2 { flex: 1; }
1488
+ .chat-ro-lead { margin: 0; color: var(--ink-dim); }
1489
+ .chat-ro-lead strong { color: var(--ink); font-weight: 600; }
1490
+ .chat-ro-row { display: flex; align-items: flex-start; gap: var(--space-3); }
1491
+ .chat-ro-row .btn { flex: none; min-width: 132px; justify-content: center; }
1492
+ .chat-ro-row .sub { white-space: normal; flex: 1 1 0; min-width: 0; line-height: 18px; }
1493
+ .chat-ro-cmd { font-family: var(--font-mono); font-size: 12px; color: var(--ink); overflow-wrap: anywhere; }
1494
+
1495
+ /* ----------------------------------------------------- new session ---
1496
+ The same column the conversation takes, and the same width. */
1497
+ .newpanel {
1498
+ min-width: 0; min-height: 0; display: flex; flex-direction: column;
1499
+ background: var(--surface); border-left: 1px solid var(--line);
1500
+ }
1501
+ .newpanel[hidden] { display: none; }
1502
+ .np-head { display: flex; align-items: center; height: var(--toolbar-h); padding: 0 var(--space-2) 0 var(--space-4); border-bottom: 1px solid var(--line); flex: none; }
1503
+ .np-title { margin: 0; font-size: 16px; font-weight: 700; }
1504
+ .np-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-4); }
1505
+ .np-field { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
1506
+ .np-label { font-size: 13px; font-weight: 600; color: var(--ink); padding: 0; }
1507
+ .np-field .sub, .np-modes > .sub, .np-note .sub { white-space: normal; line-height: 18px; }
1508
+ .np-field code { font-family: var(--font-mono); font-size: 12px; color: var(--ink); overflow-wrap: anywhere; }
1509
+ .np-select, .np-first {
1510
+ box-sizing: border-box; width: 100%;
1511
+ background: var(--ground); border: 1px solid var(--line); border-radius: var(--radius-md); color: var(--ink);
1512
+ }
1513
+ .np-select { height: 40px; padding: 0 var(--space-3); font-family: var(--font-mono); font-size: 13px; }
1514
+ .np-first { resize: none; padding: var(--space-2) var(--space-3); font-family: inherit; font-size: 14px; line-height: 20px; }
1515
+ .np-modes { border: 0; margin: 0; padding: 0; min-width: 0; display: flex; flex-direction: column; gap: var(--space-2); }
1516
+ .np-modes legend { margin-bottom: var(--space-2); }
1517
+ .np-mode {
1518
+ display: flex; gap: var(--space-2); padding: var(--space-3); cursor: pointer;
1519
+ border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--ground);
1520
+ }
1521
+ .np-mode.on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--ground)); }
1522
+ .np-mode input { margin: 2px 0 0; flex: none; accent-color: var(--accent); }
1523
+ .np-mode-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
1524
+ .np-mode-text .sub { white-space: normal; line-height: 18px; }
1525
+ .np-mode-word { font-size: 13px; font-weight: 600; }
1526
+ .np-note { display: flex; gap: var(--space-2); padding: var(--space-3); border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--ground); }
1527
+ .np-note .ic { color: var(--ink-dim); margin-top: 1px; }
1528
+ .np-note .sub { flex: 1 1 0; min-width: 0; }
1529
+ .np-error { font-size: 13px; color: var(--status-bad); }
1530
+ .np-error[hidden] { display: none; }
1531
+ .np-foot { display: flex; gap: var(--space-2); justify-content: flex-end; padding: var(--space-3) var(--space-4); border-top: 1px solid var(--line); flex: none; }
1302
1532
 
1303
1533
  /* The chat divider only exists while there is a conversation beside it. */
1304
1534
  #chat-split[hidden] { display: none; }
1305
1535
 
1306
1536
  .chat-wide { position: relative; }
1307
- .chat-head .chat-grow { margin-left: 2px; }
1308
-
1309
- @media (max-width: 1100px) {
1310
- /* Two side panels and a graph do not fit; the conversation wins because it is
1311
- the thing being read. */
1312
- .shell { grid-template-columns: 0 0 1fr 5px var(--chat-w, 420px); }
1313
- .shell > .side, .shell > #resizer { display: none; }
1314
- }
1315
- @media (max-width: 760px) {
1316
- .shell, .shell.no-chat, .shell.no-side { grid-template-columns: 1fr; }
1317
- .shell > .side, .shell > #resizer, .shell > #chat-split { display: none; }
1318
- .inspector { display: none; }
1319
- }
1320
-
1321
- /* A live session is opened from the fleet the same way it is from the list. */
1322
- .fleet-row.clickable { cursor: pointer; }
1323
- .fleet-row.clickable:hover { background: var(--surface-2); }
1324
- .fleet-row[aria-current="true"] { background: var(--surface-2); box-shadow: inset 3px 0 0 var(--good); }
1537
+ .chat-head .chat-grow { flex: none; }
1325
1538
 
1326
1539
  /* ============================================================ scrollbars === */
1327
1540
 
@@ -1349,63 +1562,40 @@ body.vresizing { cursor: row-resize; user-select: none; }
1349
1562
  /* Where two scrollable panes meet, the bar should not read as a third border. */
1350
1563
  .side *::-webkit-scrollbar, .chat *::-webkit-scrollbar, .inspector *::-webkit-scrollbar { width: 8px; }
1351
1564
 
1352
- /* ------------------------------------------------------- sidebar rail --- */
1565
+ /* ------------------------------------------------------- navigator rail --- */
1353
1566
 
1354
- /* The control that brings the sidebar back sits where the sidebar was. It
1355
- started in the header beside the theme toggle — the far corner from the edge
1356
- it acts on — and was, fairly, hard to find. */
1567
+ /* What the navigator leaves behind when it is collapsed: its three tabs as
1568
+ icons, and at the foot the control that opens it again. It sits where the
1569
+ navigator was, on the edge it acts on. */
1357
1570
  .side-rail {
1358
1571
  position: absolute; left: 0; top: 0; bottom: 0; z-index: 25;
1359
- width: 22px;
1360
- display: flex; flex-direction: column; align-items: center; justify-content: center;
1361
- gap: 10px;
1572
+ width: var(--rail-w);
1573
+ display: flex; flex-direction: column; align-items: center; gap: 6px;
1574
+ padding: var(--space-3) 0;
1362
1575
  background: var(--surface);
1363
- border: none; border-right: 1px solid var(--line);
1364
- color: var(--ink-faint);
1365
- cursor: pointer;
1366
- padding: 0;
1576
+ border-right: 1px solid var(--line);
1367
1577
  }
1368
1578
  .side-rail[hidden] { display: none; }
1369
- .side-rail:hover { background: var(--surface-2); color: var(--ink); border-right-color: var(--accent); }
1370
- .side-rail:focus-visible { outline: none; border-right-color: var(--accent); color: var(--ink); }
1371
-
1372
- .side-rail-arrow { font-size: 15px; line-height: 1; }
1373
- /* Turned along the rail, so the strip says what it opens rather than being an
1374
- unlabelled sliver. */
1375
- .side-rail-label {
1376
- writing-mode: vertical-rl;
1377
- font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em;
1378
- text-transform: uppercase;
1579
+ .rail-btn {
1580
+ width: 40px; height: 40px; padding: 0; flex: none;
1581
+ display: flex; align-items: center; justify-content: center;
1582
+ border: 0; border-radius: var(--radius-md); background: none;
1583
+ color: var(--ink-dim); cursor: pointer;
1379
1584
  }
1380
-
1381
- /* The rail takes the space the sidebar left, so the graph does not sit under it. */
1382
- .shell.no-side { grid-template-columns: 22px 0 1fr 5px var(--chat-w, 420px); }
1383
- .shell.no-side.no-chat { grid-template-columns: 22px 0 1fr 0 0; }
1384
-
1385
- @media (max-width: 760px) { .side-rail { display: none; } }
1585
+ .rail-btn:hover, .rail-btn.on { background: var(--surface-raised); color: var(--ink); }
1586
+ .rail-fill { flex: 1; }
1386
1587
 
1387
1588
  /* --------------------------------------------------------- full screen --- */
1388
1589
 
1389
- #fullscreen.on {
1390
- color: var(--accent);
1391
- border-color: color-mix(in srgb, var(--accent) 45%, transparent);
1392
- }
1393
-
1394
- /* In full screen the window chrome is gone, so the panel's own top edge is the
1395
- only frame left; it should not look like a floating bar. */
1396
- :fullscreen .bar { border-top: none; }
1397
-
1398
-
1399
- /* ---------------------------------------------------------- node mark --- */
1400
-
1401
- .cv-mark { width: 14px; height: 14px; flex: none; color: var(--node-color); }
1402
- /* Both logos carry their own colours; only the generic glyphs take the node's. */
1403
- .cv-node[data-source="kit"] .cv-mark,
1404
- .cv-node[data-source="builtin"] .cv-mark { color: inherit; }
1405
- /* A type the palette does not know keeps a neutral mark, like its colour. */
1406
- .cv-mark.cv-mark-unknown { color: var(--ink-faint); opacity: 0.7; }
1407
- .cv-node[data-source="kit"] .cv-mark { opacity: 1; }
1408
- .cv-node[data-source="builtin"] .cv-mark { opacity: 0.85; }
1590
+ /* Full screen is the canvas and nothing beside it: the navigator, its rail,
1591
+ the inspector and the conversation step out until Esc. */
1592
+ :root:fullscreen .shell { grid-template-columns: 0 0 minmax(0, 1fr) 0 0 0; }
1593
+ :root:fullscreen .shell > .side,
1594
+ :root:fullscreen .shell > .side-rail,
1595
+ :root:fullscreen .shell > .resizer,
1596
+ :root:fullscreen .shell > .inspector,
1597
+ :root:fullscreen .shell > .chat,
1598
+ :root:fullscreen .shell > .newpanel { display: none; }
1409
1599
 
1410
1600
  /* ------------------------------------------------- tab bar that fits --- */
1411
1601
 
@@ -1420,34 +1610,111 @@ body.vresizing { cursor: row-resize; user-select: none; }
1420
1610
  .tab-dot, .tab-x, .tab-badge { flex: none; }
1421
1611
 
1422
1612
  /* The status and the controls keep their size; the tabs are what scrolls. */
1423
- .chat-state, .chat-cost, .chat-stop, .chat-grow { flex: none; }
1424
- .chat-cost { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
1425
1613
 
1426
- /* Below this the status text is what has to go, not the way out of a tab. */
1427
- @media (max-width: 1400px) {
1428
- .chat-head .chat-cost { display: none; }
1429
- }
1430
1614
 
1431
- /* ------------------------------------------------ foldable side sections */
1432
-
1433
- .twisty {
1434
- flex: none; width: 16px; height: 16px; padding: 0; margin-right: -2px;
1435
- display: grid; place-items: center;
1436
- background: none; border: 0; cursor: pointer; border-radius: 4px;
1437
- font-size: 10px; line-height: 1; color: var(--ink-faint);
1438
- transition: background 0.12s, color 0.12s;
1615
+ /* ======================================================== breakpoints ===
1616
+ Four widths, one frame. Each query names only what changes at that width;
1617
+ everything it does not name is the frame as drawn above.
1618
+
1619
+ >= 1280 everything open, the inspector docked beside the canvas
1620
+ 1024 - 1279 the inspector floats over the canvas, which keeps its width
1621
+ 640 - 1023 the navigator is its rail; opened, it floats over the canvas
1622
+ < 640 one column: the stage, and whatever is opened covers it
1623
+
1624
+ app.js collapses the navigator on entering the third band and restores the
1625
+ viewer's own choice on leaving it, so the rail is what a narrow window opens
1626
+ on without the remembered state being overwritten. */
1627
+
1628
+ @media (min-width: 1280px) {
1629
+ /* Docked: back in the grid, in the column kept for it. */
1630
+ .shell.no-chat > .inspector { position: static; grid-column: 4; box-shadow: none; }
1631
+ }
1632
+
1633
+ @media (max-width: 1279px) {
1634
+ /* Beside the navigator the conversation takes two fifths of the window at most. At 1024 the design's 480px
1635
+ left the graph 262px (measured); this leaves it 332. */
1636
+ .shell { grid-template-columns: var(--side-w, var(--nav-w)) 5px minmax(0, 1fr) auto 5px min(var(--chat-w, 480px), 40vw); }
1637
+ .shell.no-side { grid-template-columns: var(--rail-w) 0 minmax(0, 1fr) auto 5px min(var(--chat-w, 480px), 40vw); }
1638
+ /* The dock goes to two rows: what is asked, then the answers under it. */
1639
+ .dock { grid-template-columns: auto minmax(0, 1fr) auto; }
1640
+ .dock-acts { grid-column: 1 / -1; justify-content: flex-end; flex-wrap: wrap; }
1641
+ /* Below this the status text is what has to go, not the way out of a tab. */
1642
+ .pane-head .pane-cost { display: none; }
1643
+ /* The mark stays and the word beside it goes: the room is the breadcrumb's. */
1644
+ .brand h1, .brand + .bar-sep { display: none; }
1645
+ }
1646
+
1647
+ @media (max-width: 1023px) {
1648
+ /* The bar keeps the mark and drops the word beside it; the session keeps its
1649
+ name and drops its project; New session keeps its plus. */
1650
+ .bar { gap: var(--space-3); }
1651
+ .bar-sep, .crumb-project, .crumb-sep, .live-detail { display: none; }
1652
+ .bar .btn.primary > span { display: none; }
1653
+ .bar .btn.primary { width: 36px; padding: 0; }
1654
+
1655
+ .shell,
1656
+ .shell.no-side { grid-template-columns: var(--rail-w) 0 minmax(0, 1fr) auto 5px min(var(--chat-w, 480px), 50vw); }
1657
+ .shell.no-chat,
1658
+ .shell.no-side.no-chat { grid-template-columns: var(--rail-w) 0 minmax(0, 1fr) auto 0 0; }
1659
+ .shell > #resizer { display: none; }
1660
+ .shell > .side {
1661
+ grid-column: auto;
1662
+ position: absolute; z-index: 40;
1663
+ left: 0; top: 0; bottom: 0;
1664
+ width: min(var(--side-w, var(--nav-w)), 86vw);
1665
+ box-shadow: 12px 0 24px -18px var(--shade);
1666
+ }
1667
+ .shell > .inspector { right: calc(min(var(--chat-w, 480px), 50vw) + 5px); }
1668
+ .shell.no-chat > .inspector { right: 0; }
1669
+ }
1670
+
1671
+ @media (max-width: 639px) {
1672
+ /* The Timeline keeps its track: the label column gives up its width, and the drawer stacks. */
1673
+ .tl { --tl-label-w: 148px; }
1674
+ .tl-lab .sub { display: none; }
1675
+ .tl-drawer { flex-direction: column; gap: var(--space-3); max-height: 55%; }
1676
+ .tl-d-main, .tl-d-error { flex: none; width: 100%; }
1677
+ /* The bar keeps the mark, the word "Studio" and the controls that act on a
1678
+ session. Full screen is the one that goes: a phone has no window to leave. */
1679
+ .bar { gap: var(--space-2); padding: 0 var(--space-3); }
1680
+ .bar .crumb, .bar .chips, .bar #fullscreen { display: none; }
1681
+ /* The session's name and its summary sit under the bar, above the view. */
1682
+ .phone-head { display: flex; flex-direction: column; gap: var(--space-2); flex: none; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--line); min-width: 0; }
1683
+ .phone-head:empty { display: none; }
1684
+ .phone-head .crumb { min-width: 0; }
1685
+ .phone-head .chips { flex-wrap: wrap; }
1686
+ /* The menu button opens the navigator as a drawer over the stage. */
1687
+ #menu-btn { display: inline-flex; }
1688
+ .shell.side-open > .side {
1689
+ display: flex; grid-column: auto; position: absolute; z-index: 45; left: 0; top: 0; bottom: 0;
1690
+ width: min(86vw, 320px); box-shadow: 12px 0 24px -18px var(--shade);
1691
+ }
1692
+ /* A request's three answers are thumb-sized, one under the other. */
1693
+ .ls { padding: var(--space-3); }
1694
+ .ls-acts { flex-direction: column; }
1695
+ .ls-acts .btn { height: 44px; }
1696
+ /* The inspector is a page here: back, not a cross. */
1697
+ .ihead-back { display: inline-flex; }
1698
+ .ihead-close { display: none; }
1699
+ /* The map and the legend are for a canvas with room around the graph; here they would cover it. */
1700
+ .cv-minimap, .cv-legend { display: none; }
1701
+ /* "Studio" comes back beside the mark, as the phone design has it. */
1702
+ .brand h1 { display: block; font-size: 0; }
1703
+ .brand-accent { font-size: 15px; font-weight: 700; color: var(--ink); }
1704
+
1705
+ .shell,
1706
+ .shell.no-chat,
1707
+ .shell.no-side,
1708
+ .shell.no-side.no-chat { grid-template-columns: 0 0 minmax(0, 1fr) auto 0 0; }
1709
+ .shell > .side,
1710
+ .shell > .side-rail,
1711
+ .shell > #chat-split { display: none; }
1712
+ /* The conversation and the inspector each take the whole stage: there is no
1713
+ second column to put them in. */
1714
+ /* They stop above the approval dock, as the inspector does: a request can be answered while a conversation
1715
+ covers the rest of the stage. */
1716
+ .shell > .chat,
1717
+ .shell > .newpanel { grid-column: auto; position: absolute; z-index: 35; inset: 0 0 var(--dock-h, 0px) 0; border-left: none; }
1718
+ .shell > .inspector,
1719
+ .shell.no-chat > .inspector { left: 0; right: 0; top: 0; width: auto; border-left: none; }
1439
1720
  }
1440
- .twisty:hover { background: var(--surface-2); color: var(--ink); }
1441
- .twisty:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
1442
-
1443
- .side-head [data-fold-for] { cursor: pointer; user-select: none; }
1444
- .side-head [data-fold-for]:hover { color: var(--ink); }
1445
-
1446
- /* Two selector classes, so this outranks the `display` these bodies get from
1447
- .fleet / .sessions / .side-filter. A single .is-folded would tie and lose. */
1448
- [data-fold-body].is-folded { display: none; }
1449
-
1450
- /* A folded section stops competing for the leftover height: without this the
1451
- sidebar keeps a tall blank gap where the project tree used to be. */
1452
- .side-block.folded { flex: none; }
1453
- .side-block.grow.folded { border-bottom: 1px solid var(--line); }