@oharness/server 0.9.3 → 0.11.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 (93) hide show
  1. package/dist/analytics.d.ts +32 -29
  2. package/dist/analytics.js +225 -278
  3. package/dist/analytics.js.map +1 -1
  4. package/dist/i18n.js +500 -64
  5. package/dist/i18n.js.map +1 -1
  6. package/dist/icons.js +1 -0
  7. package/dist/icons.js.map +1 -1
  8. package/dist/main.js +4 -0
  9. package/dist/main.js.map +1 -1
  10. package/dist/model-order.js +1 -0
  11. package/dist/model-order.js.map +1 -1
  12. package/dist/page/client/account.js +570 -27
  13. package/dist/page/client/account.js.map +1 -1
  14. package/dist/page/client/base.js +55 -12
  15. package/dist/page/client/base.js.map +1 -1
  16. package/dist/page/client/browser.js +1 -1
  17. package/dist/page/client/composer.js +802 -59
  18. package/dist/page/client/composer.js.map +1 -1
  19. package/dist/page/client/computer.d.ts +27 -0
  20. package/dist/page/client/computer.js +358 -0
  21. package/dist/page/client/computer.js.map +1 -0
  22. package/dist/page/client/dialogs.js +168 -6
  23. package/dist/page/client/dialogs.js.map +1 -1
  24. package/dist/page/client/events.js +119 -9
  25. package/dist/page/client/events.js.map +1 -1
  26. package/dist/page/client/files.js +86 -1
  27. package/dist/page/client/files.js.map +1 -1
  28. package/dist/page/client/index.js +8 -0
  29. package/dist/page/client/index.js.map +1 -1
  30. package/dist/page/client/panels.js +14 -10
  31. package/dist/page/client/panels.js.map +1 -1
  32. package/dist/page/client/ripple.d.ts +13 -0
  33. package/dist/page/client/ripple.js +247 -0
  34. package/dist/page/client/ripple.js.map +1 -0
  35. package/dist/page/client/routines.js +432 -45
  36. package/dist/page/client/routines.js.map +1 -1
  37. package/dist/page/client/shell.js +386 -104
  38. package/dist/page/client/shell.js.map +1 -1
  39. package/dist/page/client/state.js +278 -19
  40. package/dist/page/client/state.js.map +1 -1
  41. package/dist/page/client/tasks.d.ts +8 -0
  42. package/dist/page/client/tasks.js +293 -0
  43. package/dist/page/client/tasks.js.map +1 -0
  44. package/dist/page/client/terminal.js +34 -7
  45. package/dist/page/client/terminal.js.map +1 -1
  46. package/dist/page/client/theme.d.ts +17 -0
  47. package/dist/page/client/theme.js +365 -0
  48. package/dist/page/client/theme.js.map +1 -0
  49. package/dist/page/client/transcript.js +196 -8
  50. package/dist/page/client/transcript.js.map +1 -1
  51. package/dist/page/client/usage.js +1 -11
  52. package/dist/page/client/usage.js.map +1 -1
  53. package/dist/page/style.js +745 -76
  54. package/dist/page/style.js.map +1 -1
  55. package/dist/page.js +94 -16
  56. package/dist/page.js.map +1 -1
  57. package/dist/prefs.d.ts +31 -0
  58. package/dist/prefs.js +13 -1
  59. package/dist/prefs.js.map +1 -1
  60. package/dist/remote-crypto.d.ts +78 -0
  61. package/dist/remote-crypto.js +216 -0
  62. package/dist/remote-crypto.js.map +1 -0
  63. package/dist/remote.d.ts +103 -0
  64. package/dist/remote.js +498 -0
  65. package/dist/remote.js.map +1 -0
  66. package/dist/server.d.ts +70 -0
  67. package/dist/server.js +1738 -73
  68. package/dist/server.js.map +1 -1
  69. package/dist/starters.d.ts +92 -0
  70. package/dist/starters.js +213 -0
  71. package/dist/starters.js.map +1 -0
  72. package/dist/suggest.d.ts +42 -0
  73. package/dist/suggest.js +215 -0
  74. package/dist/suggest.js.map +1 -0
  75. package/dist/terminal.d.ts +28 -2
  76. package/dist/terminal.js +45 -3
  77. package/dist/terminal.js.map +1 -1
  78. package/dist/theme-assets.d.ts +39 -0
  79. package/dist/theme-assets.js +109 -0
  80. package/dist/theme-assets.js.map +1 -0
  81. package/dist/theme-store.d.ts +50 -0
  82. package/dist/theme-store.js +124 -0
  83. package/dist/theme-store.js.map +1 -0
  84. package/dist/theme-tools.d.ts +25 -0
  85. package/dist/theme-tools.js +243 -0
  86. package/dist/theme-tools.js.map +1 -0
  87. package/dist/turn-shots.d.ts +43 -0
  88. package/dist/turn-shots.js +80 -0
  89. package/dist/turn-shots.js.map +1 -0
  90. package/dist/user-theme.d.ts +143 -0
  91. package/dist/user-theme.js +510 -0
  92. package/dist/user-theme.js.map +1 -0
  93. package/package.json +2 -2
@@ -96,7 +96,14 @@ body.inset-titlebar #side-top{-webkit-app-region:drag}
96
96
  width:236px;flex:0 0 236px;background:var(--panel);border-radius:12px;margin:0 8px 8px 0;
97
97
  display:flex;flex-direction:column;overflow:hidden;
98
98
  }
99
- #side-new{padding:0 8px 2px;display:flex;flex-direction:column}
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;
104
+ }
105
+ #side-search:hover{background:var(--hover);color:var(--fg)}
106
+ #side-search .ico-sm{width:16px;height:16px}
100
107
  /* Folded away on a desktop, the whole sidebar goes, rail and all, and the
101
108
  page has the window: keeping the rail left a strip of chrome and pushed the
102
109
  tabs a rail's width plus the traffic lights in from the edge. */
@@ -161,6 +168,10 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
161
168
  .fold .ico-sm{width:16px;height:16px;flex:0 0 auto;opacity:.5}
162
169
  .fold.open .ico-sm,.fold:hover .ico-sm{opacity:.9}
163
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)}
164
175
  /* The routine group's run count, where a project header keeps its actions. */
165
176
  .fold .count{flex:0 0 auto;font-size:11px;color:var(--dim);padding-right:4px}
166
177
  /* A gap between projects, but not above the first one. The selector names the
@@ -172,9 +183,13 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
172
183
  edge, where the count and the ⋯ already live and a third thing would crowd
173
184
  them; dim, because it marks a row rather than asking for attention. */
174
185
  .pin-mark{
175
- width:11px;height:11px;flex:0 0 auto;margin-left:5px;opacity:.5;
176
- 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;
177
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}
178
193
  /* Dragging: the row being carried fades, and the one under the pointer grows
179
194
  the edge it would be dropped against. A line on the row rather than a
180
195
  floating element, because this list is rebuilt on every repaint and a
@@ -257,7 +272,7 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
257
272
  already had two rules in it and a third would have made a list of five
258
273
  entries look like three lists. */
259
274
  .row-menu .menu-head{
260
- 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);
261
276
  }
262
277
  /* A section label, not a surface: the fill belongs to the account's facts
263
278
  above, and on a one-word caption it read as a second card pressed against
@@ -265,6 +280,25 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
265
280
  .row-menu .menu-head.menu-section{
266
281
  margin:10px 0 2px;padding:4px 10px;background:transparent;
267
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}
268
302
  .menu-head-main{font-size:14px;font-weight:500;color:var(--fg);font-variant-numeric:tabular-nums}
269
303
  .menu-head-sub{margin-top:2px;font-size:12px;color:var(--dim)}
270
304
  /* The billing card borrows .menu-head's surface but not its caption type: the
@@ -319,6 +353,15 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
319
353
  .theme-btn:hover{color:var(--fg)}
320
354
  .theme-btn[aria-pressed="true"]{background:var(--bg);color:var(--fg)}
321
355
  .theme-btn .ico-sm{width:14px;height:14px}
356
+ /* Under it, only while a custom theme is on: its name, and the way back. */
357
+ .menu-custom-theme .menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
358
+ .custom-theme-actions{display:flex;gap:4px;flex:none}
359
+ .custom-theme-btn{
360
+ height:24px;padding:0 8px;border:0;border-radius:6px;background:var(--field);color:var(--fg);
361
+ font:inherit;font-size:12px;cursor:pointer;
362
+ }
363
+ .custom-theme-btn:hover{background:var(--sel)}
364
+ .custom-theme-btn:disabled{opacity:.4;cursor:default}
322
365
  /* The letter that does the same thing without the mouse. Dim, right-aligned,
323
366
  and only ever one character — a menu whose right edge is a column of chords
324
367
  is a keyboard reference, not a menu. */
@@ -329,6 +372,15 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
329
372
  .row-menu .danger .menu-key{color:var(--dim)}
330
373
  /* A rule, not a gap: the group below it is the one that cannot be undone. */
331
374
  .row-menu .menu-sep{height:1px;margin:5px 8px;background:var(--line)}
375
+ /* The + menu's groups are told apart by space alone: no rule between them,
376
+ the same way nothing in it is outlined. */
377
+ #attach-picker .menu-sep{height:10px;margin:0;background:none}
378
+ /* Each group of the + menu opens with its own heading; the ones after the
379
+ first get room above them, which is what sets the groups apart. */
380
+ #attach-picker .menu-head:not(:first-child){padding-top:12px}
381
+ /* Tall enough for all three groups at once; the list scrolls only on a short
382
+ window, not on every one. */
383
+ #attach-picker .menu-body{max-height:min(72vh,560px)}
332
384
  /* Sized from the title it stands in for, so the row does not jump when a
333
385
  rename starts. */
