@oharness/server 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/i18n.js +356 -62
  2. package/dist/i18n.js.map +1 -1
  3. package/dist/main.js +4 -0
  4. package/dist/main.js.map +1 -1
  5. package/dist/page/client/account.js +430 -23
  6. package/dist/page/client/account.js.map +1 -1
  7. package/dist/page/client/base.js +20 -1
  8. package/dist/page/client/base.js.map +1 -1
  9. package/dist/page/client/composer.js +709 -58
  10. package/dist/page/client/composer.js.map +1 -1
  11. package/dist/page/client/computer.d.ts +27 -0
  12. package/dist/page/client/computer.js +358 -0
  13. package/dist/page/client/computer.js.map +1 -0
  14. package/dist/page/client/dialogs.js +93 -6
  15. package/dist/page/client/dialogs.js.map +1 -1
  16. package/dist/page/client/events.js +94 -7
  17. package/dist/page/client/events.js.map +1 -1
  18. package/dist/page/client/files.js +86 -1
  19. package/dist/page/client/files.js.map +1 -1
  20. package/dist/page/client/index.js +4 -0
  21. package/dist/page/client/index.js.map +1 -1
  22. package/dist/page/client/panels.js +12 -6
  23. package/dist/page/client/panels.js.map +1 -1
  24. package/dist/page/client/routines.js +296 -41
  25. package/dist/page/client/routines.js.map +1 -1
  26. package/dist/page/client/shell.js +229 -51
  27. package/dist/page/client/shell.js.map +1 -1
  28. package/dist/page/client/state.js +273 -19
  29. package/dist/page/client/state.js.map +1 -1
  30. package/dist/page/client/tasks.d.ts +8 -0
  31. package/dist/page/client/tasks.js +293 -0
  32. package/dist/page/client/tasks.js.map +1 -0
  33. package/dist/page/client/transcript.js +174 -6
  34. package/dist/page/client/transcript.js.map +1 -1
  35. package/dist/page/client/usage.js +1 -11
  36. package/dist/page/client/usage.js.map +1 -1
  37. package/dist/page/style.js +706 -104
  38. package/dist/page/style.js.map +1 -1
  39. package/dist/page.js +161 -54
  40. package/dist/page.js.map +1 -1
  41. package/dist/prefs.d.ts +14 -0
  42. package/dist/prefs.js +8 -1
  43. package/dist/prefs.js.map +1 -1
  44. package/dist/remote-crypto.d.ts +78 -0
  45. package/dist/remote-crypto.js +216 -0
  46. package/dist/remote-crypto.js.map +1 -0
  47. package/dist/remote.d.ts +103 -0
  48. package/dist/remote.js +498 -0
  49. package/dist/remote.js.map +1 -0
  50. package/dist/server.d.ts +70 -0
  51. package/dist/server.js +1291 -49
  52. package/dist/server.js.map +1 -1
  53. package/dist/starters.d.ts +92 -0
  54. package/dist/starters.js +213 -0
  55. package/dist/starters.js.map +1 -0
  56. package/dist/theme.js +5 -0
  57. package/dist/theme.js.map +1 -1
  58. package/dist/turn-shots.d.ts +27 -0
  59. package/dist/turn-shots.js +71 -0
  60. package/dist/turn-shots.js.map +1 -0
  61. package/package.json +2 -2
@@ -32,32 +32,105 @@ code,pre,.mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,
32
32
 
33
33
  /* sidebar */
34
34
  aside{
35
- width:236px;flex:0 0 236px;background:var(--panel);
36
- display:flex;flex-direction:column;overflow:hidden;transition:flex-basis .18s,width .18s;
35
+ --side-w:296px;width:var(--side-w);flex:0 0 var(--side-w);
36
+ display:flex;flex-direction:column;align-items:flex-end;overflow:hidden;
37
+ }
38
+ /* Folding, the sidebar narrows while its contents keep their width, pinned
39
+ to its right edge, so they slide out to the left behind the frame rather
40
+ than squeezing; the tab strip's inset grows by as much as the sidebar
41
+ loses, so the first tab glides to its place beside the toggle. Only once a
42
+ hand has touched the toggle: a page opened folded starts folded. */
43
+ #side-top,#side-body{width:var(--side-w);flex-shrink:0}
44
+ body.nav-anim>aside{transition:width .26s cubic-bezier(.2,0,0,1),flex-basis .26s cubic-bezier(.2,0,0,1)}
45
+ body.nav-anim #tabs{transition:padding-left .26s cubic-bezier(.2,0,0,1)}
46
+ body.nav-anim .wordmark{transition:opacity .14s}
47
+ body.nav-collapsed .wordmark{opacity:0}
48
+ @media (prefers-reduced-motion:reduce){body.nav-anim>aside,body.nav-anim #tabs,body.nav-anim .wordmark{transition:none}}
49
+ /*
50
+ * A frame with the panel as a card inside it.
51
+ *
52
+ * The top bar and the rail are one L in the window's own colour, --frame, and
53
+ * the panel and the page sit inside it. It takes a third colour: a top bar
54
+ * over the sidebar in either of the other two runs into the surface below it
55
+ * in that colour — a grey one boxed the white rail in, a white one ran on
56
+ * into the page. The panel is a card set into the frame, rounded all round
57
+ * and clear of the page by a margin. The traffic lights sit on the frame, one
58
+ * colour, where the rail alone is too narrow to hold them.
59
+ */
60
+ #side-top{height:36px;flex:0 0 36px;background:var(--frame);display:flex;align-items:center;overflow:hidden}
61
+ /* Just past the toggle, which ends at 40px on the web and at 108px beside
62
+ the traffic lights, and fixed there, so the mark stays in the corner when
63
+ the sidebar folds and only the name goes with it. */
64
+ .brand{position:fixed;z-index:30;top:0;left:48px;height:36px;display:flex;align-items:center;gap:8px;white-space:nowrap;pointer-events:none}
65
+ body.inset-titlebar .brand{left:116px}
66
+ body.inset-titlebar #side-top{-webkit-app-region:drag}
67
+ #side-body{flex:1 1 auto;display:flex;min-height:0;background:var(--frame)}
68
+ /*
69
+ * Two columns: a rail of the app's places, and the panel of the work.
70
+ *
71
+ * The rail is the frame and the panel a card in it, so the two read as
72
+ * different surfaces without a line between them.
73
+ */
74
+ #rail{
75
+ width:52px;flex:0 0 52px;background:var(--frame);display:flex;flex-direction:column;
76
+ align-items:center;padding:8px 0 10px;gap:6px;overflow:hidden;
77
+ }
78
+ #rail #nav{padding:0;gap:4px;align-items:center}
79
+ #rail .nav-item{width:36px;height:36px;padding:0;justify-content:center;border-radius:9px;color:var(--dim)}
80
+ #rail .nav-item:hover{color:var(--fg)}
81
+ #rail .nav-item.active{color:var(--fg)}
82
+ /* Drawn at 18px on the 16 grid, the shared 1.5 stroke would render at 1.7px
83
+ and read heavier than every other icon on the page; 1.2 lands near 1.35. */
84
+ #rail .nav-item .ico{width:18px;height:18px;stroke-width:1.2}
85
+ /* The panel's icons follow at their own 14-16px, so the rail and the panel
86
+ beside it draw at one weight. Matched on the attribute, so the mark keeps its 2.5. */
87
+ #side-panel svg[stroke-width="1.5"]{stroke-width:1.25}
88
+ #rail .nav-item>span{display:none}
89
+ .rail-tip{
90
+ position:fixed;z-index:80;transform:translateY(-50%);padding:4px 9px;border-radius:7px;
91
+ background:var(--accent);color:var(--bg);font-size:12px;line-height:1.4;white-space:nowrap;
92
+ pointer-events:none;opacity:0;transition:opacity .08s;
93
+ }
94
+ .rail-tip.on{opacity:1}
95
+ #side-panel{
96
+ width:236px;flex:0 0 236px;background:var(--panel);border-radius:12px;margin:0 8px 8px 0;
97
+ display:flex;flex-direction:column;overflow:hidden;
98
+ }
99
+ #side-new{padding:0 8px 2px;display:flex;align-items:center;gap:2px}
100
+ #side-new #new{flex:1;min-width:0}
101
+ #side-search{
102
+ display:grid;place-items:center;flex:0 0 auto;width:28px;height:28px;border:0;border-radius:6px;
103
+ background:transparent;color:var(--dim);cursor:pointer;padding:0;
37
104
  }
38
- /* Folded away on a desktop. Zero width rather than display:none, for the
39
- slide, and so nothing inside has to be rebuilt when it comes back. */
105
+ #side-search:hover{background:var(--hover);color:var(--fg)}
106
+ #side-search .ico-sm{width:16px;height:16px}
107
+ /* Folded away on a desktop, the whole sidebar goes, rail and all, and the
108
+ page has the window: keeping the rail left a strip of chrome and pushed the
109
+ tabs a rail's width plus the traffic lights in from the edge. */
40
110
  body.nav-collapsed>aside{width:0;flex-basis:0}
41
- #nav-collapse,#nav-expand{
42
- width:28px;height:28px;border:0;border-radius:7px;background:transparent;color:var(--dim);
43
- cursor:pointer;padding:0;display:grid;place-items:center;flex:0 0 auto;-webkit-app-region:no-drag;
44
- }
45
- #nav-collapse{margin-left:auto}
46
- #nav-collapse:hover,#nav-expand:hover{background:var(--hover);color:var(--fg)}
47
- #nav-collapse svg,#nav-expand svg{width:16px;height:16px;display:block;shape-rendering:geometricPrecision}
48
- #nav-expand{display:none;position:absolute;z-index:5;top:7px;left:8px}
49
- body.nav-collapsed #nav-expand{display:grid}
50
- body.nav-collapsed #tabs{padding-left:44px}
51
- /* The traffic lights sit where the sidebar's corner was; step past them. */
52
- body.inset-titlebar.nav-collapsed #nav-expand{left:80px}
53
- body.inset-titlebar.nav-collapsed #tabs{padding-left:116px;-webkit-app-region:drag}
54
- body.inset-titlebar.nav-collapsed #tabs>*{-webkit-app-region:no-drag}
55
- aside header{padding:15px 14px 8px;display:flex;gap:8px;align-items:center}
56
- /* The desktop shell hides the title bar, so on macOS the traffic lights float
57
- * over this corner. The wordmark moves below them, and the strip they sit in
58
- * becomes the window's drag handle — without it a title-bar-less window has
59
- * nothing to grab. */
60
- body.inset-titlebar aside header{padding-top:38px;-webkit-app-region:drag}
111
+ /* In the top bar, at the same spot folded or not, so the click that folded the
112
+ sidebar can bring it straight back. On a desktop it steps past the traffic
113
+ lights (14-74px, set in desktop/main.js). */
114
+ #nav-toggle{
115
+ position:fixed;z-index:30;top:4px;left:12px;width:28px;height:28px;border:0;border-radius:7px;
116
+ background:transparent;color:var(--dim);cursor:pointer;padding:0;
117
+ display:grid;place-items:center;-webkit-app-region:no-drag;
118
+ }
119
+ #nav-toggle:hover{background:var(--hover);color:var(--fg)}
120
+ #nav-toggle svg{width:16px;height:16px;display:block;shape-rendering:geometricPrecision}
121
+ body.inset-titlebar #nav-toggle{left:80px}
122
+ body.shelf-full #nav-toggle{display:none}
123
+ /* The window has no title bar, so the strip's empty stretch is its handle.
124
+ Folded, the strip is the whole top bar and starts past the mark beside the
125
+ toggle: 66px on the web, 134px beside the traffic lights. */
126
+ body.inset-titlebar #tabs{-webkit-app-region:drag}
127
+ body.inset-titlebar #tabs>*{-webkit-app-region:no-drag}
128
+ body.nav-collapsed #tabs{padding-left:74px}
129
+ body.inset-titlebar.nav-collapsed #tabs{padding-left:142px}
130
+ /* The name is in the top bar on a wide screen, so the panel's header is only
131
+ the phone drawer's, where it carries the name and the close button. */
132
+ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:center}
133
+ #side-panel{padding-top:8px}
61
134
  .mark{width:18px;height:18px;color:var(--fg);opacity:.85;flex:0 0 auto}
