privateboard 0.1.8 → 0.1.10

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
@@ -166,13 +166,22 @@
166
166
  .body-grid {
167
167
  display: grid;
168
168
  grid-template-columns: var(--sidebar-w) 8px 1fr;
169
+ grid-template-rows: 1fr;
169
170
  gap: 0;
170
171
  overflow: hidden;
171
172
  min-height: 0;
172
173
  /* `position: relative` so the floating sidebar-expand-btn
173
174
  (absolute-positioned) anchors to the body-grid's top-left
174
175
  edge — which, after the topbar's retirement, is the viewport's
175
- top-left under the 8px page padding. */
176
+ top-left under the 8px page padding.
177
+ `grid-template-rows: 1fr` is the load-bearing fix that lets
178
+ the height chain reach all the way down to `[data-chat-messages]`.
179
+ Without it the implicit row is `auto` (= content height), so
180
+ `.main`/`.main-view`/`.body`/`.chat-col`/`.chat` all collapse
181
+ to content size — and any centring rule on `.chat` has no free
182
+ space to work with. With `1fr`, every flex/grid layer below
183
+ inherits a viewport-relative height and the composer can
184
+ actually centre vertically inside `.chat`. */
176
185
  position: relative;
177
186
  }
178
187
 
@@ -559,19 +568,9 @@
559
568
  overflow: hidden;
560
569
  text-overflow: ellipsis;
561
570
  }
562
- /* Time tag · same register as `.row-time` in the rooms list
563
- (10.5px mono, text-dim) so both lists read as one consistent
564
- "title · time" rhythm. Earlier this was 9px uppercase faint —
565
- visually a different "kind of metadata" from the rooms time. */
566
- .agent-row .agent-row-time {
567
- color: var(--text-dim);
568
- font-size: 10.5px;
569
- font-family: var(--mono);
570
- letter-spacing: 0;
571
- text-transform: none;
572
- white-space: nowrap;
573
- flex-shrink: 0;
574
- }
571
+ /* Time tag removed · the agent-row no longer surfaces creation
572
+ time. The space is given back to the title (which can now use
573
+ the full row width minus the hover-revealed pin button). */
575
574
  .agent-row .agent-row-subtitle {
576
575
  font-family: var(--mono);
577
576
  font-size: 9.5px;
@@ -647,24 +646,50 @@
647
646
  align-items: center;
648
647
  justify-content: center;
649
648
  }