334
386
  .session .title-edit{
@@ -445,27 +497,19 @@ main{flex:1;display:flex;flex-direction:column;min-width:0}
445
497
  * Same two shapes as the connector list: the vendor's own icon when the
446
498
  * server has one, and a lettered tile when it does not. Sized to the row —
447
499
  * the type is 13px, so the mark is 15px and not the 28px tile a card gets. */
448
- /* One block per vendor, whatever the vendor ships.
500
+ /* The mark as the vendor drew it, on nothing.
449
501
  *
450
- * A column of these is scanned by its left edge, and the edge has to be the
451
- * same one every time. Left alone, it is not: a vendor shipping a tile fills
452
- * its square, a vendor shipping bare ink covers half of it, and one shipping
453
- * a thin outline covers less than that — the eye finds no common edge and
454
- * reads the lighter rows as hanging off the line, even where the boxes are
455
- * centred on the text to the pixel.
456
- *
457
- * So a mark that did not bring a background is given one: the same faint
458
- * fill, the same radius, the same inset. A mark that did bring one keeps it
459
- * and fills the box, because drawing a second shape around a shape is the
460
- * thing this is trying to avoid. Either way the block is identical and the
461
- * ink inside it is the vendor's business. */
462
- .vmark{width:15px;height:15px;flex:0 0 auto;border-radius:4px;object-fit:contain;
463
- padding:1.5px;box-sizing:border-box;background:var(--sel)}
464
- .vmark.filled{padding:0;background:none}
502
+ * A faint tile under every bare mark once gave the column a common edge, but
503
+ * it also shrank the ink to two thirds of the box and put a grey square
504
+ * beside every name — in a list that is all one vendor, a column of the same
505
+ * grey squares. The box is still the same size on every row, so the marks
506
+ * still line up; the ink now fills it. A vendor that ships its own tile keeps
507
+ * it (.filled), since that tile is its mark. */
508
+ .vmark{width:16px;height:16px;flex:0 0 auto;border-radius:3px;object-fit:contain;box-sizing:border-box}
465
509
  /* The fallback, and the reason a missing icon is not a broken row: a lettered
466
510
  tile in a colour of its own still tells two vendors apart at a glance. */
467
511
  .vmono{
468
- width:15px;height:15px;flex:0 0 auto;border-radius:4px;box-sizing:border-box;
512
+ width:16px;height:16px;flex:0 0 auto;border-radius:4px;box-sizing:border-box;
469
513
  display:grid;place-items:center;color:#fff;
470
514
  font-size:9px;font-weight:600;letter-spacing:0;text-transform:uppercase;
471
515
  }
@@ -474,8 +518,7 @@ main{flex:1;display:flex;flex-direction:column;min-width:0}
474
518
  /* Typed as svg.vglyph so it outranks the .ico-sm every drawn icon carries:
475
519
  .6 is right for a chrome glyph and too faint for a vendor's mark. Ours is
476
520
  bare strokes, so it takes the same block as any other bare mark. */
477
- svg.vglyph{width:15px;height:15px;opacity:1;
478
- padding:1.5px;box-sizing:border-box;border-radius:4px;background:var(--sel)}
521
+ svg.vglyph{width:16px;height:16px;opacity:1;box-sizing:border-box}
479
522
  /* Black-on-transparent is what a good half of these vendors ship, and in the
480
523
  dark theme that is a black square on a black row. Inverted only where the
481
524
  bytes were measured to be exactly that. */
@@ -501,9 +544,6 @@ details.provider>summary .vmark,details.provider>summary .vmono,details.provider
501
544
  .pp-item .vmark,.pp-item .vmono,.pp-item .vglyph{
502
545
  width:20px;height:20px;border-radius:5px;font-size:11px;
503
546
  }
504
- details.provider>summary .vmark,.pp-item .vmark,
505
- details.provider>summary .vglyph,.pp-item .vglyph{padding:2.5px}
506
- details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
507
547
  .menu-head{
508
548
  padding:8px 10px 4px;font-size:11px;font-weight:600;letter-spacing:.04em;
509
549
  text-transform:uppercase;color:var(--dim);
@@ -564,18 +604,45 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
564
604
  justify-content:flex-end;flex-wrap:wrap;
565
605
  }
566
606
  /*
567
- * The context meter: how much of the model's window this session is using.
568
- *
569
- * Its own track colour rather than --line. Every other neutral token here is a
570
- * hairline at 6-10% alpha, which at 64x4 pixels disappears into the background
571
- * in both schemes — what was left read as a stray green dash rather than a bar
572
- * with something still to fill. The fill itself is set from the value (green,
573
- * then warn, then err) in applyState.
607
+ * The context meter: how much of the model's window this session is using,
608
+ * as a ring that fills clockwise. A ring rather than a bar because it sits in
609
+ * a row of words and has to read at a glance at the size of one; the numbers
610
+ * are in its tooltip. The sweep is a conic fill and the hole a mask, so there
611
+ * is no stroke anywhere. Its own track colour rather than --line, which at
612
+ * this size disappears into the background. --ctx-pct and --ctx-fill are set
613
+ * from the value in applyState.
574
614
  */
575
- #ctx{width:64px;height:4px;background:var(--track);border-radius:2px;overflow:hidden}
576
- #ctx i{display:block;height:100%;width:0;background:var(--ok);transition:width .3s}
615
+ #ctx{
616
+ width:14px;height:14px;border-radius:50%;flex:0 0 auto;cursor:default;
617
+ background:conic-gradient(var(--ctx-fill,var(--brand)) calc(var(--ctx-pct,0) * 1%),var(--track) 0);
618
+ -webkit-mask:radial-gradient(circle,transparent 4.2px,#000 4.6px);
619
+ mask:radial-gradient(circle,transparent 4.2px,#000 4.6px);
620
+ }
621
+ #ctx{border:0;padding:0}
622
+ /* Focus as a change of size, not a ring: this design draws no outlines. */
623
+ #ctx{transition:transform .12s}
624
+ #ctx:focus-visible,#ctx[aria-expanded="true"]{outline:none;transform:scale(1.2)}
625
+ #ctx:hover{transform:scale(1.2)}
626
+ /* The context panel. Every part is a strength of the brand colour: the two
627
+ things the model is given every turn lighter, the conversation full, as it
628
+ is the part a person can do something about. */
629
+ :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)}
630
+ .ctx-panel{width:300px;min-width:0;padding:14px 14px 8px;gap:10px;font-size:12.5px}
631
+ .ctx-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
632
+ .ctx-title{font-weight:600;color:var(--fg)}
633
+ .ctx-total{color:var(--dim);font-variant-numeric:tabular-nums}
634
+ .ctx-bar{position:relative;display:flex;height:8px;border-radius:4px;background:var(--track);overflow:hidden}
635
+ .ctx-bar i{display:block;height:100%;flex:0 0 auto}
636
+ .ctx-tick{position:absolute;top:0;bottom:0;width:2px;margin-left:-1px;background:var(--warn)}
637
+ .ctx-rows{display:flex;flex-direction:column;gap:5px}
638
+ .ctx-row{display:flex;align-items:center;gap:8px}
639
+ .ctx-dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto}
640
+ .ctx-label{flex:1;color:var(--fg)}
641
+ .ctx-value{color:var(--dim);font-variant-numeric:tabular-nums}
642
+ .ctx-notes{display:flex;flex-direction:column;gap:2px;color:var(--dim);font-size:11.5px;line-height:1.5}
643
+ .ctx-compact{margin:0 -8px}
577
644
  #ctx,#ctxtext{display:none}
578
- #meta.has-ctx #ctx,#meta.has-ctx #ctxtext{display:block}
645
+ #meta.has-ctx #ctx{display:block}
579
646
 
580
647
  /*
581
648
  * Over the transcript, not over the whole window.
@@ -672,7 +739,12 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
672
739
  * "position:relative" is what #jump anchors to; it belongs over this column
673
740
  * rather than over the drawer.
674
741
  */
675
- #main{flex:1;display:flex;flex-direction:column;min-width:0;position:relative}
742
+ #main{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;isolation:isolate}
743
+ /* The dot field behind a new session (client/ripple.ts). Under everything in
744
+ #main — isolation makes #main the floor it sits on — and deaf to the
745
+ pointer, so it only ever watches clicks go by. */
746
+ #ripple{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none;opacity:0;transition:opacity .6s ease}
747
+ #ripple.on{opacity:1}
676
748
  #log{flex:1;overflow-y:auto;padding:20px 16px;scroll-behavior:smooth;min-width:0}
