privateboard 0.1.8 → 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
@@ -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;
@@ -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,7 +2786,7 @@
2760
2786
  text-transform: uppercase;
2761
2787
  color: var(--lime);
2762
2788
  }
2763
- /* Mode chip · "Report" / "Bento" / "Magazine" / "Newspaper".
2789
+ /* Mode chip · "Report" / "Magazine" / "Newspaper".
2764
2790
  Sits between the // report kicker and the FILED stamp, styled as
2765
2791
  a hairline mono pill so the user sees the report type at a glance
2766
2792
  without it competing with the kicker for primary emphasis.
@@ -3605,13 +3631,56 @@
3605
3631
  margin-bottom: 18px;
3606
3632
  font-family: var(--mono);
3607
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. */
3608
3639
  .followup-parent-subject {
3609
3640
  font-size: 13px;
3610
3641
  font-weight: 600;
3611
3642
  color: var(--text);
3612
3643
  line-height: 1.4;
3613
3644
  margin-bottom: 4px;
3645
+ word-break: break-word;
3614
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;
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); }
3615
3684
  .followup-parent-meta {
3616
3685
  font-size: 10px;
3617
3686
  color: var(--text-soft);
@@ -4175,12 +4244,29 @@
4175
4244
  html[data-status="adjourned"] .paused-bar { display: none; }
4176
4245
 
4177
4246
  /* Body · single chat column. Live-notes panel was removed, so there's
4178
- 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. */
4179
4256
  .body {
4180
4257
  display: grid;
4181
4258
  grid-template-columns: 1fr;
4259
+ grid-template-rows: 1fr;
4182
4260
  overflow: hidden;
4183
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;
4184
4270
  }
4185
4271
 
4186
4272
  /* Chat (zen plain text) */
@@ -7022,14 +7108,76 @@
7022
7108
  edge so the page has one clear focal point. Refined: generous
7023
7109
  vertical rhythm, single-accent colour discipline, mono micro-type
7024
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. */
7025
7126
  .cmp {
7026
7127
  max-width: 760px;
7027
7128
  margin: 0 auto;
7028
- 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));
7029
7177
  }
7030
7178
  .cmp-hero {
7031
7179
  text-align: center;
7032
- margin-bottom: 36px;
7180
+ margin-bottom: 22px;
7033
7181
  }
7034
7182
  .cmp-greet {
7035
7183
  font-family: var(--mono);
@@ -7038,11 +7186,11 @@
7038
7186
  text-transform: uppercase;
7039
7187
  color: var(--text-faint);
7040
7188
  font-weight: 700;
7041
- margin-bottom: 22px;
7189
+ margin-bottom: 14px;
7042
7190
  }
7043
7191
  .cmp-prompt {
7044
7192
  font-family: var(--font-human);
7045
- font-size: 32px;
7193
+ font-size: 28px;
7046
7194
  font-weight: 600;
7047
7195
  color: var(--text);
7048
7196
  line-height: 1.2;
@@ -7087,7 +7235,13 @@
7087
7235
  }
7088
7236
 
7089
7237
  /* Toolbar inside input frame · cast on left, tune in the middle,
7090
- 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. */
7091
7245
  .cmp-toolbar {
7092
7246
  display: flex;
7093
7247
  align-items: center;
@@ -7095,6 +7249,7 @@
7095
7249
  padding: 8px 8px 8px 14px;
7096
7250
  border-top: 0.5px solid var(--line);
7097
7251
  background: var(--panel-2);
7252
+ min-height: 48px;
7098
7253
  }
7099
7254
 