62
135
  .mark svg{width:18px;height:18px;display:block}
63
136
  /* Slightly tightened and a touch heavier than body text, which is what makes
@@ -95,6 +168,10 @@ body.inset-titlebar aside header{padding-top:38px;-webkit-app-region:drag}
95
168
  .fold .ico-sm{width:16px;height:16px;flex:0 0 auto;opacity:.5}
96
169
  .fold.open .ico-sm,.fold:hover .ico-sm{opacity:.9}
97
170
  .fold-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
171
+ /* A worktree hangs under the project it came from: indented by its icon's
172
+ * width, no rule or outline to say so. */
173
+ .fold.wt{padding-left:24px;color:var(--dim)}
174
+ .fold.wt.open,.fold.wt:hover{color:var(--fg)}
98
175
  /* The routine group's run count, where a project header keeps its actions. */
99
176
  .fold .count{flex:0 0 auto;font-size:11px;color:var(--dim);padding-right:4px}
100
177
  /* A gap between projects, but not above the first one. The selector names the
@@ -106,9 +183,13 @@ body.inset-titlebar aside header{padding-top:38px;-webkit-app-region:drag}
106
183
  edge, where the count and the ⋯ already live and a third thing would crowd
107
184
  them; dim, because it marks a row rather than asking for attention. */
108
185
  .pin-mark{
109
- width:11px;height:11px;flex:0 0 auto;margin-left:5px;opacity:.5;
110
- vertical-align:-1px;display:inline-block;
186
+ flex:0 0 auto;margin-left:3px;padding:2px;border-radius:4px;opacity:.5;
187
+ display:inline-grid;place-items:center;cursor:pointer;
111
188
  }
189
+ .pin-mark svg{width:11px;height:11px;display:block}
190
+ /* It is the unpin control, so it answers the pointer like one: full ink and a
191
+ fill on hover, the same as the ⋯ beside it. */
192
+ .pin-mark:hover,.pin-mark:focus-visible{opacity:1;background:var(--sel);outline:none}
112
193
  /* Dragging: the row being carried fades, and the one under the pointer grows
113
194
  the edge it would be dropped against. A line on the row rather than a
114
195
  floating element, because this list is rebuilt on every repaint and a
@@ -191,7 +272,7 @@ body.inset-titlebar aside header{padding-top:38px;-webkit-app-region:drag}
191
272
  already had two rules in it and a third would have made a list of five
192
273
  entries look like three lists. */
193
274
  .row-menu .menu-head{
194
- padding:9px 10px;margin-bottom:4px;border-radius:8px;background:var(--field);
275
+ padding:12px 12px 11px;margin-bottom:6px;border-radius:12px;background:var(--field);
195
276
  }
196
277
  /* A section label, not a surface: the fill belongs to the account's facts
197
278
  above, and on a one-word caption it read as a second card pressed against
@@ -199,6 +280,25 @@ body.inset-titlebar aside header{padding-top:38px;-webkit-app-region:drag}
199
280
  .row-menu .menu-head.menu-section{
200
281
  margin:10px 0 2px;padding:4px 10px;background:transparent;
201
282
  }
283
+ /* The workspaces: a heading and its choices on one faint surface — lighter
284
+ than the account card above it, so the card stays the menu's one solid
285
+ thing and this reads as a quiet group rather than a second card. Inside,
286
+ the workspace in use carries a stronger fill, and hover sits between. */
287
+ .row-menu .menu-choices{margin:8px 0 4px;padding:2px 4px 4px;border-radius:12px;background:color-mix(in srgb,var(--fg) 2%,transparent)}
288
+ .row-menu .menu-choices .menu-head.menu-section{margin:4px 0 0;padding-left:6px}
289
+ .row-menu .menu-choices .menu-item{border-radius:9px}
290
+ .row-menu .menu-choices .menu-item:hover{background:color-mix(in srgb,var(--fg) 4%,transparent)}
291
+ .row-menu .menu-choices .menu-item.on{background:color-mix(in srgb,var(--fg) 6%,transparent)}
292
+ .row-menu .menu-choices .menu-item.on .menu-label{font-weight:500}
293
+ .row-menu .menu-choices .menu-section-action:hover,.row-menu .menu-choices .menu-section-action:focus-visible{background:color-mix(in srgb,var(--fg) 5%,transparent)}
294
+ /* A section heading with its one action at the right: the label keeps the
295
+ caption type, the action does not — it is a button, not part of the name. */
296
+ .row-menu .menu-section-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding-right:4px}
297
+ .menu-section-action{
298
+ height:22px;padding:0 8px;border:0;border-radius:6px;background:transparent;color:var(--dim);
299
+ font:inherit;font-size:12px;font-weight:500;letter-spacing:0;text-transform:none;cursor:pointer;
300
+ }
301
+ .menu-section-action:hover,.menu-section-action:focus-visible{background:var(--hover);color:var(--fg);outline:none}
202
302
  .menu-head-main{font-size:14px;font-weight:500;color:var(--fg);font-variant-numeric:tabular-nums}
203
303
  .menu-head-sub{margin-top:2px;font-size:12px;color:var(--dim)}
204
304
  /* The billing card borrows .menu-head's surface but not its caption type: the
@@ -263,6 +363,15 @@ body.inset-titlebar aside header{padding-top:38px;-webkit-app-region:drag}
263
363
  .row-menu .danger .menu-key{color:var(--dim)}
264
364
  /* A rule, not a gap: the group below it is the one that cannot be undone. */
265
365
  .row-menu .menu-sep{height:1px;margin:5px 8px;background:var(--line)}
366
+ /* The + menu's groups are told apart by space alone: no rule between them,
367
+ the same way nothing in it is outlined. */
368
+ #attach-picker .menu-sep{height:10px;margin:0;background:none}
369
+ /* Each group of the + menu opens with its own heading; the ones after the
370
+ first get room above them, which is what sets the groups apart. */
371
+ #attach-picker .menu-head:not(:first-child){padding-top:12px}
372
+ /* Tall enough for all three groups at once; the list scrolls only on a short
373
+ window, not on every one. */
374
+ #attach-picker .menu-body{max-height:min(72vh,560px)}
266
375
  /* Sized from the title it stands in for, so the row does not jump when a
267
376
  rename starts. */
