privateboard 0.1.37 → 0.1.40

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 (76) hide show
  1. package/dist/boot.js +1415 -91
  2. package/dist/boot.js.map +1 -1
  3. package/dist/cli.js +1415 -91
  4. package/dist/cli.js.map +1 -1
  5. package/dist/server.js +1271 -81
  6. package/dist/server.js.map +1 -1
  7. package/dist/version.d.ts +1 -1
  8. package/dist/version.js +1 -1
  9. package/dist/version.js.map +1 -1
  10. package/package.json +1 -1
  11. package/public/__avatar3d_test.html +156 -0
  12. package/public/adjourn-overlay.css +2 -2
  13. package/public/agent-overlay.css +27 -15
  14. package/public/agent-overlay.js +3 -1
  15. package/public/agent-profile.css +331 -41
  16. package/public/agent-profile.js +499 -75
  17. package/public/app-updater.css +1 -1
  18. package/public/app.js +2090 -547
  19. package/public/avatar-3d-snap.js +205 -0
  20. package/public/avatar-3d.js +792 -0
  21. package/public/avatar-customizer.html +274 -0
  22. package/public/avatar3d-editor.css +240 -0
  23. package/public/avatar3d-editor.js +481 -0
  24. package/public/avatars/3d/chair.png +0 -0
  25. package/public/avatars/3d/first-principles.png +0 -0
  26. package/public/avatars/3d/historian.png +0 -0
  27. package/public/avatars/3d/long-horizon.png +0 -0
  28. package/public/avatars/3d/phenomenologist.png +0 -0
  29. package/public/avatars/3d/socrates.png +0 -0
  30. package/public/avatars/3d/user-empathy.png +0 -0
  31. package/public/avatars/3d/value-investor.png +0 -0
  32. package/public/core-avatars.js +86 -0
  33. package/public/home-3d-loader.js +15 -4
  34. package/public/home-3d-mock.js +18 -7
  35. package/public/home.html +80 -18
  36. package/public/i18n.js +279 -4
  37. package/public/icons/avatar_1779855104027.glb +0 -0
  38. package/public/icons/logo.png +0 -0
  39. package/public/icons/new-style.glb +0 -0
  40. package/public/icons/new-style2.glb +0 -0
  41. package/public/icons/new-style3.glb +0 -0
  42. package/public/icons/new-style4.glb +0 -0
  43. package/public/icons/new-style5.glb +0 -0
  44. package/public/icons/office.glb +0 -0
  45. package/public/icons/stuff.glb +0 -0
  46. package/public/index.html +203 -182
  47. package/public/mention-picker.js +1 -1
  48. package/public/new-agent.css +7 -7
  49. package/public/new-agent.js +46 -20
  50. package/public/office-viewer.html +340 -0
  51. package/public/onboarding.css +5 -5
  52. package/public/quote-cta.css +5 -4
  53. package/public/quote-cta.js +50 -5
  54. package/public/room-settings.css +24 -9
  55. package/public/stuff-viewer.html +330 -0
  56. package/public/thread.css +1211 -0
  57. package/public/user-settings.css +16 -19
  58. package/public/user-settings.js +86 -78
  59. package/public/vendor/BufferGeometryUtils.js +1434 -0
  60. package/public/vendor/DRACOLoader.js +739 -0
  61. package/public/vendor/GLTFLoader.js +4860 -0
  62. package/public/vendor/RoomEnvironment.js +185 -0
  63. package/public/vendor/SkeletonUtils.js +496 -0
  64. package/public/vendor/draco/draco_decoder.js +34 -0
  65. package/public/vendor/draco/draco_decoder.wasm +0 -0
  66. package/public/vendor/draco/draco_encoder.js +33 -0
  67. package/public/vendor/draco/draco_wasm_wrapper.js +117 -0
  68. package/public/vendor/meshopt_decoder.module.js +196 -0
  69. package/public/voice-3d-banner.js +12 -0
  70. package/public/voice-3d.js +1407 -432
  71. package/public/voice-clone.css +875 -0
  72. package/public/voice-clone.js +1351 -0
  73. package/public/voice-replay.css +3 -3
  74. package/public/voice-replay.js +21 -0
  75. package/public/avatar-skill.js +0 -629
  76. package/public/icons/folded-sidebar.png +0 -0
