privateboard 0.1.7 → 0.1.9

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
 
@@ -647,24 +656,50 @@
647
656
  align-items: center;
648
657
  justify-content: center;
649
658
  }
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. */
659
+ /* CHAIR badge · pure typographic kicker, no pill geometry.
660
+ Earlier this was an outlined hairline pill (cyan border + cyan
661
+ text + transparent fill) the only colored hairline box
662
+ anywhere on the page, so it read as a sticker rather than part
663
+ of the design system. The refined treatment drops border /
664
+ background / padding / radius and keeps the chair's identity
665
+ signal in pure typography:
666
+ · mono uppercase, generous letter-spacing (matches section
667
+ headers, brief banner kickers, time tags)
668
+ · cyan color (the chair's accent stays)
669
+ · em-dash sigil prefix separates it from the title — soft
670
+ enough to feel like part of the same line, distinct enough
671
+ that the eye lands on it as a label, not a name fragment
672
+ · weight 600 (one notch below 700) so it doesn't compete
673
+ with the title's emphasis
674
+ The avatar already carries the `⊘` immutability cue and the
675
+ section header already says "Chair", so this badge is now the
676
+ quietest possible identity stamp instead of a third tier of
677
+ decoration. */
653
678
  .agent-row .agent-row-chair-badge {
654
679
  font-family: var(--mono);
655
- font-size: 8.5px;
656
- font-weight: 700;
657
- letter-spacing: 0.18em;
680
+ font-size: 9px;
681
+ font-weight: 600;
682
+ letter-spacing: 0.2em;
658
683
  text-transform: uppercase;
659
- padding: 1px 6px;
660
- color: var(--cyan, #6A9B97);
684
+ /* Track the active theme's primary accent — `--lime` remaps per
685
+ theme (gold in regent, blue in apple, red in pinterest,
686
+ magenta in amuse, etc.). The earlier `--cyan` was static and
687
+ clashed in themes whose palette didn't include teal. */
688
+ color: var(--lime);
661
689
  background: transparent;
662
- border: 0.5px solid var(--cyan, #6A9B97);
663
- border-radius: 2px;
690
+ border: none;
691
+ padding: 0;
664
692
  white-space: nowrap;
665
693
  align-self: center;
666
694
  margin-left: auto;
667
695
  }
696
+ .agent-row .agent-row-chair-badge::before {
697
+ content: "—";
698
+ color: var(--text-faint);
699
+ margin-right: 8px;
700
+ font-weight: 400;
701
+ letter-spacing: 0;
702
+ }
668
703
  .agent-row .agent-row-chair-role { color: var(--text-soft); font-weight: 600; }
669
704
  .agent-row .agent-row-chair-note { color: var(--text-faint); }
670
705
  /* Hide the Chair section's count badge — there's only ever one. */
@@ -943,6 +978,13 @@
943
978
  opacity: 1;
944
979
  color: var(--lime);
945
980
  }
981
+ /* Pinned · the lime pin glyph stays visible permanently, so the
982
+ in-flow time tag has to hide too — otherwise the two stack on
983
+ the right edge (pin + "7h" overlapping in the same 18px slot).
984
+ The hover rule above only handles hover; pinned rows live in
985
+ "always visible" state and need their own hide. */
986
+ .agent-row.pinned .agent-row-time,
987
+ .session-row.pinned .row-time { visibility: hidden; }
946
988
 
947
989
  /* The shell wraps the link + delete button as siblings so the click on
948
990
  the X button doesn't get absorbed by the anchor's navigation.
@@ -1262,6 +1304,7 @@
1262
1304
  grid-template-rows: auto 1fr auto;
1263
1305
  overflow: hidden;
1264
1306
  min-height: 0;
1307
+ height: 100%;
1265
1308
  }
1266
1309
  .main-view[data-main-view="agent"] {
1267
1310
  /* Switch from grid to block so the profile card flows naturally
@@ -1375,7 +1418,7 @@
1375
1418
  .notes-filters {
1376
1419
  display: flex;
1377
1420
  gap: 2px;
1378
- margin: 22px 0 6px;
1421
+ margin: 22px 0 2px;
1379
1422
  flex-wrap: wrap;
1380
1423
  }
1381
1424
  .reports-filter-chip,
@@ -1422,8 +1465,8 @@
1422
1465
  in favour of a single rule + flat list (chips already disclose
1423
1466
  recency, no need for repeated section headers). */
1424
1467
  .reports-list-wrap {
1425
- margin-top: 14px;
1426
- padding-top: 14px;
1468
+ margin-top: 8px;
1469
+ padding-top: 8px;
1427
1470
  border-top: 1px solid var(--border);
1428
1471
  }
1429
1472
 
@@ -1514,6 +1557,18 @@
1514
1557
  color: var(--text-faint);
1515
1558
  letter-spacing: 0.06em;
1516
1559
  }