268
377
  .session .title-edit{
@@ -379,27 +488,19 @@ main{flex:1;display:flex;flex-direction:column;min-width:0}
379
488
  * Same two shapes as the connector list: the vendor's own icon when the
380
489
  * server has one, and a lettered tile when it does not. Sized to the row —
381
490
  * the type is 13px, so the mark is 15px and not the 28px tile a card gets. */
382
- /* One block per vendor, whatever the vendor ships.
491
+ /* The mark as the vendor drew it, on nothing.
383
492
  *
384
- * A column of these is scanned by its left edge, and the edge has to be the
385
- * same one every time. Left alone, it is not: a vendor shipping a tile fills
386
- * its square, a vendor shipping bare ink covers half of it, and one shipping
387
- * a thin outline covers less than that — the eye finds no common edge and
388
- * reads the lighter rows as hanging off the line, even where the boxes are
389
- * centred on the text to the pixel.
390
- *
391
- * So a mark that did not bring a background is given one: the same faint
392
- * fill, the same radius, the same inset. A mark that did bring one keeps it
393
- * and fills the box, because drawing a second shape around a shape is the
394
- * thing this is trying to avoid. Either way the block is identical and the
395
- * ink inside it is the vendor's business. */
396
- .vmark{width:15px;height:15px;flex:0 0 auto;border-radius:4px;object-fit:contain;
397
- padding:1.5px;box-sizing:border-box;background:var(--sel)}
398
- .vmark.filled{padding:0;background:none}
493
+ * A faint tile under every bare mark once gave the column a common edge, but
494
+ * it also shrank the ink to two thirds of the box and put a grey square
495
+ * beside every name — in a list that is all one vendor, a column of the same
496
+ * grey squares. The box is still the same size on every row, so the marks
497
+ * still line up; the ink now fills it. A vendor that ships its own tile keeps
498
+ * it (.filled), since that tile is its mark. */
499
+ .vmark{width:16px;height:16px;flex:0 0 auto;border-radius:3px;object-fit:contain;box-sizing:border-box}
399
500
  /* The fallback, and the reason a missing icon is not a broken row: a lettered
400
501
  tile in a colour of its own still tells two vendors apart at a glance. */
401
502
  .vmono{
402
- width:15px;height:15px;flex:0 0 auto;border-radius:4px;box-sizing:border-box;
503
+ width:16px;height:16px;flex:0 0 auto;border-radius:4px;box-sizing:border-box;
403
504
  display:grid;place-items:center;color:#fff;
404
505
  font-size:9px;font-weight:600;letter-spacing:0;text-transform:uppercase;
405
506
  }
@@ -408,8 +509,7 @@ main{flex:1;display:flex;flex-direction:column;min-width:0}
408
509
  /* Typed as svg.vglyph so it outranks the .ico-sm every drawn icon carries:
409
510
  .6 is right for a chrome glyph and too faint for a vendor's mark. Ours is
410
511
  bare strokes, so it takes the same block as any other bare mark. */
411
- svg.vglyph{width:15px;height:15px;opacity:1;
412
- padding:1.5px;box-sizing:border-box;border-radius:4px;background:var(--sel)}
512
+ svg.vglyph{width:16px;height:16px;opacity:1;box-sizing:border-box}
413
513
  /* Black-on-transparent is what a good half of these vendors ship, and in the
414
514
  dark theme that is a black square on a black row. Inverted only where the
415
515
  bytes were measured to be exactly that. */
@@ -435,9 +535,6 @@ details.provider>summary .vmark,details.provider>summary .vmono,details.provider
435
535
  .pp-item .vmark,.pp-item .vmono,.pp-item .vglyph{
436
536
  width:20px;height:20px;border-radius:5px;font-size:11px;
437
537
  }
438
- details.provider>summary .vmark,.pp-item .vmark,
439
- details.provider>summary .vglyph,.pp-item .vglyph{padding:2.5px}
440
- details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
441
538
  .menu-head{
442
539
  padding:8px 10px 4px;font-size:11px;font-weight:600;letter-spacing:.04em;
443
540
  text-transform:uppercase;color:var(--dim);
@@ -498,18 +595,45 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
498
595
  justify-content:flex-end;flex-wrap:wrap;
499
596
  }
500
597
  /*
501
- * The context meter: how much of the model's window this session is using.
502
- *
503
- * Its own track colour rather than --line. Every other neutral token here is a
504
- * hairline at 6-10% alpha, which at 64x4 pixels disappears into the background
505
- * in both schemes — what was left read as a stray green dash rather than a bar
506
- * with something still to fill. The fill itself is set from the value (green,
507
- * then warn, then err) in applyState.
598
+ * The context meter: how much of the model's window this session is using,
599
+ * as a ring that fills clockwise. A ring rather than a bar because it sits in
600
+ * a row of words and has to read at a glance at the size of one; the numbers
601
+ * are in its tooltip. The sweep is a conic fill and the hole a mask, so there
602
+ * is no stroke anywhere. Its own track colour rather than --line, which at
603
+ * this size disappears into the background. --ctx-pct and --ctx-fill are set
604
+ * from the value in applyState.
508
605
  */
509
- #ctx{width:64px;height:4px;background:var(--track);border-radius:2px;overflow:hidden}
510
- #ctx i{display:block;height:100%;width:0;background:var(--ok);transition:width .3s}
606
+ #ctx{
607
+ width:14px;height:14px;border-radius:50%;flex:0 0 auto;cursor:default;
608
+ background:conic-gradient(var(--ctx-fill,var(--brand)) calc(var(--ctx-pct,0) * 1%),var(--track) 0);
609
+ -webkit-mask:radial-gradient(circle,transparent 4.2px,#000 4.6px);
610
+ mask:radial-gradient(circle,transparent 4.2px,#000 4.6px);
611
+ }
612
+ #ctx{border:0;padding:0}
613
+ /* Focus as a change of size, not a ring: this design draws no outlines. */
614
+ #ctx{transition:transform .12s}
615
+ #ctx:focus-visible,#ctx[aria-expanded="true"]{outline:none;transform:scale(1.2)}
616
+ #ctx:hover{transform:scale(1.2)}
617
+ /* The context panel. Every part is a strength of the brand colour: the two
618
+ things the model is given every turn lighter, the conversation full, as it
619
+ is the part a person can do something about. */
620
+ :root{--ctx-system:color-mix(in srgb,var(--brand) 35%,transparent);--ctx-tools:color-mix(in srgb,var(--brand) 65%,transparent);--ctx-messages:var(--brand)}
621
+ .ctx-panel{width:300px;min-width:0;padding:14px 14px 8px;gap:10px;font-size:12.5px}
622
+ .ctx-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
623
+ .ctx-title{font-weight:600;color:var(--fg)}
624
+ .ctx-total{color:var(--dim);font-variant-numeric:tabular-nums}
625
+ .ctx-bar{position:relative;display:flex;height:8px;border-radius:4px;background:var(--track);overflow:hidden}
626
+ .ctx-bar i{display:block;height:100%;flex:0 0 auto}
627
+ .ctx-tick{position:absolute;top:0;bottom:0;width:2px;margin-left:-1px;background:var(--warn)}
628
+ .ctx-rows{display:flex;flex-direction:column;gap:5px}
629
+ .ctx-row{display:flex;align-items:center;gap:8px}
630
+ .ctx-dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto}
631
+ .ctx-label{flex:1;color:var(--fg)}
632
+ .ctx-value{color:var(--dim);font-variant-numeric:tabular-nums}
633
+ .ctx-notes{display:flex;flex-direction:column;gap:2px;color:var(--dim);font-size:11.5px;line-height:1.5}
634
+ .ctx-compact{margin:0 -8px}
511
635
  #ctx,#ctxtext{display:none}
512
- #meta.has-ctx #ctx,#meta.has-ctx #ctxtext{display:block}
636
+ #meta.has-ctx #ctx{display:block}
513
637
 
514
638
  /*
515
639
  * Over the transcript, not over the whole window.
@@ -518,31 +642,43 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
518
642
  * is in front. Spanning both left a band of empty chrome above the drawer that
519
643
  * belonged to neither.
520
644
  *
521
- * The strip is the shelf the tabs stand on, in the sidebar's grey, and the
522
- * front tab is cut from the page under it — the shape every browser uses.
523
- * It used to be the other way round: a transparent strip on the white
524
- * transcript with the front tab as a grey pill, so the row read as part of
525
- * the conversation rather than as the chrome above it. No bottom padding,
526
- * or a grey line would sit between the front tab and the page it is part of.
645
+ * The strip is the top of the frame, at #side-top's height, so one bar runs
646
+ * across the window, and the front tab is cut from the page under it — the
647
+ * shape every browser uses. No bottom padding, or a line of frame would sit
648
+ * between the front tab and the page it is part of; no left padding, so the
649
+ * first tab's edge is the page's.
527
650
  */
528
651
  #tabs{
529
- display:flex;align-items:center;gap:2px;padding:6px 12px 0;overflow-x:auto;flex:0 0 auto;
530
- background:var(--panel);
652
+ display:flex;align-items:flex-end;gap:2px;height:36px;padding:0 12px 0 0;overflow-x:auto;flex:0 0 auto;
653
+ background:var(--frame);
531
654
  }
532
655
  #tabs:empty{display:none}
656
+ /* A scrollbar here takes its height out of the strip and lifts every tab off
657
+ the page, and a hovered tab's buttons were enough to bring one on. Tabs
658
+ shrink first, as a browser's do, and past that the strip still scrolls by
659
+ wheel or trackpad, only without the bar. */
660
+ #tabs{scrollbar-width:none}
661
+ #tabs::-webkit-scrollbar{display:none}
533
662
  .tab{
534
- display:flex;align-items:center;gap:7px;max-width:230px;height:30px;padding:0 6px 0 10px;
663
+ display:flex;align-items:center;gap:7px;max-width:230px;min-width:72px;height:30px;padding:0 14px;
535
664
  border:0;border-radius:8px 8px 0 0;background:transparent;color:var(--dim);
536
- font:inherit;font-size:12.5px;cursor:pointer;flex:0 0 auto;
537
- }
538
- .tab:hover{background:var(--hover);color:var(--fg)}
665
+ font:inherit;font-size:12.5px;cursor:pointer;flex:0 1 auto;
666
+ }
667
+ /* The tab under the pointer or in front keeps its width, so its buttons fit
668
+ and it is the others that give way. */
669
+ .tab:hover,.tab:focus-within,.tab.on{flex-shrink:0}
670
+ /* Hovered, a tab lifts halfway toward the page it would become, the way a
671
+ browser's do, rather than sinking into a darker grey the front tab never
672
+ is. Mixed from --bg so the dark theme lifts the same way. */
673
+ .tab:hover{background:color-mix(in srgb,var(--bg) 60%,transparent);color:var(--fg)}
539
674
  .tab.on{background:var(--bg);color:var(--fg)}
540
- .tab .label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
675
+ .tab-page .ico-sm{width:14px;height:14px;opacity:.75}
676
+ .tab .label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
541
677
  /* Which project a tab is in, ahead of its title. Set quieter than the title,
542
678
  which is still what identifies it. */
543
679
  .tab .where{
544
680
  max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
545
- font-size:11px;opacity:.6;flex:0 0 auto;
681
+ font-size:11px;opacity:.6;flex:0 1 auto;min-width:0;
546
682
  }
547
683
  .tab .where::after{content:"/";padding-left:5px;opacity:.5}
548
684
  .tab .dot{width:6px;height:6px;border-radius:50%;background:var(--brand);flex:0 0 auto}
@@ -556,6 +692,10 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
556
692
  transparent — a transparent button still holds its space. A running
557
693
  browser keeps its button, because its dot is what says so. */
558
694
  .tab:hover .x,.tab:focus-within .x{display:grid}
695
+ /* Even padding while the title is alone, as a label is set; the right side
696
+ gives some back when a button comes in, so the button sits near the edge
697
+ and the tab grows by less. */
698
+ .tab:hover,.tab:focus-within,.tab:has(.tab-browser.running){padding-right:6px}
559
699
  /* The tab's browser button's size. Much smaller and the 1.5px strokes thin
560
700
  below a pixel and blur; the full 16px outweighs the tab's own text. */
561
701
  .tab .x svg{width:14px;height:14px}
@@ -571,7 +711,7 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
571
711
  .tab:hover .tab-browser,.tab.on .tab-browser,.tab-browser.running{opacity:.62}
572
712
  .tab-browser:hover,.tab-browser.on{opacity:1;background:var(--sel)}
573
713
  .tab-browser svg{width:14px;height:14px}
574
- .tab-browser i{display:none;position:absolute;right:2px;top:2px;width:5px;height:5px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 1.5px var(--panel)}
714
+ .tab-browser i{display:none;position:absolute;right:2px;top:2px;width:5px;height:5px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 1.5px var(--frame)}
575
715
  .tab.on .tab-browser i{box-shadow:0 0 0 1.5px var(--bg)}
576
716
  .tab-browser.running i{display:block}
577
717
  #tab-add{
@@ -774,6 +914,83 @@ body.browser-resizing #browser-pane{transition:none}
774
914
  }
775
915
  #browser-pane.live #browser-frame{cursor:crosshair}
776
916
  .browser-idle{margin:auto;text-align:center;max-width:32ch}