677
749
  /*
678
750
  * Models, Routines and Settings are views, not messages. They used to be
@@ -856,6 +928,87 @@ body.browser-resizing #browser-pane{transition:none}
856
928
  }
857
929
  #browser-pane.live #browser-frame{cursor:crosshair}
858
930
  .browser-idle{margin:auto;text-align:center;max-width:32ch}
931
+ /* The Computer card: the window the agent is working in, floating over the
932
+ conversation's top-right corner while a turn works in it. Filled surfaces
933
+ and a shadow, no outline — the menus' elevation, since it sits above the
934
+ page the same way. The picture is letterboxed; the agent's cursor and its
935
+ click ripples sit over it in the conversation's accent. */
936
+ #computer-float{
937
+ position:absolute;z-index:30;top:52px;right:16px;width:340px;
938
+ transition:width .18s ease;cursor:grab;touch-action:none;
939
+ }
940
+ #computer-float[hidden]{display:none}
941
+ /* Larger, to read the window rather than glance at it. */
942
+ #computer-float.big{width:min(62%,880px)}
943
+ #computer-float.dragging{transition:none;user-select:none;cursor:grabbing}
944
+ /* The picture is the card: its corners, its shadow, and the window's own
945
+ proportions (set from the stream in computer.ts), so nothing frames it —
946
+ no bar above it, no fill around it, no bands at its sides. */
947
+ #computer-body{
948
+ aspect-ratio:16/10;max-height:70vh;position:relative;overflow:hidden;border-radius:12px;
949
+ /* Opaque, never --field: that is a 2% wash, and with no picture to fill the
950
+ card (no Screen Recording, a minimised window) the conversation behind it
951
+ showed through under the message. --panel laid over --bg, so a theme
952
+ whose panel is translucent still gets a solid card. */
953
+ background:linear-gradient(var(--panel),var(--panel)),var(--bg);box-shadow:0 1px 2px #0000001a,0 10px 30px #00000033;
954
+ }
955
+ @media (prefers-color-scheme:dark){:root:not([data-theme="light"]) #computer-body{box-shadow:0 1px 2px #0000004d,0 10px 30px #00000080}}
956
+ :root[data-theme="dark"] #computer-body{box-shadow:0 1px 2px #0000004d,0 10px 30px #00000080}
957
+ /* Over the picture: a pill naming the app on the left, round buttons on the
958
+ right. Translucent dark in both themes, since what is under them is
959
+ somebody else's window, light or dark. The buttons come up with the
960
+ pointer, so a card nobody is reaching for is all picture. */
961
+ #computer-head{
962
+ position:absolute;z-index:2;top:8px;left:8px;right:8px;
963
+ display:flex;align-items:center;gap:6px;pointer-events:none;
964
+ }
965
+ #computer-label{
966
+ display:inline-flex;align-items:center;gap:6px;min-width:0;max-width:100%;margin-right:auto;
967
+ height:24px;padding:0 9px;border-radius:999px;font-size:11.5px;color:#fff;
968
+ background:#000000a6;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
969
+ }
970
+ #computer-live{width:6px;height:6px;flex:0 0 auto;display:grid;place-items:center}
971
+ #computer-live[hidden]{display:none}
972
+ #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}
973
+ #computer-pill i{width:7px;height:7px}
974
+ @keyframes computer-pulse{50%{opacity:.35}}
975
+ #computer-where{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
976
+ #computer-head button{
977
+ pointer-events:auto;width:28px;height:28px;padding:0;border:0;border-radius:50%;flex:0 0 auto;
978
+ display:grid;place-items:center;cursor:pointer;color:#fff;
979
+ background:#000000a6;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
980
+ opacity:0;transition:opacity .15s,background .15s;
981
+ }
982
+ #computer-float:hover #computer-head button,#computer-head button:focus-visible{opacity:1}
983
+ @media (hover:none){#computer-head button{opacity:1}}
984
+ #computer-head button:hover{background:#000000d9}
985
+ #computer-head button svg{width:13px;height:13px}
986
+ #computer-stop:hover{background:#c62828e6!important}
987
+ #computer-stop svg{width:11px!important;height:11px!important}
988
+ .computer-video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
989
+ .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}
990
+ .computer-cursor.on{opacity:1}
991
+ .computer-cursor svg{width:22px;height:22px;display:block;filter:drop-shadow(0 1px 2px #00000059)}
992
+ .computer-cursor path{fill:var(--brand);stroke:#fff;stroke-width:1.6;stroke-linejoin:round}
993
+ .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}
994
+ @keyframes computer-ring{from{transform:scale(.3);opacity:.9}to{transform:scale(1.5);opacity:0}}
995
+ .computer-empty{position:absolute;inset:0;display:grid;place-items:center;padding:24px;text-align:center;color:var(--dim);font-size:12.5px}
996
+ .computer-empty[hidden],.computer-empty:empty{display:none}
997
+ /* Folded: where it is working, one click from the card again. */
998
+ #computer-pill{
999
+ position:absolute;z-index:30;top:52px;right:16px;max-width:280px;
1000
+ display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 12px;
1001
+ border:0;border-radius:999px;background:var(--panel);color:var(--fg);font:inherit;font-size:12px;cursor:pointer;
1002
+ box-shadow:0 1px 2px #0000000f,0 4px 14px #0000001f;
1003
+ }
1004
+ #computer-pill[hidden]{display:none}
1005
+ #computer-pill span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1006
+ #computer-pill:hover{background:var(--hover)}
1007
+ .att.computer .n.clickable{cursor:pointer}
1008
+ @media (max-width:900px){
1009
+ #computer-float,#computer-float.big{width:calc(100% - 24px);right:12px}
1010
+ }
1011
+ .embed #computer-float,.embed #computer-pill{display:none!important}
859
1012
  /* One column at a time on a narrow window: two would leave neither readable. */
860
1013
  #browser-hide .ico-x{display:none}
861
1014
  #browser-scrim{display:none}
@@ -928,7 +1081,16 @@ body.panelled #composer,body.panelled #jump,body.panelled #changes-btn{display:n
928
1081
  #explorer-actions button:hover{background:var(--hover);color:var(--fg)}
929
1082
  #explorer-actions svg{width:15px;height:15px}
930
1083
  #explorer-body{flex:1;display:flex;min-height:0}
931
- #explorer-tree{width:var(--explorer-tree-width,min(40%,260px));flex:0 0 auto;overflow:auto;padding:2px 6px 12px}
1084
+ #explorer-side{width:var(--explorer-tree-width,min(40%,260px));flex:0 0 auto;display:flex;flex-direction:column;min-height:0}
1085
+ #explorer-find{
1086
+ display:flex;align-items:center;gap:6px;margin:2px 8px 6px;padding:0 8px;height:30px;border-radius:8px;
1087
+ background:var(--hover);color:var(--dim);cursor:text;
1088
+ }
1089
+ #explorer-find svg{width:13px;height:13px;flex:0 0 auto}
1090
+ #explorer-q{flex:1;min-width:0;border:0;outline:0;background:transparent;color:var(--fg);font:inherit;font-size:12.5px;padding:0}
1091
+ #explorer-q::-webkit-search-cancel-button{cursor:pointer}
1092
+ #explorer-tree{flex:1;min-height:0;overflow:auto;padding:2px 6px 12px}
1093
+ .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}
932
1094
  /* The line between the tree and the file, and the handle that moves it: a
933
1095
  gap wide enough to grab, drawn only while it is being pointed at. */
934
1096
  #explorer-split{flex:0 0 7px;margin:0 -3px;position:relative;z-index:1;cursor:col-resize;touch-action:none;outline:0}
@@ -1204,7 +1366,11 @@ body.shelf-full #main,body.shelf-full aside,body.shelf-full #top{display:none}
1204
1366
  * positioned from the viewport instead and reuses only the surface styling.
1205
1367
  */
1206
1368
  .menu.pop{position:fixed;bottom:auto;left:auto;min-width:220px}
1207
- #log{padding:28px 24px 8px}
1369
+ /* The scrollbar's gutter is kept on both sides, and the composer below gives
1370
+ up the same width on each side (see #composer), so a message and the box it
1371
+ was typed in share both edges at every window width. Taken from one side
1372
+ only, it pushed every message half a scrollbar left of the box. */
1373
+ #log{padding:28px 24px 8px;scrollbar-gutter:stable both-edges}
1208
1374
  /*
1209
1375
  * One column width, named once.
1210
1376
  *
@@ -1239,6 +1405,52 @@ body.shelf-full #main,body.shelf-full aside,body.shelf-full #top{display:none}
1239
1405
  * they are answering. */
1240
1406
  .connection-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:2px}
1241
1407
  .connection-card .err{color:var(--fg)}
1408
+ /* A theme the agent proposed. The swatches are the proposal's colours, not the
1409
+ page's, so each carries a hairline that reads on both. */
1410
+ .theme-card-head{display:flex;align-items:center;gap:10px}
1411
+ .theme-card-name{font-weight:600;font-size:13.5px;flex:1;min-width:0}
1412
+ .theme-card-status{font-size:12px;color:var(--dim)}
1413
+ .theme-card-status.applied{color:var(--ok)}
1414
+ .theme-swatches{display:flex;flex-direction:column;gap:6px}
1415
+ .theme-swatch-row{display:flex;align-items:center;gap:6px}
1416
+ .theme-swatch-label{width:44px;font-size:12px;color:var(--dim)}
1417
+ .theme-swatch{width:22px;height:22px;border-radius:999px;box-shadow:inset 0 0 0 1px #8888884d}
1418
+ .theme-report{display:flex;flex-direction:column;gap:2px}
1419
+ .theme-report-title{font-size:12px;font-weight:600;color:var(--dim)}
1420
+ .theme-report-line{font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dim);overflow-wrap:anywhere}
1421
+ button.ghost.theme-apply{background:var(--accent);color:var(--bg)}
1422
+ button.ghost.theme-apply:hover{background:var(--accent);opacity:.88}
1423
+ button.ghost.theme-apply:disabled{opacity:.4;cursor:default}
1424
+ .connection-card p.theme-safe-hint{font-size:12px}
1425
+ .theme-images{display:flex;flex-wrap:wrap;gap:8px}
1426
+ .theme-image{display:flex;flex-direction:column;gap:4px;font-size:11.5px;color:var(--dim)}
1427
+ .theme-image img{width:72px;height:48px;object-fit:cover;border-radius:6px;box-shadow:inset 0 0 0 1px #8888884d;background:var(--bg)}
1428
+
1429
+ /* Image slots a theme can fill (user-theme.ts). The theme sets only the
1430
+ custom properties; these rules decide where each image goes, and every one
1431
+ of them is decoration: nothing here takes clicks or covers anything.
1432
+
1433
+ The conversation background is the scroll container's own background, so it
1434
+ stays put while messages scroll over it and cannot cover them. A layer of
1435
+ the page colour sits on top of it, at the theme's scrim or at the minimum
1436
+ the page measured for readable text (client/theme.ts), whichever is more. */
1437
+ #log{
1438
+ --wall-cover:color-mix(in srgb,var(--bg) max(var(--wall-scrim,0%),var(--wall-scrim-min,0%)),transparent);
1439
+ background-color:var(--wall-base,transparent);
1440
+ background-image:linear-gradient(var(--wall-cover),var(--wall-cover)),var(--wall-image,none);
1441
+ background-size:100% 100%,var(--wall-size,auto);
1442
+ background-repeat:no-repeat,var(--wall-repeat,no-repeat);
1443
+ background-position:0 0,var(--wall-position,center);
1444
+ background-blend-mode:var(--wall-blend,normal);
1445
+ }
1446
+ #empty-art{
1447
+ display:var(--empty-art-display,none);height:var(--empty-art-height,180px);max-width:100%;margin:0 0 24px;
1448
+ background:var(--empty-art,none) var(--empty-art-position,left center)/contain no-repeat;pointer-events:none;
1449
+ }
1450
+ .turn.assistant>.who::before{
1451
+ content:"";display:var(--avatar-display,none);width:20px;height:20px;margin-right:8px;vertical-align:-6px;
1452
+ border-radius:var(--avatar-radius,999px);background:var(--avatar,none) center/cover no-repeat;
1453
+ }
1242
1454
 
1243
1455
  /* A question from the agent, in the transcript rather than a modal: answering
1244
1456
  * may take a look back up the conversation, which a modal would cover. Options
@@ -1312,6 +1524,8 @@ body.shelf-full #main,body.shelf-full aside,body.shelf-full #top{display:none}
1312
1524
  max-width:220px;max-height:160px;border-radius:8px;cursor:zoom-in;
1313
1525
  background:var(--bg);object-fit:cover;
1314
1526
  }
1527
+ /* A clip a little larger than a picture, since it is watched, not glanced at. */
1528
+ .user .shots video{max-width:min(320px,100%);max-height:200px;border-radius:8px;background:#000;display:block}
1315
1529
  .user{margin-bottom:18px}
1316
1530
  .panel{max-width:720px;margin:8px auto 0}
1317
1531
  .panel-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding-block:48px;font-size:13px}
@@ -1517,6 +1731,15 @@ details.setting>summary:focus-visible{outline:none;background:var(--hover)}
1517
1731
  .section-title{
1518
1732
  font-size:13px;font-weight:600;color:var(--dim);margin:22px 0 8px;letter-spacing:.01em;
1519
1733
  }
1734
+ /* Advanced folds under its heading; the heading is the only control, with the
1735
+ same chevron a settings row turns. */
1736
+ details.settings-advanced>summary.section-title{
1737
+ display:flex;align-items:center;gap:6px;cursor:pointer;list-style:none;width:max-content;
1738
+ }
1739
+ details.settings-advanced>summary::-webkit-details-marker{display:none}
1740
+ details.settings-advanced>summary:hover{color:var(--fg)}
1741
+ details.settings-advanced>summary:focus-visible{outline:none;color:var(--fg)}
1742
+ details.settings-advanced[open]>summary .chev{transform:rotate(90deg)}
1520
1743
  /*
1521
1744
  * The window picker, and the chart under it.
1522
1745
  *
@@ -1529,10 +1752,20 @@ details.setting>summary:focus-visible{outline:none;background:var(--hover)}
1529
1752
  /* A segmented choice on a settings row sits in the row's line, like a switch. */