1560
+ /* Report-mode type chip in the kicker line · "REPORT" / "BENTO" /
1561
+ "MAGAZINE" / "NEWSPAPER". Mono register matching the kicker.
1562
+ Slight color shift per mode so the type is glanceable without
1563
+ being a heavy badge. */
1564
+ .reports-item-type {
1565
+ color: var(--text-soft);
1566
+ letter-spacing: 0.16em;
1567
+ font-weight: 700;
1568
+ }
1569
+ .reports-item-type[data-mode="magazine"] { color: var(--amber, #C8A36F); }
1570
+ .reports-item-type[data-mode="newspaper"] { color: var(--text); }
1571
+ .reports-item-type[data-mode="ppt"] { color: var(--cyan, #6FC3C8); }
1517
1572
  .reports-item-sep {
1518
1573
  color: var(--text-faint);
1519
1574
  opacity: 0.7;
@@ -2090,8 +2145,8 @@
2090
2145
  the chip strip → divider → list rhythm reads identically. Group
2091
2146
  headers were dropped in favour of a single hair under the chips. */
2092
2147
  .notes-list-wrap {
2093
- margin-top: 14px;
2094
- padding-top: 14px;
2148
+ margin-top: 8px;
2149
+ padding-top: 8px;
2095
2150
  border-top: 1px solid var(--border);
2096
2151
  }
2097
2152
 
@@ -2117,15 +2172,17 @@
2117
2172
  .notes-item-subject { max-width: 160px; }
2118
2173
  }
2119
2174
 
2120
- /* Room head — generously padded so the title has breathing
2121
- room above and below. */
2175
+ /* Room head — compact register · the title is the only
2176
+ vertical anchor users actually read here, so we keep the
2177
+ padding moderately tight while still leaving the kicker +
2178
+ title pair some breathing room. */
2122
2179
  .room-head {
2123
2180
  background: var(--panel-2);
2124
2181
  border-bottom: 0.5px solid var(--line-bright);
2125
- padding: 16px 18px;
2182
+ padding: 11px 16px;
2126
2183
  display: grid;
2127
2184
  grid-template-columns: 1fr auto;
2128
- gap: 14px;
2185
+ gap: 12px;
2129
2186
  align-items: center;
2130
2187
  }
2131
2188
  /* When the sidebar is collapsed the room-head gains a leading
@@ -2144,36 +2201,51 @@
2144
2201
  at this level. */
2145
2202
  .room-info { min-width: 0; overflow: visible; }
2146
2203
 
2147
- .room-id {
2204
+ /* ─── Compact kicker · single mono line at top of room-head ───
2205
+ Replaces the prior `.room-id` row (`MEETING ROOM` badge +
2206
+ `// ROOM #N · TONE`) AND the `.room-meta` row (tone/intensity/
2207
+ report tagged pills + status stamp). Net: three stacked rows
2208
+ collapse into one. Tone / intensity / brief-style remain
2209
+ editable in the room-settings overlay. */
2210
+ .room-kicker {
2148
2211
  display: flex;
2149
2212
  align-items: center;
2150
- gap: 8px;
2151
- margin-bottom: 6px;
2152
- }
2153
- .room-name {
2213
+ gap: 6px;
2214
+ font-family: var(--mono);
2154
2215
  font-size: 9.5px;
2155
- color: var(--bg);
2156
- background: var(--lime);
2157
- padding: 1px 7px;
2216
+ letter-spacing: 0.12em;
2158
2217
  text-transform: uppercase;
2159
- letter-spacing: 0.1em;
2218
+ color: var(--text-dim);
2219
+ margin-bottom: 5px;
2220
+ flex-wrap: wrap;
2160
2221
  font-weight: 700;
2222
+ line-height: 1.3;
2161
2223
  }
2162
- .session-num {
2163
- font-size: 9.5px;
2164
- color: var(--text-dim);
2165
- text-transform: uppercase;
2166
- letter-spacing: 0.08em;
2224
+ .kicker-num { color: var(--text-soft); }
2225
+ .kicker-tone { color: var(--lime); cursor: help; }
2226
+ .kicker-intensity { color: var(--amber); }
2227
+ .kicker-sep { color: var(--text-faint); opacity: 0.6; }
2228
+ .kicker-status { display: inline-flex; align-items: center; gap: 5px; }
2229
+ .kicker-status::before {
2230
+ font-size: 11px;
2231
+ line-height: 1;
2232
+ display: inline-block;
2167
2233
  }
2234
+ .kicker-status.status-live { color: var(--lime); }
2235
+ .kicker-status.status-live::before { content: "●"; font-size: 7px; animation: pulse 1.5s infinite; }
2236
+ .kicker-status.status-paused { color: var(--amber); }
2237
+ .kicker-status.status-paused::before { content: "❚❚"; font-size: 9px; letter-spacing: -0.1em; }
2238
+ .kicker-status.status-adjourned { color: var(--text-dim); }
2239
+ .kicker-status.status-adjourned::before { content: "▣"; font-size: 10px; }
2168
2240
 
2169
2241
  .room-subject {
2170
- font-size: 17px;
2242
+ font-size: 14px;
2171
2243
  font-weight: 600;
2172
2244
  color: var(--text);
2173
2245
  line-height: 1.3;
2174
- margin-bottom: 10px;
2246
+ margin-bottom: 0;
2175
2247
  font-family: var(--sans);
2176
- letter-spacing: -0.012em;
2248
+ letter-spacing: -0.005em;
2177
2249
  overflow: hidden;
2178
2250
  text-overflow: ellipsis;
2179
2251
  white-space: nowrap;
@@ -2262,64 +2334,18 @@
2262
2334
  to { opacity: 1; transform: translateY(0); }
2263
2335
  }
2264
2336
 
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
- }
2337
+ /* `.room-meta` / `.meta-stamp` REMOVED. The tone/intensity/
2338
+ report tags + status stamp that lived in this row are now
2339
+ part of `.room-kicker` (single mono line above the subject).
2340
+ Tone / intensity / brief style remain editable in the
2341
+ room-settings overlay. */
2315
2342
 
2316
2343
  .head-actions { display: flex; align-items: center; gap: 6px; }
2317
2344
  .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. */
2345
+ /* Avatar size aligned with the new compact header rhythm (22px) ·
2346
+ keeps the toolbar height in proportion to the 14px title. */
2321
2347
  .head-cast img {
2322
- width: 26px; height: 26px;
2348
+ width: 22px; height: 22px;
2323
2349
  image-rendering: pixelated;
2324
2350
  image-rendering: crisp-edges;
2325
2351
  border: 0.5px solid var(--line-strong);
@@ -2334,9 +2360,9 @@
2334
2360
  /* Stacked count tile · sits at the end of the avatar cascade and
2335
2361
  matches the avatar height so the row stays flush. */
2336
2362
  .head-cast .cast-count {
2337
- height: 26px;
2338
- min-width: 26px;
2339
- padding: 0 6px;
2363
+ height: 22px;
2364
+ min-width: 22px;
2365
+ padding: 0 5px;
2340
2366
  background: var(--panel-3);
2341
2367
  color: var(--text);
2342
2368
  border: 0.5px solid var(--lime-dim);
@@ -2345,7 +2371,7 @@
2345
2371
  align-items: center;
2346
2372
  justify-content: center;
2347
2373
  font-family: var(--mono);
2348
- font-size: 11px;
2374
+ font-size: 10px;
2349
2375
  font-weight: 700;
2350
2376
  letter-spacing: 0;
2351
2377
  }
@@ -2760,6 +2786,23 @@
2760
2786
  text-transform: uppercase;
2761
2787
  color: var(--lime);
2762
2788
  }
2789
+ /* Mode chip · "Report" / "Magazine" / "Newspaper".
2790
+ Sits between the // report kicker and the FILED stamp, styled as
2791
+ a hairline mono pill so the user sees the report type at a glance
2792
+ without it competing with the kicker for primary emphasis.
2793
+ `margin-right: auto` pushes the stamp to the right edge while the
2794
+ chip stays anchored next to the kicker. */
2795
+ .brief-banner-type {
2796
+ font-size: 9px;
2797
+ font-weight: 600;
2798
+ letter-spacing: 0.12em;
2799
+ text-transform: uppercase;
2800
+ color: var(--text-soft);
2801
+ border: 0.5px solid var(--line-bright, var(--line));
2802
+ padding: 2px 8px;
2803
+ margin-right: auto;
2804
+ white-space: nowrap;
2805
+ }
2763
2806
  .brief-banner-stamp {
2764
2807
  font-size: 9px;
2765
2808
  letter-spacing: 0.12em;
@@ -3588,13 +3631,56 @@
3588
3631
  margin-bottom: 18px;
3589
3632
  font-family: var(--mono);
3590
3633
  }
3634
+ /* Subject · clamp to 2 lines by default. The Show more / less
3635
+ toggle is rendered hidden and unhidden by the follow-up
3636
+ overlay's post-mount measurement only when the text actually
3637
+ overflows. Mirrors the adjourn-overlay / room-settings clamp
3638
+ pattern so all three modals read identically. */
3591
3639
  .followup-parent-subject {
3592
3640
  font-size: 13px;
3593
3641
  font-weight: 600;
3594
3642
  color: var(--text);
3595
3643
  line-height: 1.4;
3596
3644
  margin-bottom: 4px;
3645
+ word-break: break-word;
3646
+ }
3647
+ .followup-parent-subject.is-clamped {
3648
+ display: -webkit-box;
3649
+ -webkit-line-clamp: 2;
3650
+ -webkit-box-orient: vertical;
3651
+ overflow: hidden;
3652
+ }
3653
+ /* Meta row · single line that pairs the Show more toggle (left,
3654
+ when present) with the adjourned-time / brief-count meta
3655
+ (right). `margin-left: auto` on the meta keeps it pinned to
3656
+ the right edge regardless of whether the toggle is shown, so
3657
+ the meta doesn't shift between states. */
3658
+ .followup-parent-meta-row {
3659
+ display: flex;
3660
+ align-items: baseline;
3661
+ gap: 12px;
3662
+ margin-bottom: 4px;
3663
+ }
3664
+ .followup-parent-meta-row .followup-parent-meta {
3665
+ margin-left: auto;
3666
+ margin-bottom: 0;
3597
3667
  }
3668
+ .followup-parent-subject-toggle {
3669
+ appearance: none;
3670
+ background: transparent;
3671
+ border: none;
3672
+ font-family: var(--mono);
3673
+ font-size: 9.5px;
3674
+ letter-spacing: 0.14em;
3675
+ text-transform: uppercase;
3676
+ color: var(--text-soft);
3677
+ cursor: pointer;
3678
+ padding: 0;
3679
+ transition: color 0.12s;
3680
+ }
3681
+ .followup-parent-subject-toggle:hover { color: var(--lime); }
3682
+ .followup-parent-subject-toggle::before { content: "[ "; color: var(--text-faint); }
3683
+ .followup-parent-subject-toggle::after { content: " ]"; color: var(--text-faint); }
3598
3684
  .followup-parent-meta {
3599
3685
  font-size: 10px;
3600
3686
  color: var(--text-soft);
@@ -4158,12 +4244,29 @@
4158
4244
  html[data-status="adjourned"] .paused-bar { display: none; }
4159
4245
 
4160
4246
  /* Body · single chat column. Live-notes panel was removed, so there's
4161
- no right-side resizer or aside to apportion space for. */
4247
+ no right-side resizer or aside to apportion space for.
4248
+ `grid-template-rows: 1fr` is required so the implicit row stretches
4249
+ to body's height instead of collapsing to content height. Without
4250
+ it, .chat-col would be only as tall as its visible children, and
4251
+ .chat (`flex: 1 1 auto`) would have nothing to grow into — making
4252
+ [data-chat-messages] sit flush at the top of a content-sized
4253
+ chat. With `1fr`, .chat-col fills body, .chat fills chat-col, and
4254
+ vertical centring of the composer inside .chat actually has free
4255
+ space to work with. */
4162
4256
  .body {
4163
4257
  display: grid;
4164
4258
  grid-template-columns: 1fr;
4259
+ grid-template-rows: 1fr;
4165
4260
  overflow: hidden;
4166
4261
  min-height: 0;
4262
+ height: 100%;
4263
+ /* Pin to .main-view's 1fr row · without this, when room-head is
4264
+ :empty + display:none (composer mode), .body becomes the first
4265
+ non-hidden grid child and auto-flows into row 1 (auto), where
4266
+ it collapses to content height. Vertical centring then has no
4267
+ free space. Explicit grid-row: 2 keeps .body in the 1fr row
4268
+ regardless of whether room-head is visible. */
4269
+ grid-row: 2;
4167
4270
  }
4168
4271
 
4169
4272
  /* Chat (zen plain text) */
@@ -7005,14 +7108,76 @@
7005
7108
  edge so the page has one clear focal point. Refined: generous
7006
7109
  vertical rhythm, single-accent colour discipline, mono micro-type
7007
7110
  paired with serif-leaning sans display. ─── */
7111
+ /* Composer (new-room AND new-agent) shared rhythm.
7112
+ Vertical centring is split across two modes:
7113
+ · Default (`.chat.chat--composer`, content fits viewport) ·
7114
+ the chat is `display: grid; align-content: center` so the
7115
+ whole .cmp block sits in the visual centre.
7116
+ · Overflow (`.chat.chat--composer.chat--composer-overflow`,
7117
+ JS-toggled when `cmp.scrollHeight > chat.clientHeight`) ·
7118
+ the chat switches to block flow + scroll, .cmp-fold (hero
7119
+ + input) is min-height ≈ 100vh with content centred inside,
7120
+ and .cmp-starters flow below as scrollable extras. Title +
7121
+ input stay visually centred on initial paint regardless of
7122
+ viewport height; starter cards live below the fold.
7123
+ Padding stays modest because vertical position no longer comes
7124
+ from top padding; it's just breathing room around the cmp box.
7125
+ Keep both composers in lock-step by editing here only. */
7008
7126
  .cmp {
7009
7127
  max-width: 760px;
7010
7128
  margin: 0 auto;
7011
- padding: 88px 32px 96px;
7129
+ padding: 32px 32px;
7130
+ width: 100%;
7131
+ }
7132
+ /* Default composer mode (content fits viewport).
7133
+ Toggled by JS via `.chat--composer` (added in
7134
+ renderEmptyState; removed in renderChat). The
7135
+ `.chat--composer-overflow` variant below handles the case
7136
+ where the composer is taller than .chat. */
7137
+ .chat.chat--composer {
7138
+ /* Grid centring · `align-content: center` is the most reliable
7139
+ cross-browser way to vertically centre a grid track. Beats
7140
+ flex auto-margins (which can resolve to 0 if any parent in
7141
+ the chain doesn't propagate height correctly) and beats
7142
+ `justify-content: safe center` (the `safe` keyword can
7143
+ silently invalidate the whole declaration in older browsers).
7144
+ When content overflows, the grid item still scrolls naturally
7145
+ inside .chat's `overflow-y: auto`. */
7146
+ display: grid !important;
7147
+ align-content: center !important;
7148
+ justify-items: stretch !important;
7149
+ }
7150
+ .chat.chat--composer > [data-chat-messages] {
7151
+ width: 100% !important;
7152
+ }
7153
+ .chat.chat--composer > [data-brief-card] {
7154
+ /* In composer mode the brief-card is empty · hide it so it
7155
+ doesn't claim a grid row that throws off the centring. */
7156
+ display: none !important;
7157
+ }
7158
+ /* Overflow mode · when .cmp's natural height exceeds .chat's height,
7159
+ centring the entire composer block would clip the title above the
7160
+ scroll area. Instead, push the .cmp's top down with a viewport-
7161
+ relative padding-top so hero + input sit at the visual centre of
7162
+ the viewport on initial paint; .cmp-starters fall below the input
7163
+ in normal document flow with the SAME spacing as the fits case
7164
+ (no `justify-content: center` void zone). Approximation: hero +
7165
+ input combined ≈ 220px tall, so half ≈ 110px; padding-top of
7166
+ `50vh - 110px` puts the hero+input vertical midpoint at 50vh.
7167
+ `max(32px, …)` keeps the original padding floor on tiny viewports
7168
+ where the calc would go negative.
7169
+ Toggled by JS (updateComposerOverflow) on render + resize. */
7170
+ .chat.chat--composer.chat--composer-overflow {
7171
+ display: block !important;
7172
+ align-content: initial !important;
7173
+ overflow-y: auto;
7174
+ }
7175
+ .chat.chat--composer.chat--composer-overflow .cmp {
7176
+ padding-top: max(32px, calc(50vh - 110px));
7012
7177
  }
7013
7178
  .cmp-hero {
7014
7179
  text-align: center;
7015
- margin-bottom: 36px;
7180
+ margin-bottom: 22px;
7016
7181
  }
7017
7182
  .cmp-greet {
7018
7183
  font-family: var(--mono);
@@ -7021,11 +7186,11 @@
7021
7186
  text-transform: uppercase;
7022
7187
  color: var(--text-faint);
7023
7188
  font-weight: 700;
7024
- margin-bottom: 22px;
7189
+ margin-bottom: 14px;
7025
7190
  }
7026
7191
  .cmp-prompt {
7027
7192
  font-family: var(--font-human);
7028
- font-size: 32px;
7193
+ font-size: 28px;
7029
7194
  font-weight: 600;
7030
7195
  color: var(--text);
7031
7196
  line-height: 1.2;
@@ -7070,7 +7235,13 @@
7070
7235
  }
7071
7236
 
7072
7237
  /* Toolbar inside input frame · cast on left, tune in the middle,
7073
- convene on the right. Hairline divider above. */
7238
+ convene on the right. Hairline divider above. min-height pinned
7239
+ to the room composer's natural height (driven by the 22px cast
7240
+ avatars + button padding) so the agent composer's toolbar — which
7241
+ has no avatar-bearing button — grows to match. Without this the
7242
+ agent input frame is a few pixels shorter than the room frame.
7243
+ Box-sizing is border-box (universal in this project), so the
7244
+ min-height includes the 8px+8px vertical padding. */
7074
7245
  .cmp-toolbar {
7075
7246
  display: flex;
7076
7247
  align-items: center;
@@ -7078,6 +7249,7 @@
7078
7249
  padding: 8px 8px 8px 14px;
7079
7250
  border-top: 0.5px solid var(--line);
7080
7251
  background: var(--panel-2);
7252
+ min-height: 48px;
7081
7253
  }
7082
7254
 
7083
7255
  .cmp-cast-btn {
@@ -7413,7 +7585,7 @@
7413
7585
  accent borders. Just hairline-separated rows that brighten on
7414
7586
  hover, with the arrow turning lime — the only visual reward. */
7415
7587
  .cmp-starters {
7416
- margin-top: 56px;
7588
+ margin-top: 28px;
7417
7589
  }
7418
7590
  .cmp-starters-rule {
7419
7591
  display: flex;
@@ -7444,7 +7616,7 @@
7444
7616
  gap: 16px;
7445
7617
  align-items: center;
7446
7618
  width: 100%;
7447
- padding: 14px 4px;
7619
+ padding: 10px 4px;
7448
7620
  background: transparent;
7449
7621
  border: none;
7450
7622
  border-bottom: 0.5px solid var(--line);
@@ -7490,7 +7662,10 @@
7490
7662
  /* ─── New-agent composer · variant of the new-room composer.
7491
7663
  Reuses .cmp / .cmp-* classes for the hero + input frame; adds
7492
7664
  ag-cmp-* for agent-specific bits and ag-prev-* for the editable
7493
- spec preview card that appears after AI generation. ─── */
7665
+ spec preview card that appears after AI generation. The vertical
7666
+ rhythm (padding, hero/starter margins, starter card density) is
7667
+ SHARED with the new-room composer via the base .cmp rules — so
7668
+ edits land in one place and both composers stay in sync. ─── */
7494
7669
  .ag-cmp .cmp-toolbar {
7495
7670
  /* Model + manual buttons sit at the left, Generate hugs the right
7496
7671
  via margin-left: auto on .cmp-go. No justify-content override
@@ -8621,12 +8796,12 @@
8621
8796
 
8622
8797
  /* Long-opener clamp · when the user wrote more than a sentence of
8623
8798
  context, the card would otherwise dominate the viewport. We
8624
- clamp the body to ~2.5 lines with a fade-out overlay; a
8799
+ clamp the body to 2 lines with a fade-out overlay; a
8625
8800
  `<button data-convene-toggle>` below toggles `.expanded` on the
8626
- opener parent to reveal the rest. 2.5 × line-height (1.32 ×
8627
- 19px ≈ 25px) = 63px. */
8801
+ opener parent to reveal the rest. 2 × line-height (1.32 × 19px
8802
+ ≈ 25px) = 50px. */
8628
8803
  .convene-opener-clamped:not(.expanded) .convene-body {
8629
- max-height: 63px;
8804
+ max-height: 50px;
8630
8805
  overflow: hidden;
8631
8806
  position: relative;
8632
8807
  }
@@ -8636,7 +8811,7 @@
8636
8811
  left: 0;
8637
8812
  right: 0;
8638
8813
  bottom: 0;
8639
- height: 32px;
8814
+ height: 24px;
8640
8815
  background: linear-gradient(to bottom, transparent, var(--panel-2));
8641
8816
  pointer-events: none;
8642
8817
  }
@@ -8939,6 +9114,7 @@
8939
9114
  min-height: 0;
8940
9115
  overflow: hidden;
8941
9116
  background: var(--panel);
9117
+ height: 100%;
8942
9118
  }
8943
9119
 
8944
9120
  /* Input bar — sits inside the chat column. Live state hosts a
@@ -9560,13 +9736,27 @@
9560
9736
  io.observe(hint);
9561
9737
  })();
9562
9738
 
9563
- /* ─── Pin toggle (visual demo) ─── */
9739
+ /* ─── Pin toggle ───
9740
+ Two paths: agent rows persist via PATCH /api/agents/:id { isPinned }
9741
+ through `app.togglePinAgent`, which mutates local state and
9742
+ re-renders the sidebar so the row moves into the Pinned bucket.
9743
+ Session (room) rows fall through to a visual-only toggle for now —
9744
+ room pinning isn't a backend feature yet, so the class flip is a
9745
+ placeholder that will go away once it lands. */
9564
9746
  (function () {
9565
9747
  document.addEventListener("click", (e) => {
9566
9748
  const btn = e.target.closest("[data-pin-toggle]");
9567
9749
  if (!btn) return;
9568
9750
  e.preventDefault();
9569
9751
  e.stopPropagation();
9752
+ // Agent row · persist the pin via the API and let the sidebar
9753
+ // re-render rebuild the Pinned / Custom / Core buckets.
9754
+ const agentShell = btn.closest(".agent-row-shell[data-agent-id]");
9755
+ if (agentShell && window.app && typeof window.app.togglePinAgent === "function") {
9756
+ window.app.togglePinAgent(agentShell.dataset.agentId);
9757
+ return;
9758
+ }
9759
+ // Fallback · session-row (room pinning is visual-only for now).
9570
9760
  const row = btn.closest(".session-row, .agent-row");
9571
9761
  if (!row) return;
9572
9762
  row.classList.toggle("pinned");
@@ -9715,35 +9905,36 @@
9715
9905
  });
9716
9906
 
9717
9907
  if (which === "rooms") {
9718
- // Resolve which sub-state to apply. The room id ROOMS_KEY
9719
- // points to is the user's explicit choice when set; on
9720
- // user-driven tab switches we additionally prefer the last
9721
- // actually-opened room over a blank "new"-composer landing,
9722
- // so re-entering the Rooms tab feels continuous (selection
9723
- // restored) instead of resetting. Initial-load (refresh /
9724
- // boot) keeps the legacy "lands on composer" intent so a
9725
- // fresh user without prior history sees the new-room invite.
9908
+ // Resolve which sub-state to apply. ROOMS_KEY is the user's
9909
+ // explicit choice preserve it on every nav (tab click,
9910
+ // refresh, deep-link). All four sub-states are first-class:
9911
+ // an explicit room id, "new" (composer), "reports", "notes".
9912
+ //
9913
+ // Earlier the fromTabClick path overrode saved="new" with
9914
+ // ROOMS_LAST_KEY, on the theory that "re-entering the Rooms
9915
+ // tab should feel continuous." In practice that broke the
9916
+ // user's expectation: pick "+ New Room", switch to Agents
9917
+ // tab, switch back, lands on a different room. The user's
9918
+ // mental model is "the sidebar remembers what I last clicked"
9919
+ // — so "new" is now sticky just like "reports" / "notes".
9920
+ // Fallback to the last-opened room (or first row) only fires
9921
+ // when there's no saved sub-state at all.
9726
9922
  const saved = lsGet(ROOMS_KEY);
9727
9923
  let target;
9728
9924
  if (saved && saved !== "new" && saved !== "reports" && saved !== "notes") {
9729
9925
  target = saved; // explicit room id
9730
- } else if (saved === "reports" || saved === "notes") {
9731
- target = saved; // cross-room destinations preserve on any nav
9926
+ } else if (saved === "reports" || saved === "notes" || saved === "new") {
9927
+ target = saved; // explicit destination preserved on any nav
9732
9928
  } else if (fromTabClick) {
9733
- // User-driven tab switch · prefer the last actually-opened
9734
- // room (or first row) over the +New Room composer, so
9735
- // re-entering the Rooms tab feels continuous instead of
9736
- // resetting to the composer. The "new" intent only persists
9737
- // within the rooms tab — once the user leaves and returns,
9738
- // we surface their last room.
9929
+ // No saved sub-state · fresh user clicking the tab. Prefer
9930
+ // the last actually-opened room so the tab feels populated
9931
+ // instead of bouncing them to a blank composer.
9739
9932
  const last = lsGet(ROOMS_LAST_KEY);
9740
9933
  if (last && document.querySelector(`.session-row-shell[data-room-id="${last}"]`)) {
9741
9934
  target = last;
9742
9935
  } else {
9743
9936
  target = firstRoomId() || "new";
9744
9937
  }
9745
- } else if (saved === "new") {
9746
- target = "new"; // initial load · preserve composer intent
9747
9938
  } else {
9748
9939
  target = "new"; // initial load with no history → composer
9749
9940
  }
@@ -9890,9 +10081,13 @@
9890
10081
  const tab = (savedTab && VALID_TABS.has(savedTab)) ? savedTab : "rooms";
9891
10082
  // Honor URL hash → if user came in with #/r/<id>, that's the
9892
10083
  // intended room; promote it to the rooms sub-state and switch
9893
- // to rooms tab regardless of saved tab.
10084
+ // to rooms tab. BUT only when the saved tab isn't already
10085
+ // "agents" — opening an agent profile doesn't update the URL
10086
+ // hash, so a refresh on the agent page would otherwise get
10087
+ // bounced to rooms by a leftover hash from the prior room
10088
+ // view. Sticky agent tab beats stale-hash deep linking.
9894
10089
  const m = (location.hash || "").match(/^#\/r\/([a-z0-9]+)/i);
9895
- if (m && m[1]) {
10090
+ if (m && m[1] && tab !== "agents") {
9896
10091
  lsSet(ROOMS_KEY, m[1]);
9897
10092
  lsSet(ROOMS_LAST_KEY, m[1]);
9898
10093
  activate("rooms", { persist: false });