7100
7255
  .cmp-cast-btn {
@@ -7430,7 +7585,7 @@
7430
7585
  accent borders. Just hairline-separated rows that brighten on
7431
7586
  hover, with the arrow turning lime — the only visual reward. */
7432
7587
  .cmp-starters {
7433
- margin-top: 56px;
7588
+ margin-top: 28px;
7434
7589
  }
7435
7590
  .cmp-starters-rule {
7436
7591
  display: flex;
@@ -7461,7 +7616,7 @@
7461
7616
  gap: 16px;
7462
7617
  align-items: center;
7463
7618
  width: 100%;
7464
- padding: 14px 4px;
7619
+ padding: 10px 4px;
7465
7620
  background: transparent;
7466
7621
  border: none;
7467
7622
  border-bottom: 0.5px solid var(--line);
@@ -7507,7 +7662,10 @@
7507
7662
  /* ─── New-agent composer · variant of the new-room composer.
7508
7663
  Reuses .cmp / .cmp-* classes for the hero + input frame; adds
7509
7664
  ag-cmp-* for agent-specific bits and ag-prev-* for the editable
7510
- 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. ─── */
7511
7669
  .ag-cmp .cmp-toolbar {
7512
7670
  /* Model + manual buttons sit at the left, Generate hugs the right
7513
7671
  via margin-left: auto on .cmp-go. No justify-content override
@@ -8638,12 +8796,12 @@
8638
8796
 
8639
8797
  /* Long-opener clamp · when the user wrote more than a sentence of
8640
8798
  context, the card would otherwise dominate the viewport. We
8641
- 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
8642
8800
  `<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. */
8801
+ opener parent to reveal the rest. 2 × line-height (1.32 × 19px
8802
+ ≈ 25px) = 50px. */
8645
8803
  .convene-opener-clamped:not(.expanded) .convene-body {
8646
- max-height: 63px;
8804
+ max-height: 50px;
8647
8805
  overflow: hidden;
8648
8806
  position: relative;
8649
8807
  }
@@ -8653,7 +8811,7 @@
8653
8811
  left: 0;
8654
8812
  right: 0;
8655
8813
  bottom: 0;
8656
- height: 32px;
8814
+ height: 24px;
8657
8815
  background: linear-gradient(to bottom, transparent, var(--panel-2));
8658
8816
  pointer-events: none;
8659
8817
  }
@@ -8956,6 +9114,7 @@
8956
9114
  min-height: 0;
8957
9115
  overflow: hidden;
8958
9116
  background: var(--panel);
9117
+ height: 100%;
8959
9118
  }
8960
9119
 
8961
9120
  /* Input bar — sits inside the chat column. Live state hosts a
@@ -9577,13 +9736,27 @@
9577
9736
  io.observe(hint);
9578
9737
  })();
9579
9738
 
9580
- /* ─── 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. */
9581
9746
  (function () {
9582
9747
  document.addEventListener("click", (e) => {
9583
9748
  const btn = e.target.closest("[data-pin-toggle]");
9584
9749
  if (!btn) return;
9585
9750
  e.preventDefault();
9586
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).
9587
9760
  const row = btn.closest(".session-row, .agent-row");
9588
9761
  if (!row) return;
9589
9762
  row.classList.toggle("pinned");
@@ -9732,35 +9905,36 @@
9732
9905
  });
9733
9906
 
9734
9907
  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.
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.
9743
9922
  const saved = lsGet(ROOMS_KEY);
9744
9923
  let target;
9745
9924
  if (saved && saved !== "new" && saved !== "reports" && saved !== "notes") {
9746
9925
  target = saved; // explicit room id
9747
- } else if (saved === "reports" || saved === "notes") {
9748
- 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
9749
9928
  } 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.
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.
9756
9932
  const last = lsGet(ROOMS_LAST_KEY);
9757
9933
  if (last && document.querySelector(`.session-row-shell[data-room-id="${last}"]`)) {
9758
9934
  target = last;
9759
9935
  } else {
9760
9936
  target = firstRoomId() || "new";
9761
9937
  }
9762
- } else if (saved === "new") {
9763
- target = "new"; // initial load · preserve composer intent
9764
9938
  } else {
9765
9939
  target = "new"; // initial load with no history → composer
9766
9940
  }
@@ -9907,9 +10081,13 @@
9907
10081
  const tab = (savedTab && VALID_TABS.has(savedTab)) ? savedTab : "rooms";
9908
10082
  // Honor URL hash → if user came in with #/r/<id>, that's the
9909
10083
  // intended room; promote it to the rooms sub-state and switch
9910
- // 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.
9911
10089
  const m = (location.hash || "").match(/^#\/r\/([a-z0-9]+)/i);
9912
- if (m && m[1]) {
10090
+ if (m && m[1] && tab !== "agents") {
9913
10091
  lsSet(ROOMS_KEY, m[1]);
9914
10092
  lsSet(ROOMS_LAST_KEY, m[1]);
9915
10093
  activate("rooms", { persist: false });