1530
1753
  .setting summary .setting-seg{margin:0 0 0 auto;flex:none}
1531
1754
  .setting summary .setting-seg .seg-item{padding:3px 10px;font-size:12px}
1755
+ /* A row with several controls opens onto parts, each a name and its control on one line. */
1756
+ .setting-part+.setting-part{margin-top:16px}
1757
+ .setting-part>*+*{margin-top:6px}
1758
+ .setting-part-line{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
1759
+ .setting-part-name{font-weight:500;font-size:13px}
1760
+ .setting-part .setting-seg{margin:0;flex:none}
1761
+ .setting-part .setting-seg .seg-item{padding:3px 10px;font-size:12px}
1532
1762
  /* Whether something a row depends on is there: a dot and a sentence. */
1533
1763
  .setting-status{display:flex;align-items:center;gap:7px;margin-top:8px;color:var(--dim)}
1534
1764
  .status-dot{flex:none;width:7px;height:7px;border-radius:50%;background:var(--dim);opacity:.6}
1535
1765
  .status-dot.ok{background:var(--ok);opacity:1}
1766
+ /* A paired phone: its name, when, and the way to forget it, on one line. */
1767
+ .remote-phone{display:flex;align-items:center;gap:10px;min-height:28px}
1768
+ .remote-phone .remote-phone-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1536
1769
  /* The presets and the calendar on one line, on the same baseline. */
1537
1770
  .usage-window{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
1538
1771
  .usage-kind-title{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
@@ -1791,13 +2024,41 @@ details.provider>summary:hover{background:var(--hover)}
1791
2024
  * with it. */
1792
2025
  .row-head .ghost+.ghost,.row-head .ghost+.switch,.row-head .switch+.ghost{margin-left:0}
1793
2026
  .routine-where{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
1794
- .routine-history{margin-top:6px}
1795
- .routine-history>summary{cursor:pointer;width:max-content}
1796
- .routine-runs{display:flex;flex-direction:column;gap:2px;margin-top:6px}
1797
- .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}
1798
- button.routine-run{cursor:pointer}
1799
- button.routine-run:hover{background:var(--hover)}
1800
- .routine-run.failed{color:var(--err)}
2027
+ .routine-history{margin-top:8px}
2028
+ .routine-history-head{display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;
2029
+ font:inherit;cursor:pointer;padding:3px 6px;margin:0 -6px 4px;border-radius:6px}
2030
+ .routine-history-head:hover{background:var(--hover);color:var(--fg)}
2031
+ .routine-history-head .chev{display:inline-block;transition:transform .15s;font-size:10px}
2032
+ .routine-history.folded .chev{transform:rotate(-90deg)}
2033
+ .routine-history.folded .routine-runs{display:none}
2034
+ .routine-runs{display:flex;flex-direction:column;gap:2px}
2035
+ /* Each run a quiet row that lights on hover: status, time, facts on one line,
2036
+ and the first line of what it said beneath. No rules between them. */
2037
+ .routine-run{display:flex;flex-direction:column;gap:3px;text-align:left;border:0;background:transparent;
2038
+ color:var(--fg);font:inherit;font-size:12.5px;padding:7px 8px;margin:0 -8px;border-radius:8px;min-width:0}
2039
+ .routine-run[role=button]{cursor:pointer}
2040
+ .routine-run[role=button]:hover,.routine-run.open{background:var(--hover)}
2041
+ .routine-run:focus-visible{outline:none;background:var(--hover)}
2042
+ .routine-run-detail{display:none;flex-direction:column;align-items:flex-start;gap:8px;margin-top:4px;cursor:default}
2043
+ .routine-run.open .routine-run-detail{display:flex}
2044
+ .routine-run.open:not(.running) .routine-run-text{display:none}
2045
+ .routine-run-full{word-break:break-word;max-height:360px;overflow:auto;width:100%;
2046
+ line-height:1.55;user-select:text;color:var(--fg);font-size:13px}
2047
+ .routine-run-full.err{color:var(--err);white-space:pre-wrap}
2048
+ .routine-run-full.md>:first-child{margin-top:0}
2049
+ .routine-run.running .routine-run-text{color:var(--fg)}
2050
+ .routine-run-top{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap}
2051
+ .routine-run-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
2052
+ .routine-run-text.err{color:var(--err)}
2053
+ .run-status{font-size:11px;font-weight:550;padding:1px 8px;border-radius:999px;flex:0 0 auto}
2054
+ .run-status.ok{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}
2055
+ .run-status.failed{color:var(--err);background:color-mix(in srgb,var(--err) 14%,transparent)}
2056
+ .run-status.skipped{color:var(--dim);background:var(--hover)}
2057
+ .run-status.running{color:var(--warn);background:color-mix(in srgb,var(--warn) 16%,transparent)}
2058
+ .run-status.running::before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;
2059
+ background:currentColor;margin-right:5px;vertical-align:1px;animation:run-pulse 1.2s ease-in-out infinite}
2060
+ @keyframes run-pulse{50%{opacity:.25}}
2061
+ .routine-more{align-self:flex-start;margin-top:4px}
1801
2062
  .small{font-size:12px;white-space:pre-wrap;word-break:break-all}
1802
2063
  .keyrow{display:flex;gap:6px;margin:8px 0 6px}
1803
2064
  .keyrow input{
@@ -1872,8 +2133,15 @@ button.routine-run:hover{background:var(--hover)}
1872
2133
  flex:1;min-width:0;
1873
2134
  font-size:12px;color:var(--fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
1874
2135
  }
1875
- #account .acct-caret{width:12px;height:12px;flex:0 0 auto;opacity:.45}
1876
- .account-btn:hover .acct-caret,#account .account-btn.menu-open .acct-caret{opacity:.8}
2136
+ /* One chevron, pointing the way the menu opens — up, from the foot of the
2137
+ sidebar — and turning down while it is open. The ⇕ it replaced is a
2138
+ select box's sign, and this row opens a menu, not a list of choices. */
2139
+ #account .acct-caret{
2140
+ width:14px;height:14px;flex:0 0 auto;margin-right:2px;opacity:.4;
2141
+ transform:rotate(180deg);transition:transform .18s ease,opacity .12s ease;
2142
+ }
2143
+ .account-btn:hover .acct-caret{opacity:.75}
2144
+ #account .account-btn.menu-open .acct-caret{opacity:.75;transform:none}
1877
2145
  #account .avatar{
1878
2146
  width:24px;height:24px;border-radius:50%;flex:0 0 auto;display:grid;place-items:center;
1879
2147
  overflow:hidden;background:var(--accent);color:var(--bg);font-size:11px;font-weight:600;
@@ -1906,6 +2174,54 @@ button.signin-site:disabled{opacity:.4;cursor:default}
1906
2174
  #browser-import-dialog .dlg-actions{padding:16px 22px 20px}
1907
2175
  #browser-import-dialog .dlg-actions button:disabled{opacity:.4;cursor:default}
1908
2176
  #browser-import-dialog input[type=text]{height:38px;padding:0 13px;font-size:13px}
2177
+ /* Feedback. Same frame as the import dialog; the kind is a row of fills, the
2178
+ chosen one taking the selection fill rather than a ring. */
2179
+ #feedback-dialog{width:min(520px,94vw);max-width:min(520px,94vw)}
2180
+ #feedback-dialog .cn-head{padding:20px 22px 2px}
2181
+ #feedback-dialog .cn-body{padding:14px 22px 4px;gap:10px}
2182
+ #feedback-dialog .dlg-actions{padding:16px 22px 20px}
2183
+ #feedback-dialog .dlg-actions button:disabled{opacity:.4;cursor:default}
2184
+ #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}
2185
+ #feedback-dialog textarea:focus-visible{background:var(--hover)}
2186
+ .fb-kinds{display:flex;gap:4px}
2187
+ .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}
2188
+ .fb-kind:hover{background:var(--hover);color:var(--fg)}
2189
+ .fb-kind.on{background:var(--sel);color:var(--fg);font-weight:550}
2190
+ .fb-note{margin:0;font-size:11.5px;color:var(--dim);line-height:1.5}
2191
+ .fb-note:empty{display:none}
2192
+ /* Attachments as square tiles in a row, the last one adding another. Fills
2193
+ only: the add tile is a softer fill, not a dashed outline. */
2194
+ .fb-media{display:flex;flex-wrap:wrap;gap:8px}
2195
+ .fb-tile{position:relative;width:76px;height:76px;flex:0 0 auto;border-radius:14px;overflow:hidden;background:var(--field)}
2196
+ .fb-thumb{display:block;width:100%;height:100%;object-fit:cover}
2197
+ .fb-tile.is-video .fb-thumb{background:#000}
2198
+ /* A play mark over a recording's first frame, so it reads as video at a glance. */
2199
+ .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)}
2200
+ .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}
2201
+ .fb-remove{
2202
+ position:absolute;top:5px;right:5px;width:20px;height:20px;padding:0;border:0;border-radius:50%;
2203
+ background:rgba(0,0,0,.55);backdrop-filter:blur(6px);color:#fff;font-size:14px;line-height:20px;text-align:center;cursor:pointer;
2204
+ opacity:.9;transition:opacity .15s,background .15s;
2205
+ }
2206
+ .fb-remove:hover{opacity:1;background:rgba(0,0,0,.75)}
2207
+ .fb-remove:disabled{display:none}
2208
+ .fb-tile.is-uploading::before,.fb-tile.is-done::before{content:"";position:absolute;inset:0;background:rgba(0,0,0,.35)}
2209
+ .fb-spinner{position:absolute;left:50%;top:50%;width:20px;height:20px;margin:-10px 0 0 -10px;border-radius:50%;
2210
+ border:2px solid rgba(255,255,255,.35);border-top-color:#fff;animation:fb-spin .8s linear infinite}
2211
+ @keyframes fb-spin{to{transform:rotate(360deg)}}
2212
+ .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}
2213
+ .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}
2214
+ .fb-add:hover,.fb-add:focus-visible{background:var(--hover);color:var(--fg);outline:none}
2215
+ .fb-add .fb-add-icon{width:20px;height:20px}
2216
+ .fb-add-label{font-size:11.5px;font-weight:500}
2217
+ /* Beside the tiles, centred on them: what goes here and the limits. It takes
2218
+ the rest of the row and drops below the tiles when the row is full. */
2219
+ .fb-media{align-items:center}
2220
+ .fb-caption{display:flex;flex-direction:column;gap:3px;flex:1;min-width:170px;padding-left:4px}
2221
+ .fb-caption-title{font-size:12.5px;font-weight:500;color:var(--fg)}
2222
+ .fb-caption-sub{font-size:11.5px;color:var(--dim);line-height:1.45}
2223
+ /* Something is being dragged over: the whole dialog is the target, said with a fill. */
2224
+ #feedback-dialog.fb-dropping{background:var(--sel)}
1909
2225
  .bi-label{font-size:12px;color:var(--dim)}