650
- /* CHAIR badge · outlined hairline pill, cyan accent only · this is
651
- the one place the chair's color signal lives. Quieter than the
652
- prior solid-cyan-fill which painted itself across the row. */
649
+ /* CHAIR badge · pure typographic kicker, no pill geometry.
650
+ Earlier this was an outlined hairline pill (cyan border + cyan
651
+ text + transparent fill) the only colored hairline box
652
+ anywhere on the page, so it read as a sticker rather than part
653
+ of the design system. The refined treatment drops border /
654
+ background / padding / radius and keeps the chair's identity
655
+ signal in pure typography:
656
+ · mono uppercase, generous letter-spacing (matches section
657
+ headers, brief banner kickers, time tags)
658
+ · cyan color (the chair's accent stays)
659
+ · em-dash sigil prefix separates it from the title — soft
660
+ enough to feel like part of the same line, distinct enough
661
+ that the eye lands on it as a label, not a name fragment
662
+ · weight 600 (one notch below 700) so it doesn't compete
663
+ with the title's emphasis
664
+ The avatar already carries the `⊘` immutability cue and the
665
+ section header already says "Chair", so this badge is now the
666
+ quietest possible identity stamp instead of a third tier of
667
+ decoration. */
653
668
  .agent-row .agent-row-chair-badge {
654
669
  font-family: var(--mono);
655
- font-size: 8.5px;
656
- font-weight: 700;
657
- letter-spacing: 0.18em;
670
+ font-size: 9px;
671
+ font-weight: 600;
672
+ letter-spacing: 0.2em;
658
673
  text-transform: uppercase;
659
- padding: 1px 6px;
660
- color: var(--cyan, #6A9B97);
674
+ /* Track the active theme's primary accent — `--lime` remaps per
675
+ theme (gold in regent, blue in apple, red in pinterest,
676
+ magenta in amuse, etc.). The earlier `--cyan` was static and
677
+ clashed in themes whose palette didn't include teal. */
678
+ color: var(--lime);
661
679
  background: transparent;
662
- border: 0.5px solid var(--cyan, #6A9B97);
663
- border-radius: 2px;
680
+ border: none;
681
+ padding: 0;
664
682
  white-space: nowrap;
665
683
  align-self: center;
666
684
  margin-left: auto;
667
685
  }
686
+ .agent-row .agent-row-chair-badge::before {
687
+ content: "—";
688
+ color: var(--text-faint);
689
+ margin-right: 8px;
690
+ font-weight: 400;
691
+ letter-spacing: 0;
692
+ }
668
693
  .agent-row .agent-row-chair-role { color: var(--text-soft); font-weight: 600; }
669
694
  .agent-row .agent-row-chair-note { color: var(--text-faint); }
670
695
  /* Hide the Chair section's count badge — there's only ever one. */
@@ -899,13 +924,10 @@
899
924
  .agents-section-header.pinned { color: var(--text-soft); }
900
925
 
901
926
  /* ─── Pin toggle (per-row · hover-revealed for non-pinned) ───
902
- Positioned absolutely so the in-flow time tag (.agent-row-time /
903
- .row-time) sits flush against the right edge — same as the rooms
904
- list. Earlier the pin took 22px (18 + 4 margin) of in-flow width
905
- even when invisible (opacity:0 still occupies space), which shoved
906
- the agent's "7h" tag inward compared to the rooms list and made
907
- the two columns of timestamps look mis-aligned. The row-shell
908
- ancestor needs `position: relative` for this to anchor correctly. */
927
+ Positioned absolutely so it overlays the right edge of the row
928
+ without taking in-flow width (opacity:0 alone would still
929
+ reserve 22px and shrink the title). The row-shell ancestor
930
+ needs `position: relative` for this to anchor correctly. */
909
931
  .agent-row-shell, .session-row-shell { position: relative; }
910
932
  .row-top-line .pin-toggle,
911
933
  .agent-row-top-line .pin-toggle {
@@ -933,10 +955,6 @@
933
955
  }
934
956
  .session-row:hover .pin-toggle,
935
957
  .agent-row:hover .pin-toggle { opacity: 0.55; }
936
- /* Hover · hide the in-flow time so the absolutely-positioned pin
937
- button has the right edge to itself. Same pattern as the rooms
938
- list (`.session-row-shell:hover .row-time { visibility: hidden }`). */
939
- .agent-row-shell:hover .agent-row-time { visibility: hidden; }
940
958
  .pin-toggle:hover { opacity: 1 !important; color: var(--lime); }
941
959
  .session-row.pinned .pin-toggle,
942
960
  .agent-row.pinned .pin-toggle {
@@ -996,10 +1014,8 @@
996
1014
  z-index: 2;
997
1015
  }
998
1016
  .session-row-shell:hover .row-delete { display: flex; }
999
- /* On hover the timestamp hides so the delete button doesn't double
1000
- up. The subtitle reserves right padding so its ellipsis stops
1001
- before the X. */
1002
- .session-row-shell:hover .row-time { visibility: hidden; }
1017
+ /* Subtitle reserves right padding so its ellipsis stops before
1018
+ the absolutely-positioned X delete button on hover. */
1003
1019
  .session-row-shell .row-subtitle { padding-right: 26px; }
1004
1020
  .row-delete:hover {
1005
1021
  border-color: var(--red);
@@ -1088,17 +1104,14 @@
1088
1104
  font-family: var(--sans);
1089
1105
  letter-spacing: -0.005em;
1090
1106
  }
1091
- .row-time {
1092
- font-size: 10.5px;
1093
- color: var(--text-dim);
1094
- flex-shrink: 0;
1095
- font-family: var(--mono);
1096
- }
1097
- .session-row-shell[data-status="live"] .row-time {
1107
+ /* Live indicator · was a `● ` prefix on the (now-removed) .row-time
1108
+ tag. Migrated to a title prefix so live rooms still read at a
1109
+ glance. The rest of the title stays its usual colour. */
1110
+ .session-row-shell[data-status="live"] .row-title::before {
1111
+ content: "● ";
1098
1112
  color: var(--lime);
1099
1113
  font-weight: 700;
1100
1114
  }
1101
- .session-row-shell[data-status="live"] .row-time::before { content: "● "; }
1102
1115
 
1103
1116
  .row-subtitle {
1104
1117
  font-size: 11.5px;
@@ -1262,6 +1275,7 @@
1262
1275
  grid-template-rows: auto 1fr auto;
1263
1276
  overflow: hidden;
1264
1277
  min-height: 0;
1278
+ height: 100%;
1265
1279
  }
1266
1280
  .main-view[data-main-view="agent"] {
1267
1281
  /* Switch from grid to block so the profile card flows naturally
@@ -1514,6 +1528,18 @@
1514
1528
  color: var(--text-faint);
1515
1529
  letter-spacing: 0.06em;
1516
1530
  }
1531
+ /* Report-mode type chip in the kicker line · "REPORT" / "BENTO" /
1532
+ "MAGAZINE" / "NEWSPAPER". Mono register matching the kicker.
1533
+ Slight color shift per mode so the type is glanceable without
1534
+ being a heavy badge. */
1535
+ .reports-item-type {
1536
+ color: var(--text-soft);
1537
+ letter-spacing: 0.16em;
1538
+ font-weight: 700;
1539
+ }
1540
+ .reports-item-type[data-mode="magazine"] { color: var(--amber, #C8A36F); }
1541
+ .reports-item-type[data-mode="newspaper"] { color: var(--text); }
1542
+ .reports-item-type[data-mode="ppt"] { color: var(--cyan, #6FC3C8); }
1517
1543
  .reports-item-sep {
1518
1544
  color: var(--text-faint);
1519
1545
  opacity: 0.7;
@@ -1790,8 +1816,7 @@
1790
1816
  .notes-item-passage { padding-right: 30px; }
1791
1817
  /* On hover · hide the trailing time tag so the delete button
1792
1818
  occupies the right-edge slot cleanly. visibility:hidden keeps
1793
- the layout box (the meta row doesn't reflow either). Same
1794
- pattern as `.session-row-shell:hover .row-time` in rooms. */
1819
+ the layout box (the meta row doesn't reflow either). */
1795
1820
  .notes-item:hover .notes-item-time { visibility: hidden; }
1796
1821
  .notes-item-link {
1797
1822
  display: block;
@@ -2117,15 +2142,17 @@
2117
2142
  .notes-item-subject { max-width: 160px; }
2118
2143
  }
2119
2144
 
2120
- /* Room head — generously padded so the title has breathing
2121
- room above and below. */
2145
+ /* Room head — compact register · the title is the only
2146
+ vertical anchor users actually read here, so we keep the
2147
+ padding moderately tight while still leaving the kicker +
2148
+ title pair some breathing room. */
2122
2149
  .room-head {
2123
2150
  background: var(--panel-2);
2124
2151
  border-bottom: 0.5px solid var(--line-bright);
2125
- padding: 16px 18px;
2152
+ padding: 11px 16px;
2126
2153
  display: grid;
2127
2154
  grid-template-columns: 1fr auto;
2128
- gap: 14px;
2155
+ gap: 12px;
2129
2156
  align-items: center;
2130
2157
  }
2131
2158
  /* When the sidebar is collapsed the room-head gains a leading
@@ -2144,36 +2171,51 @@
2144
2171
  at this level. */
2145
2172
  .room-info { min-width: 0; overflow: visible; }
2146
2173
 
2147
- .room-id {
2174
+ /* ─── Compact kicker · single mono line at top of room-head ───
2175
+ Replaces the prior `.room-id` row (`MEETING ROOM` badge +
2176
+ `// ROOM #N · TONE`) AND the `.room-meta` row (tone/intensity/
2177
+ report tagged pills + status stamp). Net: three stacked rows
2178
+ collapse into one. Tone / intensity / brief-style remain
2179
+ editable in the room-settings overlay. */
2180
+ .room-kicker {
2148
2181
  display: flex;
2149
2182
  align-items: center;
2150
- gap: 8px;
2151
- margin-bottom: 6px;
2152
- }
2153
- .room-name {
2183
+ gap: 6px;
2184
+ font-family: var(--mono);
2154
2185
  font-size: 9.5px;
2155
- color: var(--bg);
2156
- background: var(--lime);
2157
- padding: 1px 7px;
2186
+ letter-spacing: 0.12em;
2158
2187
  text-transform: uppercase;
2159
- letter-spacing: 0.1em;
2188
+ color: var(--text-dim);
2189
+ margin-bottom: 5px;
2190
+ flex-wrap: wrap;
2160
2191
  font-weight: 700;
2192
+ line-height: 1.3;
2161
2193
  }
2162
- .session-num {
2163
- font-size: 9.5px;
2164
- color: var(--text-dim);
2165
- text-transform: uppercase;
2166
- letter-spacing: 0.08em;
2194
+ .kicker-num { color: var(--text-soft); }
2195
+ .kicker-tone { color: var(--lime); cursor: help; }
2196
+ .kicker-intensity { color: var(--amber); }
2197
+ .kicker-sep { color: var(--text-faint); opacity: 0.6; }
2198
+ .kicker-status { display: inline-flex; align-items: center; gap: 5px; }
2199
+ .kicker-status::before {
2200
+ font-size: 11px;
2201
+ line-height: 1;
2202
+ display: inline-block;
2167
2203
  }
2204
+ .kicker-status.status-live { color: var(--lime); }
2205
+ .kicker-status.status-live::before { content: "●"; font-size: 7px; animation: pulse 1.5s infinite; }
2206
+ .kicker-status.status-paused { color: var(--amber); }
2207
+ .kicker-status.status-paused::before { content: "❚❚"; font-size: 9px; letter-spacing: -0.1em; }
2208
+ .kicker-status.status-adjourned { color: var(--text-dim); }
2209
+ .kicker-status.status-adjourned::before { content: "▣"; font-size: 10px; }
2168
2210
 
2169
2211
  .room-subject {
2170
- font-size: 17px;
2212
+ font-size: 14px;
2171
2213
  font-weight: 600;
2172
2214
  color: var(--text);
2173
2215
  line-height: 1.3;
2174
- margin-bottom: 10px;
2216
+ margin-bottom: 0;
2175
2217
  font-family: var(--sans);
2176
- letter-spacing: -0.012em;
2218
+ letter-spacing: -0.005em;
2177
2219
  overflow: hidden;
2178
2220
  text-overflow: ellipsis;
2179
2221
  white-space: nowrap;
@@ -2262,64 +2304,18 @@
2262
2304
  to { opacity: 1; transform: translateY(0); }
2263
2305
  }
2264
2306
 
2265
- /* ─── Room subtitle / meta line ───
2266
- Compact tag row beneath the room subject. */
2267
- .room-meta {
2268
- display: flex;
2269
- align-items: center;
2270
- flex-wrap: wrap;
2271
- gap: 6px 8px;
2272
- font-family: var(--mono);
2273
- font-size: 10px;
2274
- color: var(--text-dim);
2275
- line-height: 1.4;
2276
- }
2277
- .room-meta .meta-stamp {
2278
- display: inline-flex;
2279
- align-items: center;
2280
- gap: 5px;
2281
- padding: 3px 8px;
2282
- border: 0.5px solid var(--lime-dim);
2283
- background: var(--panel);
2284
- color: var(--lime);
2285
- font-weight: 700;
2286
- font-size: 9.5px;
2287
- letter-spacing: 0.06em;
2288
- text-transform: lowercase;
2289
- }
2290
- .room-meta .meta-stamp::before {
2291
- content: "●";
2292
- font-size: 7px;
2293
- line-height: 1;
2294
- color: var(--lime);
2295
- animation: pulse 1.5s infinite;
2296
- }
2297
- html[data-status="paused"] .room-meta .meta-stamp {
2298
- color: var(--amber);
2299
- border-color: var(--amber-dim);
2300
- }
2301
- html[data-status="paused"] .room-meta .meta-stamp::before {
2302
- content: "❚❚";
2303
- color: var(--amber);
2304
- animation: none;
2305
- }
2306
- html[data-status="adjourned"] .room-meta .meta-stamp {
2307
- color: var(--text-soft);
2308
- border-color: var(--line-bright);
2309
- }
2310
- html[data-status="adjourned"] .room-meta .meta-stamp::before {
2311
- content: "▣";
2312
- color: var(--text-soft);
2313
- animation: none;
2314
- }
2307
+ /* `.room-meta` / `.meta-stamp` REMOVED. The tone/intensity/
2308
+ report tags + status stamp that lived in this row are now
2309
+ part of `.room-kicker` (single mono line above the subject).
2310
+ Tone / intensity / brief style remain editable in the
2311
+ room-settings overlay. */
2315
2312
 
2316
2313
  .head-actions { display: flex; align-items: center; gap: 6px; }
2317
2314
  .head-cast { display: flex; align-items: center; }
2318
- /* Avatar size matches the settings icon (26px square) so the row of
2319
- director portraits, the count tile, and the button all read as
2320
- one toolbar at consistent height. */
2315
+ /* Avatar size aligned with the new compact header rhythm (22px) ·
2316
+ keeps the toolbar height in proportion to the 14px title. */
2321
2317
  .head-cast img {
2322
- width: 26px; height: 26px;
2318
+ width: 22px; height: 22px;
2323
2319
  image-rendering: pixelated;
2324
2320
  image-rendering: crisp-edges;
2325
2321
  border: 0.5px solid var(--line-strong);
@@ -2334,9 +2330,9 @@
2334
2330
  /* Stacked count tile · sits at the end of the avatar cascade and
2335
2331
  matches the avatar height so the row stays flush. */
2336
2332
  .head-cast .cast-count {
2337
- height: 26px;
2338
- min-width: 26px;
2339
- padding: 0 6px;
2333
+ height: 22px;
2334
+ min-width: 22px;
2335
+ padding: 0 5px;
2340
2336
  background: var(--panel-3);
2341
2337
  color: var(--text);
2342
2338
  border: 0.5px solid var(--lime-dim);
@@ -2345,7 +2341,7 @@
2345
2341
  align-items: center;
2346
2342
  justify-content: center;
2347
2343
  font-family: var(--mono);
2348
- font-size: 11px;
2344
+ font-size: 10px;
2349
2345
  font-weight: 700;
2350
2346
  letter-spacing: 0;
2351
2347
  }
@@ -2760,7 +2756,7 @@
2760
2756
  text-transform: uppercase;
2761
2757
  color: var(--lime);
2762
2758
  }
2763
- /* Mode chip · "Report" / "Bento" / "Magazine" / "Newspaper".
2759
+ /* Mode chip · "Report" / "Magazine" / "Newspaper".
2764
2760
  Sits between the // report kicker and the FILED stamp, styled as
2765
2761
  a hairline mono pill so the user sees the report type at a glance
2766
2762
  without it competing with the kicker for primary emphasis.
@@ -3605,13 +3601,56 @@
3605
3601
  margin-bottom: 18px;
3606
3602
  font-family: var(--mono);
3607
3603
  }
3604
+ /* Subject · clamp to 2 lines by default. The Show more / less
3605
+ toggle is rendered hidden and unhidden by the follow-up
3606
+ overlay's post-mount measurement only when the text actually
3607
+ overflows. Mirrors the adjourn-overlay / room-settings clamp
3608
+ pattern so all three modals read identically. */
3608
3609
  .followup-parent-subject {
3609
3610
  font-size: 13px;
3610
3611
  font-weight: 600;
3611
3612
  color: var(--text);
3612
3613
  line-height: 1.4;
3613
3614
  margin-bottom: 4px;
3615
+ word-break: break-word;
3616
+ }
3617
+ .followup-parent-subject.is-clamped {
3618
+ display: -webkit-box;
3619
+ -webkit-line-clamp: 2;
3620
+ -webkit-box-orient: vertical;
3621
+ overflow: hidden;
3622
+ }
3623
+ /* Meta row · single line that pairs the Show more toggle (left,
3624
+ when present) with the adjourned-time / brief-count meta
3625
+ (right). `margin-left: auto` on the meta keeps it pinned to
3626
+ the right edge regardless of whether the toggle is shown, so
3627
+ the meta doesn't shift between states. */
3628
+ .followup-parent-meta-row {
3629
+ display: flex;
3630
+ align-items: baseline;
3631
+ gap: 12px;
3632
+ margin-bottom: 4px;
3633
+ }
3634
+ .followup-parent-meta-row .followup-parent-meta {
3635
+ margin-left: auto;
3636
+ margin-bottom: 0;
3614
3637
  }
3638
+ .followup-parent-subject-toggle {
3639
+ appearance: none;
3640
+ background: transparent;
3641
+ border: none;
3642
+ font-family: var(--mono);
3643
+ font-size: 9.5px;
3644
+ letter-spacing: 0.14em;
3645
+ text-transform: uppercase;
3646
+ color: var(--text-soft);
3647
+ cursor: pointer;
3648
+ padding: 0;
3649
+ transition: color 0.12s;
3650
+ }
3651
+ .followup-parent-subject-toggle:hover { color: var(--lime); }
3652
+ .followup-parent-subject-toggle::before { content: "[ "; color: var(--text-faint); }
3653
+ .followup-parent-subject-toggle::after { content: " ]"; color: var(--text-faint); }
3615
3654
  .followup-parent-meta {
3616
3655
  font-size: 10px;
3617
3656
  color: var(--text-soft);
@@ -4175,12 +4214,29 @@
4175
4214
  html[data-status="adjourned"] .paused-bar { display: none; }
4176
4215
 
4177
4216
  /* Body · single chat column. Live-notes panel was removed, so there's
4178
- no right-side resizer or aside to apportion space for. */
4217
+ no right-side resizer or aside to apportion space for.
4218
+ `grid-template-rows: 1fr` is required so the implicit row stretches
4219
+ to body's height instead of collapsing to content height. Without
4220
+ it, .chat-col would be only as tall as its visible children, and
4221
+ .chat (`flex: 1 1 auto`) would have nothing to grow into — making
4222
+ [data-chat-messages] sit flush at the top of a content-sized
4223
+ chat. With `1fr`, .chat-col fills body, .chat fills chat-col, and
4224
+ vertical centring of the composer inside .chat actually has free
4225
+ space to work with. */
4179
4226
  .body {
4180
4227
  display: grid;
4181
4228
  grid-template-columns: 1fr;
4229
+ grid-template-rows: 1fr;
4182
4230
  overflow: hidden;
4183
4231
  min-height: 0;
4232
+ height: 100%;
4233
+ /* Pin to .main-view's 1fr row · without this, when room-head is
4234
+ :empty + display:none (composer mode), .body becomes the first
4235
+ non-hidden grid child and auto-flows into row 1 (auto), where
4236
+ it collapses to content height. Vertical centring then has no
4237
+ free space. Explicit grid-row: 2 keeps .body in the 1fr row
4238
+ regardless of whether room-head is visible. */
4239
+ grid-row: 2;
4184
4240
  }
4185
4241
 
4186
4242
  /* Chat (zen plain text) */
@@ -7022,14 +7078,76 @@
7022
7078
  edge so the page has one clear focal point. Refined: generous
7023
7079
  vertical rhythm, single-accent colour discipline, mono micro-type
7024
7080
  paired with serif-leaning sans display. ─── */
7081
+ /* Composer (new-room AND new-agent) shared rhythm.
7082
+ Vertical centring is split across two modes:
7083
+ · Default (`.chat.chat--composer`, content fits viewport) ·
7084
+ the chat is `display: grid; align-content: center` so the
7085
+ whole .cmp block sits in the visual centre.
7086
+ · Overflow (`.chat.chat--composer.chat--composer-overflow`,
7087
+ JS-toggled when `cmp.scrollHeight > chat.clientHeight`) ·
7088
+ the chat switches to block flow + scroll, .cmp-fold (hero
7089
+ + input) is min-height ≈ 100vh with content centred inside,
7090
+ and .cmp-starters flow below as scrollable extras. Title +
7091
+ input stay visually centred on initial paint regardless of
7092
+ viewport height; starter cards live below the fold.
7093
+ Padding stays modest because vertical position no longer comes
7094
+ from top padding; it's just breathing room around the cmp box.
7095
+ Keep both composers in lock-step by editing here only. */
7025
7096
  .cmp {
7026
7097
  max-width: 760px;
7027
7098
  margin: 0 auto;
7028
- padding: 88px 32px 96px;
7099
+ padding: 32px 32px;
7100
+ width: 100%;
7101
+ }
7102
+ /* Default composer mode (content fits viewport).
7103
+ Toggled by JS via `.chat--composer` (added in
7104
+ renderEmptyState; removed in renderChat). The
7105
+ `.chat--composer-overflow` variant below handles the case
7106
+ where the composer is taller than .chat. */
7107
+ .chat.chat--composer {
7108
+ /* Grid centring · `align-content: center` is the most reliable
7109
+ cross-browser way to vertically centre a grid track. Beats
7110
+ flex auto-margins (which can resolve to 0 if any parent in
7111
+ the chain doesn't propagate height correctly) and beats
7112
+ `justify-content: safe center` (the `safe` keyword can
7113
+ silently invalidate the whole declaration in older browsers).
7114
+ When content overflows, the grid item still scrolls naturally
7115
+ inside .chat's `overflow-y: auto`. */
7116
+ display: grid !important;
7117
+ align-content: center !important;
7118
+ justify-items: stretch !important;
7119
+ }
7120
+ .chat.chat--composer > [data-chat-messages] {
7121
+ width: 100% !important;
7122
+ }
7123
+ .chat.chat--composer > [data-brief-card] {
7124
+ /* In composer mode the brief-card is empty · hide it so it
7125
+ doesn't claim a grid row that throws off the centring. */
7126
+ display: none !important;
7127
+ }
7128
+ /* Overflow mode · when .cmp's natural height exceeds .chat's height,
7129
+ centring the entire composer block would clip the title above the
7130
+ scroll area. Instead, push the .cmp's top down with a viewport-
7131
+ relative padding-top so hero + input sit at the visual centre of
7132
+ the viewport on initial paint; .cmp-starters fall below the input
7133
+ in normal document flow with the SAME spacing as the fits case
7134
+ (no `justify-content: center` void zone). Approximation: hero +
7135
+ input combined ≈ 220px tall, so half ≈ 110px; padding-top of
7136
+ `50vh - 110px` puts the hero+input vertical midpoint at 50vh.
7137
+ `max(32px, …)` keeps the original padding floor on tiny viewports
7138
+ where the calc would go negative.
7139
+ Toggled by JS (updateComposerOverflow) on render + resize. */
7140
+ .chat.chat--composer.chat--composer-overflow {
7141
+ display: block !important;
7142
+ align-content: initial !important;
7143
+ overflow-y: auto;
7144
+ }
7145
+ .chat.chat--composer.chat--composer-overflow .cmp {
7146
+ padding-top: max(32px, calc(50vh - 110px));
7029
7147
  }
7030
7148
  .cmp-hero {
7031
7149
  text-align: center;
7032
- margin-bottom: 36px;
7150
+ margin-bottom: 22px;
7033
7151
  }
7034
7152
  .cmp-greet {
7035
7153
  font-family: var(--mono);
@@ -7038,11 +7156,11 @@
7038
7156
  text-transform: uppercase;
7039
7157
  color: var(--text-faint);
7040
7158
  font-weight: 700;
7041
- margin-bottom: 22px;
7159
+ margin-bottom: 14px;
7042
7160
  }
7043
7161
  .cmp-prompt {
7044
7162
  font-family: var(--font-human);
7045
- font-size: 32px;
7163
+ font-size: 28px;
7046
7164
  font-weight: 600;
7047
7165
  color: var(--text);
7048
7166
  line-height: 1.2;
@@ -7087,7 +7205,13 @@
7087
7205
  }
7088
7206
 
7089
7207
  /* Toolbar inside input frame · cast on left, tune in the middle,
7090
- convene on the right. Hairline divider above. */
7208
+ convene on the right. Hairline divider above. min-height pinned
7209
+ to the room composer's natural height (driven by the 22px cast
7210
+ avatars + button padding) so the agent composer's toolbar — which
7211
+ has no avatar-bearing button — grows to match. Without this the
7212
+ agent input frame is a few pixels shorter than the room frame.
7213
+ Box-sizing is border-box (universal in this project), so the
7214
+ min-height includes the 8px+8px vertical padding. */
7091
7215
  .cmp-toolbar {
7092
7216
  display: flex;
7093
7217
  align-items: center;
@@ -7095,6 +7219,7 @@
7095
7219
  padding: 8px 8px 8px 14px;
7096
7220
  border-top: 0.5px solid var(--line);
7097
7221
  background: var(--panel-2);
7222
+ min-height: 48px;
7098
7223
  }
7099
7224
 
7100
7225
  .cmp-cast-btn {
@@ -7430,7 +7555,7 @@
7430
7555
  accent borders. Just hairline-separated rows that brighten on
7431
7556
  hover, with the arrow turning lime — the only visual reward. */
7432
7557
  .cmp-starters {
7433
- margin-top: 56px;
7558
+ margin-top: 28px;
7434
7559
  }
7435
7560
  .cmp-starters-rule {
7436
7561
  display: flex;
@@ -7461,7 +7586,7 @@
7461
7586
  gap: 16px;
7462
7587
  align-items: center;
7463
7588
  width: 100%;
7464
- padding: 14px 4px;
7589
+ padding: 10px 4px;
7465
7590
  background: transparent;
7466
7591
  border: none;
7467
7592
  border-bottom: 0.5px solid var(--line);
@@ -7507,7 +7632,10 @@
7507
7632
  /* ─── New-agent composer · variant of the new-room composer.
7508
7633
  Reuses .cmp / .cmp-* classes for the hero + input frame; adds
7509
7634
  ag-cmp-* for agent-specific bits and ag-prev-* for the editable
7510
- spec preview card that appears after AI generation. ─── */
7635
+ spec preview card that appears after AI generation. The vertical
7636
+ rhythm (padding, hero/starter margins, starter card density) is
7637
+ SHARED with the new-room composer via the base .cmp rules — so
7638
+ edits land in one place and both composers stay in sync. ─── */
7511
7639
  .ag-cmp .cmp-toolbar {
7512
7640
  /* Model + manual buttons sit at the left, Generate hugs the right
7513
7641
  via margin-left: auto on .cmp-go. No justify-content override
@@ -8638,12 +8766,12 @@
8638
8766
 
8639
8767
  /* Long-opener clamp · when the user wrote more than a sentence of
8640
8768
  context, the card would otherwise dominate the viewport. We
8641
- clamp the body to ~2.5 lines with a fade-out overlay; a
8769
+ clamp the body to 2 lines with a fade-out overlay; a
8642
8770
  `<button data-convene-toggle>` below toggles `.expanded` on the
8643
- opener parent to reveal the rest. 2.5 × line-height (1.32 ×
8644
- 19px ≈ 25px) = 63px. */
8771
+ opener parent to reveal the rest. 2 × line-height (1.32 × 19px
8772
+ ≈ 25px) = 50px. */
8645
8773
  .convene-opener-clamped:not(.expanded) .convene-body {
8646
- max-height: 63px;
8774
+ max-height: 50px;
8647
8775
  overflow: hidden;
8648
8776
  position: relative;
8649
8777
  }
@@ -8653,7 +8781,7 @@
8653
8781
  left: 0;
8654
8782
  right: 0;
8655
8783
  bottom: 0;
8656
- height: 32px;
8784
+ height: 24px;
8657
8785
  background: linear-gradient(to bottom, transparent, var(--panel-2));
8658
8786
  pointer-events: none;
8659
8787
  }
@@ -8956,6 +9084,7 @@
8956
9084
  min-height: 0;
8957
9085
  overflow: hidden;
8958
9086
  background: var(--panel);
9087
+ height: 100%;
8959
9088
  }
8960
9089
 
8961
9090
  /* Input bar — sits inside the chat column. Live state hosts a
@@ -9577,13 +9706,27 @@
9577
9706
  io.observe(hint);
9578
9707
  })();
9579
9708
 
9580
- /* ─── Pin toggle (visual demo) ─── */
9709
+ /* ─── Pin toggle ───
9710
+ Two paths: agent rows persist via PATCH /api/agents/:id { isPinned }
9711
+ through `app.togglePinAgent`, which mutates local state and
9712
+ re-renders the sidebar so the row moves into the Pinned bucket.
9713
+ Session (room) rows fall through to a visual-only toggle for now —
9714
+ room pinning isn't a backend feature yet, so the class flip is a
9715
+ placeholder that will go away once it lands. */
9581
9716
  (function () {
9582
9717
  document.addEventListener("click", (e) => {
9583
9718
  const btn = e.target.closest("[data-pin-toggle]");
9584
9719
  if (!btn) return;
9585
9720
  e.preventDefault();
9586
9721
  e.stopPropagation();
9722
+ // Agent row · persist the pin via the API and let the sidebar
9723
+ // re-render rebuild the Pinned / Custom / Core buckets.
9724
+ const agentShell = btn.closest(".agent-row-shell[data-agent-id]");
9725
+ if (agentShell && window.app && typeof window.app.togglePinAgent === "function") {
9726
+ window.app.togglePinAgent(agentShell.dataset.agentId);
9727
+ return;
9728
+ }
9729
+ // Fallback · session-row (room pinning is visual-only for now).
9587
9730
  const row = btn.closest(".session-row, .agent-row");
9588
9731
  if (!row) return;
9589
9732
  row.classList.toggle("pinned");
@@ -9732,35 +9875,36 @@
9732
9875
  });
9733
9876
 
9734
9877
  if (which === "rooms") {
9735
- // Resolve which sub-state to apply. The room id ROOMS_KEY
9736
- // points to is the user's explicit choice when set; on
9737
- // user-driven tab switches we additionally prefer the last
9738
- // actually-opened room over a blank "new"-composer landing,
9739
- // so re-entering the Rooms tab feels continuous (selection
9740
- // restored) instead of resetting. Initial-load (refresh /
9741
- // boot) keeps the legacy "lands on composer" intent so a
9742
- // fresh user without prior history sees the new-room invite.
9878
+ // Resolve which sub-state to apply. ROOMS_KEY is the user's
9879
+ // explicit choice preserve it on every nav (tab click,
9880
+ // refresh, deep-link). All four sub-states are first-class:
9881
+ // an explicit room id, "new" (composer), "reports", "notes".
9882
+ //
9883
+ // Earlier the fromTabClick path overrode saved="new" with
9884
+ // ROOMS_LAST_KEY, on the theory that "re-entering the Rooms
9885
+ // tab should feel continuous." In practice that broke the
9886
+ // user's expectation: pick "+ New Room", switch to Agents
9887
+ // tab, switch back, lands on a different room. The user's
9888
+ // mental model is "the sidebar remembers what I last clicked"
9889
+ // — so "new" is now sticky just like "reports" / "notes".
9890
+ // Fallback to the last-opened room (or first row) only fires
9891
+ // when there's no saved sub-state at all.
9743
9892
  const saved = lsGet(ROOMS_KEY);
9744
9893
  let target;
9745
9894
  if (saved && saved !== "new" && saved !== "reports" && saved !== "notes") {
9746
9895
  target = saved; // explicit room id
9747
- } else if (saved === "reports" || saved === "notes") {
9748
- target = saved; // cross-room destinations preserve on any nav
9896
+ } else if (saved === "reports" || saved === "notes" || saved === "new") {
9897
+ target = saved; // explicit destination preserved on any nav
9749
9898
  } else if (fromTabClick) {
9750
- // User-driven tab switch · prefer the last actually-opened
9751
- // room (or first row) over the +New Room composer, so
9752
- // re-entering the Rooms tab feels continuous instead of
9753
- // resetting to the composer. The "new" intent only persists
9754
- // within the rooms tab — once the user leaves and returns,
9755
- // we surface their last room.
9899
+ // No saved sub-state · fresh user clicking the tab. Prefer
9900
+ // the last actually-opened room so the tab feels populated
9901
+ // instead of bouncing them to a blank composer.
9756
9902
  const last = lsGet(ROOMS_LAST_KEY);
9757
9903
  if (last && document.querySelector(`.session-row-shell[data-room-id="${last}"]`)) {
9758
9904
  target = last;
9759
9905
  } else {
9760
9906
  target = firstRoomId() || "new";
9761
9907
  }
9762
- } else if (saved === "new") {
9763
- target = "new"; // initial load · preserve composer intent
9764
9908
  } else {
9765
9909
  target = "new"; // initial load with no history → composer
9766
9910
  }
@@ -9907,9 +10051,13 @@
9907
10051
  const tab = (savedTab && VALID_TABS.has(savedTab)) ? savedTab : "rooms";
9908
10052
  // Honor URL hash → if user came in with #/r/<id>, that's the
9909
10053
  // intended room; promote it to the rooms sub-state and switch
9910
- // to rooms tab regardless of saved tab.
10054
+ // to rooms tab. BUT only when the saved tab isn't already
10055
+ // "agents" — opening an agent profile doesn't update the URL
10056
+ // hash, so a refresh on the agent page would otherwise get
10057
+ // bounced to rooms by a leftover hash from the prior room
10058
+ // view. Sticky agent tab beats stale-hash deep linking.
9911
10059
  const m = (location.hash || "").match(/^#\/r\/([a-z0-9]+)/i);
9912
- if (m && m[1]) {
10060
+ if (m && m[1] && tab !== "agents") {
9913
10061
  lsSet(ROOMS_KEY, m[1]);
9914
10062
  lsSet(ROOMS_LAST_KEY, m[1]);
9915
10063
  activate("rooms", { persist: false });