917
+ /* The Computer card: the window the agent is working in, floating over the
918
+ conversation's top-right corner while a turn works in it. Filled surfaces
919
+ and a shadow, no outline — the menus' elevation, since it sits above the
920
+ page the same way. The picture is letterboxed; the agent's cursor and its
921
+ click ripples sit over it in the conversation's accent. */
922
+ #computer-float{
923
+ position:absolute;z-index:30;top:52px;right:16px;width:340px;
924
+ transition:width .18s ease;cursor:grab;touch-action:none;
925
+ }
926
+ #computer-float[hidden]{display:none}
927
+ /* Larger, to read the window rather than glance at it. */
928
+ #computer-float.big{width:min(62%,880px)}
929
+ #computer-float.dragging{transition:none;user-select:none;cursor:grabbing}
930
+ /* The picture is the card: its corners, its shadow, and the window's own
931
+ proportions (set from the stream in computer.ts), so nothing frames it —
932
+ no bar above it, no fill around it, no bands at its sides. */
933
+ #computer-body{
934
+ aspect-ratio:16/10;max-height:70vh;position:relative;overflow:hidden;border-radius:12px;
935
+ background:var(--field);box-shadow:0 1px 2px #0000001a,0 10px 30px #00000033;
936
+ }
937
+ @media (prefers-color-scheme:dark){:root:not([data-theme="light"]) #computer-body{box-shadow:0 1px 2px #0000004d,0 10px 30px #00000080}}
938
+ :root[data-theme="dark"] #computer-body{box-shadow:0 1px 2px #0000004d,0 10px 30px #00000080}
939
+ /* Over the picture: a pill naming the app on the left, round buttons on the
940
+ right. Translucent dark in both themes, since what is under them is
941
+ somebody else's window, light or dark. The buttons come up with the
942
+ pointer, so a card nobody is reaching for is all picture. */
943
+ #computer-head{
944
+ position:absolute;z-index:2;top:8px;left:8px;right:8px;
945
+ display:flex;align-items:center;gap:6px;pointer-events:none;
946
+ }
947
+ #computer-label{
948
+ display:inline-flex;align-items:center;gap:6px;min-width:0;max-width:100%;margin-right:auto;
949
+ height:24px;padding:0 9px;border-radius:999px;font-size:11.5px;color:#fff;
950
+ background:#000000a6;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
951
+ }
952
+ #computer-live{width:6px;height:6px;flex:0 0 auto;display:grid;place-items:center}
953
+ #computer-live[hidden]{display:none}
954
+ #computer-live i,#computer-pill i{width:6px;height:6px;border-radius:50%;background:var(--ok);animation:computer-pulse 1.6s ease-in-out infinite}
955
+ #computer-pill i{width:7px;height:7px}
956
+ @keyframes computer-pulse{50%{opacity:.35}}
957
+ #computer-where{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
958
+ #computer-head button{
959
+ pointer-events:auto;width:28px;height:28px;padding:0;border:0;border-radius:50%;flex:0 0 auto;
960
+ display:grid;place-items:center;cursor:pointer;color:#fff;
961
+ background:#000000a6;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
962
+ opacity:0;transition:opacity .15s,background .15s;
963
+ }
964
+ #computer-float:hover #computer-head button,#computer-head button:focus-visible{opacity:1}
965
+ @media (hover:none){#computer-head button{opacity:1}}
966
+ #computer-head button:hover{background:#000000d9}
967
+ #computer-head button svg{width:13px;height:13px}
968
+ #computer-stop:hover{background:#c62828e6!important}
969
+ #computer-stop svg{width:11px!important;height:11px!important}
970
+ .computer-video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
971
+ .computer-cursor{position:absolute;left:0;top:0;width:22px;height:22px;opacity:0;pointer-events:none;transition:transform .26s cubic-bezier(.3,.7,.2,1),opacity .3s}
972
+ .computer-cursor.on{opacity:1}
973
+ .computer-cursor svg{width:22px;height:22px;display:block;filter:drop-shadow(0 1px 2px #00000059)}
974
+ .computer-cursor path{fill:var(--brand);stroke:#fff;stroke-width:1.6;stroke-linejoin:round}
975
+ .computer-ring{position:absolute;width:30px;height:30px;margin:-15px 0 0 -15px;border-radius:50%;background:color-mix(in srgb,var(--brand) 35%,transparent);pointer-events:none;animation:computer-ring .5s ease-out forwards}
976
+ @keyframes computer-ring{from{transform:scale(.3);opacity:.9}to{transform:scale(1.5);opacity:0}}
977
+ .computer-empty{position:absolute;inset:0;display:grid;place-items:center;padding:24px;text-align:center;color:var(--dim);font-size:12.5px}
978
+ .computer-empty[hidden],.computer-empty:empty{display:none}
979
+ /* Folded: where it is working, one click from the card again. */
980
+ #computer-pill{
981
+ position:absolute;z-index:30;top:52px;right:16px;max-width:280px;
982
+ display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 12px;
983
+ border:0;border-radius:999px;background:var(--panel);color:var(--fg);font:inherit;font-size:12px;cursor:pointer;
984
+ box-shadow:0 1px 2px #0000000f,0 4px 14px #0000001f;
985
+ }
986
+ #computer-pill[hidden]{display:none}
987
+ #computer-pill span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
988
+ #computer-pill:hover{background:var(--hover)}
989
+ .att.computer .n.clickable{cursor:pointer}
990
+ @media (max-width:900px){
991
+ #computer-float,#computer-float.big{width:calc(100% - 24px);right:12px}
992
+ }
993
+ .embed #computer-float,.embed #computer-pill{display:none!important}
777
994
  /* One column at a time on a narrow window: two would leave neither readable. */
778
995
  #browser-hide .ico-x{display:none}
779
996
  #browser-scrim{display:none}
@@ -846,7 +1063,16 @@ body.panelled #composer,body.panelled #jump,body.panelled #changes-btn{display:n
846
1063
  #explorer-actions button:hover{background:var(--hover);color:var(--fg)}
847
1064
  #explorer-actions svg{width:15px;height:15px}
848
1065
  #explorer-body{flex:1;display:flex;min-height:0}
849
- #explorer-tree{width:var(--explorer-tree-width,min(40%,260px));flex:0 0 auto;overflow:auto;padding:2px 6px 12px}
1066
+ #explorer-side{width:var(--explorer-tree-width,min(40%,260px));flex:0 0 auto;display:flex;flex-direction:column;min-height:0}
1067
+ #explorer-find{
1068
+ display:flex;align-items:center;gap:6px;margin:2px 8px 6px;padding:0 8px;height:30px;border-radius:8px;
1069
+ background:var(--hover);color:var(--dim);cursor:text;
1070
+ }
1071
+ #explorer-find svg{width:13px;height:13px;flex:0 0 auto}
1072
+ #explorer-q{flex:1;min-width:0;border:0;outline:0;background:transparent;color:var(--fg);font:inherit;font-size:12.5px;padding:0}
1073
+ #explorer-q::-webkit-search-cancel-button{cursor:pointer}
1074
+ #explorer-tree{flex:1;min-height:0;overflow:auto;padding:2px 6px 12px}
1075
+ .explorer-dir{color:var(--dim);font-size:11.5px;margin-left:6px;overflow:hidden;text-overflow:ellipsis;min-width:0;direction:rtl;text-align:left}
850
1076
  /* The line between the tree and the file, and the handle that moves it: a
851
1077
  gap wide enough to grab, drawn only while it is being pointed at. */
852
1078
  #explorer-split{flex:0 0 7px;margin:0 -3px;position:relative;z-index:1;cursor:col-resize;touch-action:none;outline:0}
@@ -1122,7 +1348,11 @@ body.shelf-full #main,body.shelf-full aside,body.shelf-full #top{display:none}
1122
1348
  * positioned from the viewport instead and reuses only the surface styling.
1123
1349
  */
1124
1350
  .menu.pop{position:fixed;bottom:auto;left:auto;min-width:220px}
1125
- #log{padding:28px 24px 8px}
1351
+ /* The scrollbar's gutter is kept on both sides, and the composer below gives
1352
+ up the same width on each side (see #composer), so a message and the box it
1353
+ was typed in share both edges at every window width. Taken from one side
1354
+ only, it pushed every message half a scrollbar left of the box. */
1355
+ #log{padding:28px 24px 8px;scrollbar-gutter:stable both-edges}
1126
1356
  /*
1127
1357
  * One column width, named once.
1128
1358
  *
@@ -1230,6 +1460,8 @@ body.shelf-full #main,body.shelf-full aside,body.shelf-full #top{display:none}
1230
1460
  max-width:220px;max-height:160px;border-radius:8px;cursor:zoom-in;
1231
1461
  background:var(--bg);object-fit:cover;
1232
1462
  }
1463
+ /* A clip a little larger than a picture, since it is watched, not glanced at. */
1464
+ .user .shots video{max-width:min(320px,100%);max-height:200px;border-radius:8px;background:#000;display:block}
1233
1465
  .user{margin-bottom:18px}
1234
1466
  .panel{max-width:720px;margin:8px auto 0}
1235
1467
  .panel-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding-block:48px;font-size:13px}
@@ -1451,6 +1683,9 @@ details.setting>summary:focus-visible{outline:none;background:var(--hover)}
1451
1683
  .setting-status{display:flex;align-items:center;gap:7px;margin-top:8px;color:var(--dim)}
1452
1684
  .status-dot{flex:none;width:7px;height:7px;border-radius:50%;background:var(--dim);opacity:.6}
1453
1685
  .status-dot.ok{background:var(--ok);opacity:1}
1686
+ /* A paired phone: its name, when, and the way to forget it, on one line. */
1687
+ .remote-phone{display:flex;align-items:center;gap:10px;min-height:28px}
1688
+ .remote-phone .remote-phone-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1454
1689
  /* The presets and the calendar on one line, on the same baseline. */
1455
1690
  .usage-window{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
1456
1691
  .usage-kind-title{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
@@ -1709,13 +1944,41 @@ details.provider>summary:hover{background:var(--hover)}
1709
1944
  * with it. */
1710
1945
  .row-head .ghost+.ghost,.row-head .ghost+.switch,.row-head .switch+.ghost{margin-left:0}
1711
1946
  .routine-where{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
1712
- .routine-history{margin-top:6px}
1713
- .routine-history>summary{cursor:pointer;width:max-content}
1714
- .routine-runs{display:flex;flex-direction:column;gap:2px;margin-top:6px}
1715
- .routine-run{display:flex;gap:10px;align-items:baseline;text-align:left;border:0;background:transparent;color:var(--fg);font:inherit;padding:4px 6px;border-radius:6px}
1716
- button.routine-run{cursor:pointer}
1717
- button.routine-run:hover{background:var(--hover)}
1718
- .routine-run.failed{color:var(--err)}
1947
+ .routine-history{margin-top:8px}
1948
+ .routine-history-head{display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;
1949
+ font:inherit;cursor:pointer;padding:3px 6px;margin:0 -6px 4px;border-radius:6px}
1950
+ .routine-history-head:hover{background:var(--hover);color:var(--fg)}
1951
+ .routine-history-head .chev{display:inline-block;transition:transform .15s;font-size:10px}
1952
+ .routine-history.folded .chev{transform:rotate(-90deg)}
1953
+ .routine-history.folded .routine-runs{display:none}
1954
+ .routine-runs{display:flex;flex-direction:column;gap:2px}
1955
+ /* Each run a quiet row that lights on hover: status, time, facts on one line,
1956
+ and the first line of what it said beneath. No rules between them. */
1957
+ .routine-run{display:flex;flex-direction:column;gap:3px;text-align:left;border:0;background:transparent;
1958
+ color:var(--fg);font:inherit;font-size:12.5px;padding:7px 8px;margin:0 -8px;border-radius:8px;min-width:0}
1959
+ .routine-run[role=button]{cursor:pointer}
1960
+ .routine-run[role=button]:hover,.routine-run.open{background:var(--hover)}
1961
+ .routine-run:focus-visible{outline:none;background:var(--hover)}
1962
+ .routine-run-detail{display:none;flex-direction:column;align-items:flex-start;gap:8px;margin-top:4px;cursor:default}
1963
+ .routine-run.open .routine-run-detail{display:flex}
1964
+ .routine-run.open:not(.running) .routine-run-text{display:none}
1965
+ .routine-run-full{word-break:break-word;max-height:360px;overflow:auto;width:100%;
1966
+ line-height:1.55;user-select:text;color:var(--fg);font-size:13px}
1967
+ .routine-run-full.err{color:var(--err);white-space:pre-wrap}
1968
+ .routine-run-full.md>:first-child{margin-top:0}
1969
+ .routine-run.running .routine-run-text{color:var(--fg)}
1970
+ .routine-run-top{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap}
1971
+ .routine-run-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
1972
+ .routine-run-text.err{color:var(--err)}
1973
+ .run-status{font-size:11px;font-weight:550;padding:1px 8px;border-radius:999px;flex:0 0 auto}
1974
+ .run-status.ok{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}
1975
+ .run-status.failed{color:var(--err);background:color-mix(in srgb,var(--err) 14%,transparent)}
1976
+ .run-status.skipped{color:var(--dim);background:var(--hover)}
1977
+ .run-status.running{color:var(--warn);background:color-mix(in srgb,var(--warn) 16%,transparent)}
1978
+ .run-status.running::before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;
1979
+ background:currentColor;margin-right:5px;vertical-align:1px;animation:run-pulse 1.2s ease-in-out infinite}
1980
+ @keyframes run-pulse{50%{opacity:.25}}
1981
+ .routine-more{align-self:flex-start;margin-top:4px}
1719
1982
  .small{font-size:12px;white-space:pre-wrap;word-break:break-all}
1720
1983
  .keyrow{display:flex;gap:6px;margin:8px 0 6px}
1721
1984
  .keyrow input{
@@ -1824,6 +2087,54 @@ button.signin-site:disabled{opacity:.4;cursor:default}
1824
2087
  #browser-import-dialog .dlg-actions{padding:16px 22px 20px}
1825
2088
  #browser-import-dialog .dlg-actions button:disabled{opacity:.4;cursor:default}
1826
2089
  #browser-import-dialog input[type=text]{height:38px;padding:0 13px;font-size:13px}
2090
+ /* Feedback. Same frame as the import dialog; the kind is a row of fills, the
2091
+ chosen one taking the selection fill rather than a ring. */
2092
+ #feedback-dialog{width:min(520px,94vw);max-width:min(520px,94vw)}
2093
+ #feedback-dialog .cn-head{padding:20px 22px 2px}
2094
+ #feedback-dialog .cn-body{padding:14px 22px 4px;gap:10px}
2095
+ #feedback-dialog .dlg-actions{padding:16px 22px 20px}
2096
+ #feedback-dialog .dlg-actions button:disabled{opacity:.4;cursor:default}
2097
+ #feedback-dialog textarea{width:100%;box-sizing:border-box;min-height:140px;padding:11px 13px;border:0;border-radius:10px;outline:none;background:var(--field);color:var(--fg);font:inherit;font-size:13px;line-height:1.55;resize:vertical}
2098
+ #feedback-dialog textarea:focus-visible{background:var(--hover)}
2099
+ .fb-kinds{display:flex;gap:4px}
2100
+ .fb-kind{height:30px;padding:0 13px;border:0;border-radius:999px;background:var(--field);color:var(--dim);font:inherit;font-size:12.5px;cursor:pointer}
2101
+ .fb-kind:hover{background:var(--hover);color:var(--fg)}
2102
+ .fb-kind.on{background:var(--sel);color:var(--fg);font-weight:550}
2103
+ .fb-note{margin:0;font-size:11.5px;color:var(--dim);line-height:1.5}
2104
+ .fb-note:empty{display:none}
2105
+ /* Attachments as square tiles in a row, the last one adding another. Fills
2106
+ only: the add tile is a softer fill, not a dashed outline. */
2107
+ .fb-media{display:flex;flex-wrap:wrap;gap:8px}
2108
+ .fb-tile{position:relative;width:76px;height:76px;flex:0 0 auto;border-radius:14px;overflow:hidden;background:var(--field)}
2109
+ .fb-thumb{display:block;width:100%;height:100%;object-fit:cover}
2110
+ .fb-tile.is-video .fb-thumb{background:#000}
2111
+ /* A play mark over a recording's first frame, so it reads as video at a glance. */
2112
+ .fb-play{position:absolute;left:50%;top:50%;width:26px;height:26px;margin:-13px 0 0 -13px;border-radius:50%;background:rgba(0,0,0,.5);backdrop-filter:blur(6px)}
2113
+ .fb-play::after{content:"";position:absolute;left:10px;top:8px;border-style:solid;border-width:5px 0 5px 8px;border-color:transparent transparent transparent #fff}
2114
+ .fb-remove{
2115
+ position:absolute;top:5px;right:5px;width:20px;height:20px;padding:0;border:0;border-radius:50%;
2116
+ background:rgba(0,0,0,.55);backdrop-filter:blur(6px);color:#fff;font-size:14px;line-height:20px;text-align:center;cursor:pointer;
2117
+ opacity:.9;transition:opacity .15s,background .15s;
2118
+ }
2119
+ .fb-remove:hover{opacity:1;background:rgba(0,0,0,.75)}
2120
+ .fb-remove:disabled{display:none}
2121
+ .fb-tile.is-uploading::before,.fb-tile.is-done::before{content:"";position:absolute;inset:0;background:rgba(0,0,0,.35)}
2122
+ .fb-spinner{position:absolute;left:50%;top:50%;width:20px;height:20px;margin:-10px 0 0 -10px;border-radius:50%;
2123
+ border:2px solid rgba(255,255,255,.35);border-top-color:#fff;animation:fb-spin .8s linear infinite}
2124
+ @keyframes fb-spin{to{transform:rotate(360deg)}}
2125
+ .fb-done{position:absolute;left:50%;top:50%;width:22px;height:22px;margin:-11px 0 0 -11px;border-radius:50%;background:#fff;color:#111;font-size:12px;font-weight:700;line-height:22px;text-align:center}
2126
+ .fb-add{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;padding:0;border:0;color:var(--dim);font:inherit;cursor:pointer;transition:background .15s,color .15s}
2127
+ .fb-add:hover,.fb-add:focus-visible{background:var(--hover);color:var(--fg);outline:none}
2128
+ .fb-add .fb-add-icon{width:20px;height:20px}
2129
+ .fb-add-label{font-size:11.5px;font-weight:500}
2130
+ /* Beside the tiles, centred on them: what goes here and the limits. It takes
2131
+ the rest of the row and drops below the tiles when the row is full. */
2132
+ .fb-media{align-items:center}
2133
+ .fb-caption{display:flex;flex-direction:column;gap:3px;flex:1;min-width:170px;padding-left:4px}
2134
+ .fb-caption-title{font-size:12.5px;font-weight:500;color:var(--fg)}
2135
+ .fb-caption-sub{font-size:11.5px;color:var(--dim);line-height:1.45}
2136
+ /* Something is being dragged over: the whole dialog is the target, said with a fill. */
2137
+ #feedback-dialog.fb-dropping{background:var(--sel)}
1827
2138
  .bi-label{font-size:12px;color:var(--dim)}
1828
2139
  .bi-sites{display:flex;flex-direction:column;gap:4px;max-height:240px;overflow:auto}
1829
2140
  .bi-site{display:flex;align-items:center;gap:10px;padding:8px 11px;border-radius:9px;background:var(--field);cursor:pointer}
@@ -1908,6 +2219,23 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
1908
2219
  }
1909
2220
  .pw-tab:hover{color:var(--fg)}
1910
2221
  .pw-tab.on{background:var(--bg);color:var(--fg);font-weight:500}
2222
+ .pw-tab{display:inline-flex;align-items:center;justify-content:center;gap:6px}
2223
+ /* The site's badge on the same tab, at this dialog's size. */
2224
+ .pw-tab-save{
2225
+ font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:999px;
2226
+ background:var(--fg);color:var(--bg);font-variant-numeric:tabular-nums;
2227
+ }
2228
+ /* Twelve months at the monthly price, struck, just left of what the year costs. */
2229
+ .pw-was{color:var(--dim);font-weight:400;margin-right:6px;text-decoration-thickness:1px}
2230
+ /* What the credits reach: a quiet heading, then one row per kind of model —
2231
+ the kind in a narrow column, its marks and names beside it. */
2232
+ .pw-media{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:7px 14px;font-size:12.5px}
2233
+ .pw-media-head{grid-column:1/-1;color:var(--dim);font-size:11.5px}
2234
+ .pw-media-kind{color:var(--dim);white-space:nowrap}
2235
+ .pw-media-models{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px}
2236
+ .pw-media-model{display:inline-flex;align-items:center;gap:5px;color:var(--fg);font-weight:500;white-space:nowrap}
2237
+ .pw-media-mark{display:inline-flex;width:14px;height:14px}
2238
+ .pw-media-mark svg{width:100%;height:100%}
1911
2239
  /* A labelled break, not a rule: what follows is a different kind of purchase,
1912
2240
  and a bare line would read as the list simply continuing. */
1913
2241
  .pw-sep{
@@ -2056,26 +2384,27 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2056
2384
  * is right for browsing and wrong for "the one about the parser, two days
2057
2385
  * ago". So this is a search field over every session in every tree opened
2058
2386
  * here, the projects themselves, and the few actions worth a shortcut. */
2059
- #palette{width:min(560px,92vw);max-height:min(60vh,520px)}
2387
+ #palette{width:min(600px,92vw);max-height:min(64vh,560px)}
2060
2388
  #palette[open]{display:flex;flex-direction:column}
2061
2389
  /* One left edge for everything: the field's magnifier, each row's icon, the
2062
- group labels and the key hints all start 22px in, and the field's text
2063
- starts where the row labels do. Rows sit 8px in so their highlight has room;
2390
+ group labels and the key hints all start 26px in, and the field's text
2391
+ starts where the row labels do. Rows sit 12px in so their highlight has room;
2064
2392
  their own 14px padding brings the content back to the shared edge. */
2065
- .cmdk-head{position:relative;padding:12px 12px 8px}
2066
- .cmdk-head .ico-sm{position:absolute;left:22px;top:calc(50% - 5.5px);pointer-events:none}
2067
- #palette .cmdk-head input{height:40px;padding:0 10px 0 35px;font-size:14px}
2068
- #cmdk-list{overflow-y:auto;padding:0 8px 8px;flex:1;scroll-padding-block:4px}
2393
+ .cmdk-head{position:relative;padding:16px 16px 10px}
2394
+ .cmdk-head .ico-sm{position:absolute;left:26px;top:calc(50% - 4.5px);pointer-events:none}
2395
+ #palette .cmdk-head input{height:42px;padding:0 12px 0 35px;font-size:14px}
2396
+ #cmdk-list{overflow-y:auto;padding:0 12px 12px;flex:1;scroll-padding-block:6px}
2069
2397
  .cmdk-group{
2070
- padding:12px 14px 5px;font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;
2398
+ padding:16px 14px 6px;font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;
2071
2399
  color:var(--dim);
2072
2400
  }
2073
- .cmdk-group:first-child{padding-top:6px}
2401
+ .cmdk-group:first-child{padding-top:8px}
2074
2402
  .cmdk-item{
2075
2403
  width:100%;display:flex;align-items:center;gap:10px;border:0;border-radius:8px;
2076
2404
  background:transparent;color:var(--fg);font:inherit;font-size:13px;cursor:pointer;
2077
- padding:8px 14px;text-align:left;
2405
+ padding:10px 14px;text-align:left;
2078
2406
  }
2407
+ .cmdk-item + .cmdk-item{margin-top:2px}
2079
2408
  /* Hover and the keyboard cursor are the same highlight: a list driven by both
2080
2409
  at once needs one answer to "which row does Enter take". */
2081
2410
  .cmdk-item.cursor{background:var(--sel)}
@@ -2087,8 +2416,7 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2087
2416
  }
2088
2417
  .cmdk-empty{padding:12px 14px;font-size:12.5px;color:var(--dim)}
2089
2418
  .cmdk-foot{
2090
- display:flex;gap:16px;padding:9px 22px 11px;font-size:11px;color:var(--dim);
2091
- box-shadow:0 -1px 0 var(--line);
2419
+ display:flex;gap:18px;padding:10px 26px 16px;font-size:11px;color:var(--dim);
2092
2420
  }
2093
2421
  .cmdk-foot kbd{
2094
2422
  font:inherit;font-size:10.5px;border:1px solid currentColor;border-radius:4px;
@@ -2108,7 +2436,10 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2108
2436
  #connect:has(#cn-aside:not([hidden])){width:min(760px,94vw)}
2109
2437
  .cn-grid{display:grid;grid-template-columns:1fr}
2110
2438
  #connect:has(#cn-aside:not([hidden])) .cn-grid{grid-template-columns:minmax(0,.85fr) minmax(0,1fr)}
2439
+ /* The sidebar's bare aside rule (a fixed width, contents pinned right) would
2440
+ * otherwise reach this one too, and set the wordmark against the divider. */
2111
2441
  .cn-aside{
2442
+ width:auto;flex:initial;align-items:stretch;
2112
2443
  display:flex;flex-direction:column;gap:20px;padding:30px 28px;
2113
2444
  background:var(--hover);border-right:1px solid var(--line);
2114
2445
  border-radius:16px 0 0 16px;
@@ -2265,6 +2596,10 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2265
2596
  three sentences side by side either wrap mid-phrase or shrink the type. Down
2266
2597
  the page they read as a short list of offers, which is what they are. */
2267
2598
  #starters{display:flex;flex-direction:column;align-items:flex-start;gap:8px}
2599
+ /* A rotated or newly written set gets a short fade, so the change is seen
2600
+ rather than noticed as a flicker. */
2601
+ #starters.fresh button{animation:starters-in .35s ease-out both}
2602
+ @keyframes starters-in{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
2268
2603
  #starters button{
2269
2604
  border:0;background:var(--panel);color:var(--fg);border-radius:999px;
2270
2605
  padding:8px 15px;font:inherit;font-size:13px;cursor:pointer;text-align:left;
@@ -2342,6 +2677,34 @@ details.tool>summary:hover::after{opacity:1}
2342
2677
  .tool-body{padding:2px 12px 10px}
2343
2678
  .tool-body pre{margin:0;overflow-x:auto;font-size:12px;max-height:340px}
2344
2679
  .tool-body .label{font-size:11px;color:var(--dim);margin:0 0 4px}
2680
+ /* The screenshots a tool took, under its result: the same thumbnail as a
2681
+ pasted image, a click away from full size. */
2682
+ .tool-shots{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
2683
+ .tool-shots img{
2684
+ max-width:220px;max-height:160px;border-radius:8px;cursor:zoom-in;
2685
+ background:var(--bg);object-fit:cover;
2686
+ }
2687
+ /* The windows a Computer use turn ended on: the result, so out in the turn
2688
+ rather than folded into a step, and a little larger than a step's shots. */
2689
+ .turn-shots{display:flex;flex-wrap:wrap;gap:10px;margin:10px 0 4px}
2690
+ .turn-shot{margin:0;display:flex;flex-direction:column;gap:5px;max-width:280px}
2691
+ .turn-shot img{
2692
+ display:block;max-width:280px;max-height:200px;border-radius:10px;cursor:zoom-in;
2693
+ background:var(--bg);object-fit:cover;
2694
+ }
2695
+ .turn-shot figcaption{font-size:11.5px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2696
+ /* The preview: the image as large as the window allows, over a dark scrim.
2697
+ Dark in both themes — a screenshot reads against black, not the page. */
2698
+ .img-preview{
2699
+ position:fixed;inset:0;z-index:90;display:grid;place-items:center;padding:32px;
2700
+ background:rgba(0,0,0,.82);cursor:zoom-out;
2701
+ }
2702
+ .img-preview img{max-width:100%;max-height:100%;border-radius:8px;object-fit:contain}
2703
+ .img-preview-count{
2704
+ position:absolute;bottom:14px;left:50%;transform:translateX(-50%);
2705
+ color:#fff;font-size:12px;opacity:.7;
2706
+ }
2707
+ .img-preview-count:empty{display:none}
2345
2708
  .tool-body .kv{margin:0 0 8px}
2346
2709
  .tool-summary{font-size:12.5px;line-height:1.5;margin:0 0 6px}
2347
2710
  details.tool-raw>summary{font-size:11px;color:var(--dim);cursor:pointer;margin:0 0 4px}
@@ -2465,6 +2828,8 @@ details.thinking[open]>summary::after{transform:translateY(-2px) rotate(45deg)}
2465
2828
 
2466
2829
  /* composer */
2467
2830
  form{padding:6px 24px 18px;background:var(--bg)}
2831
+ /* 24px plus the 11px the transcript's scrollbar gutter takes on each side. */
2832
+ @media (min-width:761px){#composer{padding-left:35px;padding-right:35px}}
2468
2833
  .composer{
2469
2834
  position:relative;
2470
2835
  max-width:720px;margin:0 auto;border-radius:16px;
@@ -2585,6 +2950,57 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2585
2950
  }
2586
2951
  .icon-btn svg{width:14px;height:14px}
2587
2952
  .icon-btn:hover{background:var(--hover)}
2953
+ /* Dictation. Listening is the harness alive, so it takes the brand: the mic
2954
+ becomes a soft lime well holding a stop square, breathing, and the panel
2955
+ above the box shows the voice in lime. Actions (done) keep --accent, as
2956
+ every action does. Fills and shadows only, never a ring. */
2957
+ .mic-btn{width:30px;height:30px;border-radius:999px;color:var(--dim);transition:background .2s,color .2s}
2958
+ .mic-btn svg{width:16px;height:16px}
2959
+ .mic-btn .mic-stop{display:none;width:14px;height:14px}
2960
+ .mic-btn:hover{color:var(--fg)}
2961
+ .mic-btn.is-recording{background:var(--brand-tint);color:var(--brand-ink);animation:mic-breathe 1.8s ease-in-out infinite}
2962
+ .mic-btn.is-recording .mic-idle{display:none}
2963
+ .mic-btn.is-recording .mic-stop{display:block}
2964
+ .mic-btn:disabled{opacity:.45;cursor:default}
2965
+ @keyframes mic-breathe{50%{background:color-mix(in srgb,var(--brand) 34%,transparent)}}
2966
+ .composer-bar .mic-btn,.composer-bar #send,.composer-bar #stop{flex-shrink:0}
2967
+ /* The panel: centred over the box's top edge, a pill of its own. One row:
2968
+ what is going on, then the voice and the two ways out while listening. */
2969
+ .dictate-pop{
2970
+ position:absolute;left:50%;bottom:calc(100% + 12px);z-index:30;transform:translateX(-50%);
2971
+ display:flex;align-items:center;gap:12px;min-height:52px;width:max-content;max-width:min(480px,calc(100% - 24px));
2972
+ padding:8px 8px 8px 20px;border-radius:999px;background:var(--panel);color:var(--fg);
2973
+ box-shadow:0 18px 44px rgba(0,0,0,.26),0 3px 10px rgba(0,0,0,.12);
2974
+ font-size:13.5px;font-weight:500;letter-spacing:.01em;animation:dp-in .18s cubic-bezier(.2,.8,.2,1);
2975
+ }
2976
+ .dictate-pop[hidden]{display:none}
2977
+ @keyframes dp-in{from{opacity:0;transform:translate(-50%,8px) scale(.97)}to{opacity:1;transform:translateX(-50%)}}
2978
+ .dp-dot{width:8px;height:8px;border-radius:50%;background:var(--brand);flex:0 0 auto;animation:dp-dot 1.4s ease-in-out infinite}
2979
+ @keyframes dp-dot{50%{opacity:.35}}
2980
+ .dp-spin{width:14px;height:14px;border-radius:50%;border:2px solid var(--brand-tint);border-top-color:var(--brand);flex:0 0 auto;animation:dp-spin .8s linear infinite}
2981
+ @keyframes dp-spin{to{transform:rotate(360deg)}}
2982
+ .dp-text{white-space:nowrap}
2983
+ .dp-time{color:var(--dim);font-weight:400;font-variant-numeric:tabular-nums;min-width:2.4em}
2984
+ .dp-wave{width:168px;height:32px;flex:0 1 auto;min-width:64px;color:var(--brand)}
2985
+ .dp-btn{width:36px;height:36px;border:0;border-radius:50%;display:grid;place-items:center;cursor:pointer;flex:0 0 auto;padding:0;font:inherit;transition:background .15s,transform .15s}
2986
+ .dp-btn:active{transform:scale(.94)}
2987
+ .dp-btn svg{width:15px;height:15px}
2988
+ .dp-cancel,.dp-close{background:var(--hover);color:var(--fg)}
2989
+ .dp-done{background:var(--accent);color:var(--bg)}
2990
+ .dp-btn:focus-visible{outline:2px solid var(--fg);outline-offset:2px}
2991
+ /* Which parts show is the state's: listening, working, or saying what went wrong. */
2992
+ .dictate-pop:not([data-state="listening"]) .dp-dot,
2993
+ .dictate-pop:not([data-state="listening"]) .dp-time,
2994
+ .dictate-pop:not([data-state="listening"]) .dp-wave,
2995
+ .dictate-pop:not([data-state="listening"]) .dp-cancel,
2996
+ .dictate-pop:not([data-state="listening"]) .dp-done,
2997
+ .dictate-pop:not([data-state="working"]) .dp-spin,
2998
+ .dictate-pop:not([data-state="error"]) .dp-close{display:none}
2999
+ .dictate-pop[data-state="working"]{padding-right:20px}
3000
+ /* The error red mixed toward the text colour: bright enough on the dark
3001
+ theme, still deep on the light one. */
3002
+ .dictate-pop[data-state="error"] .dp-text{color:color-mix(in srgb,var(--err) 62%,var(--fg));white-space:normal;line-height:1.45}
3003
+ @media (prefers-reduced-motion:reduce){.dictate-pop,.dp-dot,.dp-spin,.mic-btn.is-recording{animation:none}}
2588
3004
  /* The folder it runs in, set at the same weight as the model beside it: three
2589
3005
  settings on one bar, none of them shouting over the others. */
2590
3006
  #workspace-picker .picker-btn{gap:6px}
@@ -2626,6 +3042,54 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2626
3042
  .effort-name.on{color:var(--fg);font-weight:600}
2627
3043
  #attachments{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
2628
3044
  #attachments:empty{display:none}
3045
+ /* The tray: what the message carries, as cards sitting just above the box.
3046
+ Pictures at a size that can be recognised, files as a name and a line;
3047
+ the same fill as the box below and no outline, with the × on the card's
3048
+ corner where a thumbnail leaves no room beside it. */
3049
+ /* The box's own width and centring, so the cards start where the box does
3050
+ rather than at the column's edge. */
3051
+ #tray{display:flex;flex-wrap:wrap;gap:8px;max-width:720px;margin:0 auto 8px}
3052
+ #tray:empty{display:none}
3053
+ .tray-card{
3054
+ position:relative;display:flex;align-items:center;gap:10px;height:64px;
3055
+ max-width:240px;padding:0 30px 0 10px;border-radius:12px;background:var(--panel);
3056
+ }
3057
+ .tray-card.tray-media{width:64px;padding:0;overflow:hidden;background:var(--field)}
3058
+ .tray-card img,.tray-card video{width:100%;height:100%;object-fit:cover;display:block}
3059
+ .tray-card img{cursor:zoom-in}
3060
+ /* A clip: its first frame where a file's icon goes, with its length and size
3061
+ beside the name. */
3062
+ .tray-thumb{position:relative;width:44px;height:44px;flex:0 0 auto;border-radius:9px;overflow:hidden;background:var(--field)}
3063
+ .tray-thumb video{width:100%;height:100%;object-fit:cover;display:block}
3064
+ .tray-duration:not(:empty)::after{content:" · "}
3065
+ .tray-play{position:absolute;left:50%;top:50%;margin:-9px 0 0 -9px;width:18px;height:18px;border-radius:50%;display:grid;place-items:center;background:#0000008c;color:#fff;pointer-events:none;transition:opacity .12s}
3066
+ .tray-card.video:hover .tray-play{opacity:0}
3067
+ .tray-play svg{width:10px;height:10px}
3068
+ .tray-icon{width:36px;height:36px;flex:0 0 auto;border-radius:9px;display:grid;place-items:center;background:var(--field);color:var(--dim)}
3069
+ .tray-icon svg{width:16px;height:16px}
3070
+ .tray-text{display:flex;flex-direction:column;gap:2px;min-width:0}
3071
+ .tray-name{font-size:12.5px;color:var(--fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3072
+ .tray-sub{font-size:11px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3073
+ .tray-card .x{
3074
+ position:absolute;top:4px;right:4px;width:20px;height:20px;padding:0;border:0;border-radius:50%;
3075
+ display:grid;place-items:center;font-size:13px;line-height:1;cursor:pointer;
3076
+ background:var(--field);color:var(--dim);
3077
+ }
3078
+ .tray-card.tray-media .x{background:#0000008c;color:#fff;opacity:0;transition:opacity .12s}
3079
+ .tray-card.tray-media:hover .x,.tray-card .x:focus-visible{opacity:1}
3080
+ /* No hover on a touch screen: the × is always there. */
3081
+ @media (hover:none){.tray-card.tray-media .x{opacity:1}}
3082
+ .tray-card .x:hover{color:var(--fg)}
3083
+ /* A phone: one row that scrolls sideways rather than rows that push the box
3084
+ up the screen, and cards a little smaller so two or three still show. */
3085
+ @media (max-width:760px){
3086
+ #tray{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch;margin-bottom:6px}
3087
+ #tray::-webkit-scrollbar{display:none}
3088
+ .tray-card{flex:0 0 auto;height:56px;max-width:200px}
3089
+ .tray-card.tray-media{width:56px}
3090
+ .tray-icon,.tray-thumb{width:36px;height:36px}
3091
+ }
3092
+ .tray-card.tray-media .x:hover{color:#fff;background:#000000b3}
2629
3093
  /* Outside the composer, tracking its width so the rows line up with the box
2630
3094
  they are waiting on. min-width:0 twice over — here and on the line inside
2631
3095
  each row — because a flex item defaults to refusing to shrink below its
@@ -2661,6 +3125,91 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2661
3125
  :root[data-theme="dark"] .q .go:hover{color:var(--brand)}
2662
3126
  .q .x{border:0;background:transparent;color:var(--dim);cursor:pointer;font:inherit;padding:0 2px}
2663
3127
  .q .x:hover{color:var(--err)}
3128
+ /* The goal being worked toward: the reason the turn keeps going, in the same
3129
+ brand ink that marks the harness at work. Its words in full colour, since
3130
+ unlike a queued line it is what everything below is for. */
3131
+ .q.goal .tag{color:var(--brand-ink)}
3132
+ :root[data-theme="dark"] .q.goal .tag{color:var(--brand)}
3133
+ .q.goal .n{color:var(--fg)}
3134
+ /* A background task: work this conversation set going. Running ones say so
3135
+ in the brand ink that marks the harness at work; ended ones step back, but
3136
+ stay a click from what they said. */
3137
+ .q.task .n{cursor:pointer}
3138
+ .q.task-running .tag,.q.task-queued .tag{color:var(--brand-ink)}
3139
+ :root[data-theme="dark"] .q.task-running .tag,:root[data-theme="dark"] .q.task-queued .tag{color:var(--brand)}
3140
+ .q.task-failed .tag{color:var(--err)}
3141
+ .q.task-completed .n,.q.task-cancelled .n,.q.task-failed .n{color:var(--dim)}
3142
+ .q.task-more{border:0;color:var(--dim);cursor:pointer;font:inherit;font-size:12px;text-align:left}
3143
+ .q.task-more:hover{color:var(--fg)}
3144
+ /* Background results read into the conversation: a card, set apart from
3145
+ what the person typed by its fill and its place on the left. */
3146
+ .task-card-row-wrap{margin-bottom:26px}
3147
+ .task-card{display:flex;flex-direction:column;gap:4px;max-width:520px;background:var(--panel);border-radius:12px;padding:10px 12px;font-size:12px}
3148
+ .task-card-head{color:var(--dim);font-size:11px;margin-bottom:2px}
3149
+ .task-card-row{display:flex;align-items:center;gap:8px;min-width:0}
3150
+ .task-card-row .tag{flex:0 0 auto;color:var(--dim);font-size:11px}
3151
+ .task-card-row.task-completed .tag{color:var(--ok)}
3152
+ .task-card-row.task-failed .tag{color:var(--err)}
3153
+ .task-card-row .n{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3154
+ .task-card-row .go{flex:0 0 auto;height:22px;padding:0 9px;border:0;border-radius:7px;background:var(--hover);color:var(--fg);cursor:pointer;font:inherit;font-size:11px}
3155
+ .task-card-row .go:hover{background:var(--brand-tint);color:var(--brand-ink)}
3156
+ /* The sheet a task opens into: down the right edge, over the conversation,
3157
+ which stays where it was underneath. Set apart by its fill, never a line. */
3158
+ #task-sheet{
3159
+ position:fixed;top:0;right:0;bottom:0;z-index:30;width:min(560px,100vw);
3160
+ display:flex;flex-direction:column;background:var(--panel);
3161
+ box-shadow:-12px 0 32px #00000014;
3162
+ }
3163
+ #task-sheet[hidden]{display:none}
3164
+ /* In the desktop app the tab strip under the sheet's top edge is the window's
3165
+ drag handle, and a drag region takes clicks whatever is drawn over it: the
3166
+ sheet's close and stop buttons would only ever move the window. */
3167
+ #task-sheet{-webkit-app-region:no-drag}
3168
+ .task-head{display:flex;align-items:flex-start;gap:10px;padding:16px 16px 10px}
3169
+ .task-titles{flex:1;min-width:0}
3170
+ .task-title{font-weight:600;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3171
+ .task-meta{color:var(--dim);font-size:12px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3172
+ .task-stop{
3173
+ flex:0 0 auto;height:28px;padding:0 10px;border:0;border-radius:8px;
3174
+ background:var(--hover);color:var(--fg);cursor:pointer;font:inherit;font-size:12px;
3175
+ }
3176
+ .task-stop:hover{color:var(--err)}
3177
+ .task-close{flex:0 0 auto;width:28px;height:28px;border:0;border-radius:8px;background:transparent;color:var(--dim);cursor:pointer;font:inherit;font-size:18px;line-height:1}
3178
+ .task-close:hover{background:var(--hover);color:var(--fg)}
3179
+ .task-chips{position:relative;display:flex;flex-wrap:nowrap;gap:6px;overflow-x:auto;padding:0 16px 10px;scrollbar-width:none;overscroll-behavior-x:contain}
3180
+ .task-chips::-webkit-scrollbar{display:none}
3181
+ .task-chips[hidden]{display:none}
3182
+ .task-chip{
3183
+ flex:0 0 auto;height:26px;padding:0 10px;border:0;border-radius:13px;
3184
+ background:var(--hover);color:var(--dim);cursor:pointer;font:inherit;font-size:12px;
3185
+ white-space:nowrap;
3186
+ }
3187
+ .task-chip.on{background:var(--brand-tint);color:var(--brand-ink)}
3188
+ :root[data-theme="dark"] .task-chip.on{color:var(--brand)}
3189
+ .task-log{flex:1;min-height:0;overflow-y:auto;padding:4px 16px 16px;display:flex;flex-direction:column;gap:10px;font-size:13px;line-height:1.55}
3190
+ .task-msg.user{align-self:flex-end;max-width:85%;background:var(--bg);border-radius:12px;padding:8px 12px;white-space:pre-wrap;overflow-wrap:anywhere}
3191
+ .task-msg.assistant{overflow-wrap:anywhere}
3192
+ .task-tool{background:var(--bg);border-radius:10px;padding:6px 10px;font-size:12px}
3193
+ .task-tool summary{display:flex;gap:8px;cursor:pointer;list-style:none;min-width:0}
3194
+ .task-tool summary::-webkit-details-marker{display:none}
3195
+ .task-tool-name{flex:0 0 auto;font-weight:600}
3196
+ .task-tool-input{flex:1;min-width:0;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono,ui-monospace,monospace)}
3197
+ .task-tool.err .task-tool-name{color:var(--err)}
3198
+ .task-tool-result{margin:6px 0 0;max-height:240px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--dim);font-size:11.5px}
3199
+ .task-note{color:var(--dim);font-size:12px}
3200
+ .task-error{color:var(--err);font-size:12px}
3201
+ .task-empty{color:var(--dim)}
3202
+ .task-working{display:flex;align-items:center;gap:8px;color:var(--dim)}
3203
+ /* The sheet's own surface runs to its bottom edge: no band behind the box. */
3204
+ .task-form{padding:8px 16px 16px;background:transparent}
3205
+ /* Field and send button as one box, the send arrow inside at its corner. */
3206
+ .task-box{display:flex;align-items:flex-end;gap:8px;background:var(--bg);border-radius:14px;padding:8px 8px 8px 12px}
3207
+ .task-input{
3208
+ flex:1;min-width:0;resize:none;border:0;padding:5px 0;max-height:160px;
3209
+ background:transparent;color:var(--fg);font:inherit;font-size:13px;line-height:1.5;outline:none;
3210
+ }
3211
+ .task-input:disabled{opacity:.6}
3212
+ .task-send{flex:0 0 auto}
2664
3213
  .att{
2665
3214
  display:flex;align-items:center;gap:7px;background:var(--bg);border-radius:8px;
2666
3215
  padding:5px 8px;font-size:12px;max-width:260px;
@@ -2674,6 +3223,37 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2674
3223
  the chip carries a mark for what it points at instead of a size. */
2675
3224
  .att .ico-sm{width:13px;height:13px;opacity:.65}
2676
3225
  .att .where{color:var(--dim);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3226
+ /* Goal mode is how this message is sent, not a thing attached to it: a
3227
+ tinted fill, never an outline, sets it apart from the files. */
3228
+ .att.goal{background:var(--brand-tint);color:var(--brand-ink)}
3229
+ :root[data-theme="dark"] .att.goal{color:var(--brand)}
3230
+ .att.goal .ico-sm{opacity:1}
3231
+ /* Computer use stays on for the whole conversation, so it sits in the chip
3232
+ row on every message: a full brand fill there shouted louder than anything
3233
+ the person typed. The file chips' own quiet fill, with the colour kept to
3234
+ the mark. Turning it off is not destructive, so the × does not go red. */
3235
+ .att.computer{color:var(--fg)}
3236
+ .att.computer .ico-sm{opacity:1;color:var(--brand-ink)}
3237
+ .att.computer .x:hover{color:var(--fg)}
3238
+ .att.computer-perm{flex-basis:100%;display:flex;flex-direction:column;align-items:stretch;gap:6px;padding:10px 12px;background:var(--field);border-radius:10px;max-width:none}
3239
+ .computer-perm .cp-head{display:flex;align-items:center;gap:8px;font-weight:600;color:var(--fg)}
3240
+ .computer-perm .cp-body{color:var(--dim);font-size:12px;white-space:normal}
3241
+ .computer-perm .cp-row{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12.5px;color:var(--fg)}
3242
+ .computer-perm .cp-ok{width:26px;text-align:center;color:var(--dim);font-size:13px;line-height:24px}
3243
+ /* The app's primary button, inverted on either theme: white on the brand
3244
+ green was unreadable in dark mode. */
3245
+ .computer-perm .cp-btn{border:0;border-radius:7px;padding:4px 12px;background:var(--accent);color:var(--bg);font:inherit;font-size:12px;font-weight:500;cursor:pointer}
3246
+ .computer-perm .cp-btn:hover{background:color-mix(in srgb,var(--accent) 85%,var(--bg))}
3247
+ /* The card's own actions, after the rows: buttons the size of Allow, but
3248
+ secondary — the page's fill on the card's, no outline — and set right, where
3249
+ Allow already sits. Loose green words read as links to somewhere else, and
3250
+ sat under the rows as if they were a third one. */
3251
+ .computer-perm .cp-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}
3252
+ .computer-perm .cp-link{
3253
+ border:0;border-radius:7px;padding:4px 12px;background:var(--bg);color:var(--fg);
3254
+ font:inherit;font-size:12px;font-weight:500;cursor:pointer;
3255
+ }
3256
+ .computer-perm .cp-link:hover{background:color-mix(in srgb,var(--fg) 8%,var(--bg))}
2677
3257
  /* Wider than the model picker's: these rows carry a name and the folder it
2678
3258
  sits in, and truncating the folder away leaves three files called index.ts. */
2679
3259
  #attach-picker .menu{min-width:min(380px,86vw)}
@@ -2761,8 +3341,11 @@ button.ghost,a.ghost{
2761
3341
  background:var(--sel);color:var(--fg);font:inherit;font-size:12.5px;cursor:pointer;
2762
3342
  }
2763
3343
  button.ghost:hover,a.ghost:hover{background:var(--hover)}
3344
+ /* Just above the composer, however tall it is: a fixed offset was covered as
3345
+ soon as attachment cards or a longer draft grew the form under it.
3346
+ --form-h is measured in transcript.ts. */
2764
3347
  #jump{
2765
- position:absolute;right:24px;bottom:118px;border-radius:999px;padding:7px 15px;border:0;
3348
+ position:absolute;right:24px;bottom:calc(var(--form-h, 108px) + 10px);border-radius:999px;padding:7px 15px;border:0;
2766
3349
  background:var(--bg);color:var(--fg);cursor:pointer;font:inherit;font-size:12px;
2767
3350
  box-shadow:0 1px 2px #0000000f,0 6px 20px #0000001f;
2768
3351
  }
@@ -2773,6 +3356,11 @@ dialog{
2773
3356
  box-shadow:0 2px 6px #00000014,0 20px 60px #00000033;
2774
3357
  }
2775
3358
  dialog::backdrop{background:rgba(0,0,0,.55)}
3359
+ /* A phone asking to pair: the code is the thing to read, so it is the largest. */
3360
+ .pair-dialog{width:min(420px,92vw);padding-bottom:16px}
3361
+ .pair-title{padding:22px 24px 6px;font-size:16px;font-weight:600}
3362
+ .pair-body{padding:0 24px;color:var(--dim);font-size:13.5px;line-height:1.5}
3363
+ .pair-code{margin:18px 24px 6px;padding:14px 0;border-radius:12px;background:var(--sel);text-align:center;font-size:30px;letter-spacing:.12em}
2776
3364
  /* The approval dialog.
2777
3365
  *
2778
3366
  * Everything here is arranged around one question: what exactly is about to
@@ -2913,19 +3501,31 @@ dialog::backdrop{background:rgba(0,0,0,.55)}
2913
3501
  */
2914
3502
  @media (max-width:760px){
2915
3503
  body>aside{
2916
- position:fixed;inset:0 auto 0 0;z-index:20;width:min(280px,84vw);flex-basis:auto;
3504
+ position:fixed;inset:0 auto 0 0;z-index:20;width:min(332px,90vw);flex-basis:auto;
2917
3505
  transform:translateX(-100%);transition:transform .2s;
2918
3506
  padding-bottom:env(safe-area-inset-bottom);
2919
3507
  }
2920
3508
  body>aside.open{transform:none;box-shadow:0 0 0 1px #0000000a,0 12px 40px #00000040}
2921
3509
  body.nav-open #scrim{display:block}
2922
3510
  #nav-close{display:grid}
2923
- #nav-collapse,body.nav-collapsed #nav-expand{display:none}
2924
- body.nav-collapsed>aside{width:min(280px,84vw);flex-basis:auto}
2925
- body.nav-collapsed #tabs{padding-left:12px}
3511
+ #nav-toggle,#side-top{display:none}
3512
+ aside header{display:flex}
3513
+ #side-panel{padding-top:0}
3514
+ #side-panel{border-radius:0;margin:0}
3515
+ body.nav-collapsed>aside{width:min(332px,90vw);flex-basis:auto}
3516
+ #side-body{width:auto;align-self:stretch}
3517
+ #side-panel,body.nav-collapsed #side-panel{flex:1 1 auto;width:auto;min-width:0;margin:0}
2926
3518
  #burger{display:grid}
2927
- #top{padding:6px 10px}
2928
- #top:has(#changes-btn:not([hidden])){padding:6px 10px}
3519
+ /* One bar, not two: the burger sits at the head of the tab strip, in the
3520
+ frame's colour, rather than on a white row of its own above it. #top
3521
+ keeps its row only while it has the changes button to show. */
3522
+ #top{padding:0;min-height:0}
3523
+ #top:has(#changes-btn:not([hidden])){padding:6px 10px 6px 52px;background:var(--frame)}
3524
+ #burger{position:fixed;z-index:15;top:0;left:4px;width:40px;height:36px;margin:0;border-radius:8px}
3525
+ #tabs,body.nav-collapsed #tabs{padding-left:44px}
3526
+ /* The front tab has the width it needs: its buttons show on a phone, where
3527
+ there is no hover to wait for, and they left the title as "Unt…". */
3528
+ .tab.on{max-width:none}
2929
3529
 
2930
3530
  /* Both panes are the screen while open, and each gets a way out of its own:
2931
3531
  the top bar with the changes button is underneath them. */
@@ -2938,7 +3538,9 @@ dialog::backdrop{background:rgba(0,0,0,.55)}
2938
3538
  #explorer-head{padding-top:max(10px,env(safe-area-inset-top))}
2939
3539
  #explorer-body{flex-direction:column}
2940
3540
  #explorer-resizer{display:none}
2941
- #explorer-tree{width:auto;height:var(--explorer-tree-height,38%);max-height:none}
3541
+ #explorer-side{width:auto;height:var(--explorer-tree-height,38%);max-height:none}
3542
+ #explorer-find{height:36px}
3543
+ #explorer-q{font-size:16px}
2942
3544
  /* Stacked, so the handle between them drags up and down — and is a strip
2943
3545
  a thumb can find rather than a hairline. */
2944
3546
  #explorer-split{flex:0 0 18px;margin:0;cursor:row-resize}
@@ -3001,7 +3603,7 @@ dialog::backdrop{background:rgba(0,0,0,.55)}
3001
3603
  .effort-stop,.effort-name{width:40px}
3002
3604
  .effort-stop{height:40px}
3003
3605
  .effort-knob{width:28px;height:28px}
3004
- #jump{right:14px;bottom:160px}
3606
+ #jump{right:14px}
3005
3607
 
3006
3608
  /* Dialogs, with the keyboard up: vh is the height without it. */
3007
3609
  #market{max-height:82dvh}