1910
2226
  .bi-sites{display:flex;flex-direction:column;gap:4px;max-height:240px;overflow:auto}
1911
2227
  .bi-site{display:flex;align-items:center;gap:10px;padding:8px 11px;border-radius:9px;background:var(--field);cursor:pointer}
@@ -1990,6 +2306,23 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
1990
2306
  }
1991
2307
  .pw-tab:hover{color:var(--fg)}
1992
2308
  .pw-tab.on{background:var(--bg);color:var(--fg);font-weight:500}
2309
+ .pw-tab{display:inline-flex;align-items:center;justify-content:center;gap:6px}
2310
+ /* The site's badge on the same tab, at this dialog's size. */
2311
+ .pw-tab-save{
2312
+ font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:999px;
2313
+ background:var(--fg);color:var(--bg);font-variant-numeric:tabular-nums;
2314
+ }
2315
+ /* Twelve months at the monthly price, struck, just left of what the year costs. */
2316
+ .pw-was{color:var(--dim);font-weight:400;margin-right:6px;text-decoration-thickness:1px}
2317
+ /* What the credits reach: a quiet heading, then one row per kind of model —
2318
+ the kind in a narrow column, its marks and names beside it. */
2319
+ .pw-media{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:7px 14px;font-size:12.5px}
2320
+ .pw-media-head{grid-column:1/-1;color:var(--dim);font-size:11.5px}
2321
+ .pw-media-kind{color:var(--dim);white-space:nowrap}
2322
+ .pw-media-models{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px}
2323
+ .pw-media-model{display:inline-flex;align-items:center;gap:5px;color:var(--fg);font-weight:500;white-space:nowrap}
2324
+ .pw-media-mark{display:inline-flex;width:14px;height:14px}
2325
+ .pw-media-mark svg{width:100%;height:100%}
1993
2326
  /* A labelled break, not a rule: what follows is a different kind of purchase,
1994
2327
  and a bare line would read as the list simply continuing. */
