privateboard 0.1.32 → 0.1.37

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.
package/public/index.html CHANGED
@@ -28,6 +28,11 @@
28
28
  --panel-2: #1A1A18;
29
29
  --panel-3: #21211E;
30
30
  --hi: #2A2A26;
31
+ /* Sidebar chrome surface · used by `.sidebar`, `.sessions-scroll`,
32
+ `.agents-scroll`, the sticky `.section-header`, and the
33
+ `.sidebar-foot::before` scroll-fade. One token so the whole
34
+ sidebar column moves together. */
35
+ --sidebar-bg: #191918;
31
36
 
32
37
  --line: #26241F;
33
38
  --line-bright: #3A3934;
@@ -64,6 +69,9 @@
64
69
  system-ui, sans-serif;
65
70
 
66
71
  --sidebar-w: 280px;
72
+ /* Collapsed icon-rail width · matches the ChatGPT folded sidebar.
73
+ Wide enough for a centered 32px hit target with breathing room. */
74
+ --mini-sidebar-w: 56px;
67
75
  }
68
76
 
69
77
  /* Logo stays on Inter so the brandmark reads consistently
@@ -484,7 +492,12 @@
484
492
  SIDEBAR
485
493
  ═══════════════════════════════════════════ */
486
494
  .sidebar {
487
- background: var(--panel);
495
+ /* Sidebar runs on its own token `--sidebar-bg` (#191918) so the
496
+ column can be tuned independently of `--panel` / `--panel-2`.
497
+ Every inner surface that needs to match (`.sessions-scroll`,
498
+ `.agents-scroll`, sticky `.section-header`, `.sidebar-foot`
499
+ fade) reads the same token. */
500
+ background: var(--sidebar-bg);
488
501
  /* Right edge owns the region divider · brighter than the other
489
502
  three sides (--line-bright vs --line) so the sidebar reads as
490
503
  visually distinct from the main column. Pairs with
@@ -505,12 +518,10 @@
505
518
  the 22px collapse-button's box; after the button was shrunk to a
506
519
  16px icon, the row would collapse 6px shorter without this lock. */
507
520
  min-height: 38px;
508
- border-bottom: 0.5px solid var(--line-bright);
509
521
  display: flex;
510
522
  justify-content: space-between;
511
523
  align-items: center;
512
524
  gap: 10px;
513
- background: var(--panel-2);
514
525
  }