@@ -0,0 +1,1211 @@
1
+ /* ═══════════════════════════════════════════════════════════════════
2
+ thread.css · Private 1:1 thread float window (Slack DM-style)
3
+ ═══════════════════════════════════════════════════════════════════
4
+
5
+ Why this lives in its own file:
6
+ - Thread is an "epic feature" — it'll grow more chrome (dock,
7
+ pinned threads, search). Keeping its styles isolated makes
8
+ review + iteration easier than burying them in index.html's
9
+ inline block.
10
+ - The visual vocabulary deliberately copies adjourn / cast-edit
11
+ overlays — same border colors, same panel hierarchy — so the
12
+ float window reads as part of the same product family, just
13
+ in a more casual register (rounded corners, smaller padding,
14
+ no classification stripe).
15
+
16
+ Architectural notes:
17
+ - The float root uses `position: fixed` with explicit `right/bottom`
18
+ so the user's screen real estate isn't disturbed (main room
19
+ keeps running underneath). Drag updates `transform: translate(...)`
20
+ so we don't fight the fixed positioning baseline.
21
+ - z-index sits above adjourn (9400) and supplement (9000) overlays
22
+ but below the agent-profile dropdowns the user might still want
23
+ to use; we land at 8500 — high enough to win the main view,
24
+ low enough that "real" modals supersede.
25
+ - Multiple threads stack with per-instance right-offset (set
26
+ inline by the mounting JS); we don't try to manage that in CSS.
27
+ ═══════════════════════════════════════════════════════════════════ */
28
+
29
+ .thread-float {
30
+ position: fixed;
31
+ right: 24px;
32
+ bottom: 24px;
33
+ /* Default 360px in both float and docked forms · matches the
34
+ sidebar's room-list width register so the panel reads as
35
+ "extended sidebar real estate" rather than a heavy overlay.
36
+ The dock variant overrides to a CSS variable (clamped 320-720)
37
+ so the user can drag-resize. */
38
+ width: 360px;
39
+ height: 520px;
40
+ max-height: calc(100vh - 96px);
41
+ z-index: 8500;
42
+ /* Frosted-glass header + composer overlay the messages list;
43
+ scroll-under requires `position: relative` so the absolute
44
+ children anchor to the panel. */
45
+ background: var(--panel);
46
+ border: 0.5px solid var(--line-strong);
47
+ border-radius: 6px;
48
+ box-shadow:
49
+ 0 8px 24px -8px rgba(0, 0, 0, 0.45),
50
+ 0 4px 12px -4px rgba(0, 0, 0, 0.3);
51
+ font-family: var(--mono);
52
+ overflow: hidden;
53
+ /* Header height token · single-line title + subtitle stack +
54
+ padding. Consumed by `.thread-float-messages` via
55
+ `padding-top: calc(var(--thread-head-h) + 14px)` so the
56
+ scrollable area starts cleanly below the absolute-positioned
57
+ head. Mirror of `.main-view`'s `--room-head-h` pattern. */
58
+ --thread-head-h: 60px;
59
+ /* No `transition` on transform · the JS drag handler writes
60
+ `transform: translate(x, y)` on every mousemove (60Hz); if
61
+ transform were animated, each frame would queue a 150ms ease
62
+ that fights the next frame's write, making the window visibly
63
+ lag behind the cursor. Fixed positioning + bare transform
64
+ writes track the mouse 1:1. */
65
+ }
66
+
67
+ /* Minimized state · the window collapses into a compact pill that
68
+ shows just the director's avatar + name. Lives in the dock bar
69
+ along with any other minimized threads. Restoring expands back
70
+ to the full window. */
71
+ .thread-float.is-minimized {
72
+ position: relative;
73
+ right: auto;
74
+ bottom: auto;
75
+ width: auto;
76
+ height: auto;
77
+ border-radius: 999px;
78
+ padding: 4px 12px 4px 4px;
79
+ /* Now that the base `.thread-float` no longer sets `display:
80
+ flex` (the absolute-positioned children don't need it), be
81
+ explicit here so the minimized pill still lays out as a row. */
82
+ display: flex;
83
+ flex-direction: row;
84
+ align-items: center;
85
+ gap: 8px;
86
+ cursor: pointer;
87
+ box-shadow: 0 2px 6px -2px rgba(0, 0, 0, 0.35);
88
+ overflow: visible;
89
+ }
90
+ .thread-float.is-minimized .thread-float-messages,
91
+ .thread-float.is-minimized .thread-float-composer,
92
+ .thread-float.is-minimized .thread-float-head-controls {
93
+ display: none;
94
+ }
95
+ .thread-float.is-minimized .thread-float-head {
96
+ /* In minimized form the header is the pill's only visible
97
+ content · pull it out of the absolute layer and back into the
98
+ pill's flex row. Clear the frosted-glass chrome too — the
99
+ pill itself draws the background. */
100
+ position: static;
101
+ background: transparent;
102
+ backdrop-filter: none;
103
+ -webkit-backdrop-filter: none;
104
+ border-bottom: 0;
105
+ border-radius: 999px;
106
+ padding: 0;
107
+ }
108
+ .thread-float.is-minimized .thread-float-head-title {
109
+ font-size: 11px;
110
+ }
111
+ .thread-float.is-minimized .thread-float-head-subtitle {
112
+ display: none;
113
+ }
114
+
115
+ /* ── Header · drag handle + identity + window controls ──────────
116
+ Frosted glass · sits absolutely positioned over the top of the
117
+ messages list. Same backdrop-filter recipe as the room's
118
+ `.room-head` so the two surfaces read as one chrome family.
119
+ Messages list scrolls UNDER this header (padding-top on the
120
+ messages container creates the initial offset). */
121
+ .thread-float-head {
122
+ position: absolute;
123
+ top: 0;
124
+ left: 0;
125
+ right: 0;
126
+ z-index: 10;
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 8px;
130
+ padding: 12px 10px 8px 12px;
131
+ background: color-mix(in srgb, var(--panel-3) 78%, var(--bg) 22%);
132
+ background: color-mix(in srgb, color-mix(in srgb, var(--panel-3) 78%, var(--bg) 22%) 88%, transparent);
133
+ backdrop-filter: blur(24px) saturate(180%);
134
+ -webkit-backdrop-filter: blur(24px) saturate(180%);
135
+ border-bottom: 0.5px solid var(--line-bright, rgba(255, 255, 255, 0.18));
136
+ border-radius: 6px 6px 0 0;
137
+ cursor: grab;
138
+ user-select: none;
139
+ }
140
+ .thread-float-head:active { cursor: grabbing; }
141
+ /* Header title block · two lines now (thread name + director
142
+ subtitle). Mirrors the room-head's title / subtitle stack
143
+ register so the two surfaces read the same way. */
144
+ .thread-float-head-title {
145
+ font-family: var(--sans, "Inter", system-ui, sans-serif);
146
+ font-size: 14px;
147
+ font-weight: 600;
148
+ color: var(--text);
149
+ white-space: nowrap;
150
+ overflow: hidden;
151
+ text-overflow: ellipsis;
152
+ }
153
+ .thread-float-head-subtitle {
154
+ font-size: 10px;
155
+ letter-spacing: 0.04em;
156
+ color: var(--text-faint, rgba(255, 255, 255, 0.55));
157
+ white-space: nowrap;
158
+ overflow: hidden;
159
+ text-overflow: ellipsis;
160
+ }
161
+
162
+ /* ── Edge resize handles ──────────────────────────────────────────
163
+ The float is bottom-anchored. The TOP handle grows/shrinks height
164
+ with the bottom pinned; the BOTTOM handle moves the bottom edge
165
+ with the cursor (top pinned) — both wired in `_wireThreadResize`.
166
+ Each sits over the header / composer edge-padding strip; the
167
+ header drag-to-move and composer still work below/above them. A
168
+ short centered grip bar fades in on hover. */
169
+ .thread-float-resize {
170
+ position: absolute;
171
+ left: 0;
172
+ right: 0;
173
+ height: 8px;
174
+ cursor: ns-resize;
175
+ z-index: 2;
176
+ -webkit-user-select: none;
177
+ user-select: none;
178
+ }
179
+ .thread-float-resize-top { top: 0; border-radius: 6px 6px 0 0; }
180
+ .thread-float-resize-bottom { bottom: 0; border-radius: 0 0 6px 6px; }
181
+ .thread-float-resize::after {
182
+ content: "";
183
+ position: absolute;
184
+ left: 50%;
185
+ transform: translateX(-50%);
186
+ width: 28px;
187
+ height: 2px;
188
+ border-radius: 1px;
189
+ background: var(--line-strong);
190
+ opacity: 0;
191
+ transition: opacity 0.12s;
192
+ }
193
+ .thread-float-resize-top::after { top: 2px; }
194
+ .thread-float-resize-bottom::after { bottom: 2px; }
195
+ .thread-float-resize:hover::after { opacity: 0.6; }
196
+ /* Minimized pill has no resize affordance. */
197
+ .thread-float.is-minimized .thread-float-resize { display: none; }
198
+
199
+ .thread-float-head-avatar {
200
+ /* Display the director's portrait raw · 32px square, no
201
+ circular mask, no panel-3 fallback ring. SVG/pixel-art
202
+ avatars in this project carry their own framing; cropping
203
+ them to a circle clipped the character art. Click opens
204
+ the director's agent-profile overlay — wired through the
205
+ standard `[data-agent-profile]` capture-phase handler in
206
+ agent-profile.js. */
207
+ width: 32px;
208
+ height: 32px;
209
+ flex-shrink: 0;
210
+ image-rendering: pixelated;
211
+ image-rendering: crisp-edges;
212
+ cursor: pointer;
213
+ transition: opacity 0.12s;
214
+ }
215
+ .thread-float-head-avatar:hover { opacity: 0.78; }
216
+
217
+ .thread-float-head-meta {
218
+ display: flex;
219
+ flex-direction: column;
220
+ gap: 2px;
221
+ flex: 1;
222
+ min-width: 0;
223
+ }
224
+ .thread-float-head-name {
225
+ font-family: var(--sans, "Inter", system-ui, sans-serif);
226
+ font-size: 12px;
227
+ font-weight: 600;
228
+ color: var(--text);
229
+ white-space: nowrap;
230
+ overflow: hidden;
231
+ text-overflow: ellipsis;
232
+ }
233
+ .thread-float-head-kicker {
234
+ font-size: 9px;
235
+ letter-spacing: 0.16em;
236
+ text-transform: uppercase;
237
+ color: var(--text-faint, rgba(255, 255, 255, 0.45));
238
+ }
239
+
240
+ .thread-float-head-controls {
241
+ display: flex;
242
+ align-items: center;
243
+ gap: 2px;
244
+ }
245
+ /* Head buttons · sidebar-nav register · 16px Lucide line icons
246
+ painted via mask-image so the glyph inherits `currentColor` (the
247
+ button text color cascades to the icon, matching the sidebar's
248
+ `.new-btn::before` pattern at index.html:1651). Each button gets
249
+ its own `--icon` custom property. The legacy Unicode-character
250
+ variant (— / ✕ / ⛶) read as different sizes / weights on every
251
+ OS and clashed with the sidebar's Lucide vocabulary. */
252
+ .thread-float-head-btn {
253
+ width: 28px;
254
+ height: 28px;
255
+ display: inline-flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ background: transparent;
259
+ border: 0;
260
+ color: var(--text-soft, rgba(255, 255, 255, 0.65));
261
+ cursor: pointer;
262
+ border-radius: 6px;
263
+ padding: 0;
264
+ /* Empty button content · the ::before paints the icon. Keeping
265
+ font-size: 0 ensures any accidental whitespace inside the
266
+ button doesn't push the icon off-center. */
267
+ font-size: 0;
268
+ line-height: 0;
269
+ transition: background 0.12s, color 0.12s;
270
+ }
271
+ .thread-float-head-btn::before {
272
+ content: "";
273
+ width: 16px;
274
+ height: 16px;
275
+ background-color: currentColor;
276
+ -webkit-mask-image: var(--icon, none);
277
+ mask-image: var(--icon, none);
278
+ -webkit-mask-repeat: no-repeat;
279
+ mask-repeat: no-repeat;
280
+ -webkit-mask-position: center;
281
+ mask-position: center;
282
+ -webkit-mask-size: 16px 16px;
283
+ mask-size: 16px 16px;
284
+ }
285
+ .thread-float-head-btn:hover {
286
+ background: var(--panel-3);
287
+ color: var(--text);
288
+ }
289
+
290
+ /* Minimize · Lucide Minus (a single short horizontal bar) */
291
+ [data-thread-minimize] {
292
+ --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14'/></svg>");
293
+ }
294
+ /* Close · Lucide X */
295
+ [data-thread-close] {
296
+ --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M18 6 6 18'/><path d='m6 6 12 12'/></svg>");
297
+ }
298
+ /* Close gets a soft red tint on hover · destructive-leaning action.
299
+ Matches the trash-chip color in `.thread-list-row-delete:hover`
300
+ so close affordances across the thread surface share one
301
+ "this removes something" register. */
302
+ .thread-float-head-btn[data-thread-close]:hover {
303
+ color: var(--red, #B5706A);
304
+ background: color-mix(in srgb, var(--red, #B5706A) 14%, transparent);
305
+ }
306
+
307
+ /* ── Messages list · reuses .msg / .msg-content / .msg-bubble
308
+ styles from the main chat, just inside our scrollable
309
+ container. The compact density tweaks below scope to messages
310
+ inside a thread float so the main chat is unaffected. ─── */
311
+
312
+ .thread-float-messages {
313
+ /* Full-bleed under the absolute header + composer · messages
314
+ scroll behind both, mirroring the room chat's chat-content
315
+ sliding under .room-head + .ib-stack. */
316
+ position: absolute;
317
+ inset: 0;
318
+ overflow-y: auto;
319
+ padding: calc(var(--thread-head-h, 56px) + 14px) 22px 130px;
320
+ display: flex;
321
+ flex-direction: column;
322
+ gap: 16px;
323
+ /* No `scroll-behavior: smooth` · entering an existing thread
324
+ snaps to the last message (seed-history sets `scrollTop =
325
+ scrollHeight`) and the jump-top button snaps to the first;
326
+ both should be instant so the user reads "now I'm at the
327
+ other end" not "watch the panel animate." Smooth scrolling
328
+ also amplifies the cost of any SSE re-jump-to-bottom on
329
+ each appended chunk during streaming. */
330
+ /* Scrollbar auto-hide · same pattern as `.ib-textarea` in
331
+ index.html (line 15024). Default: invisible; the global
332
+ scroll listener in app.js adds `.is-scrolling` on each
333
+ scroll event and removes it ~800ms after the last one.
334
+ Result: clean reading surface in idle state, scrollbar
335
+ appears the moment the user scrolls. */
336
+ scrollbar-width: none;
337
+ }
338
+ .thread-float-messages::-webkit-scrollbar {
339
+ width: 0;
340
+ height: 0;
341
+ /* Override the browser default white scrollbar gutter even
342
+ when widthless · WebKit otherwise paints the track stripe
343
+ visibly on light themes. */
344
+ background: transparent;
345
+ }
346
+ .thread-float-messages.is-scrolling { scrollbar-width: thin; }
347
+ .thread-float-messages.is-scrolling::-webkit-scrollbar {
348
+ width: 5px;
349
+ background: transparent;
350
+ }
351
+ .thread-float-messages.is-scrolling::-webkit-scrollbar-thumb {
352
+ background: color-mix(in srgb, var(--ink, currentColor) 28%, transparent);
353
+ border-radius: 4px;
354
+ }
355
+ .thread-float-messages.is-scrolling::-webkit-scrollbar-track {
356
+ background: transparent;
357
+ }
358
+ /* Firefox can't be styled per-element track-vs-thumb · use
359
+ `scrollbar-color` with the same translucent thumb + transparent
360
+ track recipe. */
361
+ .thread-float-messages.is-scrolling {
362
+ scrollbar-color: color-mix(in srgb, var(--ink, currentColor) 28%, transparent) transparent;
363
+ }
364
+ .thread-float-messages article.msg {
365
+ /* Slightly tighter than the main chat density · float is small. */
366
+ margin: 0;
367
+ /* Collapse the 32px avatar column · this is a 1:1 chat with one
368
+ director and the panel header already shows their avatar +
369
+ name. Repeating the head-shot on every message is redundant
370
+ and chews real estate that ChatGPT-style threads spend on
371
+ prose. Override the main chat's grid (32px / 1fr) to a single
372
+ 1fr column so the message content flows edge-to-edge inside
373
+ the messages list's 24px horizontal padding. */
374
+ grid-template-columns: 1fr;
375
+ }
376
+ /* Hide ALL avatars inside thread bubbles (director + chair + user
377
+ alike). User already has its own `.msg.user .msg-av { display:
378
+ none }` rule below; this generalises to every author kind. */
379
+ .thread-float-messages article.msg .msg-av {
380
+ display: none;
381
+ }
382
+ .thread-float-messages article.msg .msg-meta {
383
+ font-size: 10px;
384
+ /* Tighter than the main chat's 10px · the row-gap (vertical) also
385
+ governs the distance down to the wrapped timestamp line below,
386
+ which read too loose at 10px. column-gap stays a touch wider for
387
+ name / model / tag separation. */
388
+ gap: 3px 8px;
389
+ }
390
+ /* Timestamp drops onto its own line beneath the director's name
391
+ (instead of the main chat's right-aligned, same-row placement).
392
+ `flex-basis: 100%` makes it wrap in the flex meta row; `margin-left`
393
+ reset + `order` keep it last and left-aligned under the name.
394
+ User messages hide `.msg-meta` entirely in threads, so this only
395
+ affects director rows. */
396
+ .thread-float-messages article.msg .msg-time {
397
+ flex-basis: 100%;
398
+ margin-left: 0;
399
+ order: 99;
400
+ }
401
+ .thread-float-messages article.msg .msg-bubble {
402
+ /* Match the room's `.msg-bubble { font-size: 16px; line-height:
403
+ 1.65 }` (index.html:7557). Keeps prose-reading rhythm
404
+ identical between main chat and thread. */
405
+ font-size: 16px;
406
+ line-height: 1.65;
407
+ }
408
+ /* In threads we don't show round-end vote cards or chair pick
409
+ kickers — there are no rounds, no chair. Hide aggressively. */
410
+ .thread-float-messages .chair-pick-kicker,
411
+ .thread-float-messages .round-end-card,
412
+ .thread-float-messages .msg-context {
413
+ display: none;
414
+ }
415
+ /* Hide the per-message model pill in threads · 1:1 private chat,
416
+ the user already knows which director / model they're talking
417
+ to (header carries the director's name + subtitle). Showing the
418
+ model on every bubble eats ~80-120px of the meta line on a
419
+ 360px panel, which forced the `🔍 web-search` badge to wrap
420
+ below the director's name instead of sitting inline with it
421
+ like in the main room. Hiding the model frees enough room for
422
+ `name + tag + ws-badge` to live on one line. */
423
+ .thread-float-messages article.msg .msg-model {
424
+ display: none;
425
+ }
426
+
427
+ /* ── Web search · chair-style tool-card promoted above the bubble ──
428
+ In thread mode `messageHtml` synthesises a `.msg-tool-card` (the
429
+ same component the chair's web-search uses in main rooms) and
430
+ inserts it ABOVE the director's bubble. The room's default card
431
+ geometry assumes a wide content column (`margin: 14px auto 28px;
432
+ max-width: 760px`), which doesn't fit a 316px-wide thread panel.
433
+ Override only the geometry — colors / banner / mark / caret
434
+ stay shared with the room card so the visual vocabulary reads
435
+ the same. */
436
+ .thread-float-messages .msg-tool-card {
437
+ margin: 6px 0 10px;
438
+ max-width: 100%;
439
+ }
440
+ .thread-float-messages .msg-tool-banner {
441
+ padding: 6px 10px;
442
+ gap: 8px;
443
+ }
444
+ .thread-float-messages .msg-tool-banner-tag { font-size: 9px; }
445
+ .thread-float-messages .msg-tool-banner-stamp { font-size: 9px; }
446
+ .thread-float-messages .msg-tool-card-body {
447
+ padding: 10px 12px;
448
+ gap: 8px;
449
+ font-size: 11px;
450
+ }
451
+ .thread-float-messages .msg-tool-card .msg-tool-sources-list {
452
+ /* Sources rows inside the card · default 28px num column shrinks
453
+ to 22px so the title/url has more room in the 316px panel. */
454
+ font-size: 11px;
455
+ }
456
+ .thread-float-messages .msg-tool-card .msg-tool-sources-list li {
457
+ padding: 6px 12px;
458
+ }
459
+ .thread-float-messages .msg-tool-card .msg-tool-sources-num {
460
+ width: 20px;
461
+ min-width: 20px;
462
+ }
463
+ .thread-float-messages .msg-tool-card .msg-tool-sources-title-text { font-size: 12px; }
464
+ .thread-float-messages .msg-tool-card .msg-tool-sources-desc { font-size: 11px; }
465
+ .thread-float-messages .msg-tool-card .msg-tool-sources-expand {
466
+ font-size: 10px;
467
+ padding: 8px 12px;
468
+ }
469
+
470
+ /* ── ChatGPT-style user bubbles · right-aligned with surface ───
471
+ User-authored messages in a thread should read as the user's
472
+ voice in a 1:1 chat, not as a peer in a multi-director room.
473
+ Override the main chat's grid layout (32px avatar + 1fr content)
474
+ with a right-aligned flex row, hide the avatar entirely, and
475
+ give the bubble its own panel-2 surface with rounded corners.
476
+ Director messages stay in the default layout (avatar + plain
477
+ bubble · matches "assistant message" register in ChatGPT). */
478
+ .thread-float-messages article.msg.user {
479
+ display: flex;
480
+ justify-content: flex-end;
481
+ grid-template-columns: none;
482
+ gap: 0;
483
+ }
484
+ .thread-float-messages article.msg.user .msg-av,
485
+ .thread-float-messages article.msg.user .msg-meta,
486
+ .thread-float-messages article.msg.user .msg-toolbar {
487
+ display: none;
488
+ }
489
+ .thread-float-messages article.msg.user .msg-content {
490
+ /* Cap so a long single-line message wraps within the window
491
+ instead of stretching to the full width and reading flat. */
492
+ max-width: 82%;
493
+ min-width: 0;
494
+ }
495
+ .thread-float-messages article.msg.user .msg-bubble {
496
+ background: var(--panel-2);
497
+ color: var(--text);
498
+ border-radius: 14px;
499
+ padding: 8px 12px;
500
+ font-family: var(--font-human, var(--sans));
501
+ /* Soft border on light themes (transparent on dark since
502
+ panel-2 already separates from the chat bg) — color-mix
503
+ keeps both palettes balanced. */
504
+ border: 0.5px solid color-mix(in srgb, var(--line-bright, rgba(255, 255, 255, 0.12)) 60%, transparent);
505
+ /* Hint the layout system this block can shrink — without it
506
+ long unbroken strings (URLs) would push the bubble past the
507
+ 82% cap. */
508
+ word-break: break-word;
509
+ overflow-wrap: anywhere;
510
+ }
511
+ /* Empty-state hint · before the user types anything */
512
+ .thread-float-empty {
513
+ display: flex;
514
+ flex-direction: column;
515
+ align-items: center;
516
+ justify-content: center;
517
+ flex: 1;
518
+ text-align: center;
519
+ padding: 24px;
520
+ color: var(--text-faint, rgba(255, 255, 255, 0.45));
521
+ font-size: 11px;
522
+ line-height: 1.6;
523
+ letter-spacing: 0.02em;
524
+ gap: 8px;
525
+ }
526
+ .thread-float-empty-tag {
527
+ font-size: 9px;
528
+ letter-spacing: 0.2em;
529
+ text-transform: uppercase;
530
+ }
531
+
532
+ /* ── Composer · 1:1 mirror of the room's `.input-bar` chrome ─────
533
+ Two-row column layout matching `.input-bar` in index.html: row
534
+ one is the textarea (autosize 24-200px), row two is the controls
535
+ strip (send / stop on the right, future per-thread actions can
536
+ slot on the left). Earlier single-row inline placement read as a
537
+ different chrome family from the room — same border-radius +
538
+ typography but visually noisy with the send chip floating next
539
+ to the textarea. */
540
+ .thread-float-composer {
541
+ /* Absolute-bottom + frosted-glass · scroll-under chrome, same
542
+ pattern as the room's floating `.input-bar`. Messages list
543
+ extends behind this card; the card's frosted glass softens
544
+ the prose behind so the visual hierarchy stays "compose >
545
+ read". 14px lateral inset = same gutter as the head's
546
+ padding-left so the composer left-edge aligns with the
547
+ room-head's title column. */
548
+ position: absolute;
549
+ left: 14px;
550
+ right: 14px;
551
+ bottom: 14px;
552
+ z-index: 10;
553
+ display: flex;
554
+ flex-direction: column;
555
+ gap: 4px;
556
+ padding: 6px 8px 8px;
557
+ background: color-mix(in srgb, var(--panel-3) 78%, var(--bg) 22%);
558
+ background: color-mix(in srgb, color-mix(in srgb, var(--panel-3) 78%, var(--bg) 22%) 88%, transparent);
559
+ backdrop-filter: blur(24px) saturate(180%);
560
+ -webkit-backdrop-filter: blur(24px) saturate(180%);
561
+ border: 0.5px solid var(--line-strong);
562
+ border-radius: 14px;
563
+ transition: border-color 0.12s;
564
+ }
565
+ /* Top row · textarea sits flush with the card edges (same as
566
+ `.ib-text-row` in index.html). */
567
+ .thread-float-composer-text-row {
568
+ display: flex;
569
+ align-items: flex-start;
570
+ padding: 6px 8px 0;
571
+ min-height: 32px;
572
+ }
573
+ /* Bottom row · controls strip. Send / stop right-aligned via
574
+ `margin-left: auto` on the trailing slot (matches
575
+ `.ib-controls-row { gap: 6px; padding: 0 4px 0 4px }`). */
576
+ .thread-float-composer-controls-row {
577
+ display: flex;
578
+ align-items: center;
579
+ gap: 6px;
580
+ padding: 0 4px 0 4px;
581
+ /* Push the lone send/stop button to the right edge of the
582
+ card so the row geometry mirrors the room's input-bar
583
+ (where pause / adjourn / vote cluster on the LEFT and
584
+ send chips on the RIGHT). Even though thread has no
585
+ left-cluster actions yet, the spacer keeps the right-side
586
+ button anchored to where the room input-bar's send sits. */
587
+ }
588
+ .thread-float-composer-controls-row > .thread-float-send,
589
+ .thread-float-composer-controls-row > .thread-float-stop {
590
+ margin-left: auto;
591
+ }
592
+ .thread-float-composer:focus-within {
593
+ border-color: var(--lime, #6FB572);
594
+ }
595
+ .thread-float-textarea {
596
+ flex: 1;
597
+ width: 100%;
598
+ font-family: var(--sans, "Inter", system-ui, sans-serif);
599
+ font-size: 14px;
600
+ line-height: 1.45;
601
+ color: var(--text);
602
+ background: transparent;
603
+ border: 0;
604
+ border-radius: 0;
605
+ padding: 4px 0;
606
+ resize: none;
607
+ /* Match the room's autosize bounds exactly · room caps at 200px
608
+ before flipping into internal scroll. */
609
+ max-height: 200px;
610
+ min-height: 24px;
611
+ outline: none;
612
+ box-sizing: border-box;
613
+ /* Match the main bar's auto-hide scrollbar treatment. */
614
+ scrollbar-width: none;
615
+ }
616
+ .thread-float-textarea::-webkit-scrollbar { width: 0; height: 0; }
617
+ .thread-float-textarea::placeholder {
618
+ color: var(--text-faint);
619
+ font-family: var(--sans, "Inter", system-ui, sans-serif);
620
+ font-size: 14px;
621
+ }
622
+
623
+ /* Send / Stop · icon-only circular buttons matching the main
624
+ room's input-bar send button (`.ib-controls-row .send-btn` in
625
+ index.html · 32×32 lime circle with an arrow-up glyph mask).
626
+ Visible text ("Send" / "Stop") is hidden via font-size:0 but
627
+ stays in the accessibility tree for screen-reader names. The
628
+ glyph itself is a CSS mask painted by `::before` so we don't
629
+ pay an extra DOM node. */
630
+ .thread-float-send,
631
+ .thread-float-stop {
632
+ width: 32px;
633
+ height: 32px;
634
+ padding: 0;
635
+ border-radius: 50%;
636
+ border: none;
637
+ cursor: pointer;
638
+ flex-shrink: 0;
639
+ align-self: flex-end;
640
+ position: relative;
641
+ /* Hide text content visually; ::before draws the glyph mask. */
642
+ font-size: 0;
643
+ line-height: 0;
644
+ overflow: hidden;
645
+ transition: background 0.12s, color 0.12s, transform 0.12s;
646
+ }
647
+ .thread-float-send {
648
+ background: var(--lime, #6FB572);
649
+ color: var(--bg);
650
+ }
651
+ .thread-float-send::before {
652
+ content: "";
653
+ position: absolute;
654
+ inset: 0;
655
+ margin: auto;
656
+ width: 16px;
657
+ height: 16px;
658
+ background-color: currentColor;
659
+ -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><line x1='12' y1='19' x2='12' y2='5'/><polyline points='5 12 12 5 19 12'/></svg>");
660
+ mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><line x1='12' y1='19' x2='12' y2='5'/><polyline points='5 12 12 5 19 12'/></svg>");
661
+ -webkit-mask-repeat: no-repeat;
662
+ mask-repeat: no-repeat;
663
+ -webkit-mask-position: center;
664
+ mask-position: center;
665
+ -webkit-mask-size: 16px 16px;
666
+ mask-size: 16px 16px;
667
+ }
668
+ .thread-float-send:hover { transform: scale(1.04); }
669
+ .thread-float-send:active { transform: scale(0.96); }
670
+ .thread-float-send[disabled] {
671
+ opacity: 0.45;
672
+ cursor: not-allowed;
673
+ transform: none;
674
+ }
675
+
676
+ /* Stop · same circle geometry, amber-tinted so the user reads
677
+ "interrupting an action in flight" rather than "primary send". */
678
+ .thread-float-stop {
679
+ background: color-mix(in srgb, var(--amber, #C99B4F) 22%, var(--panel-3));
680
+ color: var(--amber, #C99B4F);
681
+ }
682
+ .thread-float-stop::before {
683
+ content: "";
684
+ position: absolute;
685
+ inset: 0;
686
+ margin: auto;
687
+ width: 10px;
688
+ height: 10px;
689
+ background-color: currentColor;
690
+ /* Square glyph · universal "stop" indicator. Drawn as a CSS
691
+ square (no SVG mask needed) since the shape is flat. */
692
+ border-radius: 1px;
693
+ }
694
+ .thread-float-stop:hover {
695
+ background: color-mix(in srgb, var(--amber, #C99B4F) 32%, var(--panel-3));
696
+ transform: scale(1.04);
697
+ }
698
+ .thread-float-stop:active { transform: scale(0.96); }
699
+
700
+ /* The legacy glyph span inside the button is no longer needed —
701
+ the ::before mask above paints the icon. Keep the rule so any
702
+ leftover DOM nodes don't render as a stray block. */
703
+ .thread-float-stop-glyph { display: none; }
704
+
705
+ /* Toggle visibility · the composer always carries both buttons in
706
+ the DOM; we flip a state class on the composer to swap them. */
707
+ .thread-float-composer .thread-float-stop { display: none; }
708
+ .thread-float-composer.is-streaming .thread-float-send { display: none; }
709
+ .thread-float-composer.is-streaming .thread-float-stop { display: inline-flex; }
710
+
711
+ /* Head icon · "All threads in this room" trigger. Mirrors the
712
+ masking-via-CSS-variable convention used by .head-add-cast /
713
+ .head-divergence in room-settings.css (the parent
714
+ `.head-icon-btn` rule already masks `--icon` onto a fixed-size
715
+ button shape). The glyph is Lucide MessagesSquare · two chat
716
+ bubbles stacked to read as "multiple private threads". */
717
+ .head-threads {
718
+ --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M14 9a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2z'/><path d='M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1'/></svg>");
719
+ }
720
+ /* Threads stay accessible in adjourned rooms — the user often
721
+ wants to dig into a finished session's perspectives privately
722
+ while the main transcript is now read-only. No `display: none`
723
+ on adjourned status here. */
724
+
725
+ /* Popover anchored under the head-threads icon. Lists every thread
726
+ spawned from this main room with the director's avatar + name +
727
+ last-updated time. Click a row to mount the thread float window.
728
+ Mirrors the `.cast-edit-pop` style (composer-pick rows) so the
729
+ visual vocabulary inside the room head stays one family. */
730
+ .thread-list-pop {
731
+ position: fixed;
732
+ z-index: 9000;
733
+ background: var(--panel);
734
+ border: 0.5px solid var(--line-strong);
735
+ border-radius: 6px;
736
+ width: 320px;
737
+ max-height: 60vh;
738
+ display: flex;
739
+ flex-direction: column;
740
+ box-shadow:
741
+ 0 8px 24px -8px rgba(0, 0, 0, 0.5),
742
+ 0 4px 12px -4px rgba(0, 0, 0, 0.3);
743
+ }
744
+ .thread-list-head {
745
+ padding: 12px 14px 8px;
746
+ border-bottom: 0.5px solid var(--line-bright, rgba(255, 255, 255, 0.18));
747
+ font-family: var(--mono);
748
+ font-size: 10px;
749
+ letter-spacing: 0.18em;
750
+ text-transform: uppercase;
751
+ color: var(--text-faint, rgba(255, 255, 255, 0.5));
752
+ }
753
+ .thread-list-body {
754
+ flex: 1;
755
+ min-height: 0;
756
+ overflow-y: auto;
757
+ padding: 4px 0;
758
+ }
759
+ .thread-list-empty {
760
+ display: flex;
761
+ flex-direction: column;
762
+ align-items: center;
763
+ gap: 12px;
764
+ padding: 32px 18px 28px;
765
+ font-family: var(--mono);
766
+ font-size: 11px;
767
+ color: var(--text-faint, rgba(255, 255, 255, 0.5));
768
+ text-align: center;
769
+ line-height: 1.55;
770
+ }
771
+ .thread-list-empty-icon {
772
+ width: 48px;
773
+ height: 48px;
774
+ display: inline-flex;
775
+ align-items: center;
776
+ justify-content: center;
777
+ background: var(--panel-2);
778
+ border: 0.5px solid var(--line-bright, rgba(255, 255, 255, 0.18));
779
+ border-radius: 50%;
780
+ color: var(--text-faint, rgba(255, 255, 255, 0.4));
781
+ flex-shrink: 0;
782
+ }
783
+ .thread-list-empty-icon svg {
784
+ width: 22px;
785
+ height: 22px;
786
+ }
787
+ /* List rows · positioned as the relative anchor for the hover-
788
+ revealed delete button. Switched off a <button> (which was the
789
+ single click target) to a flex container with two distinct
790
+ click targets — the body and the trash chip. Click handlers in
791
+ app.js look at [data-thread-open] for the row body and
792
+ [data-thread-delete] for the trash. */
793
+ .thread-list-row {
794
+ position: relative;
795
+ display: flex;
796
+ align-items: center;
797
+ gap: 10px;
798
+ padding: 8px 14px;
799
+ background: transparent;
800
+ border: 0;
801
+ width: 100%;
802
+ text-align: left;
803
+ font-family: var(--mono);
804
+ transition: background 0.12s;
805
+ }
806
+ .thread-list-row-body {
807
+ flex: 1;
808
+ display: flex;
809
+ align-items: center;
810
+ gap: 10px;
811
+ background: transparent;
812
+ border: 0;
813
+ padding: 0;
814
+ cursor: pointer;
815
+ min-width: 0;
816
+ text-align: left;
817
+ }
818
+ .thread-list-row:hover {
819
+ background: var(--panel-2);
820
+ }
821
+ .thread-list-row-av {
822
+ width: 28px;
823
+ height: 28px;
824
+ border-radius: 50%;
825
+ flex-shrink: 0;
826
+ background: var(--panel-3);
827
+ }
828
+ .thread-list-row-meta {
829
+ flex: 1;
830
+ display: flex;
831
+ flex-direction: column;
832
+ gap: 2px;
833
+ min-width: 0;
834
+ }
835
+
836
+ /* Delete trash chip · hover-only on its row. Click triggers a
837
+ confirm() dialog · destructive action so we want the extra
838
+ guard rail. */
839
+ .thread-list-row-delete {
840
+ display: inline-flex;
841
+ align-items: center;
842
+ justify-content: center;
843
+ width: 24px;
844
+ height: 24px;
845
+ background: transparent;
846
+ border: 0;
847
+ border-radius: 4px;
848
+ color: var(--text-faint, rgba(255, 255, 255, 0.45));
849
+ cursor: pointer;
850
+ opacity: 0;
851
+ flex-shrink: 0;
852
+ transition: opacity 0.12s, color 0.12s, background 0.12s;
853
+ }
854
+ .thread-list-row-delete svg {
855
+ width: 13px;
856
+ height: 13px;
857
+ pointer-events: none;
858
+ }
859
+ .thread-list-row:hover .thread-list-row-delete,
860
+ .thread-list-row-delete:focus-visible {
861
+ opacity: 1;
862
+ }
863
+ .thread-list-row-delete:hover {
864
+ color: var(--red, #B5706A);
865
+ background: color-mix(in srgb, var(--red, #B5706A) 14%, transparent);
866
+ outline: none;
867
+ }
868
+ .thread-list-row-name {
869
+ font-family: var(--sans, "Inter", system-ui, sans-serif);
870
+ font-size: 14px;
871
+ font-weight: 400;
872
+ color: var(--text);
873
+ white-space: nowrap;
874
+ overflow: hidden;
875
+ text-overflow: ellipsis;
876
+ }
877
+ .thread-list-row-time {
878
+ font-size: 9px;
879
+ letter-spacing: 0.14em;
880
+ text-transform: uppercase;
881
+ color: var(--text-faint, rgba(255, 255, 255, 0.45));
882
+ }
883
+ .thread-list-row-active {
884
+ font-size: 9px;
885
+ letter-spacing: 0.14em;
886
+ text-transform: uppercase;
887
+ color: var(--lime, #6FB572);
888
+ flex-shrink: 0;
889
+ }
890
+
891
+ /* ── Dock bar · holds minimized threads at the bottom-right of
892
+ the viewport ─────────────────────────────────────────────── */
893
+
894
+ .thread-dock {
895
+ position: fixed;
896
+ right: 24px;
897
+ bottom: 24px;
898
+ z-index: 8400;
899
+ display: flex;
900
+ flex-direction: row-reverse;
901
+ gap: 8px;
902
+ pointer-events: none;
903
+ }
904
+ .thread-dock > .thread-float {
905
+ pointer-events: auto;
906
+ }
907
+
908
+ /* ── Thread trigger affordance · per-bubble + head-cast ────────
909
+ Two surfaces, ONE icon vocabulary so the user reads "this opens
910
+ a private 1:1" regardless of which surface they click.
911
+
912
+ Per-bubble · Discord-style message hover toolbar. A floating
913
+ chrome bar pinned to the top-right of the message that appears
914
+ on hover and overlaps the message's upper edge. Icon-only (no
915
+ text label) — Discord uses pure icons for the reaction / reply /
916
+ pin / more row, on the assumption that the action vocabulary is
917
+ small and discoverable on hover. The toolbar's outer chrome is
918
+ the wrapper; the reply icon is one of its slots (future per-
919
+ message actions slot into the same row). */
920
+ /* Toolbar wrapper · Discord-style hover container floating at the
921
+ top-right of the message. Houses one (or more, future) per-message
922
+ actions. Hidden by default, slides in on article hover. */
923
+ .msg-toolbar {
924
+ position: absolute;
925
+ /* Sits at the RIGHT END of the meta row (the timestamp no longer
926
+ pins to the right — see `.msg-time` — so the far-right is free),
927
+ vertically aligned with that line rather than floating above the
928
+ message. The small negative top centres the ~26px chip on the
929
+ ~16px meta line; right:0 puts it flush with the content column's
930
+ right edge. */
931
+ top: -4px;
932
+ right: 0;
933
+ z-index: 3;
934
+ display: inline-flex;
935
+ align-items: center;
936
+ gap: 2px;
937
+ background: var(--panel);
938
+ border: 0.5px solid var(--line-strong, rgba(255, 255, 255, 0.32));
939
+ border-radius: 999px;
940
+ padding: 2px;
941
+ opacity: 0;
942
+ transform: translateY(2px);
943
+ transition: opacity 0.12s ease, transform 0.12s ease;
944
+ box-shadow:
945
+ 0 3px 10px -3px rgba(0, 0, 0, 0.45),
946
+ 0 1px 2px rgba(0, 0, 0, 0.22);
947
+ }
948
+ article.msg:hover .msg-toolbar,
949
+ .msg-toolbar:focus-within {
950
+ opacity: 1;
951
+ transform: translateY(0);
952
+ }
953
+
954
+ /* Reply chip · icon + "Thread" label as a compact rounded pill.
955
+ Tag-style — Slack-like reply affordance with the action name
956
+ spelled out next to the icon so the user reads it as a button,
957
+ not just a glyph. */
958
+ .msg-thread-trigger {
959
+ display: inline-flex;
960
+ align-items: center;
961
+ gap: 4px;
962
+ height: 22px;
963
+ padding: 0 9px 0 7px;
964
+ background: transparent;
965
+ border: 0;
966
+ color: var(--text-soft, rgba(255, 255, 255, 0.68));
967
+ cursor: pointer;
968
+ border-radius: 999px;
969
+ font-family: var(--mono);
970
+ font-size: 10px;
971
+ letter-spacing: 0.06em;
972
+ line-height: 1;
973
+ transition: color 0.12s, background 0.12s;
974
+ }
975
+ .msg-thread-trigger svg {
976
+ width: 12px;
977
+ height: 12px;
978
+ pointer-events: none;
979
+ flex-shrink: 0;
980
+ }
981
+ .msg-thread-trigger span {
982
+ text-transform: uppercase;
983
+ pointer-events: none;
984
+ }
985
+ .msg-thread-trigger:hover,
986
+ .msg-thread-trigger:focus-visible {
987
+ color: var(--lime, #6FB572);
988
+ background: var(--panel-2);
989
+ outline: none;
990
+ }
991
+
992
+ /* Anchor target · the toolbar is `position: absolute`; the .msg
993
+ article needs an explicit positioning context so the toolbar
994
+ pins to the message corner instead of escaping up to the chat
995
+ scroll container. Inert when no toolbar is present (non-director
996
+ bubbles), so safe to apply globally. */
997
+ article.msg {
998
+ position: relative;
999
+ }
1000
+
1001
+ /* Head-cast trigger · the same reply icon as a hover badge on each
1002
+ director avatar in the room header. Click of avatar still opens
1003
+ the agent profile overlay (existing behavior · data-agent), so
1004
+ the badge gets its own click target via the wrapping <span>.
1005
+ Lets users open a thread with a director who hasn't spoken yet
1006
+ — without this, the per-bubble trigger was the only entry and it
1007
+ only existed on directors who'd already taken a turn. */
1008
+ .head-cast-wrap {
1009
+ position: relative;
1010
+ display: inline-block;
1011
+ line-height: 0;
1012
+ }
1013
+ .head-cast-thread {
1014
+ position: absolute;
1015
+ right: -4px;
1016
+ bottom: -4px;
1017
+ width: 16px;
1018
+ height: 16px;
1019
+ display: inline-flex;
1020
+ align-items: center;
1021
+ justify-content: center;
1022
+ background: var(--panel-2);
1023
+ border: 0.5px solid var(--line-strong, rgba(255, 255, 255, 0.32));
1024
+ color: var(--text);
1025
+ border-radius: 50%;
1026
+ cursor: pointer;
1027
+ padding: 0;
1028
+ opacity: 0;
1029
+ transform: scale(0.85);
1030
+ transition: opacity 0.12s, transform 0.12s, color 0.12s, background 0.12s;
1031
+ }
1032
+ .head-cast-thread svg {
1033
+ width: 9px;
1034
+ height: 9px;
1035
+ pointer-events: none;
1036
+ }
1037
+ .head-cast-wrap:hover .head-cast-thread {
1038
+ opacity: 1;
1039
+ transform: scale(1);
1040
+ }
1041
+ .head-cast-thread:hover {
1042
+ color: var(--lime, #6FB572);
1043
+ }
1044
+
1045
+ /* ═══════════════════════════════════════════════════════════════════
1046
+ Docked mode · the thread expands into a 420px right side panel
1047
+ sitting next to the chat-col instead of floating bottom-right.
1048
+ ═══════════════════════════════════════════════════════════════════
1049
+
1050
+ Layout strategy:
1051
+ - `body.has-thread-dock` flips `.main-view[data-main-view="room"]`
1052
+ to a 2-column grid: chat-col on the left (1fr), docked panel on
1053
+ the right (420px fixed).
1054
+ - The thread element gets `.is-docked` which (a) resets its
1055
+ fixed-positioning anchors so it lays out as a grid child, and
1056
+ (b) flattens its border-radius / shadow so it reads as part of
1057
+ the room chrome, not a floating overlay.
1058
+ - Drag handles, resize edges, and the minimize button are
1059
+ `display: none` in docked mode — none of them make sense for a
1060
+ full-height side panel.
1061
+ - Below 1200px viewport width the dock falls back to floating
1062
+ (the chat-col gets too narrow otherwise). The JS guard
1063
+ `_canDockHere()` already prevents NEW docks at narrow widths;
1064
+ this CSS catches the case where a docked thread is open and
1065
+ the window is resized down.
1066
+ ═══════════════════════════════════════════════════════════════════ */
1067
+
1068
+ body.has-thread-dock .main-view[data-main-view="room"] {
1069
+ display: grid;
1070
+ /* Width is variable-driven · `--thread-dock-w` is set inline by
1071
+ the resize handler on body, defaulting to 360px. Min/max clamp
1072
+ guards against the user dragging into uncomfortable extremes:
1073
+ the chat-col (1fr) would otherwise compress below the input-bar
1074
+ content width or the dock would balloon past two-thirds of the
1075
+ viewport. */
1076
+ grid-template-columns: 1fr clamp(320px, var(--thread-dock-w, 360px), 720px);
1077
+ grid-template-rows: 1fr;
1078
+ }
1079
+
1080
+ .thread-float.is-docked {
1081
+ /* `position: relative` (not static) so the absolute-positioned
1082
+ left-edge resize handle, head, messages, and composer all
1083
+ anchor to the panel itself. */
1084
+ position: relative;
1085
+ right: auto;
1086
+ bottom: auto;
1087
+ width: 100%;
1088
+ height: 100%;
1089
+ box-sizing: border-box;
1090
+ max-height: none;
1091
+ min-height: 0;
1092
+ border: 0;
1093
+ border-left: 0.5px solid var(--line-bright);
1094
+ border-radius: 0;
1095
+ box-shadow: none;
1096
+ z-index: auto;
1097
+ transform: none !important;
1098
+ /* The left-edge resize handle sits at `left: -3px` (straddling
1099
+ the chat ↔ panel seam). The float-mode `overflow: hidden`
1100
+ would otherwise clip it; allow overflow on the docked variant
1101
+ since the rounded-corner scrollbar concern doesn't apply when
1102
+ the panel has flat edges. */
1103
+ overflow: visible;
1104
+ }
1105
+ /* Docked · header drops below the room's own .room-head strip so
1106
+ the room's icons (// Threads / divergence / etc on the right
1107
+ side of head-actions) stay reachable. The room-head is
1108
+ `position: absolute; top: 0` on the main-view at z-index 40 and
1109
+ spans full main-view width; the thread head needs to start
1110
+ BELOW it instead of overlapping. */
1111
+ .thread-float.is-docked .thread-float-head {
1112
+ cursor: default;
1113
+ border-radius: 0;
1114
+ top: var(--room-head-h, 56px);
1115
+ }
1116
+ /* Docked messages list · clear BOTH room-head and the thread's
1117
+ own head before content starts. */
1118
+ .thread-float.is-docked .thread-float-messages {
1119
+ padding-top: calc(var(--room-head-h, 56px) + var(--thread-head-h, 50px) + 14px);
1120
+ }
1121
+ /* Hide affordances that have no docked-mode meaning */
1122
+ .thread-float.is-docked .thread-float-resize,
1123
+ .thread-float.is-docked [data-thread-minimize] {
1124
+ display: none !important;
1125
+ }
1126
+
1127
+ /* Left-edge resize handle · only visible in docked mode. A thin
1128
+ vertical strip the user can drag to widen / narrow the side
1129
+ panel. The handle sits ON the panel's left border (slightly
1130
+ negative left offset so it straddles the seam between chat-col
1131
+ and dock panel), with `col-resize` cursor. The visible track
1132
+ only blooms on hover so the resting state stays clean. JS in
1133
+ `_wireThreadDockResize` reads pointer-x and writes
1134
+ `--thread-dock-w` on body. */
1135
+ .thread-dock-resize-handle {
1136
+ position: absolute;
1137
+ top: 0;
1138
+ bottom: 0;
1139
+ left: -3px;
1140
+ width: 6px;
1141
+ cursor: col-resize;
1142
+ z-index: 3;
1143
+ display: none;
1144
+ user-select: none;
1145
+ }
1146
+ .thread-dock-resize-handle::after {
1147
+ content: "";
1148
+ position: absolute;
1149
+ top: 50%;
1150
+ left: 50%;
1151
+ transform: translate(-50%, -50%);
1152
+ width: 2px;
1153
+ height: 32px;
1154
+ background: var(--line-strong);
1155
+ border-radius: 1px;
1156
+ opacity: 0;
1157
+ transition: opacity 0.12s, background 0.12s;
1158
+ }
1159
+ .thread-dock-resize-handle:hover::after,
1160
+ body.is-thread-dock-resizing .thread-dock-resize-handle::after {
1161
+ opacity: 0.6;
1162
+ }
1163
+ body.is-thread-dock-resizing .thread-dock-resize-handle::after {
1164
+ background: var(--lime, #6FB572);
1165
+ }
1166
+ /* Only the docked variant shows the handle. The element stays in
1167
+ the DOM in floating mode (so we don't have to re-render the
1168
+ thread on every maximize toggle) — just keep it `display:
1169
+ none` until the panel docks. */
1170
+ .thread-float.is-docked .thread-dock-resize-handle { display: block; }
1171
+ body.is-thread-dock-resizing { cursor: col-resize; user-select: none; }
1172
+ body.is-thread-dock-resizing .thread-float.is-docked,
1173
+ body.is-thread-dock-resizing .chat-col {
1174
+ /* Suppress text selection across both columns while dragging so
1175
+ the user doesn't accidentally highlight messages. */
1176
+ user-select: none;
1177
+ }
1178
+
1179
+ /* Narrow-viewport fallback · the 420px side panel + chat-col
1180
+ becomes unreadable below 1200px (chat-col gets ~< 600px which
1181
+ eats the input-bar's 960px max-width content rhythm). Force the
1182
+ docked panel back to floating geometry so the chat keeps its
1183
+ full real estate. The grid override on the room view also
1184
+ reverts here so the layout doesn't fight the float positioning. */
1185
+ @media (max-width: 1200px) {
1186
+ body.has-thread-dock .main-view[data-main-view="room"] {
1187
+ display: grid;
1188
+ grid-template-columns: 1fr;
1189
+ grid-template-rows: 1fr;
1190
+ }
1191
+ .thread-float.is-docked {
1192
+ position: fixed;
1193
+ right: 24px;
1194
+ bottom: 24px;
1195
+ width: 420px;
1196
+ height: 520px;
1197
+ max-height: calc(100vh - 96px);
1198
+ border: 0.5px solid var(--line-strong);
1199
+ border-radius: 6px;
1200
+ box-shadow:
1201
+ 0 8px 24px -8px rgba(0, 0, 0, 0.45),
1202
+ 0 4px 12px -4px rgba(0, 0, 0, 0.3);
1203
+ }
1204
+ .thread-float.is-docked .thread-float-head {
1205
+ cursor: grab;
1206
+ border-radius: 6px 6px 0 0;
1207
+ }
1208
+ /* Resize handle has no purpose when the dock is fixed-positioned
1209
+ (it would drag a non-existent grid column). Hide. */
1210
+ .thread-float.is-docked .thread-dock-resize-handle { display: none; }
1211
+ }