1995
2328
  .pw-sep{
@@ -2098,7 +2431,7 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2098
2431
  #browse-list{overflow-y:auto;padding:4px 8px;flex:1}
2099
2432
  #browse-here{display:block;padding:2px 4px;color:var(--dim);word-break:break-all}
2100
2433
  #project-picker[open]{display:flex;flex-direction:column}
2101
- #proj-list{overflow-y:auto;padding:4px 8px}
2434
+ #proj-list{overflow-y:auto;overflow-x:hidden;padding:4px 8px}
2102
2435
  .proj-foot{padding:4px 8px 10px;box-shadow:0 -1px 0 var(--line)}
2103
2436
  .proj-item{
2104
2437
  width:100%;display:flex;align-items:center;gap:9px;border:0;border-radius:8px;
@@ -2123,41 +2456,56 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2123
2456
  .fold-row .fold-new{margin-right:2px}
2124
2457
  .fold-row .more:hover,.fold-row .fold.menu-open ~ .more:last-child{background:var(--sel);color:var(--fg)}
2125
2458
  .fold-row .more .ico-sm{width:14px;height:14px;opacity:1}
2126
- .proj-row{display:flex;align-items:center;border-radius:8px}
2127
- .proj-row:hover{background:var(--hover)}
2459
+ /* A project row: name over its directory, a tick on the one this window is
2460
+ in, and everything else behind ··· — rename, export and forget are rare,
2461
+ and three icons on every row read as a list of things to do to it. The lit
2462
+ row is the one Enter opens, moved by the arrows or the pointer alike. */
2463
+ .proj-row{display:flex;align-items:center;gap:2px;border-radius:10px;padding-right:6px}
2464
+ .proj-row.active,.proj-row.menu-open{background:var(--hover)}
2128
2465
  .proj-row .proj-item:hover{background:transparent}
2129
- .proj-forget{
2130
- border:0;background:transparent;color:var(--dim);font:inherit;font-size:13px;
2131
- cursor:pointer;padding:6px 9px;border-radius:8px;opacity:0;flex:none;
2132
- }
2133
- .proj-row:hover .proj-forget,.proj-forget:focus-visible{opacity:1}
2134
- .proj-forget:hover{background:var(--sel);color:var(--fg)}
2466
+ .proj-row .proj-item{flex:1;width:auto;min-width:0;padding:7px 8px}
2467
+ .proj-row .avatar{flex:none}
2468
+ .proj-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
2469
+ .proj-text .path{flex:none;font-weight:500}
2470
+ .proj-dir{font-size:11.5px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2471
+ .proj-item .tag{flex:none}
2472
+ .proj-check{flex:none;width:16px;height:16px;display:grid;place-items:center;color:var(--fg)}
2473
+ .proj-check svg{width:14px;height:14px}
2474
+ .proj-more{
2475
+ flex:none;display:grid;place-items:center;width:26px;height:26px;padding:0;border:0;border-radius:7px;
2476
+ background:transparent;color:var(--dim);cursor:pointer;opacity:0;
2477
+ }
2478
+ .proj-more svg{width:14px;height:14px}
2479
+ .proj-row.active .proj-more,.proj-row.menu-open .proj-more,.proj-more:focus-visible{opacity:1}
2480
+ .proj-more:hover,.proj-row.menu-open .proj-more{background:var(--sel);color:var(--fg)}
2481
+ .proj-empty{padding:18px 8px;text-align:center;color:var(--dim);font-size:13px}
2135
2482
  /* The command palette.
2136
2483
  *
2137
2484
  * The sidebar shows five sessions per project and folds the rest away, which
2138
2485
  * is right for browsing and wrong for "the one about the parser, two days
2139
2486
  * ago". So this is a search field over every session in every tree opened
2140
2487
  * here, the projects themselves, and the few actions worth a shortcut. */
2141
- #palette{width:min(560px,92vw);max-height:min(60vh,520px)}
2488
+ #palette{width:min(600px,92vw);max-height:min(64vh,560px)}
2142
2489
  #palette[open]{display:flex;flex-direction:column}
2143
2490
  /* One left edge for everything: the field's magnifier, each row's icon, the
2144
- group labels and the key hints all start 22px in, and the field's text
2145
- starts where the row labels do. Rows sit 8px in so their highlight has room;
2491
+ group labels and the key hints all start 26px in, and the field's text
2492
+ starts where the row labels do. Rows sit 12px in so their highlight has room;
2146
2493
  their own 14px padding brings the content back to the shared edge. */
2147
- .cmdk-head{position:relative;padding:12px 12px 8px}
2148
- .cmdk-head .ico-sm{position:absolute;left:22px;top:calc(50% - 5.5px);pointer-events:none}
2149
- #palette .cmdk-head input{height:40px;padding:0 10px 0 35px;font-size:14px}
2150
- #cmdk-list{overflow-y:auto;padding:0 8px 8px;flex:1;scroll-padding-block:4px}
2494
+ .cmdk-head{position:relative;padding:16px 16px 10px}
2495
+ .cmdk-head .ico-sm{position:absolute;left:26px;top:calc(50% - 4.5px);pointer-events:none}
2496
+ #palette .cmdk-head input{height:42px;padding:0 12px 0 35px;font-size:14px}
2497
+ #cmdk-list{overflow-y:auto;padding:0 12px 12px;flex:1;scroll-padding-block:6px}
2151
2498
  .cmdk-group{
2152
- padding:12px 14px 5px;font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;
2499
+ padding:16px 14px 6px;font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;
2153
2500
  color:var(--dim);
2154
2501
  }
2155
- .cmdk-group:first-child{padding-top:6px}
2502
+ .cmdk-group:first-child{padding-top:8px}
2156
2503
  .cmdk-item{
2157
2504
  width:100%;display:flex;align-items:center;gap:10px;border:0;border-radius:8px;
2158
2505
  background:transparent;color:var(--fg);font:inherit;font-size:13px;cursor:pointer;
2159
- padding:8px 14px;text-align:left;
2506
+ padding:10px 14px;text-align:left;
2160
2507
  }
2508
+ .cmdk-item + .cmdk-item{margin-top:2px}
2161
2509
  /* Hover and the keyboard cursor are the same highlight: a list driven by both
2162
2510
  at once needs one answer to "which row does Enter take". */
2163
2511
  .cmdk-item.cursor{background:var(--sel)}
@@ -2169,8 +2517,7 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2169
2517
  }
2170
2518
  .cmdk-empty{padding:12px 14px;font-size:12.5px;color:var(--dim)}
2171
2519
  .cmdk-foot{
2172
- display:flex;gap:16px;padding:9px 22px 11px;font-size:11px;color:var(--dim);
2173
- box-shadow:0 -1px 0 var(--line);
2520
+ display:flex;gap:18px;padding:10px 26px 16px;font-size:11px;color:var(--dim);
2174
2521
  }
2175
2522
  .cmdk-foot kbd{
2176
2523
  font:inherit;font-size:10.5px;border:1px solid currentColor;border-radius:4px;
@@ -2190,7 +2537,10 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2190
2537
  #connect:has(#cn-aside:not([hidden])){width:min(760px,94vw)}
2191
2538
  .cn-grid{display:grid;grid-template-columns:1fr}
2192
2539
  #connect:has(#cn-aside:not([hidden])) .cn-grid{grid-template-columns:minmax(0,.85fr) minmax(0,1fr)}
2540
+ /* The sidebar's bare aside rule (a fixed width, contents pinned right) would
2541
+ * otherwise reach this one too, and set the wordmark against the divider. */
2193
2542
  .cn-aside{
2543
+ width:auto;flex:initial;align-items:stretch;
2194
2544
  display:flex;flex-direction:column;gap:20px;padding:30px 28px;
2195
2545
  background:var(--hover);border-right:1px solid var(--line);
2196
2546
  border-radius:16px 0 0 16px;
@@ -2268,6 +2618,18 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2268
2618
  * the one thing the window exists for and stands over a 42px field. */
2269
2619
  .cn-body button.signin-site{height:44px;border-radius:10px;font-size:14px;font-weight:550}
2270
2620
  .cn-note .mono{white-space:nowrap}
2621
+ /* A vendor's mark beside its name, at the title's cap height. */
2622
+ .cn-title{display:flex;align-items:center;gap:10px}
2623
+ .cn-title .cn-title-mark{width:24px;height:24px;flex:0 0 auto;border-radius:6px}
2624
+ /* A link-weight action inside a note: words to press, not another button. */
2625
+ .small button.cn-inline,.cn-note button.cn-inline{
2626
+ border:0;background:none;padding:0;font:inherit;color:var(--fg);
2627
+ font-weight:550;cursor:pointer;text-decoration:underline;text-underline-offset:2px;
2628
+ }
2629
+ .small button.cn-inline:hover,.cn-note button.cn-inline:hover{opacity:.75}
2630
+ /* Only the sign-in: no actions row, so the body closes the dialog itself. */
2631
+ #connect.cn-keyless .dlg-actions{display:none}
2632
+ #connect.cn-keyless .cn-body{padding-bottom:24px;gap:14px}
2271
2633
  .cn-local{display:flex;gap:6px;align-items:flex-start}
2272
2634
  .cn-local .ico-sm{width:13px;height:13px;flex:0 0 auto;margin-top:1.5px;color:var(--fg)}
2273
2635
  .cn-local strong{color:var(--fg);font-weight:600}
@@ -2347,6 +2709,10 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2347
2709
  three sentences side by side either wrap mid-phrase or shrink the type. Down
2348
2710
  the page they read as a short list of offers, which is what they are. */
2349
2711
  #starters{display:flex;flex-direction:column;align-items:flex-start;gap:8px}
2712
+ /* A rotated or newly written set gets a short fade, so the change is seen
2713
+ rather than noticed as a flicker. */
2714
+ #starters.fresh button{animation:starters-in .35s ease-out both}
2715
+ @keyframes starters-in{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
2350
2716
  #starters button{
2351
2717
  border:0;background:var(--panel);color:var(--fg);border-radius:999px;
2352
2718
  padding:8px 15px;font:inherit;font-size:13px;cursor:pointer;text-align:left;
@@ -2424,6 +2790,48 @@ details.tool>summary:hover::after{opacity:1}
2424
2790
  .tool-body{padding:2px 12px 10px}
2425
2791
  .tool-body pre{margin:0;overflow-x:auto;font-size:12px;max-height:340px}
2426
2792
  .tool-body .label{font-size:11px;color:var(--dim);margin:0 0 4px}
2793
+ /* The screenshots a tool took, under its result: the same thumbnail as a
2794
+ pasted image, a click away from full size. */
2795
+ .tool-shots{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
2796
+ .tool-shots img{
2797
+ max-width:220px;max-height:160px;border-radius:8px;cursor:zoom-in;
2798
+ background:var(--bg);object-fit:cover;
2799
+ }
2800
+ /* The windows and browser tabs a turn ended on: the result, so out in the turn
2801
+ rather than folded into a step, and a little larger than a step's shots. */
2802
+ /* Five across, the count fitTurnShots shows; three on a phone, as two rows. */
2803
+ .turn-shots{
2804
+ display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;
2805
+ margin:10px 0 4px;
2806
+ }
2807
+ @media (max-width:600px){.turn-shots{grid-template-columns:repeat(3,minmax(0,1fr))}}
2808
+ .turn-shot{margin:0;display:flex;flex-direction:column;gap:5px;min-width:0}
2809
+ .turn-shot[hidden]{display:none}
2810
+ .turn-shot-frame{position:relative}
2811
+ .turn-shot img{
2812
+ display:block;width:100%;aspect-ratio:16/10;border-radius:8px;cursor:zoom-in;
2813
+ background:var(--bg);object-fit:cover;object-position:top;
2814
+ }
2815
+ /* The pictures past the row, counted on its last one: a click opens the
2816
+ preview there, and the arrow keys go on through the rest. */
2817
+ .turn-shot-more{
2818
+ position:absolute;inset:0;border:0;border-radius:8px;cursor:zoom-in;
2819
+ background:rgba(0,0,0,.55);color:#fff;font-size:18px;font-weight:600;
2820
+ }
2821
+ .turn-shot-more:hover{background:rgba(0,0,0,.45)}
2822
+ .turn-shot figcaption{font-size:11.5px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2823
+ /* The preview: the image as large as the window allows, over a dark scrim.
2824
+ Dark in both themes — a screenshot reads against black, not the page. */
2825
+ .img-preview{
2826
+ position:fixed;inset:0;z-index:90;display:grid;place-items:center;padding:32px;
2827
+ background:rgba(0,0,0,.82);cursor:zoom-out;
2828
+ }
2829
+ .img-preview img{max-width:100%;max-height:100%;border-radius:8px;object-fit:contain}
2830
+ .img-preview-count{
2831
+ position:absolute;bottom:14px;left:50%;transform:translateX(-50%);
2832
+ color:#fff;font-size:12px;opacity:.7;
2833
+ }
2834
+ .img-preview-count:empty{display:none}
2427
2835
  .tool-body .kv{margin:0 0 8px}
2428
2836
  .tool-summary{font-size:12.5px;line-height:1.5;margin:0 0 6px}
2429
2837
  details.tool-raw>summary{font-size:11px;color:var(--dim);cursor:pointer;margin:0 0 4px}
@@ -2547,6 +2955,8 @@ details.thinking[open]>summary::after{transform:translateY(-2px) rotate(45deg)}
2547
2955
 
2548
2956
  /* composer */
2549
2957
  form{padding:6px 24px 18px;background:var(--bg)}
2958
+ /* 24px plus the 11px the transcript's scrollbar gutter takes on each side. */
2959
+ @media (min-width:761px){#composer{padding-left:35px;padding-right:35px}}
2550
2960
  .composer{
2551
2961
  position:relative;
2552
2962
  max-width:720px;margin:0 auto;border-radius:16px;
@@ -2650,6 +3060,9 @@ textarea::placeholder{color:var(--dim)}
2650
3060
  color:var(--dim);
2651
3061
  }
2652
3062
  #input-mirror mark .at svg{width:11px;height:11px;display:block;margin-left:.5px}
3063
+ /* The rest of the suggested message, after what has been typed of it: the
3064
+ placeholder's colour, since it is the same offer, half taken. */
3065
+ #input-mirror .ghost{color:var(--dim)}
2653
3066
  /* The mirror sits exactly under the textarea, so every metric that decides
2654
3067
  where a glyph lands has to match: font, line-height, wrapping, and the
2655
3068
  padding, which is zero on the composer's textarea and must be zero here. */
@@ -2667,6 +3080,57 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2667
3080
  }
2668
3081
  .icon-btn svg{width:14px;height:14px}
2669
3082
  .icon-btn:hover{background:var(--hover)}
3083
+ /* Dictation. Listening is the harness alive, so it takes the brand: the mic
3084
+ becomes a soft lime well holding a stop square, breathing, and the panel
3085
+ above the box shows the voice in lime. Actions (done) keep --accent, as
3086
+ every action does. Fills and shadows only, never a ring. */
3087
+ .mic-btn{width:30px;height:30px;border-radius:999px;color:var(--dim);transition:background .2s,color .2s}
3088
+ .mic-btn svg{width:16px;height:16px}
3089
+ .mic-btn .mic-stop{display:none;width:14px;height:14px}
3090
+ .mic-btn:hover{color:var(--fg)}
3091
+ .mic-btn.is-recording{background:var(--brand-tint);color:var(--brand-ink);animation:mic-breathe 1.8s ease-in-out infinite}
3092
+ .mic-btn.is-recording .mic-idle{display:none}
3093
+ .mic-btn.is-recording .mic-stop{display:block}
3094
+ .mic-btn:disabled{opacity:.45;cursor:default}
3095
+ @keyframes mic-breathe{50%{background:color-mix(in srgb,var(--brand) 34%,transparent)}}
3096
+ .composer-bar .mic-btn,.composer-bar #send,.composer-bar #stop{flex-shrink:0}
3097
+ /* The panel: centred over the box's top edge, a pill of its own. One row:
3098
+ what is going on, then the voice and the two ways out while listening. */
3099
+ .dictate-pop{
3100
+ position:absolute;left:50%;bottom:calc(100% + 12px);z-index:30;transform:translateX(-50%);
3101
+ display:flex;align-items:center;gap:12px;min-height:52px;width:max-content;max-width:min(480px,calc(100% - 24px));
3102
+ padding:8px 8px 8px 20px;border-radius:999px;background:var(--panel);color:var(--fg);
3103
+ box-shadow:0 18px 44px rgba(0,0,0,.26),0 3px 10px rgba(0,0,0,.12);
3104
+ font-size:13.5px;font-weight:500;letter-spacing:.01em;animation:dp-in .18s cubic-bezier(.2,.8,.2,1);
3105
+ }
3106
+ .dictate-pop[hidden]{display:none}
3107
+ @keyframes dp-in{from{opacity:0;transform:translate(-50%,8px) scale(.97)}to{opacity:1;transform:translateX(-50%)}}
3108
+ .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}
3109
+ @keyframes dp-dot{50%{opacity:.35}}
3110
+ .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}
3111
+ @keyframes dp-spin{to{transform:rotate(360deg)}}
3112
+ .dp-text{white-space:nowrap}
3113
+ .dp-time{color:var(--dim);font-weight:400;font-variant-numeric:tabular-nums;min-width:2.4em}
3114
+ .dp-wave{width:168px;height:32px;flex:0 1 auto;min-width:64px;color:var(--brand)}
3115
+ .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}
3116
+ .dp-btn:active{transform:scale(.94)}
3117
+ .dp-btn svg{width:15px;height:15px}
3118
+ .dp-cancel,.dp-close{background:var(--hover);color:var(--fg)}
3119
+ .dp-done{background:var(--accent);color:var(--bg)}
3120
+ .dp-btn:focus-visible{outline:2px solid var(--fg);outline-offset:2px}
3121
+ /* Which parts show is the state's: listening, working, or saying what went wrong. */
3122
+ .dictate-pop:not([data-state="listening"]) .dp-dot,
3123
+ .dictate-pop:not([data-state="listening"]) .dp-time,
3124
+ .dictate-pop:not([data-state="listening"]) .dp-wave,
3125
+ .dictate-pop:not([data-state="listening"]) .dp-cancel,
3126
+ .dictate-pop:not([data-state="listening"]) .dp-done,
3127
+ .dictate-pop:not([data-state="working"]) .dp-spin,
3128
+ .dictate-pop:not([data-state="error"]) .dp-close{display:none}
3129
+ .dictate-pop[data-state="working"]{padding-right:20px}
3130
+ /* The error red mixed toward the text colour: bright enough on the dark
3131
+ theme, still deep on the light one. */
3132
+ .dictate-pop[data-state="error"] .dp-text{color:color-mix(in srgb,var(--err) 62%,var(--fg));white-space:normal;line-height:1.45}
3133
+ @media (prefers-reduced-motion:reduce){.dictate-pop,.dp-dot,.dp-spin,.mic-btn.is-recording{animation:none}}
2670
3134
  /* The folder it runs in, set at the same weight as the model beside it: three
2671
3135
  settings on one bar, none of them shouting over the others. */
2672
3136
  #workspace-picker .picker-btn{gap:6px}
@@ -2708,6 +3172,54 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2708
3172
  .effort-name.on{color:var(--fg);font-weight:600}
2709
3173
  #attachments{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
2710
3174
  #attachments:empty{display:none}
3175
+ /* The tray: what the message carries, as cards sitting just above the box.
3176
+ Pictures at a size that can be recognised, files as a name and a line;
3177
+ the same fill as the box below and no outline, with the × on the card's
3178
+ corner where a thumbnail leaves no room beside it. */
3179
+ /* The box's own width and centring, so the cards start where the box does
3180
+ rather than at the column's edge. */
3181
+ #tray{display:flex;flex-wrap:wrap;gap:8px;max-width:720px;margin:0 auto 8px}
3182
+ #tray:empty{display:none}
3183
+ .tray-card{
3184
+ position:relative;display:flex;align-items:center;gap:10px;height:64px;
3185
+ max-width:240px;padding:0 30px 0 10px;border-radius:12px;background:var(--panel);
3186
+ }
3187
+ .tray-card.tray-media{width:64px;padding:0;overflow:hidden;background:var(--field)}
3188
+ .tray-card img,.tray-card video{width:100%;height:100%;object-fit:cover;display:block}
3189
+ .tray-card img{cursor:zoom-in}
3190
+ /* A clip: its first frame where a file's icon goes, with its length and size
3191
+ beside the name. */
3192
+ .tray-thumb{position:relative;width:44px;height:44px;flex:0 0 auto;border-radius:9px;overflow:hidden;background:var(--field)}
3193
+ .tray-thumb video{width:100%;height:100%;object-fit:cover;display:block}
3194
+ .tray-duration:not(:empty)::after{content:" · "}
3195
+ .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}
3196
+ .tray-card.video:hover .tray-play{opacity:0}
3197
+ .tray-play svg{width:10px;height:10px}
3198
+ .tray-icon{width:36px;height:36px;flex:0 0 auto;border-radius:9px;display:grid;place-items:center;background:var(--field);color:var(--dim)}
3199
+ .tray-icon svg{width:16px;height:16px}
3200
+ .tray-text{display:flex;flex-direction:column;gap:2px;min-width:0}
3201
+ .tray-name{font-size:12.5px;color:var(--fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3202
+ .tray-sub{font-size:11px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3203
+ .tray-card .x{
3204
+ position:absolute;top:4px;right:4px;width:20px;height:20px;padding:0;border:0;border-radius:50%;
3205
+ display:grid;place-items:center;font-size:13px;line-height:1;cursor:pointer;
3206
+ background:var(--field);color:var(--dim);
3207
+ }
3208
+ .tray-card.tray-media .x{background:#0000008c;color:#fff;opacity:0;transition:opacity .12s}
3209
+ .tray-card.tray-media:hover .x,.tray-card .x:focus-visible{opacity:1}
3210
+ /* No hover on a touch screen: the × is always there. */
3211
+ @media (hover:none){.tray-card.tray-media .x{opacity:1}}
3212
+ .tray-card .x:hover{color:var(--fg)}
3213
+ /* A phone: one row that scrolls sideways rather than rows that push the box
3214
+ up the screen, and cards a little smaller so two or three still show. */
3215
+ @media (max-width:760px){
3216
+ #tray{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch;margin-bottom:6px}
3217
+ #tray::-webkit-scrollbar{display:none}
3218
+ .tray-card{flex:0 0 auto;height:56px;max-width:200px}
3219
+ .tray-card.tray-media{width:56px}
3220
+ .tray-icon,.tray-thumb{width:36px;height:36px}
3221
+ }
3222
+ .tray-card.tray-media .x:hover{color:#fff;background:#000000b3}
2711
3223
  /* Outside the composer, tracking its width so the rows line up with the box
2712
3224
  they are waiting on. min-width:0 twice over — here and on the line inside
2713
3225
  each row — because a flex item defaults to refusing to shrink below its
@@ -2743,6 +3255,91 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2743
3255
  :root[data-theme="dark"] .q .go:hover{color:var(--brand)}
2744
3256
  .q .x{border:0;background:transparent;color:var(--dim);cursor:pointer;font:inherit;padding:0 2px}
2745
3257
  .q .x:hover{color:var(--err)}
3258
+ /* The goal being worked toward: the reason the turn keeps going, in the same
3259
+ brand ink that marks the harness at work. Its words in full colour, since
3260
+ unlike a queued line it is what everything below is for. */
3261
+ .q.goal .tag{color:var(--brand-ink)}
3262
+ :root[data-theme="dark"] .q.goal .tag{color:var(--brand)}
3263
+ .q.goal .n{color:var(--fg)}
3264
+ /* A background task: work this conversation set going. Running ones say so
3265
+ in the brand ink that marks the harness at work; ended ones step back, but
3266
+ stay a click from what they said. */
3267
+ .q.task .n{cursor:pointer}
3268
+ .q.task-running .tag,.q.task-queued .tag{color:var(--brand-ink)}
3269
+ :root[data-theme="dark"] .q.task-running .tag,:root[data-theme="dark"] .q.task-queued .tag{color:var(--brand)}
3270
+ .q.task-failed .tag{color:var(--err)}
3271
+ .q.task-completed .n,.q.task-cancelled .n,.q.task-failed .n{color:var(--dim)}
3272
+ .q.task-more{border:0;color:var(--dim);cursor:pointer;font:inherit;font-size:12px;text-align:left}
3273
+ .q.task-more:hover{color:var(--fg)}
3274
+ /* Background results read into the conversation: a card, set apart from
3275
+ what the person typed by its fill and its place on the left. */
3276
+ .task-card-row-wrap{margin-bottom:26px}
3277
+ .task-card{display:flex;flex-direction:column;gap:4px;max-width:520px;background:var(--panel);border-radius:12px;padding:10px 12px;font-size:12px}
3278
+ .task-card-head{color:var(--dim);font-size:11px;margin-bottom:2px}
3279
+ .task-card-row{display:flex;align-items:center;gap:8px;min-width:0}
3280
+ .task-card-row .tag{flex:0 0 auto;color:var(--dim);font-size:11px}
3281
+ .task-card-row.task-completed .tag{color:var(--ok)}
3282
+ .task-card-row.task-failed .tag{color:var(--err)}
3283
+ .task-card-row .n{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3284
+ .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}
3285
+ .task-card-row .go:hover{background:var(--brand-tint);color:var(--brand-ink)}
3286
+ /* The sheet a task opens into: down the right edge, over the conversation,
3287
+ which stays where it was underneath. Set apart by its fill, never a line. */
3288
+ #task-sheet{
3289
+ position:fixed;top:0;right:0;bottom:0;z-index:30;width:min(560px,100vw);
3290
+ display:flex;flex-direction:column;background:var(--panel);
3291
+ box-shadow:-12px 0 32px #00000014;
3292
+ }
3293
+ #task-sheet[hidden]{display:none}
3294
+ /* In the desktop app the tab strip under the sheet's top edge is the window's
3295
+ drag handle, and a drag region takes clicks whatever is drawn over it: the
3296
+ sheet's close and stop buttons would only ever move the window. */
3297
+ #task-sheet{-webkit-app-region:no-drag}
3298
+ .task-head{display:flex;align-items:flex-start;gap:10px;padding:16px 16px 10px}
3299
+ .task-titles{flex:1;min-width:0}
3300
+ .task-title{font-weight:600;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3301
+ .task-meta{color:var(--dim);font-size:12px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3302
+ .task-stop{
3303
+ flex:0 0 auto;height:28px;padding:0 10px;border:0;border-radius:8px;
3304
+ background:var(--hover);color:var(--fg);cursor:pointer;font:inherit;font-size:12px;
3305
+ }
3306
+ .task-stop:hover{color:var(--err)}
3307
+ .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}
3308
+ .task-close:hover{background:var(--hover);color:var(--fg)}
3309
+ .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}
3310
+ .task-chips::-webkit-scrollbar{display:none}
3311
+ .task-chips[hidden]{display:none}
3312
+ .task-chip{
3313
+ flex:0 0 auto;height:26px;padding:0 10px;border:0;border-radius:13px;
3314
+ background:var(--hover);color:var(--dim);cursor:pointer;font:inherit;font-size:12px;
3315
+ white-space:nowrap;
3316
+ }
3317
+ .task-chip.on{background:var(--brand-tint);color:var(--brand-ink)}
3318
+ :root[data-theme="dark"] .task-chip.on{color:var(--brand)}
3319
+ .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}
3320
+ .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}
3321
+ .task-msg.assistant{overflow-wrap:anywhere}
3322
+ .task-tool{background:var(--bg);border-radius:10px;padding:6px 10px;font-size:12px}
3323
+ .task-tool summary{display:flex;gap:8px;cursor:pointer;list-style:none;min-width:0}
3324
+ .task-tool summary::-webkit-details-marker{display:none}
3325
+ .task-tool-name{flex:0 0 auto;font-weight:600}
3326
+ .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)}
3327
+ .task-tool.err .task-tool-name{color:var(--err)}
3328
+ .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}
3329
+ .task-note{color:var(--dim);font-size:12px}
3330
+ .task-error{color:var(--err);font-size:12px}
3331
+ .task-empty{color:var(--dim)}
3332
+ .task-working{display:flex;align-items:center;gap:8px;color:var(--dim)}
3333
+ /* The sheet's own surface runs to its bottom edge: no band behind the box. */
3334
+ .task-form{padding:8px 16px 16px;background:transparent}
3335
+ /* Field and send button as one box, the send arrow inside at its corner. */
3336
+ .task-box{display:flex;align-items:flex-end;gap:8px;background:var(--bg);border-radius:14px;padding:8px 8px 8px 12px}
3337
+ .task-input{
3338
+ flex:1;min-width:0;resize:none;border:0;padding:5px 0;max-height:160px;
3339
+ background:transparent;color:var(--fg);font:inherit;font-size:13px;line-height:1.5;outline:none;
3340
+ }
3341
+ .task-input:disabled{opacity:.6}
3342
+ .task-send{flex:0 0 auto}
2746
3343
  .att{
2747
3344
  display:flex;align-items:center;gap:7px;background:var(--bg);border-radius:8px;
2748
3345
  padding:5px 8px;font-size:12px;max-width:260px;
@@ -2756,6 +3353,37 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2756
3353
  the chip carries a mark for what it points at instead of a size. */
2757
3354
  .att .ico-sm{width:13px;height:13px;opacity:.65}
2758
3355
  .att .where{color:var(--dim);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3356
+ /* Goal mode is how this message is sent, not a thing attached to it: a
3357
+ tinted fill, never an outline, sets it apart from the files. */
3358
+ .att.goal{background:var(--brand-tint);color:var(--brand-ink)}
3359
+ :root[data-theme="dark"] .att.goal{color:var(--brand)}
3360
+ .att.goal .ico-sm{opacity:1}
3361
+ /* Computer use stays on for the whole conversation, so it sits in the chip
3362
+ row on every message: a full brand fill there shouted louder than anything
3363
+ the person typed. The file chips' own quiet fill, with the colour kept to
3364
+ the mark. Turning it off is not destructive, so the × does not go red. */
3365
+ .att.computer{color:var(--fg)}
3366
+ .att.computer .ico-sm{opacity:1;color:var(--brand-ink)}
3367
+ .att.computer .x:hover{color:var(--fg)}
3368
+ .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}
3369
+ .computer-perm .cp-head{display:flex;align-items:center;gap:8px;font-weight:600;color:var(--fg)}
3370
+ .computer-perm .cp-body{color:var(--dim);font-size:12px;white-space:normal}
3371
+ .computer-perm .cp-row{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12.5px;color:var(--fg)}
3372
+ .computer-perm .cp-ok{width:26px;text-align:center;color:var(--dim);font-size:13px;line-height:24px}
3373
+ /* The app's primary button, inverted on either theme: white on the brand
3374
+ green was unreadable in dark mode. */
3375
+ .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}
3376
+ .computer-perm .cp-btn:hover{background:color-mix(in srgb,var(--accent) 85%,var(--bg))}
3377
+ /* The card's own actions, after the rows: buttons the size of Allow, but
3378
+ secondary — the page's fill on the card's, no outline — and set right, where
3379
+ Allow already sits. Loose green words read as links to somewhere else, and
3380
+ sat under the rows as if they were a third one. */
3381
+ .computer-perm .cp-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}
3382
+ .computer-perm .cp-link{
3383
+ border:0;border-radius:7px;padding:4px 12px;background:var(--bg);color:var(--fg);
3384
+ font:inherit;font-size:12px;font-weight:500;cursor:pointer;
3385
+ }
3386
+ .computer-perm .cp-link:hover{background:color-mix(in srgb,var(--fg) 8%,var(--bg))}
2759
3387
  /* Wider than the model picker's: these rows carry a name and the folder it
2760
3388
  sits in, and truncating the folder away leaves three files called index.ts. */
2761
3389
  #attach-picker .menu{min-width:min(380px,86vw)}
@@ -2766,8 +3394,14 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2766
3394
  should not look identical — one of them means Enter takes this row — but the
2767
3395
  menu is a quiet surface and the difference is worth one tone, not two. */
2768
3396
  #attach-picker .menu-item.cursor{background:var(--sel)}
2769
- #attach-picker .menu-item.cursor .ico-sm{opacity:.8}
2770
- #attach-picker .menu-item .ico-sm{opacity:.55}
3397
+ /* Solid tokens rather than the ink at partial opacity: a translucent stroke
3398
+ picks up whatever row background sits behind it and its anti-aliased edge
3399
+ blends twice, so a 1.5px line read thinner and greyer on hover than at rest.
3400
+ At rest the marks take the section labels' grey; a row in play — under the
3401
+ pointer, under the keyboard cursor, or switched on — lights its mark to the
3402
+ label's own colour. */
3403
+ #attach-picker .menu-item .ico-sm{opacity:1;color:var(--dim)}
3404
+ #attach-picker .menu-item:hover .ico-sm,#attach-picker .menu-item.cursor .ico-sm{color:var(--fg)}
2771
3405
  /* The stacked rows carry a name over a sentence, so they are already two lines
2772
3406
  tall; the mark beside them is a label for the kind, not an illustration, and
2773
3407
  at 16px it was the widest thing in a row whose job is the words. */
@@ -2785,7 +3419,7 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2785
3419
  #attach-picker .menu-item.stacked .tick{margin-top:0}
2786
3420
  #attach-picker .menu-item.stacked{padding-top:5px;padding-bottom:5px}
2787
3421
  #attach-picker .menu-item.stacked .stack{gap:1px}
2788
- #attach-picker .menu-item.on .ico-sm{opacity:.9}
3422
+ #attach-picker .menu-item.on .ico-sm{color:var(--fg)}
2789
3423
  .menu-hint{padding:6px 8px 8px;font-size:11.5px;color:var(--dim)}
2790
3424
  /* A row that has something to say about itself: the name on one line and a
2791
3425
  sentence under it. Beside the name, that sentence took the whole row and
@@ -2843,8 +3477,11 @@ button.ghost,a.ghost{
2843
3477
  background:var(--sel);color:var(--fg);font:inherit;font-size:12.5px;cursor:pointer;
2844
3478
  }
2845
3479
  button.ghost:hover,a.ghost:hover{background:var(--hover)}
3480
+ /* Just above the composer, however tall it is: a fixed offset was covered as
3481
+ soon as attachment cards or a longer draft grew the form under it.
3482
+ --form-h is measured in transcript.ts. */
2846
3483
  #jump{
2847
- position:absolute;right:24px;bottom:118px;border-radius:999px;padding:7px 15px;border:0;
3484
+ position:absolute;right:24px;bottom:calc(var(--form-h, 108px) + 10px);border-radius:999px;padding:7px 15px;border:0;
2848
3485
  background:var(--bg);color:var(--fg);cursor:pointer;font:inherit;font-size:12px;
2849
3486
  box-shadow:0 1px 2px #0000000f,0 6px 20px #0000001f;
2850
3487
  }