515
526
  /* Lock-up · brand mark + wordmark in the slot the prior
516
527
  "// CONTROL" title held. Anchored as a link so a click on the
@@ -662,14 +673,6 @@
662
673
  html.is-electron-mac .room-head .head-cast img {
663
674
  -webkit-app-region: no-drag;
664
675
  }
665
- /* The expand-btn's ::after halo paints OUTSIDE the button rect
666
- (inset: -16px), so it doesn't inherit the button's no-drag and
667
- gets eaten by `.room-head { drag }` above. Stamp no-drag on the
668
- pseudo directly so the 48×48 hit zone keeps registering clicks.
669
- (Same shape as the sidebar-collapse-btn::after override.) */
670
- html.is-electron-mac .room-head .room-head-expand::after {
671
- -webkit-app-region: no-drag;
672
- }
673
676
  /* ─── macOS Electron · top-strip drag coverage for non-room views.
674
677
  When the room view is hidden (All Reports / All Notes / Search /
675
678
  Agent Profile) or the room view is shown with no current room (the
@@ -687,21 +690,16 @@
687
690
  agent-profile card (agent-profile.js). No interactive descendants,
688
691
  so the no-drag exception below mostly no-ops for it.
689
692
 
690
- `.main-view[data-main-view="search"]` · the search view has no
691
- native page-head element. The pseudo `::before` becomes a sticky
692
- 36px drag overlay at the top of the scrolling container; the
693
- `.search-page` gets matching top padding (only in electron) so
694
- content sits below the strip instead of underneath it. */
693
+ (Search no longer has a main-view of its own it lives as a
694
+ floating overlay since the v0.1.33 redesign so no drag-strip
695
+ equivalent is needed.) */
695
696
  /* `position: absolute` so the 36px drag strip floats above
696
- `.chat-col` without taking a grid-row of vertical space. Without
697
- this, the new-room / new-agent composer (and its `.cmp-bg-deco`
698
- pixel backdrop, anchored to `.chat-col` top) were both pushed down
699
- by 36px — the deco's top band ended up clipped below the visible
700
- area. The strip stays transparent (`background: transparent`) so
701
- the deco shows through, and `pointer-events: none` lets clicks
702
- fall through to the composer below — the drag region itself
703
- activates via `-webkit-app-region: drag` which Electron handles
704
- before the renderer's hit-testing. */
697
+ `.chat-col` without taking a grid-row of vertical space. The
698
+ strip stays transparent (`background: transparent`) and
699
+ `pointer-events: none` lets clicks fall through to the composer
700
+ below — the drag region itself activates via
701
+ `-webkit-app-region: drag` which Electron handles before the
702
+ renderer's hit-testing. */
705
703
  html.is-electron-mac.no-room .room-head:empty {
706
704
  display: block;
707
705
  position: absolute;
@@ -739,24 +737,6 @@
739
737
  html.is-electron-mac .notes-page-head [contenteditable="true"] {
740
738
  -webkit-app-region: no-drag;
741
739
  }
742
- html.is-electron-mac .main-view[data-main-view="search"] {
743
- position: relative;
744
- }
745
- html.is-electron-mac .main-view[data-main-view="search"]::before {
746
- content: "";
747
- position: sticky;
748
- top: 0;
749
- display: block;
750
- width: 100%;
751
- height: 36px;
752
- margin-bottom: -36px;
753
- background: transparent;
754
- -webkit-app-region: drag;
755
- z-index: 50;
756
- }
757
- html.is-electron-mac .main-view[data-main-view="search"] .search-page {
758
- padding-top: 36px;
759
- }
760
740
  /* When the sidebar is floating as a peek overlay, every drag region
761
741
  declared above for the main-view's top strip MUST be suppressed.
762
742
  drag regions are computed as bounding rectangles regardless of
@@ -792,9 +772,6 @@
792
772
  html.is-electron-mac body.sidebar-peek .room-head:empty {
793
773
  -webkit-app-region: no-drag;
794
774
  }
795
- html.is-electron-mac body.sidebar-peek .main-view[data-main-view="search"]::before {
796
- -webkit-app-region: no-drag;
797
- }
798
775
  /* macOS Electron · round the four outer corners of the entire
799
776
  `.body-grid` (sidebar + 8px resizer + main as one rectangle), not
800
777
  each panel individually. The grid already has `overflow: hidden`
@@ -819,6 +796,22 @@
819
796
  `enter-full-screen` / `leave-full-screen` events. */
820
797
  html.is-fullscreen .control { padding: 0; }
821
798
  html.is-fullscreen .body-grid { border-radius: 0; }
799
+ /* macOS fullscreen safe-area · the system menu bar (incl. the
800
+ traffic-light cluster + the macOS screen-recording indicator)
801
+ slides down over the top ~28 px of the window when the cursor
802
+ enters that zone. In normal windowed mode the menu bar lives
803
+ above the window and isn't a concern, but in fullscreen our
804
+ content extends edge-to-edge so anything we paint in the top
805
+ 28 px gets obscured the moment the user hovers up to access
806
+ traffic lights — and the things they're trying to access
807
+ (window close / minimize, the head-record button at the
808
+ right) sit precisely there. Push the .room-head contents
809
+ down by 28 px in fullscreen so the action buttons clear the
810
+ reveal zone. The .room-head itself still starts at top: 0
811
+ (drag region for window-drag), only its inner content shifts. */
812
+ html.is-fullscreen.is-electron-mac .room-head {
813
+ padding-top: 28px;
814
+ }
822
815
  /* macOS Electron · let the BrowserWindow's `vibrancy: "under-window"`
823
816
  layer show through. The vibrancy lives behind the renderer, so
824
817
  anything painted with a fully opaque background covers it. We
@@ -841,22 +834,28 @@
841
834
  room. Single column = main fills the whole body-grid cleanly.
842
835
  The user's preferred --sidebar-w stays in localStorage so
843
836
  expanding restores the same width. */
837
+ /* Collapsed · the full sidebar + resizer drop out and a fixed-width
838
+ mini icon rail takes column 1. Source order [sidebar, resizer,
839
+ mini, main] + both hidden siblings → grid auto-places mini in
840
+ col 1, main in col 2. */
844
841
  body.sidebar-collapsed .body-grid {
845
- grid-template-columns: 1fr !important;
842
+ grid-template-columns: var(--mini-sidebar-w) 1fr !important;
846
843
  }
847
844
  body.sidebar-collapsed .sidebar,
848
845
  body.sidebar-collapsed .col-resizer {
849
846
  display: none;
850
847
  }
848
+ body.sidebar-collapsed .mini-sidebar {
849
+ display: flex;
850
+ }
851
851
  /* ─── Hover peek · floating sidebar while collapsed ───
852
- When the user hovers the collapsed-state trigger (the floating
853
- `.sidebar-expand-btn` or the in-header `.room-head-expand`), JS
854
- adds `.sidebar-peek` to body. The sidebar reappears as a floating
855
- overlay anchored to the body-grid's top-left edge so it sits ABOVE
856
- the chat content without re-flowing the grid. Mouseleave (with
857
- short grace) drops the class and the sidebar disappears again.
858
- This is purely an addition to the collapsed state — the column
859
- never reclaims its grid track. */
852
+ When the cursor reaches the viewport's left edge (over the mini
853
+ rail), JS adds `.sidebar-peek` to body. The full sidebar reappears
854
+ as a floating overlay anchored to the body-grid's top-left edge so
855
+ it sits ABOVE the chat content (and the mini rail) without
856
+ re-flowing the grid. Mouseleave drops the class and the float
857
+ disappears again, leaving the mini rail. This is purely an addition
858
+ to the collapsed state — the column never reclaims its grid track. */
860
859
  body.sidebar-collapsed.sidebar-peek .sidebar {
861
860
  display: flex;
862
861
  position: absolute;
@@ -892,10 +891,10 @@
892
891
  glass with hairline border, lime on hover. z-index sits above
893
892
  chat content but below modal overlays (which use 1500+). */
894
893
  /* Round chip · fold glyph centred in a 28px circle. `--bg` fill
895
- (white on light, near-black on dark) masks the cmp-bg-deco
896
- constellation behind it; `--lime-dim` ring anchors it to the
897
- product's accent. Hover intensifies both ring + icon to full
898
- `--lime` for an obvious interactive state. */
894
+ (white on light, near-black on dark) sits against the composer
895
+ panel; `--lime-dim` ring anchors it to the product's accent.
896
+ Hover intensifies both ring + icon to full `--lime` for an
897
+ obvious interactive state. */
899
898
  .sidebar-expand-btn {
900
899
  display: none;
901
900
  position: absolute;
@@ -915,12 +914,14 @@
915
914
  appearance: none;
916
915
  padding: 0;
917
916
  }
918
- /* Show floating button only when sidebar is collapsed AND we're
919
- in the empty / no-room state. When a room is loaded, the
920
- in-header `.room-head-expand` button takes over instead, so
921
- the icon doesn't sit on top of the room title. */
917
+ /* Retired · the persistent mini icon rail now owns the collapsed
918
+ state, and its logo button expands. The old floating round chip
919
+ would sit on top of the rail's top icon (both pin to the body-grid
920
+ top-left), so it stays hidden in every state. Kept in the DOM (and
921
+ this rule kept as `display: none`) so the edge-peek mousemove guard
922
+ that references `[data-sidebar-expand]` still resolves cleanly. */
922
923
  html.no-room body.sidebar-collapsed .sidebar-expand-btn {
923
- display: inline-flex;
924
+ display: none;
924
925
  }
925
926
  .sidebar-expand-btn:hover {
926
927
  color: var(--lime);
@@ -963,110 +964,310 @@
963
964
  -webkit-app-region: no-drag;
964
965
  }
965
966
 
966
- /* ─── In-header expand button ───
967
- Lives at the leading edge of `.room-head` (rendered by
968
- renderHeader() in app.js). Visual twin of `.sidebar-collapse-btn`
969
- so the collapse/expand pair reads as the SAME control across
970
- the two places it can appear (in-sidebar when open · in-header
971
- when sidebar collapsed AND a room is loaded). Icon-only · no
972
- chrome · same Lucide PanelLeft mask · same hover-lime cascade.
973
- Always rendered in the mirrored variant (panel folded to the
974
- right edge) since this button only ever appears in the collapsed
975
- state · clicking it expands the sidebar back. */
976
- .room-head-expand {
977
- appearance: none;
967
+ /* ─── Mini (collapsed) sidebar · ChatGPT-style icon rail ───
968
+ A persistent narrow rail shown while collapsed. `display: none`
969
+ by default; flipped to `flex` by the collapsed-state rule above.
970
+ Shares the sidebar chrome surface (`--sidebar-bg`) + a hairline
971
+ right edge so it reads as the same column the full sidebar used. */
972
+ .mini-sidebar {
973
+ display: none;
974
+ flex-direction: column;
975
+ align-items: center;
976
+ width: var(--mini-sidebar-w);
977
+ height: 100%;
978
+ min-height: 0;
979
+ background: var(--sidebar-bg);
980
+ border-right: 1px solid var(--line);
981
+ padding: 10px 0;
982
+ /* `visible` (not `hidden`) so the per-icon hover tooltips can
983
+ escape the 56px rail and pop out to the right. The icon set is
984
+ short enough that it never overflows the rail vertically, so
985
+ there's nothing to clip anyway. The enclosing `.body-grid`
986
+ still bounds everything. */
987
+ overflow: visible;
988
+ }
989
+ .mini-top {
990
+ display: flex;
991
+ flex-direction: column;
992
+ align-items: center;
993
+ gap: 4px;
994
+ flex: 1 1 auto;
995
+ min-height: 0;
996
+ width: 100%;
997
+ }
998
+ .mini-foot {
999
+ display: flex;
1000
+ flex-direction: column;
1001
+ align-items: center;
1002
+ flex: 0 0 auto;
1003
+ padding-top: 8px;
1004
+ }
1005
+ /* Icon button · 32×32 centered hit target. Stroke icons ride
1006
+ currentColor (text-soft idle → text on hover) via the shared
1007
+ mask-image vocabulary, matching the full sidebar's nav glyphs. */
1008
+ .mini-btn {
1009
+ position: relative;
1010
+ display: flex;
1011
+ align-items: center;
1012
+ justify-content: center;
1013
+ width: 32px;
1014
+ height: 32px;
1015
+ padding: 0;
1016
+ border: none;
978
1017
  background: transparent;
979
- border: 0;
980
1018
  color: var(--text-soft);
1019
+ border-radius: 8px;
981
1020
  cursor: pointer;
982
- width: 16px;
983
- height: 16px;
984
- padding: 0;
1021
+ text-decoration: none;
985
1022
  flex-shrink: 0;
986
- transition: color 0.12s;
987
- /* position: relative anchors the ::after hit-area expander so
988
- the cursor doesn't have to land precisely on the 16px icon. */
989
- position: relative;
990
- display: none;
1023
+ transition: background 0.12s, color 0.12s;
1024
+ appearance: none;
991
1025
  }
992
- .room-head-expand::before {
1026
+ /* Hover tooltip · CSS-only via ::after + data-tip, mirroring the
1027
+ `.ib-action::after` pattern. Pops to the RIGHT of the icon (the
1028
+ rail hugs the window's left edge, so above/left would clip) after
1029
+ ~300ms dwell. data-tip is populated from data-i18n-tip by i18n.js,
1030
+ with a static English fallback for the pre-i18n frame. */
1031
+ .mini-btn[data-tip]::after {
1032
+ content: attr(data-tip);
1033
+ position: absolute;
1034
+ left: calc(100% + 10px);
1035
+ top: 50%;
1036
+ transform: translateY(-50%) translateX(-3px);
1037
+ background: var(--panel-2);
1038
+ border: 0.5px solid var(--line-strong);
1039
+ padding: 5px 9px;
1040
+ font-family: var(--mono);
1041
+ font-size: 10px;
1042
+ letter-spacing: 0.04em;
1043
+ color: var(--text);
1044
+ white-space: nowrap;
1045
+ pointer-events: none;
1046
+ opacity: 0;
1047
+ visibility: hidden;
1048
+ box-shadow: 0 4px 14px -6px rgba(0, 0, 0, 0.55);
1049
+ transition: opacity 0.14s ease, transform 0.14s ease, visibility 0s linear 0.18s;
1050
+ z-index: 250;
1051
+ }
1052
+ .mini-btn[data-tip]:hover::after,
1053
+ .mini-btn[data-tip]:focus-visible::after {
1054
+ opacity: 1;
1055
+ visibility: visible;
1056
+ transform: translateY(-50%) translateX(0);
1057
+ transition: opacity 0.14s ease 0.3s, transform 0.14s ease 0.3s, visibility 0s linear 0.3s;
1058
+ }
1059
+ .mini-btn:hover {
1060
+ background: var(--panel-2);
1061
+ color: var(--text);
1062
+ }
1063
+ /* Selected state · the icon for the current destination (reports /
1064
+ notes / new-room / new-agent via shared trigger attrs, rooms /
1065
+ agents via setMiniRailContext). Settled `--panel-3` chip + full
1066
+ --text glyph, matching the full sidebar's `.new-btn.active`
1067
+ language (background-only, no accent — per project CSS rules).
1068
+ Placed after :hover so an active icon keeps its chip on hover
1069
+ instead of dropping to the lighter hover tone. */
1070
+ .mini-btn.active,
1071
+ .mini-btn.active:hover {
1072
+ background: var(--panel-3);
1073
+ color: var(--text);
1074
+ }
1075
+ /* Glyph · same 18px Lucide mask the full sidebar nav buttons use.
1076
+ `--icon` is supplied per-button by the shared rules below (data-
1077
+ attribute scoped so both the .new-btn and .mini-btn carriers pick
1078
+ it up from one definition). */
1079
+ .mini-btn::before {
993
1080
  content: "";
994
- display: block;
995
- width: 16px;
996
- height: 16px;
1081
+ width: 18px;
1082
+ height: 18px;
1083
+ flex-shrink: 0;
997
1084
  background-color: currentColor;
998
- -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' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='18' height='18' rx='2'/><path d='M9 3v18'/><path d='M5 8h2'/><path d='M5 12h2'/><path d='M5 16h2'/></svg>");
999
- 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' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='18' height='18' rx='2'/><path d='M9 3v18'/><path d='M5 8h2'/><path d='M5 12h2'/><path d='M5 16h2'/></svg>");
1085
+ -webkit-mask-image: var(--icon, none);
1086
+ mask-image: var(--icon, none);
1000
1087
  -webkit-mask-repeat: no-repeat;
1001
1088
  mask-repeat: no-repeat;
1002
1089
  -webkit-mask-position: center;
1003
1090
  mask-position: center;
1004
- -webkit-mask-size: 16px 16px;
1005
- mask-size: 16px 16px;
1006
- /* Same scaleX(-1) the sidebar-collapse-btn uses when
1007
- body.sidebar-collapsed · since this button is ONLY visible
1008
- in the collapsed state, the flip is unconditional here. */
1009
- transform: scaleX(-1);
1091
+ -webkit-mask-size: 18px 18px;
1092
+ mask-size: 18px 18px;
1093
+ transition: color 0.12s;
1010
1094
  }
1011
- /* Invisible hit-area expander · matches `.sidebar-collapse-btn::after`
1012
- so the click rectangle grows to 48×48 around the 16px icon. */
1013
- .room-head-expand::after {
1095
+ /* Tab + user buttons render real children (.ic / avatar), not a mask
1096
+ glyph. Suppress the shared ::before without an `--icon` its
1097
+ `currentColor` fill would paint a solid square block next to the
1098
+ real content. (The logo keeps its ::before for the hover fold-icon
1099
+ below.) */
1100
+ .mini-tab::before,
1101
+ .mini-user::before { content: none; }
1102
+ .mini-tab .ic { width: 18px; height: 18px; }
1103
+ /* Logo → fold glyph on hover · telegraphs "click to expand" using the
1104
+ same Lucide PanelLeft + 3-rows icon as the in-sidebar collapse
1105
+ button. At rest the brand mark shows; on hover it fades out and the
1106
+ fold glyph (overlaid, currentColor) fades in. The explicit fold
1107
+ mask here also overrides the generic .mini-btn::before, which —
1108
+ lacking an --icon — would otherwise paint a solid square. */
1109
+ .mini-logo { position: relative; }
1110
+ /* The chair avatar is a two-layer stack: open-eye frame always
1111
+ visible, closed-eye frame flashed in by the blink keyframe. The
1112
+ wrapper carries an occasional idle hop. */
1113
+ .mini-logo-av {
1114
+ position: relative;
1115
+ width: 26px;
1116
+ height: 26px;
1117
+ transition: opacity 0.12s;
1118
+ animation: chair-hop 7s ease-in-out infinite;
1119
+ }
1120
+ .mini-logo-av img {
1121
+ position: absolute;
1122
+ inset: 0;
1123
+ width: 100%;
1124
+ height: 100%;
1125
+ image-rendering: pixelated;
1126
+ image-rendering: crisp-edges;
1127
+ }
1128
+ /* Closed-eye frame · hidden at rest, briefly opaque on the blink
1129
+ keyframe. A different cycle length from the hop keeps the two from
1130
+ locking into a robotic sync. */
1131
+ .mini-logo-av .cl-blink {
1132
+ opacity: 0;
1133
+ animation: chair-blink 5.7s ease-in-out infinite;
1134
+ }
1135
+ .mini-logo:hover .mini-logo-av { opacity: 0; }
1136
+ /* Occasional double-bounce hop · still ~85% of the cycle, then a
1137
+ quick two-step hop so the chair reads as "alive" without being
1138
+ busy. */
1139
+ @keyframes chair-hop {
1140
+ 0%, 60%, 100% { transform: translateY(0); }
1141
+ 66% { transform: translateY(-4px); }
1142
+ 72% { transform: translateY(0); }
1143
+ 77% { transform: translateY(-2px); }
1144
+ 82% { transform: translateY(0); }
1145
+ }
1146
+ /* Single crisp blink near the end of the cycle (~170ms closed). */
1147
+ @keyframes chair-blink {
1148
+ 0%, 93.5%, 100% { opacity: 0; }
1149
+ 94%, 96.5% { opacity: 1; }
1150
+ 97% { opacity: 0; }
1151
+ }
1152
+ /* Respect reduced-motion · hold the open-eye frame, no hop / blink. */
1153
+ @media (prefers-reduced-motion: reduce) {
1154
+ .mini-logo-av,
1155
+ .mini-logo-av .cl-blink { animation: none; }
1156
+ }
1157
+ .mini-logo::before {
1014
1158
  content: "";
1015
1159
  position: absolute;
1016
- inset: -16px;
1160
+ top: 50%;
1161
+ left: 50%;
1162
+ /* scaleX(-1) mirrors the fold so the rows flip to the right edge —
1163
+ the same direction the in-sidebar collapse button uses in its
1164
+ collapsed state to telegraph "the panel expands out from here". */
1165
+ transform: translate(-50%, -50%) scaleX(-1);
1166
+ -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' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='18' height='18' rx='2'/><path d='M9 3v18'/><path d='M5 8h2'/><path d='M5 12h2'/><path d='M5 16h2'/></svg>");
1167
+ 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' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='18' height='18' rx='2'/><path d='M9 3v18'/><path d='M5 8h2'/><path d='M5 12h2'/><path d='M5 16h2'/></svg>");
1168
+ opacity: 0;
1169
+ transition: opacity 0.12s;
1017
1170
  }
1018
- body.sidebar-collapsed .room-head-expand {
1019
- display: block;
1020
- /* Extra 5px breathing room between the icon and the title cluster
1021
- on top of the room-head's own 12px column gap · the icon sat
1022
- too close to the kicker / subject otherwise. Scoped to the
1023
- collapsed state since the button itself only exists then. */
1024
- margin-right: 5px;
1171
+ .mini-logo:hover::before { opacity: 1; }
1172
+ /* Divider between the action group and the rooms/agents switchers. */
1173
+ .mini-sep {
1174
+ width: 20px;
1175
+ height: 1px;
1176
+ background: var(--line-bright);
1177
+ margin: 6px 0;
1178
+ flex-shrink: 0;
1179
+ }
1180
+ .mini-user { width: 32px; height: 32px; }
1181
+ .mini-user .mini-user-av { width: 26px; height: 26px; }
1182
+ .mini-user:hover { background: var(--panel-2); }
1183
+
1184
+ /* macOS Electron · clicks must reach the rail's buttons (the window
1185
+ drag region otherwise swallows them), and the traffic-light cluster
1186
+ is hidden while collapsed (see syncElectronTrafficLights), so the
1187
+ logo can sit near the top with normal padding. */
1188
+ html.is-electron-mac .mini-sidebar {
1189
+ -webkit-app-region: no-drag;
1025
1190
  }
1026
- .room-head-expand:hover { color: var(--lime); }
1027
1191
 
1028
- /* ─── Sidebar tabs (Rooms / Agents) — segmented control with icon ─── */
1192
+ /* ─── Sidebar tabs (Rooms / Agents) — bar-style nav ───
1193
+ Reference: `public/icons/bar.png`. Two layers:
1194
+ 1. Outer "bar" container · its OWN background, sits inset from
1195
+ the sidebar edges, with a small-radius rounded rectangle
1196
+ frame. NO inner padding — the active tab fills the bar
1197
+ edge-to-edge so the hairline border IS the divider, not an
1198
+ inset gap.
1199
+ 2. ACTIVE tab · rounded rectangle, hairline border, brighter
1200
+ bg, shows icon + label. Height matches the container's so
1201
+ there's zero gap between them on top / bottom.
1202
+ 3. INACTIVE tabs · icon-only, sit transparently on the bar with
1203
+ generous breathing room around the glyph. */
1029
1204
  .sidebar-tabs {
1030
1205
  display: flex;
1031
- gap: 2px;
1032
- padding: 6px 6px 8px;
1206
+ align-items: stretch; /* tabs fill container height edge-to-edge */
1207
+ /* No justify-content · `space-between` glued the inactive tab to
1208
+ the far right which doesn't match the reference. Instead the
1209
+ active tab takes its DOM position (left edge) and the inactive
1210
+ tab uses auto margins (below) to float centred in the leftover
1211
+ space — mirrors bar.png where the "Chat" pill hugs the left
1212
+ edge and the icon-only siblings sit in the bar's middle/right
1213
+ area with empty space on both sides. */
1214
+ margin: 5px 10px 10px;
1215
+ /* No inner padding · the active tab's hairline border is flush
1216
+ with the container's edge, matching the reference. */
1217
+ padding: 0;
1033
1218
  flex-shrink: 0;
1034
- background: var(--panel);
1219
+ background: var(--panel-2);
1220
+ border-radius: 10px;
1035
1221
  }
1036
1222
  .sidebar-tab {
1037
- flex: 1;
1038
- padding: 6px 8px;
1223
+ /* Equal-width slots · bar.png's three-item distribution adapted
1224
+ for our 2-tab case. Each tab occupies 50% of the container's
1225
+ inner width so the two halves stay visually balanced regardless
1226
+ of which side carries the pill chrome. Without this the active
1227
+ was content-sized + the inactive icon-sized → wildly mismatched
1228
+ slot widths that read as a layout bug. */
1229
+ flex: 1 1 0;
1230
+ padding: 0 14px;
1231
+ min-height: 30px;
1039
1232
  display: inline-flex;
1040
1233
  align-items: center;
1041
1234
  justify-content: center;
1042
- gap: 6px;
1235
+ gap: 7px;
1043
1236
  font-family: var(--sans);
1044
- font-size: 12px;
1237
+ font-size: 13px;
1045
1238
  font-weight: 500;
1046
1239
  color: var(--text-dim);
1047
1240
  cursor: pointer;
1048
1241
  text-decoration: none;
1049
- transition: background 0.15s, color 0.15s;
1242
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
1050
1243
  letter-spacing: -0.005em;
1051
- border-radius: 4px;
1244
+ border-radius: 10px;
1245
+ /* Transparent placeholder border keeps inactive + active tabs the
1246
+ same outer height so toggling doesn't shift the row by 1px. */
1247
+ border: 1px solid transparent;
1248
+ background: transparent;
1052
1249
  }
1053
1250
  .sidebar-tab .ico {
1054
- width: 14px;
1055
- height: 14px;
1251
+ width: 15px;
1252
+ height: 15px;
1056
1253
  flex-shrink: 0;
1057
1254
  color: currentColor;
1058
- opacity: 0.85;
1255
+ opacity: 0.9;
1059
1256
  }
1257
+ /* Label hidden by default; revealed only on the active tab. Keeps the
1258
+ `<span data-i18n>` in the DOM for translation + screen readers. */
1259
+ .sidebar-tab > span { display: none; }
1060
1260
  .sidebar-tab:hover {
1061
- background: var(--panel-2);
1062
1261
  color: var(--text-soft);
1063
1262
  }
1064
1263
  .sidebar-tab.active {
1065
- background: var(--panel-3);
1066
1264
  color: var(--text);
1067
1265
  font-weight: 600;
1266
+ background: var(--panel-3);
1267
+ border-color: var(--line-bright);
1068
1268
  }
1069
1269
  .sidebar-tab.active .ico { opacity: 1; }
1270
+ .sidebar-tab.active > span { display: inline; }
1070
1271
 
1071
1272
  /* Sidebar tab panels */
1072
1273
  .sidebar-panel {
@@ -1086,28 +1287,41 @@
1086
1287
  pins flush to the scroll-viewport edge. Any padding-top here
1087
1288
  creates a sliver between the static nav buttons above and the
1088
1289
  pinned header which can briefly expose the body --bg through
1089
- sub-pixel render races during fast scroll. Explicit panel bg
1090
- belt-and-braces the inheritance chain. */
1290
+ sub-pixel render races during fast scroll. Explicit sidebar
1291
+ bg belts-and-braces the inheritance chain. Shares
1292
+ `--sidebar-bg` with the rest of the sidebar column. */
1091
1293
  padding: 0 4px 2px;
1092
- background: var(--panel);
1294
+ background: var(--sidebar-bg);
1093
1295
  }
1296
+ /* Unified with `.section-header` (rooms list dividers) · same
1297
+ padding, type scale, sticky pin behavior, and sidebar-bg
1298
+ box-shadow chrome that masks the parent's lateral padding +
1299
+ absorbs the sub-pixel sliver during fast scroll. */
1094
1300
  .agents-section-header {
1095
- padding: 8px 8px 6px;
1096
- font-family: var(--mono);
1097
- font-size: 9px;
1098
- letter-spacing: 0.14em;
1099
- text-transform: uppercase;
1100
- color: var(--text-faint);
1101
1301
  display: flex;
1102
1302
  align-items: center;
1103
1303
  gap: 6px;
1104
- margin-top: 4px;
1105
- }
1106
- .agents-section-header .line {
1107
- flex: 1;
1108
- height: 1px;
1109
- background: var(--line-bright);
1304
+ padding: 8px 12px 4px 10px;
1305
+ font-family: var(--mono);
1306
+ font-size: 10px;
1307
+ letter-spacing: 0.14em;
1308
+ text-transform: uppercase;
1309
+ font-weight: 400;
1310
+ color: var(--text-dim);
1311
+ position: sticky;
1312
+ top: 0;
1313
+ z-index: 2;
1314
+ background: var(--sidebar-bg);
1315
+ box-shadow:
1316
+ -4px 0 0 var(--sidebar-bg),
1317
+ 4px 0 0 var(--sidebar-bg),
1318
+ 0 -2px 0 var(--sidebar-bg);
1110
1319
  }
1320
+ /* Right-side divider line is no longer drawn (mirrors `.section-
1321
+ header` which leaves the trailing space for an optional badge);
1322
+ the markup still emits an empty <span class="line"> so existing
1323
+ JS isn't touched. */
1324
+ .agents-section-header .line { display: none; }
1111
1325
 
1112
1326
  /* Shell wraps the agent-row link + delete button as siblings — same
1113
1327
  pattern as session-row-shell · the X click never gets absorbed by
@@ -1234,7 +1448,7 @@
1234
1448
  .agent-row.is-chair {
1235
1449
  background: transparent;
1236
1450
  margin: 1px 6px;
1237
- border-radius: 4px;
1451
+ border-radius: 10px;
1238
1452
  }
1239
1453
  .agent-row.is-chair:hover { background: var(--panel-2); }
1240
1454
  .agent-row.is-chair.active { background: var(--panel-3); }
@@ -1308,14 +1522,10 @@
1308
1522
 
1309
1523
  /* "Building" section · in-flight Full persona build placeholder.
1310
1524
  Lime accent on the header to distinguish from inert pinned /
1311
- custom / core sections. */
1525
+ custom / core sections · mirrors `.section-header.live`. */
1312
1526
  .agents-section-header.building {
1313
1527
  color: var(--lime);
1314
1528
  }
1315
- .agents-section-header.building .line {
1316
- background: var(--lime-dim, var(--line));
1317
- opacity: 0.4;
1318
- }
1319
1529
  /* Building row · same dimensions as a normal agent row but the
1320
1530
  avatar slot is a pulsing CRT-style frame instead of a face,
1321
1531
  and the row carries a small "BUILD" / "READY" tag chip.
@@ -1421,7 +1631,7 @@
1421
1631
  letter-spacing: -0.005em;
1422
1632
  text-align: left;
1423
1633
  flex-shrink: 0;
1424
- border-radius: 4px;
1634
+ border-radius: 10px;
1425
1635
  transition: background 0.12s, color 0.12s;
1426
1636
  display: flex;
1427
1637
  align-items: center;
@@ -1463,27 +1673,33 @@
1463
1673
  [data-convene-trigger]::before {
1464
1674
  --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='M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'/><path d='M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z'/></svg>");
1465
1675
  }
1466
- /* "New agent" · UserPlus (Lucide) — silhouette with a small plus
1467
- to its right, Claude's "add member" pattern. */
1676
+ /* "New agent" · UserRoundPlus (Lucide) — rounder, bigger-headed
1677
+ "add person" glyph. Replaces the thinner UserPlus, whose small
1678
+ low-left silhouette + floating plus read a size smaller than the
1679
+ box-filling neighbours (SquarePen / FileText) — most visible in
1680
+ the collapsed mini rail where icons stand alone without labels. */
1468
1681
  [data-agent-composer-trigger]::before {
1469
- --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='M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2'/><circle cx='9' cy='7' r='4'/><line x1='19' x2='19' y1='8' y2='14'/><line x1='22' x2='16' y1='11' y2='11'/></svg>");
1682
+ --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='M2 21a8 8 0 0 1 13.292-6'/><circle cx='10' cy='8' r='5'/><path d='M19 16v6'/><path d='M22 19h-6'/></svg>");
1470
1683
  }
1471
1684
  /* "All Reports" · FileText (Lucide) — single document with three
1472
1685
  text lines, cleaner than the previous gradient-stack glyph. */
1473
- .new-btn.nav-reports::before {
1686
+ .new-btn.nav-reports::before,
1687
+ .mini-reports::before {
1474
1688
  --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='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z'/><path d='M14 2v4a2 2 0 0 0 2 2h4'/><path d='M10 9H8'/><path d='M16 13H8'/><path d='M16 17H8'/></svg>");
1475
1689
  }
1476
1690
  /* "All Notes" · Bookmark (Lucide) — pennant-shaped bookmark glyph
1477
1691
  mirroring the qcta save-button icon. Matches the chairman's-notes
1478
1692
  vocabulary across the app (sidebar entry, save action, in-room
1479
1693
  overlay all share the bookmark register). */
1480
- .new-btn.nav-notes::before {
1694
+ .new-btn.nav-notes::before,
1695
+ .mini-notes::before {
1481
1696
  --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='M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z'/></svg>");
1482
1697
  }
1483
1698
  /* "Search" · Search (Lucide) — circle + diagonal handle. Standard
1484
1699
  magnifying-glass glyph in the same Lucide line-icon vocabulary as
1485
1700
  the rest of the sidebar. */
1486
- .new-btn.nav-search::before {
1701
+ .new-btn.nav-search::before,
1702
+ .mini-search::before {
1487
1703
  --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'><circle cx='11' cy='11' r='8'/><path d='m21 21-4.3-4.3'/></svg>");
1488
1704
  }
1489
1705
  /* All Reports / All Notes / Search nav-button shape · label-only. */
@@ -1522,11 +1738,12 @@
1522
1738
  header that can briefly expose the body --bg through sub-pixel
1523
1739
  render races during fast scroll (visible as a thin "see-through"
1524
1740
  seam right above the ADJOURNED / LIVE / PAUSED divider).
1525
- Explicit panel bg belts-and-braces the inheritance chain so the
1526
- region is unambiguously --panel even if a child's bg falls
1527
- through. */
1741
+ Explicit sidebar bg belts-and-braces the inheritance chain so
1742
+ the region is unambiguously `--sidebar-bg` even if a child's
1743
+ bg falls through. Tracks `.sidebar`'s surface — change via
1744
+ the shared `--sidebar-bg` token. */
1528
1745
  padding: 0 4px 2px;
1529
- background: var(--panel);
1746
+ background: var(--sidebar-bg);
1530
1747
  }
1531
1748
 
1532
1749
  .section-header {
@@ -1548,27 +1765,26 @@
1548
1765
  color: var(--text-dim);
1549
1766
  text-transform: uppercase;
1550
1767
  letter-spacing: 0.14em;
1551
- font-weight: 600;
1768
+ font-weight: 400;
1552
1769
  /* Sticky · the LIVE / PAUSED / ADJOURNED dividers stay pinned
1553
1770
  to the top of the scroll viewport as the user scrolls down
1554
1771
  a long room list. Parent `.sessions-scroll` is the scroll
1555
- container. Background is the same `--panel` as the sidebar
1556
- so rows passing under the header don't bleed through; the
1557
- box-shadow extends that background 4px to either side (to
1558
- mask the parent's lateral 4px padding) and 2px upward (to
1559
- absorb any sub-pixel sliver between the scroll-viewport edge
1560
- and the pinned header during fast scroll without it, brief
1561
- repaint races can expose the body --bg as a thin seam above
1562
- the divider). z-index keeps the header above the rows during
1563
- the scroll transition. */
1772
+ container. Background matches `.sidebar` so rows passing
1773
+ under the header don't bleed through; the box-shadow extends
1774
+ that background 4px to either side (mask the parent's
1775
+ lateral 4px padding) and 2px upward (absorb sub-pixel sliver
1776
+ between the scroll-viewport edge and the pinned header
1777
+ during fast scroll). z-index keeps the header above the rows
1778
+ during the scroll transition. Tracks `.sidebar`'s surface
1779
+ via the shared `--sidebar-bg` token. */
1564
1780
  position: sticky;
1565
1781
  top: 0;
1566
1782
  z-index: 2;
1567
- background: var(--panel);
1783
+ background: var(--sidebar-bg);
1568
1784
  box-shadow:
1569
- -4px 0 0 var(--panel),
1570
- 4px 0 0 var(--panel),
1571
- 0 -2px 0 var(--panel);
1785
+ -4px 0 0 var(--sidebar-bg),
1786
+ 4px 0 0 var(--sidebar-bg),
1787
+ 0 -2px 0 var(--sidebar-bg);
1572
1788
  }
1573
1789
  .section-header.live { color: var(--lime); }
1574
1790
  .section-header.draft { color: var(--amber); }
@@ -1584,13 +1800,6 @@
1584
1800
  }
1585
1801
  .row-status.paused { color: var(--amber); }
1586
1802
  .section-header .line { flex: 1; }
1587
- .pin-glyph {
1588
- width: 10px;
1589
- height: 10px;
1590
- fill: currentColor;
1591
- flex-shrink: 0;
1592
- }
1593
- .agents-section-header.pinned { color: var(--text-soft); }
1594
1803
 
1595
1804
  /* ─── Pin toggle (per-row · hover-revealed for non-pinned) ───
1596
1805
  Positioned absolutely so it overlays the right edge of the row
@@ -1636,7 +1845,7 @@
1636
1845
  .session-row-shell {
1637
1846
  position: relative;
1638
1847
  margin: 2px 6px;
1639
- border-radius: 4px;
1848
+ border-radius: 10px;
1640
1849
  overflow: hidden;
1641
1850
  }
1642
1851
  .session-row-shell.active .session-row { background: var(--panel-3); }
@@ -1645,10 +1854,10 @@
1645
1854
  display: block;
1646
1855
  /* Horizontal padding kept in sync with .agent-row so the agents-tab
1647
1856
  and rooms-tab lists share the same horizontal rhythm in the
1648
- sidebar. Vertical padding 5px single-line title with the
1649
- subtitle row removed; tight hitbox matching the .new-btn nav
1857
+ sidebar. Vertical 6px gives the row a slightly more comfortable
1858
+ hitbox while still reading as compact alongside the .new-btn nav
1650
1859
  buttons above. */
1651
- padding: 5px 10px;
1860
+ padding: 6px 10px;
1652
1861
  text-decoration: none;
1653
1862
  color: var(--text);
1654
1863
  cursor: pointer;
@@ -1794,7 +2003,7 @@
1794
2003
  }
1795
2004
 
1796
2005
  .sidebar-foot {
1797
- border-top: 0.5px solid var(--line-bright);
2006
+ position: relative;
1798
2007
  padding: 0 10px;
1799
2008
  /* Fixed 44px min-height so the sidebar footer sits on a
1800
2009
  predictable baseline regardless of which main view is
@@ -1807,7 +2016,23 @@
1807
2016
  align-items: center;
1808
2017
  gap: 8px;
1809
2018
  flex-shrink: 0;
1810
- background: var(--panel-2);
2019
+ }
2020
+ /* Scroll fade · the rooms / agents list above .sidebar-foot can
2021
+ scroll behind it (foot is now transparent, no border-top). This
2022
+ ::before sits just above the foot and fades content from fully
2023
+ transparent to the sidebar's surface colour, so list items
2024
+ dissolve instead of clipping hard against the foot.
2025
+ pointer-events: none keeps clicks reaching whatever's behind.
2026
+ End-stop matches `--sidebar-bg` so the fade lands seamlessly. */
2027
+ .sidebar-foot::before {
2028
+ content: "";
2029
+ position: absolute;
2030
+ left: 0;
2031
+ right: 0;
2032
+ bottom: 100%;
2033
+ height: 24px;
2034
+ background: linear-gradient(to bottom, transparent 0%, var(--sidebar-bg) 100%);
2035
+ pointer-events: none;
1811
2036
  }
1812
2037
  .user-block {
1813
2038
  display: flex;
@@ -1835,11 +2060,12 @@
1835
2060
  overflow: hidden;
1836
2061
  }
1837
2062
  /* Pixel-art variant · when prefs.avatarSeed exists, app.renderUserBlock
1838
- swaps the initial-letter chip for an AvatarSkill SVG. Drop the
1839
- lime fill (the SVG is its own surface) and use the bg color
1840
- instead, keep the box border-less so the SVG bleeds edge-to-edge. */
2063
+ swaps the initial-letter chip for an AvatarSkill SVG. The pixel SVG
2064
+ is transparent, so we drop the fill entirely (was --lime, then
2065
+ --bg) and let it sit directly on the surface behind it — no boxed
2066
+ square around the character. */
1841
2067
  .user-av.has-pixel-av {
1842
- background: var(--bg);
2068
+ background: transparent;
1843
2069
  }
1844
2070
  .user-av.has-pixel-av svg {
1845
2071
  width: 100%;
@@ -1983,20 +2209,6 @@
1983
2209
 
1984
2210
  /* All Notes view · same scroll register as All Reports so the two
1985
2211
  cross-room aggregation destinations read as a paired set. */
1986
- /* Search view · same scroll chrome as reports / notes so the
1987
- three cross-cutting destinations behave identically. */
1988
- .main-view[data-main-view="search"] {
1989
- display: flex;
1990
- flex-direction: column;
1991
- overflow-y: auto;
1992
- scrollbar-width: none;
1993
- }
1994
- .main-view[data-main-view="search"]::-webkit-scrollbar { width: 8px; }
1995
- .main-view[data-main-view="search"]::-webkit-scrollbar-thumb { background: transparent; }
1996
- .main-view[data-main-view="search"]::-webkit-scrollbar-track { background: transparent; }
1997
- .main-view[data-main-view="search"]:hover { scrollbar-width: thin; }
1998
- .main-view[data-main-view="search"]:hover::-webkit-scrollbar-thumb { background: var(--line-bright); }
1999
-
2000
2212
  .main-view[data-main-view="notes"] {
2001
2213
  display: block;
2002
2214
  overflow-y: auto;
@@ -2062,11 +2274,13 @@
2062
2274
  }
2063
2275
 
2064
2276
  /* ─── Filter strip · All / Today / This week / Earlier ────────────
2065
- Mirrors `.sidebar-tab` exactly same active treatment (panel-3
2066
- bg + lime label), same 4px corner radius, same hover (panel-2 bg).
2067
- One coherent vocabulary across every segmented control. The count
2068
- rides inline after the label as quiet mono micro-type — no nested
2069
- chip chrome.
2277
+ Segmented control · multiple chips must be visible at once (the
2278
+ time-range filter is a pick-one-of-N for a list view), so this
2279
+ stays SEGMENTED rather than tracking the sidebar tabs' newer
2280
+ pill-shape "active grows, inactive collapses to icon-only" vocab.
2281
+ Active chip gets the panel-3 bg + text colour upgrade; hover gets
2282
+ panel-2; 4px corner radius keeps it clearly chip-not-pill. The
2283
+ count rides inline after the label as quiet mono micro-type.
2070
2284
  Comma-extended to `.notes-filters` / `.notes-filter-chip` so the
2071
2285
  All Notes page reuses the exact same chip vocabulary — both
2072
2286
  cross-room aggregation destinations stay visually identical. */
@@ -2458,705 +2672,227 @@
2458
2672
  }
2459
2673
 
2460
2674
  /* ──────────────────────────────────────────────────────────
2461
- Search page · cross-room keyword search results.
2462
- Two postures driven by `.is-initial` / `.has-results` on
2463
- `.search-page` (toggled by `app.renderSearchPage` /
2464
- `app.runSearch`):
2465
- · is-initial · vertically-centred Google-style hero with
2466
- a wide search input and a mono caption beneath. Reads
2467
- as "this IS the page" rather than chrome above an
2468
- empty list.
2469
- · has-results · the hero collapses, the input shrinks to
2470
- a compact head row paired with the result-count meta,
2471
- and the list below uses Google-style 3-line rows
2472
- (mono source breadcrumb · sans link title · sans
2473
- snippet body).
2474
- The same input element serves both states so the focus /
2475
- value / cursor position survive the swap. */
2476
- .search-page {
2477
- /* No `flex: 1` here · search-page must grow naturally with
2478
- its content (especially long result lists) so the sticky
2479
- `.search-card`'s containing block extends through all the
2480
- rows. With `flex: 1`, the search-page was clamped to one
2481
- viewport tall and the sticky card un-stuck after scrolling
2482
- past that — exactly the "sticky disappears after a few
2483
- scrolls" bug. `min-height: 100%` is still the floor so the
2484
- is-initial hero can vertical-centre against a viewport-
2485
- sized stage. */
2486
- width: 100%;
2487
- max-width: 920px;
2488
- margin: 0 auto;
2489
- padding: 32px 32px 40px;
2675
+ Search overlay · floating command-palette layer that
2676
+ replaces the prior `.search-page` standalone view. Triggered
2677
+ by `[data-search-trigger]` (sidebar Search nav) and Cmd+K /
2678
+ Ctrl+K globally. Layers over whatever main-view is mounted
2679
+ (room / agent / reports / notes) — the view stays mounted
2680
+ underneath so closing returns the user exactly where they
2681
+ were. Esc / scrim-click / X close. Result rows link to
2682
+ `#/r/{roomId}?m={msgId}&q={q}` (hash router), preserving
2683
+ click-navigation + keyword flash on the destination message
2684
+ from the prior page. */
2685
+ .search-overlay {
2686
+ position: fixed;
2687
+ inset: 0;
2688
+ z-index: 9000;
2490
2689
  display: flex;
2491
- flex-direction: column;
2492
- min-height: 100%;
2493
- box-sizing: border-box;
2494
- /* NO `position: relative` here · the deco layers below
2495
- anchor to `.main-view[data-main-view="search"]` instead
2496
- (which IS position:relative) so they fill main-view's
2497
- width — the visible room content area to the right of
2498
- the sidebar — rather than the .search-page's own 920px
2499
- column. Earlier the deco was anchored here and used a
2500
- `width: 100vw` trick to escape, but `vw` units include
2501
- the sidebar's slice of the viewport, so the deco's left
2502
- edge ended up underneath the sidebar (and the corner
2503
- brackets sat where the user couldn't see them). */
2504
- }
2505
- /* ─── Initial / empty state · 8-bit ambient deco + wide card ───
2506
- The page surface gets a static 8-bit "constellation" overlay
2507
- at the top — scattered pixel dots + a few lime accents —
2508
- drawn with `shape-rendering: crispEdges` to match the
2509
- round-table stage / chair-sprite vocabulary. Hero is text-
2510
- only (longer wordmark, subline) since the prior pixel mark
2511
- read too noisy. The card itself is 760px and `width: 100%`
2512
- so it actually fills the centred column instead of shrinking
2513
- to the input's intrinsic width. */
2514
- .search-page.is-initial {
2690
+ align-items: flex-start;
2515
2691
  justify-content: center;
2516
- /* Nudge slightly above true-centre so the hero feels
2517
- visually weighted. */
2518
- padding-bottom: 12vh;
2519
- padding-top: 0;
2520
- }
2521
- /* Has-results · inner-scroll layout. The PAGE itself is sized
2522
- to exactly main-view height (so nothing scrolls at this
2523
- level), and the results list inside gets its own scroll
2524
- via `flex: 1 + overflow-y: auto`. This puts the head row
2525
- in a non-scrolling top slot — no `position: sticky`
2526
- gymnastics, no containing-block-too-short failure mode.
2527
- The previous sticky approach disappeared as soon as the
2528
- user scrolled past one screen because the sticky's
2529
- containing block (the page) ran out; this design has no
2530
- such ceiling. */
2531
- .search-page.has-results {
2532
- height: 100%;
2533
- min-height: 0;
2534
- padding-top: 32px;
2535
- padding-bottom: 0;
2536
- flex-shrink: 0;
2537
- }
2538
- /* 8-bit ambient deco · absolute-positioned overlay at the top
2539
- of the page. Pure decoration · pointer-events: none, aria-
2540
- hidden, fades out the lower edge with a CSS mask so it
2541
- doesn't compete with the hero / card below.
2542
-
2543
- Width · `left: 0; right: 0` resolves against the nearest
2544
- positioned ancestor, which is `.main-view[data-main-view=
2545
- "search"]` (it sits to the right of the sidebar). The deco
2546
- therefore spans the FULL main-view width, not the viewport
2547
- — so the sidebar never overlaps. .search-page is static
2548
- (no position:relative) so the deco escapes its 920px cap
2549
- naturally. */
2550
- .search-bg-deco {
2551
- position: absolute;
2552
- top: 0;
2553
- left: 0;
2554
- right: 0;
2555
- height: 280px;
2692
+ padding: 88px 24px 24px;
2556
2693
  pointer-events: none;
2557
- z-index: -1;
2558
- -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
2559
- mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
2560
- opacity: 0.85;
2561
- }
2562
- .search-bg-deco svg { display: block; width: 100%; height: 100%; }
2563
-
2564
- /* Positioning anchor for the deco layers · main-view sits
2565
- to the right of the sidebar so its bounds = the visible
2566
- "room content" width. `isolation: isolate` scopes the
2567
- deco's z-index: -1 so it sits behind search-page content
2568
- without leaking past main-view's background. */
2569
- .main-view[data-main-view="search"] {
2570
- position: relative;
2571
- isolation: isolate;
2572
2694
  }
2573
-
2574
- .search-page.has-results .search-bg-deco {
2575
- /* Compact in has-results · keep a slim band of pixel
2576
- constellation behind the head row so the page still
2577
- reads as the search page rather than a generic list. */
2578
- height: 130px;
2579
- opacity: 0.45;
2580
- -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 50%, transparent 100%);
2581
- mask-image: linear-gradient(180deg, #000 0%, #000 50%, transparent 100%);
2582
- transition: opacity 0.18s ease, height 0.22s ease;
2583
- }
2584
-
2585
- /* Results-only deco · second layer that ONLY shows in
2586
- has-results. Adds proper 8-bit characters on top of the
2587
- constellation: CRT scanlines (CSS), pixel antennas at
2588
- the corners, scattered pixel "plus" sparkles, and a
2589
- dashed horizon line at the bottom edge. The combination
2590
- gives the results header genuine "search station"
2591
- atmosphere instead of a thin star field. */
2592
- .search-results-deco {
2695
+ .search-overlay[hidden] { display: none; }
2696
+ .search-overlay-scrim {
2593
2697
  position: absolute;
2594
- top: 0;
2595
- /* Spans full main-view width · the deco's `position:
2596
- absolute` resolves to `.main-view[data-main-view=
2597
- "search"]` (now position:relative) since .search-page
2598
- is static. No `100vw` trick needed — main-view already
2599
- sits to the right of the sidebar. */
2600
- left: 0;
2601
- right: 0;
2602
- height: 130px;
2603
- pointer-events: none;
2604
- z-index: -1;
2605
- opacity: 0;
2606
- transition: opacity 0.22s ease;
2607
- /* Faint CRT scanlines · 1px lime-tinted line every 5px.
2608
- Subtle enough not to compete with the input but
2609
- persistent enough to read as "this surface is alive." */
2610
- background-image: repeating-linear-gradient(
2611
- 0deg,
2612
- transparent 0px,
2613
- transparent 4px,
2614
- rgba(111, 181, 114, 0.035) 4px,
2615
- rgba(111, 181, 114, 0.035) 5px
2616
- );
2617
- -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 60%, transparent 100%);
2618
- mask-image: linear-gradient(180deg, #000 0%, #000 60%, transparent 100%);
2619
- }
2620
- .search-page.has-results .search-results-deco {
2621
- opacity: 0.85;
2622
- }
2623
- .search-results-deco svg { display: block; width: 100%; height: 100%; }
2624
- .search-hero {
2625
- text-align: center;
2626
- max-width: 760px;
2627
- margin: 0 auto 26px;
2628
- overflow: hidden;
2629
- transition:
2630
- opacity 0.22s ease,
2631
- max-height 0.24s ease,
2632
- margin 0.24s ease;
2633
- }
2634
- .search-page.is-initial .search-hero {
2635
- opacity: 1;
2636
- max-height: 220px;
2637
- }
2638
- .search-page.has-results .search-hero {
2639
- opacity: 0;
2640
- max-height: 0;
2641
- margin-top: 0;
2642
- margin-bottom: 0;
2643
- pointer-events: none;
2644
- }
2645
- .search-hero-title {
2646
- font-family: var(--font-human);
2647
- font-size: 28px;
2648
- font-weight: 600;
2649
- letter-spacing: -0.02em;
2650
- color: var(--text);
2651
- line-height: 1.05;
2652
- margin: 0 0 12px 0;
2653
- }
2654
- .search-hero-sub {
2655
- font-family: var(--mono);
2656
- font-size: 11px;
2657
- letter-spacing: 0.18em;
2658
- text-transform: uppercase;
2659
- color: var(--text-faint);
2660
- margin: 0;
2698
+ inset: 0;
2699
+ background: rgba(0, 0, 0, 0.42);
2700
+ -webkit-backdrop-filter: blur(2px);
2701
+ backdrop-filter: blur(2px);
2702
+ pointer-events: auto;
2661
2703
  }
2662
-
2663
- /* Search card · the substantial input affordance for the
2664
- is-initial state. `width: 100%` is load-bearing — without
2665
- it the card collapses to its content's intrinsic width
2666
- (the input had no flex anchor in is-initial because the
2667
- wrap was display:flex with the input as a single child),
2668
- making the whole hero look narrow. With `width: 100%`
2669
- plus `max-width: 760px` the card always fills the
2670
- centred column. In has-results the card chrome strips
2671
- and the inner input-wrap takes back its own border so
2672
- the compact head row can flex input + meta side-by-side. */
2673
- .search-card {
2674
- display: block;
2704
+ .search-overlay-card {
2705
+ position: relative;
2675
2706
  width: 100%;
2676
- max-width: 760px;
2677
- margin: 0 auto;
2678
- box-sizing: border-box;
2679
- /* Match the new-room composer's `.cmp-input-frame` beat-for-beat ·
2680
- solid `var(--bg)` surface (was frosted glass; the previous
2681
- backdrop-filter + color-mix made the card read as its own
2682
- material register inside the chat-col, which clashed with the
2683
- calm framed-input look the composer ships). Same `0.5px
2684
- --accent-line` hairline border, same 14px radius, same
2685
- `overflow: hidden` so the inner `.search-topbar` panel-strip
2686
- sits flush with the rounded corners. */
2687
- background: var(--bg);
2688
- border: 0.5px solid var(--accent-line);
2707
+ max-width: 640px;
2708
+ max-height: calc(100vh - 112px);
2709
+ display: flex;
2710
+ flex-direction: column;
2711
+ background: var(--panel-2);
2712
+ -webkit-backdrop-filter: blur(20px) saturate(160%);
2713
+ backdrop-filter: blur(20px) saturate(160%);
2714
+ border: 0.5px solid var(--line-bright);
2689
2715
  border-radius: 14px;
2716
+ box-shadow:
2717
+ 0 24px 60px -20px rgba(0, 0, 0, 0.55),
2718
+ 0 6px 16px -8px rgba(0, 0, 0, 0.4);
2690
2719
  overflow: hidden;
2691
- transition:
2692
- border-color 0.18s,
2693
- max-width 0.22s ease,
2694
- margin 0.22s ease,
2695
- padding 0.22s ease;
2696
- }
2697
- .search-card:focus-within {
2698
- /* Default border is `--lime-dim` (subtle brand tint); focus lifts
2699
- to full `--lime` for a clear "active" register. */
2700
- border-color: var(--lime);
2701
- }
2702
- /* Topbar inside the search card · mirrors `.cmp-topbar` from the
2703
- new-room composer · brand-tinted `--strip-bg` fill with an
2704
- `--accent-line` bottom hairline. Hosts the old `.search-hero-sub`
2705
- hint line ("across every room · keyword · message body · room
2706
- name") so the card itself surfaces its scope instead of needing
2707
- a separate sub-headline above. Only present in is-initial — the
2708
- has-results state collapses it alongside the hero. */
2709
- .search-topbar {
2710
- display: flex;
2711
- align-items: center;
2712
- flex-wrap: wrap;
2713
- gap: 4px;
2714
- padding: 6px 14px;
2715
- background: var(--strip-bg);
2716
- border-bottom: 0.5px solid var(--accent-line);
2717
- min-height: 36px;
2718
- }
2719
- .search-topbar-hint {
2720
- font-family: var(--mono);
2721
- font-size: 10px;
2722
- letter-spacing: 0.16em;
2723
- text-transform: uppercase;
2724
- color: var(--text-faint);
2725
- /* Same baseline tonality as `.search-hero-sub` had outside the
2726
- card · text-faint mono micro-caps sit quietly above the input. */
2720
+ pointer-events: auto;
2727
2721
  }
2728
- .search-page.has-results .search-topbar { display: none; }
2729
- .search-page.has-results .search-card {
2722
+ .search-overlay-input-row {
2730
2723
  display: flex;
2731
2724
  align-items: center;
2732
- gap: 14px;
2733
- max-width: none;
2734
- /* No `margin-bottom` here · the 18px breathing room below
2735
- the head row moved INSIDE `.search-results` (via the
2736
- ::before pseudo down at line ~XXX). With it on the card,
2737
- that 18px lived OUTSIDE the scroll area and permanently
2738
- stole vertical real estate; the scroll content was
2739
- truncated 18px earlier than the visible band. Moving the
2740
- gap into the scroll content means it scrolls away
2741
- naturally as the user reads down. */
2742
- margin: 0;
2743
- padding-top: 12px;
2744
- padding-bottom: 14px;
2745
- /* The head row is now in a NON-scrolling top slot of
2746
- search-page (the page is fixed at main-view height; the
2747
- scroll lives inside `.search-results`). No `position:
2748
- sticky` needed — the card stays put because the surface
2749
- around it doesn't move. */
2750
- background: var(--panel);
2751
- border: none;
2725
+ gap: 10px;
2726
+ padding: 12px 12px 12px 16px;
2727
+ border-bottom: 1px solid var(--line-bright);
2752
2728
  flex-shrink: 0;
2753
- z-index: 2;
2754
- /* Stacking context for the full-width ::after pseudo. */
2755
- position: relative;
2756
- isolation: isolate;
2757
- }
2758
- /* Full-width head band · ::after extends a panel-coloured
2759
- strip + 0.5px under-line from -100vmax to +100vmax past
2760
- the card's 920px box, clipped by main-view's overflow.
2761
- z-index: -1 keeps it behind the card's children (input /
2762
- chips / meta) inside the card's own stacking context. */
2763
- .search-page.has-results .search-card::after {
2764
- content: "";
2765
- position: absolute;
2766
- inset: 0 -100vmax;
2767
- background: var(--panel);
2768
- border-bottom: 0.5px solid var(--line);
2769
- z-index: -1;
2770
- pointer-events: none;
2771
2729
  }
2772
- .search-page.has-results .search-card:focus-within {
2773
- border-color: transparent;
2774
- border-bottom-color: var(--lime);
2775
- }
2776
-
2777
- /* Input wrap · borderless inside the card in is-initial,
2778
- bordered pill in has-results. */
2779
- .search-input-wrap {
2780
- position: relative;
2781
- display: flex;
2782
- align-items: center;
2730
+ .search-overlay-icon { color: var(--text-dim); flex-shrink: 0; }
2731
+ .search-overlay-input {
2783
2732
  flex: 1;
2784
2733
  min-width: 0;
2785
- transition: border-color 0.18s, background 0.18s;
2786
- }
2787
- .search-page.is-initial .search-input-wrap {
2788
- background: transparent;
2789
2734
  border: none;
2790
- padding: 6px 4px 0;
2791
- }
2792
- .search-page.has-results .search-input-wrap {
2793
- background: var(--panel-2);
2794
- border: 0.5px solid var(--line);
2795
- }
2796
- .search-page.has-results .search-input-wrap:focus-within {
2797
- border-color: var(--lime);
2798
- background: var(--panel);
2799
- }
2800
- .search-input-icon {
2801
- display: flex;
2802
- align-items: center;
2803
- justify-content: center;
2804
- color: var(--text-faint);
2805
- flex-shrink: 0;
2806
- transition: width 0.22s ease, color 0.12s;
2807
- }
2808
- /* Hide the leading magnifier in BOTH states · the hero card
2809
- is its own affordance, and in has-results the input is
2810
- unmistakably a search field by context (sort chips +
2811
- result-count meta beside it). The icon was reading as
2812
- redundant chrome. */
2813
- .search-input-icon { display: none; }
2814
- .search-input-wrap:focus-within .search-input-icon { color: var(--lime); }
2815
- .search-input {
2816
- flex: 1;
2817
- min-width: 0;
2735
+ outline: none;
2818
2736
  background: transparent;
2819
- border: none;
2820
2737
  color: var(--text);
2821
2738
  font-family: var(--font-human);
2739
+ font-size: 15px;
2740
+ line-height: 1.4;
2822
2741
  letter-spacing: -0.005em;
2823
- outline: none;
2824
- transition: padding 0.22s ease, font-size 0.22s ease;
2825
- }
2826
- .search-page.is-initial .search-input {
2827
- /* Type spec matches `.cmp-input` (14 px sans / line-height 1.55 /
2828
- -0.003em letter-spacing) and we reuse the same `14 px`
2829
- horizontal padding. No `min-height` here · an `<input>` always
2830
- vertical-centres its text content, so a forced 84 px height
2831
- (which works for the cmp's textarea where text top-aligns)
2832
- just leaves a tall band of empty space above the cursor. The
2833
- hero card's presence comes from the bottom toolbar + frame
2834
- padding, not from a stretched input. */
2835
- padding: 8px 14px 12px;
2836
- font-size: 14px;
2837
- line-height: 1.55;
2838
- letter-spacing: -0.003em;
2742
+ padding: 4px 0;
2839
2743
  }
2840
- /* Defuse Chrome's autofill bg · when the browser remembers a
2841
- prior query, the input gets a pale yellow/gray autofill
2842
- surface that fights our intended black card. The giant
2843
- inset box-shadow trick paints the autofill area with our
2844
- own surface color so the input stays visually consistent
2845
- with the rest of the card whether autofill engaged or not. */
2846
- .search-input:-webkit-autofill,
2847
- .search-input:-webkit-autofill:hover,
2848
- .search-input:-webkit-autofill:focus {
2849
- -webkit-box-shadow: 0 0 0 1000px var(--bg) inset !important;
2744
+ .search-overlay-input::placeholder { color: var(--text-dim); font-weight: 400; }
2745
+ .search-overlay-input:-webkit-autofill,
2746
+ .search-overlay-input:-webkit-autofill:focus {
2747
+ -webkit-box-shadow: 0 0 0 1000px var(--panel-2) inset !important;
2850
2748
  -webkit-text-fill-color: var(--text) !important;
2851
2749
  caret-color: var(--text);
2852
2750
  transition: background-color 5000s ease-in-out 0s;
2853
2751
  }
2854
- .search-page.has-results .search-input {
2855
- /* Horizontal padding matches the new-room composer's `.cmp-input`
2856
- (14 px each side) so the text-to-input-box gutter reads the same
2857
- across the two surfaces. The earlier 6 px was visibly tighter
2858
- than the new-room textarea and made the head-row input feel
2859
- cramped against its panel-2 pill. */
2860
- padding: 9px 14px;
2861
- font-size: 13px;
2862
- }
2863
- .search-input::placeholder {
2864
- color: var(--text-faint);
2865
- font-style: normal;
2866
- font-weight: 400;
2867
- }
2868
- /* Override the OLDER `.search-input:focus` rule (line ~796)
2869
- from the sidebar's search-block · that rule sets
2870
- `background: var(--panel-2)` on focus, which turned our
2871
- hero input GRAY whenever the user clicked into it. Both
2872
- features share the class name `.search-input`, so an
2873
- unscoped focus rule there leaks here. Scoping under
2874
- `.search-page` raises specificity and pins the input
2875
- transparent on focus. */
2876
- .search-page .search-input:focus,
2877
- .search-page .search-input:focus-visible {
2878
- background: transparent;
2879
- border: none;
2880
- outline: none;
2881
- }
2882
- .search-input-clear {
2883
- background: transparent;
2884
- border: none;
2885
- color: var(--text-faint);
2886
- cursor: pointer;
2887
- transition:
2888
- color 0.12s,
2889
- width 0.22s ease,
2890
- height 0.22s ease;
2891
- flex-shrink: 0;
2892
- display: inline-flex;
2893
- align-items: center;
2894
- justify-content: center;
2895
- }
2896
- .search-page.is-initial .search-input-clear {
2897
- width: 36px;
2898
- height: 36px;
2899
- font-size: 13px;
2900
- margin-right: 6px;
2901
- }
2902
- .search-page.has-results .search-input-clear {
2903
- width: 32px;
2904
- height: 32px;
2905
- font-size: 12px;
2906
- }
2907
- .search-input-clear:hover { color: var(--text); }
2908
- /* Hide the clear button when the input is empty (set by JS
2909
- via the .is-empty class on .search-card). Keeps the
2910
- hero's right edge clean when the user hasn't typed yet. */
2911
- .search-card.is-empty .search-input-clear { display: none; }
2912
-
2913
- /* Internal toolbar · footer of the card in is-initial.
2914
- Mono hint on the left, lime send button on the right.
2915
- Hidden entirely in has-results (the card is collapsed
2916
- to a head row by then). */
2917
- /* `.search-input-toolbar` and its mono "keyword · message body ·
2918
- room name" hint were retired · the frosted card alone is the
2919
- affordance, no extra footer-bar needed. */
2920
- /* Starter chips · static suggestions below the card in
2921
- is-initial. Click → pre-fill the input + trigger search.
2922
- Hidden in has-results. */
2923
- .search-starters {
2924
- display: flex;
2925
- align-items: center;
2926
- justify-content: center;
2927
- gap: 6px;
2928
- margin: 20px auto 0;
2929
- max-width: 760px;
2930
- font-family: var(--mono);
2931
- font-size: 10px;
2932
- letter-spacing: 0.14em;
2933
- text-transform: uppercase;
2934
- color: var(--text-faint);
2935
- flex-wrap: wrap;
2936
- }
2937
- .search-page.has-results .search-starters { display: none; }
2938
- .search-starters-label { color: var(--text-faint); margin-right: 4px; }
2939
- .search-starter {
2940
- background: transparent;
2941
- border: 0.5px solid var(--line);
2942
- color: var(--text-soft);
2943
- font: inherit;
2944
- letter-spacing: inherit;
2945
- text-transform: inherit;
2946
- cursor: pointer;
2947
- padding: 5px 10px;
2948
- transition: color 0.12s, border-color 0.12s;
2949
- }
2950
- .search-starter:hover {
2951
- color: var(--lime);
2952
- border-color: var(--lime);
2953
- }
2954
-
2955
- /* Result-count meta · sits beside the shrunk input in the
2956
- has-results head row. Hidden entirely in is-initial. */
2957
- .search-results-meta {
2958
- font-family: var(--mono);
2959
- font-size: 10px;
2960
- letter-spacing: 0.14em;
2961
- text-transform: uppercase;
2962
- color: var(--text-faint);
2963
- white-space: nowrap;
2964
- flex-shrink: 0;
2965
- }
2966
- .search-page.is-initial .search-results-meta { display: none; }
2967
-
2968
- /* Sort chip group · "Newest" / "Oldest" toggle that re-sorts
2969
- the result list client-side (no re-fetch). Only visible in
2970
- has-results · the head row's flex layout slots it between
2971
- the input and the meta count. Active chip uses the lime
2972
- accent border + caps; inactives are line-faint until hover. */
2973
- .search-results-sort {
2974
- display: none;
2975
- align-items: center;
2976
- gap: 4px;
2977
- flex-shrink: 0;
2978
- font-family: var(--mono);
2979
- font-size: 10px;
2980
- letter-spacing: 0.14em;
2981
- text-transform: uppercase;
2982
- }
2983
- .search-page.has-results .search-results-sort {
2984
- display: inline-flex;
2985
- }
2986
- .search-results-sort .srs-label {
2987
- color: var(--text-faint);
2988
- margin-right: 4px;
2989
- }
2990
- .search-results-sort button {
2991
- background: transparent;
2992
- border: 0.5px solid var(--line);
2993
- color: var(--text-faint);
2994
- font: inherit;
2995
- letter-spacing: inherit;
2996
- text-transform: inherit;
2997
- padding: 5px 9px;
2998
- cursor: pointer;
2999
- transition: color 0.12s, border-color 0.12s;
3000
- }
3001
- .search-results-sort button:hover {
3002
- color: var(--text-soft);
3003
- border-color: var(--text-faint);
3004
- }
3005
- .search-results-sort button.active {
3006
- color: var(--lime);
3007
- border-color: var(--lime);
3008
- }
3009
- .search-results-sort button.active:hover {
3010
- color: var(--lime);
3011
- border-color: var(--lime);
2752
+ .search-overlay-close-btn {
2753
+ background: transparent;
2754
+ border: none;
2755
+ color: var(--text-dim);
2756
+ cursor: pointer;
2757
+ width: 28px;
2758
+ height: 28px;
2759
+ display: inline-flex;
2760
+ align-items: center;
2761
+ justify-content: center;
2762
+ border-radius: 6px;
2763
+ transition: color 0.12s, background 0.12s;
2764
+ flex-shrink: 0;
3012
2765
  }
3013
-
3014
- /* Results · only visible in has-results. Top padding gives a
3015
- small breathing room below the head row's hairline. */
3016
- .search-results {
3017
- padding-top: 4px;
2766
+ .search-overlay-close-btn:hover {
2767
+ color: var(--text);
2768
+ background: var(--surface-hover, rgba(255, 255, 255, 0.05));
3018
2769
  }
3019
- .search-page.is-initial .search-results { display: none; }
3020
- /* Inner scroll container · in has-results, `.search-results`
3021
- owns the vertical scroll instead of `.main-view`. The
3022
- search-page itself is sized to exactly main-view height,
3023
- so the head row (`.search-card`) sits in a non-scrolling
3024
- top slot and stays there permanently. */
3025
- .search-page.has-results .search-results {
2770
+ .search-overlay-body {
3026
2771
  flex: 1;
3027
2772
  min-height: 0;
3028
2773
  overflow-y: auto;
3029
- padding-bottom: 40px;
3030
2774
  scrollbar-width: thin;
3031
2775
  }
3032
- /* Initial breathing-room spacer · 18px tall block that sits
3033
- INSIDE the scroll content (before the result list). On
3034
- first render it gives the user the same gap below the
3035
- head row that a `margin-bottom: 18px` would; once they
3036
- start scrolling, this spacer scrolls away with the rest
3037
- of the list (it's part of the scrollable content), so it
3038
- doesn't permanently consume visible scroll real estate
3039
- the way an outside-the-scroll margin did. */
3040
- .search-page.has-results .search-results::before {
3041
- content: "";
3042
- display: block;
3043
- height: 18px;
3044
- }
3045
- .search-page.has-results .search-results::-webkit-scrollbar { width: 8px; }
3046
- .search-page.has-results .search-results::-webkit-scrollbar-thumb { background: transparent; }
3047
- .search-page.has-results .search-results::-webkit-scrollbar-track { background: transparent; }
3048
- .search-page.has-results .search-results:hover::-webkit-scrollbar-thumb { background: var(--line-bright); }
3049
-
3050
- /* Google-style result row · 3 stacked text lines (mono
3051
- source breadcrumb · sans link title · sans snippet body).
3052
- No row hover background, no per-row border — only the
3053
- title line lights up on hover so the row reads calm. */
3054
- .search-results-list {
3055
- list-style: none;
3056
- margin: 0;
3057
- padding: 0;
3058
- }
3059
- .search-results-list > li {
3060
- margin-bottom: 22px;
2776
+ .search-overlay-body::-webkit-scrollbar { width: 8px; }
2777
+ .search-overlay-body::-webkit-scrollbar-thumb { background: transparent; }
2778
+ .search-overlay-body::-webkit-scrollbar-track { background: transparent; }
2779
+ .search-overlay-body:hover::-webkit-scrollbar-thumb { background: var(--line-bright); }
2780
+ .search-overlay-empty {
2781
+ padding: 56px 24px;
2782
+ display: flex;
2783
+ flex-direction: column;
2784
+ align-items: center;
2785
+ text-align: center;
2786
+ color: var(--text-dim);
3061
2787
  }
3062
- .search-results-list > li:last-child { margin-bottom: 0; }
3063
- .sr-row {
3064
- display: block;
3065
- text-decoration: none;
3066
- color: inherit;
2788
+ .search-overlay-empty-icon { color: var(--text-dim); margin-bottom: 18px; }
2789
+ .search-overlay-empty-text {
2790
+ font-family: var(--font-human);
2791
+ font-size: 14px;
2792
+ color: var(--text-soft);
2793
+ margin-bottom: 6px;
2794
+ letter-spacing: -0.003em;
3067
2795
  }
3068
- .sr-source {
3069
- display: flex;
3070
- align-items: baseline;
3071
- gap: 6px;
2796
+ .search-overlay-empty-hint {
3072
2797
  font-family: var(--mono);
3073
2798
  font-size: 10px;
3074
- letter-spacing: 0.12em;
2799
+ letter-spacing: 0.14em;
3075
2800
  text-transform: uppercase;
3076
- color: var(--text-faint);
3077
- margin-bottom: 4px;
3078
- overflow: hidden;
3079
- white-space: nowrap;
3080
- text-overflow: ellipsis;
2801
+ color: var(--text-dim);
3081
2802
  }
3082
- .sr-source-author {
3083
- color: var(--text-soft);
3084
- font-weight: 600;
2803
+ .search-overlay-list { list-style: none; margin: 0; padding: 6px 0; }
2804
+ .search-overlay-list > li { margin: 0; }
2805
+ .so-row {
2806
+ display: grid;
2807
+ grid-template-columns: 20px 1fr auto;
2808
+ column-gap: 12px;
2809
+ row-gap: 2px;
2810
+ align-items: center;
2811
+ padding: 8px 16px;
2812
+ text-decoration: none;
2813
+ color: inherit;
2814
+ transition: background 0.08s;
2815
+ cursor: pointer;
3085
2816
  }
3086
- .sr-source-sep {
3087
- color: var(--text-faint);
3088
- margin: 0 2px;
2817
+ .so-row:hover,
2818
+ .so-row:focus-visible {
2819
+ background: var(--surface-hover, rgba(255, 255, 255, 0.04));
2820
+ outline: none;
3089
2821
  }
3090
- .sr-source-time {
3091
- color: var(--text-faint);
2822
+ .so-row-icon {
2823
+ grid-column: 1;
2824
+ grid-row: 1 / 3;
2825
+ color: var(--text-dim);
2826
+ display: flex;
2827
+ align-items: center;
2828
+ justify-content: center;
2829
+ align-self: start;
2830
+ margin-top: 1px;
3092
2831
  }
3093
- .sr-title {
2832
+ .so-row-title {
2833
+ grid-column: 2;
2834
+ grid-row: 1;
3094
2835
  font-family: var(--font-human);
3095
- font-size: 17px;
3096
- font-weight: 500;
3097
- line-height: 1.3;
2836
+ font-size: 14px;
2837
+ line-height: 1.35;
3098
2838
  color: var(--text);
3099
- margin: 0 0 4px 0;
3100
- letter-spacing: -0.005em;
3101
- transition: color 0.12s;
2839
+ font-weight: 500;
2840
+ letter-spacing: -0.003em;
3102
2841
  overflow: hidden;
3103
2842
  text-overflow: ellipsis;
3104
2843
  white-space: nowrap;
3105
2844
  }
3106
- .sr-row:hover .sr-title { color: var(--lime); }
3107
- .sr-snippet {
2845
+ .so-row-meta {
2846
+ grid-column: 3;
2847
+ grid-row: 1;
2848
+ font-family: var(--mono);
2849
+ font-size: 10px;
2850
+ letter-spacing: 0.08em;
2851
+ text-transform: uppercase;
2852
+ color: var(--text-dim);
2853
+ white-space: nowrap;
2854
+ flex-shrink: 0;
2855
+ }
2856
+ .so-row-snippet {
2857
+ grid-column: 2 / 4;
2858
+ grid-row: 2;
3108
2859
  font-family: var(--font-human);
3109
- font-size: 13px;
3110
- line-height: 1.55;
2860
+ font-size: 12px;
2861
+ line-height: 1.45;
3111
2862
  color: var(--text-soft);
3112
- word-break: break-word;
3113
- display: -webkit-box;
3114
- -webkit-line-clamp: 2;
3115
- -webkit-box-orient: vertical;
3116
2863
  overflow: hidden;
2864
+ text-overflow: ellipsis;
2865
+ white-space: nowrap;
3117
2866
  }
3118
- .sr-snippet mark {
3119
- background: var(--lime);
3120
- color: var(--bg, #0c0c0c);
3121
- padding: 0 2px;
3122
- border-radius: 2px;
2867
+ .so-row-snippet mark {
2868
+ background: transparent;
2869
+ color: var(--lime);
3123
2870
  font-weight: 600;
2871
+ padding: 0;
2872
+ border-radius: 0;
3124
2873
  }
3125
-
3126
- /* Empty / hint card · only ever rendered inside .search-results
3127
- so it's tied to the has-results state. Centered block, calm
3128
- copy. */
3129
- .search-empty {
3130
- padding: 40px 0;
3131
- text-align: center;
3132
- color: var(--text-faint);
3133
- }
3134
- .search-empty-kicker {
2874
+ .search-overlay-status { padding: 40px 16px; text-align: center; color: var(--text-dim); }
2875
+ .search-overlay-status-kicker {
3135
2876
  display: block;
3136
2877
  font-family: var(--mono);
3137
2878
  font-size: 10px;
3138
2879
  letter-spacing: 0.18em;
3139
2880
  text-transform: uppercase;
3140
2881
  color: var(--lime);
3141
- margin-bottom: 8px;
2882
+ margin-bottom: 6px;
3142
2883
  }
3143
- .search-empty-msg {
2884
+ .search-overlay-status-msg {
3144
2885
  font-family: var(--font-human);
3145
- font-size: 14px;
2886
+ font-size: 13px;
3146
2887
  color: var(--text-soft);
3147
2888
  line-height: 1.5;
3148
- max-width: 520px;
2889
+ max-width: 360px;
3149
2890
  margin: 0 auto;
3150
2891
  }
3151
-
3152
- /* Reduced-motion · snap state changes, no transitions. */
3153
2892
  @media (prefers-reduced-motion: reduce) {
3154
- .search-hero,
3155
- .search-card,
3156
- .search-input,
3157
- .search-input-icon,
3158
- .search-input-clear,
3159
- .sr-title { transition: none; }
2893
+ .so-row { transition: none; }
2894
+ .search-overlay-scrim,
2895
+ .search-overlay-card { backdrop-filter: none; -webkit-backdrop-filter: none; }
3160
2896
  }
3161
2897
 
3162
2898
  /* Search-result jump · in-place keyword pulse + article outline.
@@ -4159,15 +3895,6 @@
4159
3895
  -webkit-backdrop-filter: none;
4160
3896
  }
4161
3897
  }
4162
- /* When the sidebar is collapsed the room-head gains a leading
4163
- auto-sized track for the in-header `.room-head-expand` button.
4164
- When expanded the button is display:none, so a 0-width track
4165
- would still leave a `gap` artifact — switching to a 3-track
4166
- template only in the collapsed state keeps the header visually
4167
- identical to before whenever the sidebar is open. */
4168
- body.sidebar-collapsed .room-head {
4169
- grid-template-columns: auto 1fr auto;
4170
- }
4171
3898
  /* `overflow: visible` so the tone-tag hover tooltip (positioned via
4172
3899
  ::after below the tag) can escape this container. The room-subject
4173
3900
  has its own overflow:hidden + text-overflow ellipsis rule, so the
@@ -6186,15 +5913,15 @@
6186
5913
 
6187
5914
  .followup-children {
6188
5915
  /* Width-match the brief card (.ending-block: max-width 760px,
6189
- margin auto). The follow-ups list slots in directly below the
6190
- brief, so they need to share the same gutter — otherwise the
6191
- follow-ups span full chat width and the alignment breaks. */
5916
+ margin auto) AND mirror `.session-analytics` chrome · same
5917
+ panel-2 surface, same --line-bright hairline, same banner-body
5918
+ split (head = panel-3 strip with mono kicker, list = body
5919
+ block with its own padding). No container-level padding ·
5920
+ inner elements own it. */
6192
5921
  max-width: 760px;
6193
5922
  margin: 24px auto 0;
6194
- padding: 14px 16px;
6195
5923
  background: var(--panel-2);
6196
- border: 0.5px solid var(--line);
6197
- font-family: var(--mono);
5924
+ border: 0.5px solid var(--line-bright);
6198
5925
  }
6199
5926
 
6200
5927
  /* ─── Session analytics card · post-adjourn summary ───
@@ -6335,21 +6062,27 @@
6335
6062
  padding: 0;
6336
6063
  display: flex;
6337
6064
  flex-direction: column;
6338
- gap: 1px;
6065
+ gap: 0;
6339
6066
  }
6067
+ /* All columns `auto` (previously `10px 1fr auto auto`) so swatch +
6068
+ name + pct + tokens cluster on the left rather than `1fr` on the
6069
+ name column stretching short labels and shoving the numbers to
6070
+ the far right of the legend. min-width on pct + tokens keeps the
6071
+ numeric columns roughly aligned across rows so the eye can still
6072
+ scan vertically. */
6340
6073
  .sa-legend-row {
6341
6074
  display: grid;
6342
- grid-template-columns: 10px 1fr auto auto;
6343
- align-items: baseline;
6344
- gap: 8px;
6075
+ grid-template-columns: 8px auto auto auto;
6076
+ align-items: center;
6077
+ gap: 6px;
6345
6078
  font-family: var(--mono);
6346
6079
  font-size: 10px;
6080
+ line-height: 1.35;
6347
6081
  color: var(--text-soft);
6348
6082
  }
6349
6083
  .sa-legend-swatch {
6350
6084
  width: 8px;
6351
6085
  height: 8px;
6352
- align-self: center;
6353
6086
  border-radius: 0;
6354
6087
  }
6355
6088
  .sa-legend-name {
@@ -6360,12 +6093,13 @@
6360
6093
  color: var(--text-soft);
6361
6094
  font-variant-numeric: tabular-nums;
6362
6095
  text-align: right;
6096
+ min-width: 30px;
6363
6097
  }
6364
6098
  .sa-legend-tokens {
6365
6099
  color: var(--text-faint);
6366
6100
  font-variant-numeric: tabular-nums;
6367
6101
  text-align: right;
6368
- min-width: 44px;
6102
+ min-width: 40px;
6369
6103
  }
6370
6104
 
6371
6105
  /* What you valued · chips for counts + list of ▲-voted points.
@@ -6449,15 +6183,26 @@
6449
6183
  color: var(--text-faint);
6450
6184
  letter-spacing: 0.04em;
6451
6185
  }
6186
+ /* Banner head · matches `.sa-banner` shape (panel-3 strip with mono
6187
+ kicker, hairline divider beneath). The text-only label slots in
6188
+ where `.sa-banner-tag` would sit in the analytics card. */
6452
6189
  .followup-children-head {
6190
+ padding: 5px 14px;
6191
+ display: flex;
6192
+ align-items: center;
6193
+ gap: 12px;
6194
+ font-family: var(--mono);
6453
6195
  font-size: 10px;
6454
- color: var(--text-soft);
6455
- letter-spacing: 0.16em;
6456
- text-transform: uppercase;
6457
6196
  font-weight: 700;
6458
- margin-bottom: 10px;
6197
+ letter-spacing: 0.18em;
6198
+ text-transform: uppercase;
6199
+ color: var(--lime);
6200
+ background: var(--panel-3);
6201
+ border-bottom: 0.5px solid var(--line);
6459
6202
  }
6203
+ /* Body block · matches `.sa-body` padding. */
6460
6204
  .followup-children-list {
6205
+ padding: 12px 14px 14px;
6461
6206
  display: flex;
6462
6207
  flex-direction: column;
6463
6208
  gap: 6px;
@@ -6627,6 +6372,14 @@
6627
6372
  flex: 1 1 auto;
6628
6373
  min-height: 0;
6629
6374
  overflow-y: auto;
6375
+ /* Chat scroller is on `--panel` — same surface as the sidebar,
6376
+ so the message stream reads as a flat continuous canvas
6377
+ (vs `.chat-col` which sits at `--panel-2` underneath as a
6378
+ lifted frame the chat sits on top of). The lifted `--panel-2`
6379
+ shows through wherever `.chat` doesn't paint (cmp starter
6380
+ panel, gutters when chat is hidden), giving the column a
6381
+ subtle warm border that the message stream itself doesn't
6382
+ inherit. */
6630
6383
  background: var(--panel);
6631
6384
  /* Top padding · clears the frosted-glass `.room-head` (which is
6632
6385
  now absolutely positioned, ~56px tall). Without this, the first
@@ -6643,7 +6396,7 @@
6643
6396
  common case (textarea + one strip); tall textarea +
6644
6397
  multiple strips can still cover the bottom message, the
6645
6398
  user scrolls a touch — acceptable. */
6646
- padding: calc(14px + var(--room-head-h, 56px)) 20px 140px;
6399
+ padding: calc(14px + var(--room-head-h, 56px)) 50px 140px 40px;
6647
6400
  transition: opacity 0.18s ease-out;
6648
6401
  }
6649
6402
  /* Note-jump loading state · the user clicked a note in the All
@@ -7810,6 +7563,42 @@
7810
7563
  .msg-bubble p:last-child { margin-bottom: 0; }
7811
7564
  .msg-bubble strong { color: var(--text); font-weight: 600; }
7812
7565
  .msg-bubble em { font-style: normal; color: var(--lime); font-weight: 500; }
7566
+ /* Karaoke-style TTS sentence highlight · when voice replay reads a
7567
+ message aloud, app.js wraps each sentence in a `.tts-sentence`
7568
+ span and adds `.is-current` to whichever one matches the audio
7569
+ cursor (currentTime / duration → character offset → sentence
7570
+ index). The wrap survives across messages and is harmless without
7571
+ `.is-current`. Highlight uses the theme accent so the user can
7572
+ scan back to "where it's reading" without re-reading the whole
7573
+ bubble. Smooth color transition · feels like a soft sweep
7574
+ instead of a strobing chip when the cursor advances. */
7575
+ .msg-bubble .tts-sentence,
7576
+ .cd-body .tts-sentence,
7577
+ .ci-body .tts-sentence {
7578
+ transition: color 0.18s ease, background 0.18s ease;
7579
+ border-radius: 2px;
7580
+ }
7581
+ .msg-bubble .tts-sentence.is-current,
7582
+ .cd-body .tts-sentence.is-current,
7583
+ .ci-body .tts-sentence.is-current {
7584
+ color: var(--lime);
7585
+ background: color-mix(in srgb, var(--lime) 12%, transparent);
7586
+ /* Keep the highlight readable when the sentence wraps across
7587
+ multiple lines · `box-decoration-break: clone` paints the
7588
+ background on each line fragment instead of one giant box. */
7589
+ -webkit-box-decoration-break: clone;
7590
+ box-decoration-break: clone;
7591
+ }
7592
+ /* Keep <em> inside the current sentence visually consistent with
7593
+ the surrounding highlight (otherwise the `em { color: var(--lime) }`
7594
+ above + the wrapper's lime tint blend into one undifferentiated
7595
+ accent and the italic emphasis loses signal). Subtle weight bump
7596
+ instead. */
7597
+ .msg-bubble .tts-sentence.is-current em,
7598
+ .cd-body .tts-sentence.is-current em,
7599
+ .ci-body .tts-sentence.is-current em {
7600
+ font-weight: 600;
7601
+ }
7813
7602
  /* Markdown blockquote · designed inset card, not a raw `>` line.
7814
7603
  Per the no-coloured-left-borders rule, the callout treatment
7815
7604
  uses a top mono kicker + panel-2 surface + indent (NOT a left
@@ -7893,9 +7682,6 @@
7893
7682
  (--font-agent) so the visual continuity reads as "another voice in
7894
7683
  the room". Only the name color + avatar border distinguish them. */
7895
7684
  .msg.chair .msg-name { color: var(--cyan, #6A9B97); }
7896
- .msg.chair .msg-av {
7897
- border: 0.5px solid var(--cyan, #6A9B97);
7898
- }
7899
7685
  /* Settings-change pings stay deliberately small — they're a status
7900
7686
  line, not a turn. */
7901
7687
  .msg.chair.kind-settings .msg-bubble {
@@ -9012,6 +8798,25 @@
9012
8798
  .rt-avatar[data-agent]:hover {
9013
8799
  filter: brightness(1.18);
9014
8800
  }
8801
+ /* Speaking squash-blink · the avatar's eyes are baked into the
8802
+ sprite (img / inline pixel-SVG), so a quick vertical scaleY
8803
+ squash is the cheapest "blink while talking" cue. Only the
8804
+ speaking seat animates; the dip is brief + shallow so it reads
8805
+ as a blink, not a bounce. transform keeps the static
8806
+ translateX(-50%) so the sprite stays centered on the chair. */
8807
+ .rt-seat-speaking .rt-avatar {
8808
+ transform-origin: center 45%;
8809
+ animation: rt-blink-squash 4.2s ease-in-out infinite;
8810
+ }
8811
+ @keyframes rt-blink-squash {
8812
+ 0%, 88%, 100% { transform: translateX(-50%) scaleY(1); }
8813
+ 92% { transform: translateX(-50%) scaleY(0.88); }
8814
+ 96% { transform: translateX(-50%) scaleY(1); }
8815
+ }
8816
+ /* Respect reduced-motion · hold the avatar at full height. */
8817
+ @media (prefers-reduced-motion: reduce) {
8818
+ .rt-seat-speaking .rt-avatar { animation: none; }
8819
+ }
9015
8820
 
9016
8821
  /* Name plate · small mono caption ABOVE the avatar (was below,
9017
8822
  but back-row director seats had their names land on the table
@@ -11599,94 +11404,6 @@
11599
11404
  margin: 0 auto;
11600
11405
  padding: 32px 32px;
11601
11406
  width: 100%;
11602
- /* `isolation: isolate` creates a stacking context here WITHOUT
11603
- making cmp the containing block for absolute descendants.
11604
- Result: `.cmp-bg-deco` belongs to cmp's stacking context
11605
- (so `z-index: -1` puts it just below cmp's static content
11606
- but ABOVE the lower-layer chat / chat-col panel bgs), while
11607
- its SPATIAL containing block is `.chat-col` (positioned
11608
- above) — so the deco can span the full chat-col width
11609
- without being constrained to cmp's 760px. The two-axis
11610
- split is what makes the Search-page-style backdrop work
11611
- inside a max-width-narrower section. */
11612
- isolation: isolate;
11613
- }
11614
- /* 8-bit ambient backdrop · pinned at the top of the composer.
11615
- Same visual language + position as `.search-bg-deco`. The
11616
- backdrop ESCAPES `.cmp`'s max-width (760px) so it spans the
11617
- full main-view width — same `left: 50%; margin-left: -50vw;
11618
- width: 100vw` pattern the Search page uses. `.main-view`'s
11619
- own `overflow: hidden` clips it to the visible content area,
11620
- so "full width" resolves to the room-content rect (sidebar
11621
- not included). Scene-tuned SVG content is injected by
11622
- `composerBgDecoSvg(scene)` in app.js · room mode draws a
11623
- mini boardroom (table + chairs), agent mode draws a row of
11624
- pixel character heads + speech bubble. */
11625
- .cmp-bg-deco {
11626
- /* Containing block is `.chat-col` (set position: relative above)
11627
- so `top/left/right: 0` snap to the FULL CHAT COLUMN — exactly
11628
- the right-pane width the user wants. No `100vw` escape · the
11629
- previous viewport-relative width spilled behind the sidebar
11630
- because the sidebar lives OUTSIDE this main-view's overflow
11631
- clip, so the deco rendered under it. Anchoring to `.chat-col`
11632
- gets us the right pane's actual content rect and nothing
11633
- beyond it. */
11634
- position: absolute;
11635
- top: 0;
11636
- left: 0;
11637
- right: 0;
11638
- height: auto;
11639
- pointer-events: none;
11640
- z-index: -1;
11641
- -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
11642
- mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
11643
- /* Opacity tuned down from 0.85 — the field of small pixel motifs
11644
- read as "fragmented dirt" especially in light, where white bg
11645
- amplifies every dot's contrast. Light gets an extra step down. */
11646
- opacity: 0.6;
11647
- }
11648
- :root[data-theme="light"] .cmp-bg-deco { opacity: 0.4; }
11649
- .cmp-bg-deco svg { display: block; width: 100%; height: 100%; }
11650
- /* 8-bit deco animations · keep subtle so the field reads as
11651
- "alive" rather than "demanding attention". All animations are
11652
- opacity / quantised transforms so the pixel-art register
11653
- holds (no smooth tweens). Each element gets an inline
11654
- `animation-delay` from the SVG generator so siblings don't
11655
- pulse in lockstep — the field shimmers organically. */
11656
- @keyframes deco-twinkle {
11657
- 0%, 100% { opacity: 1; }
11658
- 50% { opacity: 0.35; }
11659
- }
11660
- @keyframes deco-shine {
11661
- 0%, 100% { opacity: 1; }
11662
- 50% { opacity: 0.55; }
11663
- }
11664
- @keyframes deco-spark {
11665
- 0%, 100% { opacity: 1; transform: scale(1); }
11666
- 50% { opacity: 0.4; transform: scale(0.7); }
11667
- }
11668
- @keyframes deco-bob {
11669
- 0%, 100% { transform: translateY(0); }
11670
- 50% { transform: translateY(-1.5px); }
11671
- }
11672
- @keyframes deco-blink {
11673
- 0%, 70%, 100% { opacity: 1; }
11674
- 80%, 92% { opacity: 0.15; }
11675
- }
11676
- .cmp-bg-deco .deco-twinkle { animation: deco-twinkle 4.5s ease-in-out infinite; }
11677
- .cmp-bg-deco .deco-shine { animation: deco-shine 3.2s ease-in-out infinite; }
11678
- .cmp-bg-deco .deco-spark { animation: deco-spark 2.4s ease-in-out infinite;
11679
- transform-box: fill-box; transform-origin: center; }
11680
- .cmp-bg-deco .deco-bob { animation: deco-bob 3.0s ease-in-out infinite;
11681
- transform-box: fill-box; transform-origin: center; }
11682
- .cmp-bg-deco .deco-blink { animation: deco-blink 4.0s ease-in-out infinite; }
11683
- /* Reduced-motion · stop animation but keep the deco visible. */
11684
- @media (prefers-reduced-motion: reduce) {
11685
- .cmp-bg-deco .deco-twinkle,
11686
- .cmp-bg-deco .deco-shine,
11687
- .cmp-bg-deco .deco-spark,
11688
- .cmp-bg-deco .deco-bob,
11689
- .cmp-bg-deco .deco-blink { animation: none; }
11690
11407
  }
11691
11408
  /* Default composer mode (content fits viewport).
11692
11409
  Toggled by JS via `.chat--composer` (added in
@@ -12588,7 +12305,7 @@
12588
12305
  }
12589
12306
 
12590
12307
  /* ─── Celebrity seed cards · new-agent composer "hire a known
12591
- mind" rail. Replaces the older AGENT_STARTERS_EN list. Six
12308
+ mind" rail. Replaces the older AGENT_STARTERS_EN list. Four
12592
12309
  portrait-style cards in a 2-col grid, each a one-click preset
12593
12310
  for the full-mode persona builder.
12594
12311
 
@@ -14650,6 +14367,70 @@
14650
14367
  font-size: 10px;
14651
14368
  color: var(--text-faint);
14652
14369
  }
14370
+ /* ─── Cast-edit popover (room header "Add director" button) ───
14371
+ Extends `.composer-pick-pop` with an explicit footer carrying
14372
+ Cancel / Confirm so the membership change is intentional. The
14373
+ list rows reuse `.composer-pick-row` styling — identical visual
14374
+ register to the new-room composer's director picker so the user
14375
+ reads it as the same primitive. */
14376
+ .cast-edit-pop {
14377
+ position: fixed;
14378
+ z-index: 9001;
14379
+ width: 340px;
14380
+ max-width: calc(100vw - 32px);
14381
+ max-height: 70vh;
14382
+ display: flex;
14383
+ flex-direction: column;
14384
+ background: var(--panel);
14385
+ border: 0.5px solid var(--line-strong);
14386
+ }
14387
+ .cast-edit-pop .composer-pick-list {
14388
+ flex: 1;
14389
+ overflow-y: auto;
14390
+ }
14391
+ .cast-edit-foot {
14392
+ display: flex;
14393
+ justify-content: flex-end;
14394
+ align-items: center;
14395
+ gap: 8px;
14396
+ padding: 8px 10px;
14397
+ border-top: 0.5px solid var(--line);
14398
+ background: var(--panel-2);
14399
+ }
14400
+ .cast-edit-floor-msg {
14401
+ flex: 1;
14402
+ font-family: var(--mono);
14403
+ font-size: 9px;
14404
+ letter-spacing: 0.06em;
14405
+ color: var(--amber, #B59560);
14406
+ visibility: hidden;
14407
+ }
14408
+ .cast-edit-floor-msg.is-visible { visibility: visible; }
14409
+ .cast-edit-btn {
14410
+ appearance: none;
14411
+ background: transparent;
14412
+ border: 0.5px solid var(--line-bright);
14413
+ color: var(--text);
14414
+ font-family: var(--mono);
14415
+ font-size: 10px;
14416
+ letter-spacing: 0.12em;
14417
+ text-transform: uppercase;
14418
+ padding: 5px 12px;
14419
+ cursor: pointer;
14420
+ transition: color 0.12s, border-color 0.12s, background 0.12s;
14421
+ }
14422
+ .cast-edit-btn:hover { color: var(--lime); border-color: var(--lime); }
14423
+ .cast-edit-btn.is-primary {
14424
+ color: var(--lime);
14425
+ border-color: var(--lime);
14426
+ }
14427
+ .cast-edit-btn.is-primary:hover { background: var(--lime-dim, transparent); }
14428
+ .cast-edit-btn:disabled {
14429
+ color: var(--text-dim);
14430
+ border-color: var(--line);
14431
+ cursor: not-allowed;
14432
+ }
14433
+ .cast-edit-btn:disabled:hover { background: transparent; }
14653
14434
  /* ─── Brief picker popover · the [View Report] click target on
14654
14435
  multi-brief rooms. Anchored under the room-head's button via
14655
14436
  position: fixed and right-aligned so it visually drops out of
@@ -15144,24 +14925,21 @@
15144
14925
  `margin: 0 14px` so the gutters match the live-chat layout. */
15145
14926
  }
15146
14927
 
15147
- /* Chat column wraps the message stream + input bar. */
14928
+ /* Chat column wraps the message stream + input bar. Surface is
14929
+ `--panel-2` — one step warmer than the sidebar's `--panel` —
14930
+ so the content reads as a lifted stage against the sidebar
14931
+ chrome. Sat at `--panel-3` previously which read too pale.
14932
+ #07 gray-step from sidebar is subtle but intentional; floating
14933
+ chrome above the chat (`.input-bar`, `.room-head`,
14934
+ `.speaking-queue` trio) keeps its own frosted-glass treatment,
14935
+ so it still differentiates from the lifted base. */
15148
14936
  .chat-col {
15149
14937
  display: flex;
15150
14938
  flex-direction: column;
15151
14939
  min-height: 0;
15152
14940
  overflow: hidden;
15153
- background: var(--panel);
14941
+ background: var(--panel-2);
15154
14942
  height: 100%;
15155
- /* `position: relative` makes chat-col the CONTAINING BLOCK
15156
- for `.cmp-bg-deco` (positioned absolute) so the deco's
15157
- `left/right: 0` snap to the full chat-col width. NOTE: no
15158
- `isolation` here · we DON'T want chat-col to also be the
15159
- stacking context. The deco's `z-index: -1` is meant to
15160
- paint within `.cmp`'s isolated stacking context (which sits
15161
- on top of chat / chat-col bgs), not get trapped below
15162
- chat-col's own panel bg. Splitting the two roles lets the
15163
- deco stretch wide while still rendering above the chat
15164
- panel — see `.cmp` + `.cmp-bg-deco` below. */
15165
14943
  position: relative;
15166
14944
  }
15167
14945
 
@@ -16293,6 +16071,45 @@
16293
16071
  <!-- Resizable handle: sidebar | main -->
16294
16072
  <div class="col-resizer" data-resize data-var="--sidebar-w" data-side="left" data-min="220" data-max="480" data-i18n-title="col_resize"></div>
16295
16073
 
16074
+ <!-- ─── Mini (collapsed) sidebar · ChatGPT-style icon rail ───
16075
+ Shown ONLY while body.sidebar-collapsed (the full .sidebar is
16076
+ display:none then). Source order matters: it sits between the
16077
+ (hidden) resizer and <main> so grid auto-placement drops it in
16078
+ column 1 and main in column 2 when collapsed.
16079
+ Icon order: logo (→ expand), new room, new agent, search,
16080
+ reports, notes, divider, rooms (→ expand+tab), agents
16081
+ (→ expand+tab). Foot: user avatar (→ settings). New room /
16082
+ agent / search / reports / notes keep the room collapsed (they
16083
+ reuse the same document-delegated data-* triggers the full
16084
+ sidebar uses); only the logo + rooms + agents expand. -->
16085
+ <aside class="mini-sidebar" aria-label="Collapsed sidebar navigation">
16086
+ <div class="mini-top">
16087
+ <button type="button" class="mini-btn mini-logo" data-sidebar-expand data-i18n-tip="sidebar_expand" data-tip="Expand sidebar" aria-label="Expand sidebar">
16088
+ <span class="mini-logo-av">
16089
+ <img class="cl-open" src="/avatars/chair.svg" alt="" aria-hidden="true">
16090
+ <img class="cl-blink" src="/avatars/chair-blink.svg" alt="" aria-hidden="true">
16091
+ </span>
16092
+ </button>
16093
+ <button type="button" class="mini-btn mini-new-room" data-convene-trigger data-i18n-tip="sidebar_new_room" data-tip="New room" aria-label="New room"></button>
16094
+ <button type="button" class="mini-btn mini-new-agent" data-agent-composer-trigger data-i18n-tip="sidebar_new_agent" data-tip="New agent" aria-label="New agent"></button>
16095
+ <button type="button" class="mini-btn mini-search" data-search-trigger data-tip="Search" aria-label="Search"></button>
16096
+ <button type="button" class="mini-btn mini-reports" data-reports-trigger data-i18n-tip="sidebar_all_reports" data-tip="Reports" aria-label="All Reports"></button>
16097
+ <button type="button" class="mini-btn mini-notes" data-notes-trigger data-i18n-tip="sidebar_all_notes" data-tip="Notes" aria-label="All Notes"></button>
16098
+ <span class="mini-sep" aria-hidden="true"></span>
16099
+ <button type="button" class="mini-btn mini-tab" data-mini-tab="rooms" data-i18n-tip="sidebar_tab_rooms" data-tip="Rooms" aria-label="Rooms">
16100
+ <i class="ic ic-rooms"></i>
16101
+ </button>
16102
+ <button type="button" class="mini-btn mini-tab" data-mini-tab="agents" data-i18n-tip="sidebar_tab_agents" data-tip="Agents" aria-label="Agents">
16103
+ <i class="ic ic-agents"></i>
16104
+ </button>
16105
+ </div>
16106
+ <div class="mini-foot">
16107
+ <a href="#" class="mini-btn mini-user" data-user-settings-trigger data-i18n-tip="settings_title" data-tip="Settings" aria-label="Settings">
16108
+ <div class="user-av mini-user-av" data-user-avatar>K</div>
16109
+ </a>
16110
+ </div>
16111
+ </aside>
16112
+
16296
16113
  <!-- ═══════════════ MAIN: room view + agent profile view ═══════════════ -->
16297
16114
  <main class="main">
16298
16115
 
@@ -16724,15 +16541,69 @@
16724
16541
  <div class="notes-page" data-notes-page></div>
16725
16542
  </div>
16726
16543
 
16727
- <!-- Search view · cross-room keyword search results.
16728
- Filled by app.renderSearchPage() on demand. -->
16729
- <div class="main-view" data-main-view="search" hidden>
16730
- <div class="search-page" data-search-page></div>
16731
- </div>
16732
-
16733
-
16734
16544
  </main>
16735
16545
 
16546
+ <!-- Search overlay · floating command-palette-style search.
16547
+ Triggered by `[data-search-trigger]` (sidebar nav) and Cmd+K /
16548
+ Ctrl+K globally. Replaces the old `.main-view[data-main-view=
16549
+ "search"]` page — current room / agent / etc. stay mounted
16550
+ underneath; the overlay layers on top with a scrim. Result row
16551
+ hrefs (`#/r/{roomId}?m={msgId}&q={q}`) reuse the hash-router
16552
+ navigation that the old search page used, so click behavior is
16553
+ unchanged. Esc / scrim-click / X close. -->
16554
+ <div class="search-overlay" data-search-overlay hidden aria-hidden="true">
16555
+ <div class="search-overlay-scrim" data-search-overlay-close></div>
16556
+ <div class="search-overlay-card" role="dialog" aria-modal="true" aria-labelledby="search-overlay-input">
16557
+ <div class="search-overlay-input-row">
16558
+ <svg class="search-overlay-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
16559
+ <circle cx="11" cy="11" r="7"/>
16560
+ <line x1="20" y1="20" x2="16.5" y2="16.5"/>
16561
+ </svg>
16562
+ <input
16563
+ id="search-overlay-input"
16564
+ type="text"
16565
+ class="search-overlay-input"
16566
+ data-search-input
16567
+ data-i18n-placeholder="search_overlay_placeholder"
16568
+ placeholder="Search rooms and messages"
16569
+ autocomplete="off"
16570
+ spellcheck="false"
16571
+ />
16572
+ <button
16573
+ type="button"
16574
+ class="search-overlay-close-btn"
16575
+ data-search-overlay-close
16576
+ data-i18n-aria="search_overlay_close"
16577
+ data-i18n-title="search_overlay_close"
16578
+ aria-label="Close"
16579
+ title="Close"
16580
+ >
16581
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
16582
+ <line x1="18" y1="6" x2="6" y2="18"/>
16583
+ <line x1="6" y1="6" x2="18" y2="18"/>
16584
+ </svg>
16585
+ </button>
16586
+ </div>
16587
+ <div class="search-overlay-body" data-search-results>
16588
+ <!-- Empty state · shown when the query is blank. `runSearch("")`
16589
+ restores this exact HTML; `runSearch("query")` replaces it
16590
+ first with a "searching…" placeholder, then with the
16591
+ `<ul class="search-overlay-list">` of result rows. -->
16592
+ <div class="search-overlay-empty">
16593
+ <svg class="search-overlay-empty-icon" viewBox="0 0 64 64" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
16594
+ <circle cx="27" cy="27" r="17"/>
16595
+ <line x1="50" y1="50" x2="39" y2="39"/>
16596
+ <line x1="21" y1="27" x2="33" y2="27" opacity="0.45"/>
16597
+ <line x1="21" y1="22" x2="28" y2="22" opacity="0.3"/>
16598
+ <line x1="21" y1="32" x2="30" y2="32" opacity="0.3"/>
16599
+ </svg>
16600
+ <div class="search-overlay-empty-text" data-i18n="search_overlay_empty_title">Search across rooms and messages</div>
16601
+ <div class="search-overlay-empty-hint" data-i18n="search_overlay_empty_hint">Type to filter live · click a result to jump</div>
16602
+ </div>
16603
+ </div>
16604
+ </div>
16605
+ </div>
16606
+
16736
16607
  <!-- Floating expand affordance · only visible when the sidebar is
16737
16608
  collapsed (body.sidebar-collapsed). Lives INSIDE .body-grid
16738
16609
  (which is position: relative) so the button's absolute
@@ -16881,6 +16752,21 @@
16881
16752
  clearSidebarPeek();
16882
16753
  return;
16883
16754
  }
16755
+ // Mini-rail rooms / agents · expand the full sidebar. The tab
16756
+ // switch + first-room/agent default is owned by the sidebar-tab
16757
+ // IIFE's click handler (it has ROOMS_KEY / appFirstRoomId /
16758
+ // activate in scope — they're NOT visible here). Both handlers
16759
+ // fire on the same click: this one expands, that one navigates.
16760
+ // The other mini buttons (new room / agent / search / reports /
16761
+ // notes) deliberately do NOT expand — they fire their own
16762
+ // document-delegated triggers and the rail stays collapsed.
16763
+ if (e.target.closest("[data-mini-tab]")) {
16764
+ e.preventDefault();
16765
+ applySidebarCollapsed(false);
16766
+ writeSidebarCollapsed(false);
16767
+ clearSidebarPeek();
16768
+ return;
16769
+ }
16884
16770
  });
16885
16771
 
16886
16772
  // ─── Sidebar edge-peek · Arc-browser style ───
@@ -16930,6 +16816,10 @@
16930
16816
  // miss `.sidebar` during the open animation. Immediate close on
16931
16817
  // exit is handled by the in→out transition branch below, which
16932
16818
  // bypasses this lock.
16819
+ // The floated full sidebar is opaque (--sidebar-bg) at 280px and
16820
+ // pins to left:0, so it cleanly covers the 56px mini rail while
16821
+ // peeked — hovering the left edge reveals the full sidebar, and
16822
+ // moving off it drops back to the rail.
16933
16823
  setPeek(true);
16934
16824
  peekLockedUntil = Date.now() + PEEK_OPEN_LOCK_MS;
16935
16825
  }
@@ -17186,17 +17076,6 @@
17186
17076
  }
17187
17077
  return;
17188
17078
  }
17189
- // "search" → cross-room keyword search view. Restores on
17190
- // refresh same as reports / notes — without this branch
17191
- // the saved token fell through to the roomId resolver,
17192
- // failed `appRoomExists("search")`, and bounced the user
17193
- // back to the new-room composer.
17194
- if (sub === "search") {
17195
- if (window.app && typeof window.app.openSearch === "function") {
17196
- window.app.openSearch();
17197
- }
17198
- return;
17199
- }
17200
17079
  if (!sub || sub === "new") {
17201
17080
  if (window.app && typeof window.app.setComposerMode === "function") {
17202
17081
  window.app.setComposerMode("room");
@@ -17299,9 +17178,9 @@
17299
17178
  // when there's no saved sub-state at all.
17300
17179
  const saved = lsGet(ROOMS_KEY);
17301
17180
  let target;
17302
- if (saved && saved !== "new" && saved !== "reports" && saved !== "notes" && saved !== "search") {
17181
+ if (saved && saved !== "new" && saved !== "reports" && saved !== "notes") {
17303
17182
  target = saved; // explicit room id
17304
- } else if (saved === "reports" || saved === "notes" || saved === "new" || saved === "search") {
17183
+ } else if (saved === "reports" || saved === "notes" || saved === "new") {
17305
17184
  target = saved; // explicit destination preserved on any nav
17306
17185
  } else if (fromTabClick) {
17307
17186
  // No saved sub-state · fresh user clicking the tab. Prefer
@@ -17356,6 +17235,34 @@
17356
17235
  tracker. These don't conflict with stopPropagation because they
17357
17236
  match elements that no other capture handler swallows. */
17358
17237
  document.addEventListener("click", (e) => {
17238
+ // Mini-rail rooms / agents switcher · expand is handled by the
17239
+ // collapse IIFE; here we own the tab switch + default selection.
17240
+ // Seed the FIRST room / agent when coming from a non-room /
17241
+ // non-agent context (reports, notes, composer, the other tab) so
17242
+ // the icon actually lands you on content instead of restoring the
17243
+ // last sub-state (e.g. "new" / "reports"). If you're already
17244
+ // viewing a room / agent profile, leave it so expand keeps place.
17245
+ const miniTab = e.target.closest("[data-mini-tab]");
17246
+ if (miniTab) {
17247
+ e.preventDefault();
17248
+ const which = miniTab.getAttribute("data-mini-tab");
17249
+ if (which === "rooms") {
17250
+ if (!window.app || !window.app.currentRoomId) {
17251
+ const first = appFirstRoomId();
17252
+ if (first) lsSet(ROOMS_KEY, first);
17253
+ }
17254
+ } else if (which === "agents") {
17255
+ const agentView = document.querySelector('[data-main-view="agent"]');
17256
+ const inAgentProfile = agentView && !agentView.hasAttribute("hidden");
17257
+ if (!inAgentProfile) {
17258
+ const firstAgent = document.querySelector('[data-sidebar-panel="agents"] .agent-row[data-agent-profile]');
17259
+ const id = firstAgent && firstAgent.getAttribute("data-agent-profile");
17260
+ if (id) lsSet(AGENTS_KEY, id);
17261
+ }
17262
+ }
17263
+ activate(which, { fromTabClick: true });
17264
+ return;
17265
+ }
17359
17266
  // Tab click
17360
17267
  const tab = e.target.closest(".sidebar-tab[data-sidebar-tab]");
17361
17268
  if (tab) {
@@ -17391,11 +17298,12 @@
17391
17298
  }
17392
17299
  return;
17393
17300
  }
17394
- // "Search" trigger · cross-room keyword search view.
17301
+ // "Search" trigger · floating search overlay. No `lsSet`
17302
+ // persistence — the overlay is a transient lens, not a
17303
+ // sub-state worth restoring across reloads.
17395
17304
  const searchBtn = e.target.closest("[data-search-trigger]");
17396
17305
  if (searchBtn) {
17397
17306
  e.preventDefault();
17398
- lsSet(ROOMS_KEY, "search");
17399
17307
  if (window.app && typeof window.app.openSearch === "function") {
17400
17308
  window.app.openSearch();
17401
17309
  }