@@ -2855,6 +3492,36 @@ dialog{
2855
3492
  box-shadow:0 2px 6px #00000014,0 20px 60px #00000033;
2856
3493
  }
2857
3494
  dialog::backdrop{background:rgba(0,0,0,.55)}
3495
+ /* A phone asking to pair: the code is the thing to read, so it is the largest. */
3496
+ .pair-dialog{width:min(420px,92vw);padding-bottom:16px}
3497
+ .pair-title{padding:22px 24px 6px;font-size:16px;font-weight:600}
3498
+ .pair-body{padding:0 24px;color:var(--dim);font-size:13.5px;line-height:1.5}
3499
+ .pair-code{margin:18px 24px 6px;padding:14px 0;border-radius:12px;background:var(--sel);text-align:center;font-size:30px;letter-spacing:.12em}
3500
+ /* An update question, in place of the OS's message box: the version is the
3501
+ title, the release notes scroll if there are many, and Later stays quiet. */
3502
+ .upd-dialog{width:min(440px,92vw);padding-bottom:16px}
3503
+ .upd-head{display:flex;align-items:center;gap:12px;padding:22px 24px 8px}
3504
+ .upd-mark{flex:none;display:grid;place-items:center;width:30px;height:30px;border-radius:9px;background:var(--sel);color:var(--fg);font-size:15px;font-weight:600}
3505
+ .upd-dialog.important .upd-mark{background:var(--accent);color:var(--bg)}
3506
+ .upd-title{font-size:16px;font-weight:600;line-height:1.35}
3507
+ .upd-notes{margin:6px 24px 10px;padding:12px 14px;max-height:220px;overflow-y:auto;border-radius:10px;background:var(--hover);font-size:13px;line-height:1.55;white-space:pre-wrap}
3508
+ .upd-body{padding:0 24px;color:var(--dim);font-size:13.5px;line-height:1.5;word-break:break-word}
3509
+ .upd-body.mono{font-size:12.5px}
3510
+ .upd-dialog .dlg-actions{justify-content:flex-end;padding:18px 20px 4px}
3511
+ /* The app's confirm and prompt (askConfirm, askText). */
3512
+ .ask-dialog{width:min(420px,92vw);padding-bottom:16px}
3513
+ .ask-dialog-title{padding:22px 24px 4px;font-size:16px;font-weight:600;line-height:1.35}
3514
+ .ask-dialog-body{padding:6px 24px 0;color:var(--dim);font-size:13.5px;line-height:1.55;white-space:pre-wrap}
3515
+ .ask-dialog-title + .ask-dialog-body{padding-top:2px}
3516
+ .ask-dialog-body:first-child{padding-top:22px;color:var(--fg);font-size:14px}
3517
+ .ask-dialog-field{
3518
+ display:block;box-sizing:border-box;width:calc(100% - 48px);margin:14px 24px 2px;padding:9px 12px;
3519
+ border:1px solid var(--line);border-radius:10px;background:var(--bg);color:var(--fg);font:inherit;font-size:14px;outline:none;
3520
+ }
3521
+ .ask-dialog-field:focus{border-color:var(--fg)}
3522
+ .ask-dialog .dlg-actions{justify-content:flex-end;padding:18px 20px 4px}
3523
+ .ask-dialog .dlg-actions button.quiet{margin-right:0}
3524
+ .dlg-actions button.approve.danger{background:var(--err);color:#fff}
2858
3525
  /* The approval dialog.
2859
3526
  *
2860
3527
  * Everything here is arranged around one question: what exactly is about to
@@ -3032,7 +3699,9 @@ dialog::backdrop{background:rgba(0,0,0,.55)}
3032
3699
  #explorer-head{padding-top:max(10px,env(safe-area-inset-top))}
3033
3700
  #explorer-body{flex-direction:column}
3034
3701
  #explorer-resizer{display:none}
3035
- #explorer-tree{width:auto;height:var(--explorer-tree-height,38%);max-height:none}
3702
+ #explorer-side{width:auto;height:var(--explorer-tree-height,38%);max-height:none}
3703
+ #explorer-find{height:36px}
3704
+ #explorer-q{font-size:16px}
3036
3705
  /* Stacked, so the handle between them drags up and down — and is a strip
3037
3706
  a thumb can find rather than a hairline. */
3038
3707
  #explorer-split{flex:0 0 18px;margin:0;cursor:row-resize}
@@ -3095,7 +3764,7 @@ dialog::backdrop{background:rgba(0,0,0,.55)}
3095
3764
  .effort-stop,.effort-name{width:40px}
3096
3765
  .effort-stop{height:40px}
3097
3766
  .effort-knob{width:28px;height:28px}
3098
- #jump{right:14px;bottom:160px}
3767
+ #jump{right:14px}
3099
3768
 
3100
3769
  /* Dialogs, with the keyboard up: vh is the height without it. */
3101
3770
  #market{max-height:82dvh}
@@ -3120,7 +3789,7 @@ dialog::backdrop{background:rgba(0,0,0,.55)}
3120
3789
  */
3121
3790
  @media (hover:none) and (pointer:coarse){
3122
3791
  .session .more,.fold-row .more{display:grid;width:28px;height:28px}
3123
- .proj-forget{opacity:1}
3792
+ .proj-more{opacity:1}
3124
3793
  .actions{position:static;opacity:1;margin-top:4px}
3125
3794
  .turn{margin-bottom:14px}
3126
3795
  .act{padding:6px 10px}