privateboard 0.1.12 → 0.1.13

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
@@ -1904,51 +1904,312 @@
1904
1904
 
1905
1905
  /* ──────────────────────────────────────────────────────────
1906
1906
  Search page · cross-room keyword search results.
1907
- Mirrors notes-page chrome (head + meta) so the three
1908
- destinations (reports / notes / search) share register. */
1907
+ Two postures driven by `.is-initial` / `.has-results` on
1908
+ `.search-page` (toggled by `app.renderSearchPage` /
1909
+ `app.runSearch`):
1910
+ · is-initial · vertically-centred Google-style hero with
1911
+ a wide search input and a mono caption beneath. Reads
1912
+ as "this IS the page" rather than chrome above an
1913
+ empty list.
1914
+ · has-results · the hero collapses, the input shrinks to
1915
+ a compact head row paired with the result-count meta,
1916
+ and the list below uses Google-style 3-line rows
1917
+ (mono source breadcrumb · sans link title · sans
1918
+ snippet body).
1919
+ The same input element serves both states so the focus /
1920
+ value / cursor position survive the swap. */
1909
1921
  .search-page {
1910
- flex: 1;
1911
- padding: 24px 32px 40px;
1912
- max-width: 920px;
1922
+ /* No `flex: 1` here · search-page must grow naturally with
1923
+ its content (especially long result lists) so the sticky
1924
+ `.search-card`'s containing block extends through all the
1925
+ rows. With `flex: 1`, the search-page was clamped to one
1926
+ viewport tall and the sticky card un-stuck after scrolling
1927
+ past that — exactly the "sticky disappears after a few
1928
+ scrolls" bug. `min-height: 100%` is still the floor so the
1929
+ is-initial hero can vertical-centre against a viewport-
1930
+ sized stage. */
1913
1931
  width: 100%;
1932
+ max-width: 920px;
1914
1933
  margin: 0 auto;
1915
- }
1916
- .search-page-head {
1934
+ padding: 32px 32px 40px;
1917
1935
  display: flex;
1918
- align-items: baseline;
1919
- justify-content: space-between;
1920
- gap: 16px;
1921
- margin-bottom: 14px;
1922
- border-bottom: 0.5px solid var(--line);
1923
- padding-bottom: 14px;
1936
+ flex-direction: column;
1937
+ min-height: 100%;
1938
+ box-sizing: border-box;
1939
+ /* NO `position: relative` here · the deco layers below
1940
+ anchor to `.main-view[data-main-view="search"]` instead
1941
+ (which IS position:relative) so they fill main-view's
1942
+ width — the visible room content area to the right of
1943
+ the sidebar — rather than the .search-page's own 920px
1944
+ column. Earlier the deco was anchored here and used a
1945
+ `width: 100vw` trick to escape, but `vw` units include
1946
+ the sidebar's slice of the viewport, so the deco's left
1947
+ edge ended up underneath the sidebar (and the corner
1948
+ brackets sat where the user couldn't see them). */
1949
+ }
1950
+ /* ─── Initial / empty state · 8-bit ambient deco + wide card ───
1951
+ The page surface gets a static 8-bit "constellation" overlay
1952
+ at the top — scattered pixel dots + a few lime accents —
1953
+ drawn with `shape-rendering: crispEdges` to match the
1954
+ round-table stage / chair-sprite vocabulary. Hero is text-
1955
+ only (longer wordmark, subline) since the prior pixel mark
1956
+ read too noisy. The card itself is 760px and `width: 100%`
1957
+ so it actually fills the centred column instead of shrinking
1958
+ to the input's intrinsic width. */
1959
+ .search-page.is-initial {
1960
+ justify-content: center;
1961
+ /* Nudge slightly above true-centre so the hero feels
1962
+ visually weighted. */
1963
+ padding-bottom: 12vh;
1964
+ padding-top: 0;
1965
+ }
1966
+ /* Has-results · inner-scroll layout. The PAGE itself is sized
1967
+ to exactly main-view height (so nothing scrolls at this
1968
+ level), and the results list inside gets its own scroll
1969
+ via `flex: 1 + overflow-y: auto`. This puts the head row
1970
+ in a non-scrolling top slot — no `position: sticky`
1971
+ gymnastics, no containing-block-too-short failure mode.
1972
+ The previous sticky approach disappeared as soon as the
1973
+ user scrolled past one screen because the sticky's
1974
+ containing block (the page) ran out; this design has no
1975
+ such ceiling. */
1976
+ .search-page.has-results {
1977
+ height: 100%;
1978
+ min-height: 0;
1979
+ padding-top: 32px;
1980
+ padding-bottom: 0;
1981
+ flex-shrink: 0;
1982
+ }
1983
+ /* 8-bit ambient deco · absolute-positioned overlay at the top
1984
+ of the page. Pure decoration · pointer-events: none, aria-
1985
+ hidden, fades out the lower edge with a CSS mask so it
1986
+ doesn't compete with the hero / card below.
1987
+
1988
+ Width · `left: 0; right: 0` resolves against the nearest
1989
+ positioned ancestor, which is `.main-view[data-main-view=
1990
+ "search"]` (it sits to the right of the sidebar). The deco
1991
+ therefore spans the FULL main-view width, not the viewport
1992
+ — so the sidebar never overlaps. .search-page is static
1993
+ (no position:relative) so the deco escapes its 920px cap
1994
+ naturally. */
1995
+ .search-bg-deco {
1996
+ position: absolute;
1997
+ top: 0;
1998
+ left: 0;
1999
+ right: 0;
2000
+ height: 280px;
2001
+ pointer-events: none;
2002
+ z-index: -1;
2003
+ -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
2004
+ mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
2005
+ opacity: 0.85;
2006
+ }
2007
+ .search-bg-deco svg { display: block; width: 100%; height: 100%; }
2008
+
2009
+ /* Positioning anchor for the deco layers · main-view sits
2010
+ to the right of the sidebar so its bounds = the visible
2011
+ "room content" width. `isolation: isolate` scopes the
2012
+ deco's z-index: -1 so it sits behind search-page content
2013
+ without leaking past main-view's background. */
2014
+ .main-view[data-main-view="search"] {
2015
+ position: relative;
2016
+ isolation: isolate;
2017
+ }
2018
+
2019
+ .search-page.has-results .search-bg-deco {
2020
+ /* Compact in has-results · keep a slim band of pixel
2021
+ constellation behind the head row so the page still
2022
+ reads as the search page rather than a generic list. */
2023
+ height: 130px;
2024
+ opacity: 0.45;
2025
+ -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 50%, transparent 100%);
2026
+ mask-image: linear-gradient(180deg, #000 0%, #000 50%, transparent 100%);
2027
+ transition: opacity 0.18s ease, height 0.22s ease;
2028
+ }
2029
+
2030
+ /* Results-only deco · second layer that ONLY shows in
2031
+ has-results. Adds proper 8-bit characters on top of the
2032
+ constellation: CRT scanlines (CSS), pixel antennas at
2033
+ the corners, scattered pixel "plus" sparkles, and a
2034
+ dashed horizon line at the bottom edge. The combination
2035
+ gives the results header genuine "search station"
2036
+ atmosphere instead of a thin star field. */
2037
+ .search-results-deco {
2038
+ position: absolute;
2039
+ top: 0;
2040
+ /* Spans full main-view width · the deco's `position:
2041
+ absolute` resolves to `.main-view[data-main-view=
2042
+ "search"]` (now position:relative) since .search-page
2043
+ is static. No `100vw` trick needed — main-view already
2044
+ sits to the right of the sidebar. */
2045
+ left: 0;
2046
+ right: 0;
2047
+ height: 130px;
2048
+ pointer-events: none;
2049
+ z-index: -1;
2050
+ opacity: 0;
2051
+ transition: opacity 0.22s ease;
2052
+ /* Faint CRT scanlines · 1px lime-tinted line every 5px.
2053
+ Subtle enough not to compete with the input but
2054
+ persistent enough to read as "this surface is alive." */
2055
+ background-image: repeating-linear-gradient(
2056
+ 0deg,
2057
+ transparent 0px,
2058
+ transparent 4px,
2059
+ rgba(111, 181, 114, 0.035) 4px,
2060
+ rgba(111, 181, 114, 0.035) 5px
2061
+ );
2062
+ -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 60%, transparent 100%);
2063
+ mask-image: linear-gradient(180deg, #000 0%, #000 60%, transparent 100%);
2064
+ }
2065
+ .search-page.has-results .search-results-deco {
2066
+ opacity: 0.85;
1924
2067
  }
1925
- .search-page-title {
2068
+ .search-results-deco svg { display: block; width: 100%; height: 100%; }
2069
+ .search-hero {
2070
+ text-align: center;
2071
+ max-width: 760px;
2072
+ margin: 0 auto 26px;
2073
+ overflow: hidden;
2074
+ transition:
2075
+ opacity 0.22s ease,
2076
+ max-height 0.24s ease,
2077
+ margin 0.24s ease;
2078
+ }
2079
+ .search-page.is-initial .search-hero {
2080
+ opacity: 1;
2081
+ max-height: 220px;
2082
+ }
2083
+ .search-page.has-results .search-hero {
2084
+ opacity: 0;
2085
+ max-height: 0;
2086
+ margin-top: 0;
2087
+ margin-bottom: 0;
2088
+ pointer-events: none;
2089
+ }
2090
+ .search-hero-title {
1926
2091
  font-family: var(--font-human);
1927
- font-size: 26px;
1928
- font-weight: 700;
1929
- letter-spacing: -0.01em;
2092
+ font-size: 28px;
2093
+ font-weight: 600;
2094
+ letter-spacing: -0.02em;
1930
2095
  color: var(--text);
1931
- margin: 0;
2096
+ line-height: 1.05;
2097
+ margin: 0 0 12px 0;
1932
2098
  }
1933
- .search-page-kicker {
2099
+ .search-hero-sub {
1934
2100
  font-family: var(--mono);
1935
- font-size: 9.5px;
1936
- letter-spacing: 0.22em;
2101
+ font-size: 11px;
2102
+ letter-spacing: 0.18em;
1937
2103
  text-transform: uppercase;
1938
2104
  color: var(--text-faint);
1939
- font-weight: 700;
1940
- margin-bottom: 4px;
2105
+ margin: 0;
1941
2106
  }
2107
+
2108
+ /* Search card · the substantial input affordance for the
2109
+ is-initial state. `width: 100%` is load-bearing — without
2110
+ it the card collapses to its content's intrinsic width
2111
+ (the input had no flex anchor in is-initial because the
2112
+ wrap was display:flex with the input as a single child),
2113
+ making the whole hero look narrow. With `width: 100%`
2114
+ plus `max-width: 760px` the card always fills the
2115
+ centred column. In has-results the card chrome strips
2116
+ and the inner input-wrap takes back its own border so
2117
+ the compact head row can flex input + meta side-by-side. */
2118
+ .search-card {
2119
+ display: block;
2120
+ width: 100%;
2121
+ max-width: 760px;
2122
+ margin: 0 auto;
2123
+ box-sizing: border-box;
2124
+ /* Mirror the new-room composer's `.cmp-input-frame` (line
2125
+ ~9989) — the frame is black (`var(--bg)`), the input
2126
+ inside is transparent so it inherits the black, and the
2127
+ toolbar at the bottom is `panel-2` (lighter). The two
2128
+ tones plus the hairline divider make it read as one
2129
+ designed input object. `overflow: hidden` is load-bearing
2130
+ so the panel-2 toolbar doesn't bleed past the frame's
2131
+ hairline border. */
2132
+ background: var(--bg);
2133
+ border: 0.5px solid var(--line-strong);
2134
+ overflow: hidden;
2135
+ transition:
2136
+ border-color 0.18s,
2137
+ background 0.18s,
2138
+ max-width 0.22s ease,
2139
+ margin 0.22s ease,
2140
+ padding 0.22s ease;
2141
+ }
2142
+ .search-card:focus-within {
2143
+ border-color: var(--lime);
2144
+ }
2145
+ .search-page.has-results .search-card {
2146
+ display: flex;
2147
+ align-items: center;
2148
+ gap: 14px;
2149
+ max-width: none;
2150
+ /* No `margin-bottom` here · the 18px breathing room below
2151
+ the head row moved INSIDE `.search-results` (via the
2152
+ ::before pseudo down at line ~XXX). With it on the card,
2153
+ that 18px lived OUTSIDE the scroll area and permanently
2154
+ stole vertical real estate; the scroll content was
2155
+ truncated 18px earlier than the visible band. Moving the
2156
+ gap into the scroll content means it scrolls away
2157
+ naturally as the user reads down. */
2158
+ margin: 0;
2159
+ padding-top: 12px;
2160
+ padding-bottom: 14px;
2161
+ /* The head row is now in a NON-scrolling top slot of
2162
+ search-page (the page is fixed at main-view height; the
2163
+ scroll lives inside `.search-results`). No `position:
2164
+ sticky` needed — the card stays put because the surface
2165
+ around it doesn't move. */
2166
+ background: var(--panel);
2167
+ border: none;
2168
+ flex-shrink: 0;
2169
+ z-index: 2;
2170
+ /* Stacking context for the full-width ::after pseudo. */
2171
+ position: relative;
2172
+ isolation: isolate;
2173
+ }
2174
+ /* Full-width head band · ::after extends a panel-coloured
2175
+ strip + 0.5px under-line from -100vmax to +100vmax past
2176
+ the card's 920px box, clipped by main-view's overflow.
2177
+ z-index: -1 keeps it behind the card's children (input /
2178
+ chips / meta) inside the card's own stacking context. */
2179
+ .search-page.has-results .search-card::after {
2180
+ content: "";
2181
+ position: absolute;
2182
+ inset: 0 -100vmax;
2183
+ background: var(--panel);
2184
+ border-bottom: 0.5px solid var(--line);
2185
+ z-index: -1;
2186
+ pointer-events: none;
2187
+ }
2188
+ .search-page.has-results .search-card:focus-within {
2189
+ border-color: transparent;
2190
+ border-bottom-color: var(--lime);
2191
+ }
2192
+
2193
+ /* Input wrap · borderless inside the card in is-initial,
2194
+ bordered pill in has-results. */
1942
2195
  .search-input-wrap {
1943
2196
  position: relative;
1944
2197
  display: flex;
1945
2198
  align-items: center;
1946
- margin: 8px 0 22px;
2199
+ flex: 1;
2200
+ min-width: 0;
2201
+ transition: border-color 0.18s, background 0.18s;
2202
+ }
2203
+ .search-page.is-initial .search-input-wrap {
2204
+ background: transparent;
2205
+ border: none;
2206
+ padding: 6px 4px 0;
2207
+ }
2208
+ .search-page.has-results .search-input-wrap {
1947
2209
  background: var(--panel-2);
1948
2210
  border: 0.5px solid var(--line);
1949
- transition: border-color 0.12s, background 0.12s;
1950
2211
  }
1951
- .search-input-wrap:focus-within {
2212
+ .search-page.has-results .search-input-wrap:focus-within {
1952
2213
  border-color: var(--lime);
1953
2214
  background: var(--panel);
1954
2215
  }
@@ -1956,135 +2217,380 @@
1956
2217
  display: flex;
1957
2218
  align-items: center;
1958
2219
  justify-content: center;
1959
- width: 36px;
1960
2220
  color: var(--text-faint);
1961
2221
  flex-shrink: 0;
1962
- }
2222
+ transition: width 0.22s ease, color 0.12s;
2223
+ }
2224
+ /* Hide the leading magnifier in BOTH states · the hero card
2225
+ is its own affordance, and in has-results the input is
2226
+ unmistakably a search field by context (sort chips +
2227
+ result-count meta beside it). The icon was reading as
2228
+ redundant chrome. */
2229
+ .search-input-icon { display: none; }
1963
2230
  .search-input-wrap:focus-within .search-input-icon { color: var(--lime); }
1964
2231
  .search-input {
1965
2232
  flex: 1;
1966
2233
  min-width: 0;
1967
- padding: 12px 8px;
1968
2234
  background: transparent;
1969
2235
  border: none;
1970
2236
  color: var(--text);
1971
2237
  font-family: var(--font-human);
1972
- font-size: 15px;
1973
2238
  letter-spacing: -0.005em;
1974
2239
  outline: none;
2240
+ transition: padding 0.22s ease, font-size 0.22s ease;
2241
+ }
2242
+ .search-page.is-initial .search-input {
2243
+ /* Same type spec as `.cmp-input` (14.5px sans, line-height
2244
+ 1.55, letter-spacing -0.003em) so the family register
2245
+ matches the new-room composer. SYMMETRIC vertical
2246
+ padding (22 top / 22 bottom) instead of the cmp's
2247
+ 20/12 — the cmp is a TEXTAREA where text starts at
2248
+ the top, but our search-input is a SINGLE-LINE input
2249
+ so the text/placeholder must vertical-centre for it
2250
+ to read right. The taller padding also gives the
2251
+ hero card more presence. */
2252
+ padding: 22px 22px;
2253
+ font-size: 14.5px;
2254
+ line-height: 1.55;
2255
+ letter-spacing: -0.003em;
2256
+ }
2257
+ /* Defuse Chrome's autofill bg · when the browser remembers a
2258
+ prior query, the input gets a pale yellow/gray autofill
2259
+ surface that fights our intended black card. The giant
2260
+ inset box-shadow trick paints the autofill area with our
2261
+ own surface color so the input stays visually consistent
2262
+ with the rest of the card whether autofill engaged or not. */
2263
+ .search-input:-webkit-autofill,
2264
+ .search-input:-webkit-autofill:hover,
2265
+ .search-input:-webkit-autofill:focus {
2266
+ -webkit-box-shadow: 0 0 0 1000px var(--bg) inset !important;
2267
+ -webkit-text-fill-color: var(--text) !important;
2268
+ caret-color: var(--text);
2269
+ transition: background-color 5000s ease-in-out 0s;
2270
+ }
2271
+ .search-page.has-results .search-input {
2272
+ padding: 9px 6px;
2273
+ font-size: 13px;
1975
2274
  }
1976
2275
  .search-input::placeholder {
1977
2276
  color: var(--text-faint);
1978
- font-style: italic;
2277
+ font-style: normal;
2278
+ font-weight: 400;
2279
+ }
2280
+ /* Override the OLDER `.search-input:focus` rule (line ~796)
2281
+ from the sidebar's search-block · that rule sets
2282
+ `background: var(--panel-2)` on focus, which turned our
2283
+ hero input GRAY whenever the user clicked into it. Both
2284
+ features share the class name `.search-input`, so an
2285
+ unscoped focus rule there leaks here. Scoping under
2286
+ `.search-page` raises specificity and pins the input
2287
+ transparent on focus. */
2288
+ .search-page .search-input:focus,
2289
+ .search-page .search-input:focus-visible {
2290
+ background: transparent;
2291
+ border: none;
2292
+ outline: none;
1979
2293
  }
1980
2294
  .search-input-clear {
1981
- width: 36px;
1982
- height: 36px;
1983
2295
  background: transparent;
1984
2296
  border: none;
1985
2297
  color: var(--text-faint);
1986
- font-size: 13px;
1987
2298
  cursor: pointer;
1988
- transition: color 0.12s;
2299
+ transition:
2300
+ color 0.12s,
2301
+ width 0.22s ease,
2302
+ height 0.22s ease;
1989
2303
  flex-shrink: 0;
2304
+ display: inline-flex;
2305
+ align-items: center;
2306
+ justify-content: center;
2307
+ }
2308
+ .search-page.is-initial .search-input-clear {
2309
+ width: 36px;
2310
+ height: 36px;
2311
+ font-size: 13px;
2312
+ margin-right: 6px;
2313
+ }
2314
+ .search-page.has-results .search-input-clear {
2315
+ width: 32px;
2316
+ height: 32px;
2317
+ font-size: 12px;
1990
2318
  }
1991
2319
  .search-input-clear:hover { color: var(--text); }
1992
-
1993
- .search-results-meta {
2320
+ /* Hide the clear button when the input is empty (set by JS
2321
+ via the .is-empty class on .search-card). Keeps the
2322
+ hero's right edge clean when the user hasn't typed yet. */
2323
+ .search-card.is-empty .search-input-clear { display: none; }
2324
+
2325
+ /* Internal toolbar · footer of the card in is-initial.
2326
+ Mono hint on the left, lime send button on the right.
2327
+ Hidden entirely in has-results (the card is collapsed
2328
+ to a head row by then). */
2329
+ .search-input-toolbar {
2330
+ /* Mirror `.cmp-toolbar` (line ~10028) — same padding /
2331
+ min-height / hairline-divider / panel-2 surface so the
2332
+ two-tone frame reads as one designed input. The panel-2
2333
+ tone is the visual cue that "this is the action row."
2334
+ It's slightly lighter than the input area's black so
2335
+ the eye registers it as a distinct band without a heavy
2336
+ border. */
2337
+ display: flex;
2338
+ align-items: center;
2339
+ justify-content: space-between;
2340
+ padding: 8px 8px 8px 14px;
2341
+ min-height: 48px;
2342
+ border-top: 0.5px solid var(--line);
2343
+ background: var(--panel-2);
2344
+ }
2345
+ .search-page.has-results .search-input-toolbar { display: none; }
2346
+ .search-toolbar-hint {
2347
+ font-family: var(--mono);
2348
+ font-size: 9.5px;
2349
+ letter-spacing: 0.16em;
2350
+ text-transform: uppercase;
2351
+ color: var(--text-faint);
2352
+ }
2353
+ /* Starter chips · static suggestions below the card in
2354
+ is-initial. Click → pre-fill the input + trigger search.
2355
+ Hidden in has-results. */
2356
+ .search-starters {
2357
+ display: flex;
2358
+ align-items: center;
2359
+ justify-content: center;
2360
+ gap: 6px;
2361
+ margin: 20px auto 0;
2362
+ max-width: 760px;
1994
2363
  font-family: var(--mono);
1995
2364
  font-size: 10px;
1996
2365
  letter-spacing: 0.14em;
1997
2366
  text-transform: uppercase;
1998
2367
  color: var(--text-faint);
1999
- margin-bottom: 14px;
2368
+ flex-wrap: wrap;
2000
2369
  }
2001
- .search-empty {
2002
- padding: 60px 0;
2003
- text-align: center;
2004
- color: var(--text-faint);
2370
+ .search-page.has-results .search-starters { display: none; }
2371
+ .search-starters-label { color: var(--text-faint); margin-right: 4px; }
2372
+ .search-starter {
2373
+ background: transparent;
2374
+ border: 0.5px solid var(--line);
2375
+ color: var(--text-soft);
2376
+ font: inherit;
2377
+ letter-spacing: inherit;
2378
+ text-transform: inherit;
2379
+ cursor: pointer;
2380
+ padding: 5px 10px;
2381
+ transition: color 0.12s, border-color 0.12s;
2005
2382
  }
2006
- .search-empty-kicker {
2007
- display: block;
2383
+ .search-starter:hover {
2384
+ color: var(--lime);
2385
+ border-color: var(--lime);
2386
+ }
2387
+
2388
+ /* Result-count meta · sits beside the shrunk input in the
2389
+ has-results head row. Hidden entirely in is-initial. */
2390
+ .search-results-meta {
2008
2391
  font-family: var(--mono);
2009
2392
  font-size: 10px;
2010
- letter-spacing: 0.18em;
2393
+ letter-spacing: 0.14em;
2011
2394
  text-transform: uppercase;
2012
- color: var(--lime);
2013
- margin-bottom: 8px;
2395
+ color: var(--text-faint);
2396
+ white-space: nowrap;
2397
+ flex-shrink: 0;
2014
2398
  }
2015
- .search-empty-msg {
2016
- font-family: var(--font-human);
2017
- font-size: 14px;
2399
+ .search-page.is-initial .search-results-meta { display: none; }
2400
+
2401
+ /* Sort chip group · "Newest" / "Oldest" toggle that re-sorts
2402
+ the result list client-side (no re-fetch). Only visible in
2403
+ has-results · the head row's flex layout slots it between
2404
+ the input and the meta count. Active chip uses the lime
2405
+ accent border + caps; inactives are line-faint until hover. */
2406
+ .search-results-sort {
2407
+ display: none;
2408
+ align-items: center;
2409
+ gap: 4px;
2410
+ flex-shrink: 0;
2411
+ font-family: var(--mono);
2412
+ font-size: 9.5px;
2413
+ letter-spacing: 0.14em;
2414
+ text-transform: uppercase;
2415
+ }
2416
+ .search-page.has-results .search-results-sort {
2417
+ display: inline-flex;
2418
+ }
2419
+ .search-results-sort .srs-label {
2420
+ color: var(--text-faint);
2421
+ margin-right: 4px;
2422
+ }
2423
+ .search-results-sort button {
2424
+ background: transparent;
2425
+ border: 0.5px solid var(--line);
2426
+ color: var(--text-faint);
2427
+ font: inherit;
2428
+ letter-spacing: inherit;
2429
+ text-transform: inherit;
2430
+ padding: 5px 9px;
2431
+ cursor: pointer;
2432
+ transition: color 0.12s, border-color 0.12s;
2433
+ }
2434
+ .search-results-sort button:hover {
2018
2435
  color: var(--text-soft);
2019
- line-height: 1.5;
2020
- max-width: 520px;
2021
- margin: 0 auto;
2436
+ border-color: var(--text-faint);
2022
2437
  }
2438
+ .search-results-sort button.active {
2439
+ color: var(--lime);
2440
+ border-color: var(--lime);
2441
+ }
2442
+ .search-results-sort button.active:hover {
2443
+ color: var(--lime);
2444
+ border-color: var(--lime);
2445
+ }
2446
+
2447
+ /* Results · only visible in has-results. Top padding gives a
2448
+ small breathing room below the head row's hairline. */
2449
+ .search-results {
2450
+ padding-top: 4px;
2451
+ }
2452
+ .search-page.is-initial .search-results { display: none; }
2453
+ /* Inner scroll container · in has-results, `.search-results`
2454
+ owns the vertical scroll instead of `.main-view`. The
2455
+ search-page itself is sized to exactly main-view height,
2456
+ so the head row (`.search-card`) sits in a non-scrolling
2457
+ top slot and stays there permanently. */
2458
+ .search-page.has-results .search-results {
2459
+ flex: 1;
2460
+ min-height: 0;
2461
+ overflow-y: auto;
2462
+ padding-bottom: 40px;
2463
+ scrollbar-width: thin;
2464
+ }
2465
+ /* Initial breathing-room spacer · 18px tall block that sits
2466
+ INSIDE the scroll content (before the result list). On
2467
+ first render it gives the user the same gap below the
2468
+ head row that a `margin-bottom: 18px` would; once they
2469
+ start scrolling, this spacer scrolls away with the rest
2470
+ of the list (it's part of the scrollable content), so it
2471
+ doesn't permanently consume visible scroll real estate
2472
+ the way an outside-the-scroll margin did. */
2473
+ .search-page.has-results .search-results::before {
2474
+ content: "";
2475
+ display: block;
2476
+ height: 18px;
2477
+ }
2478
+ .search-page.has-results .search-results::-webkit-scrollbar { width: 8px; }
2479
+ .search-page.has-results .search-results::-webkit-scrollbar-thumb { background: transparent; }
2480
+ .search-page.has-results .search-results::-webkit-scrollbar-track { background: transparent; }
2481
+ .search-page.has-results .search-results:hover::-webkit-scrollbar-thumb { background: var(--line-bright); }
2023
2482
 
2024
- /* Flat list · one row per hit; provenance shown as a footer
2025
- line on each row (`from <Room>`) instead of grouping into
2026
- per-room clusters. Avatar / author / timestamp removed in
2027
- favour of a denser, snippet-first list. */
2028
- .search-flat-list {
2483
+ /* Google-style result row · 3 stacked text lines (mono
2484
+ source breadcrumb · sans link title · sans snippet body).
2485
+ No row hover background, no per-row border only the
2486
+ title line lights up on hover so the row reads calm. */
2487
+ .search-results-list {
2029
2488
  list-style: none;
2030
2489
  margin: 0;
2031
2490
  padding: 0;
2032
- border-top: 0.5px solid var(--line);
2033
2491
  }
2034
- .search-row {
2492
+ .search-results-list > li {
2493
+ margin-bottom: 22px;
2494
+ }
2495
+ .search-results-list > li:last-child { margin-bottom: 0; }
2496
+ .sr-row {
2035
2497
  display: block;
2036
- padding: 12px 14px;
2037
2498
  text-decoration: none;
2038
2499
  color: inherit;
2039
- border-bottom: 0.5px solid var(--line);
2040
- transition: background 0.1s;
2041
2500
  }
2042
- .search-row:hover { background: var(--panel-2); }
2043
- .search-row-snippet {
2501
+ .sr-source {
2502
+ display: flex;
2503
+ align-items: baseline;
2504
+ gap: 6px;
2505
+ font-family: var(--mono);
2506
+ font-size: 10px;
2507
+ letter-spacing: 0.12em;
2508
+ text-transform: uppercase;
2509
+ color: var(--text-faint);
2510
+ margin-bottom: 4px;
2511
+ overflow: hidden;
2512
+ white-space: nowrap;
2513
+ text-overflow: ellipsis;
2514
+ }
2515
+ .sr-source-author {
2516
+ color: var(--text-soft);
2517
+ font-weight: 600;
2518
+ }
2519
+ .sr-source-sep {
2520
+ color: var(--text-faint);
2521
+ margin: 0 2px;
2522
+ }
2523
+ .sr-source-time {
2524
+ color: var(--text-faint);
2525
+ }
2526
+ .sr-title {
2044
2527
  font-family: var(--font-human);
2045
- font-size: 14px;
2046
- line-height: 1.5;
2528
+ font-size: 17px;
2529
+ font-weight: 500;
2530
+ line-height: 1.3;
2047
2531
  color: var(--text);
2532
+ margin: 0 0 4px 0;
2533
+ letter-spacing: -0.005em;
2534
+ transition: color 0.12s;
2535
+ overflow: hidden;
2536
+ text-overflow: ellipsis;
2537
+ white-space: nowrap;
2538
+ }
2539
+ .sr-row:hover .sr-title { color: var(--lime); }
2540
+ .sr-snippet {
2541
+ font-family: var(--font-human);
2542
+ font-size: 13px;
2543
+ line-height: 1.55;
2544
+ color: var(--text-soft);
2048
2545
  word-break: break-word;
2049
- /* Two-line clamp · keeps every row a uniform height so the
2050
- list scans cleanly. The snippet is already centered on the
2051
- match (LEAD/TRAIL chars on either side) so the visible
2052
- lines almost always include the keyword. */
2053
2546
  display: -webkit-box;
2054
2547
  -webkit-line-clamp: 2;
2055
2548
  -webkit-box-orient: vertical;
2056
2549
  overflow: hidden;
2057
2550
  }
2058
- .search-row-snippet mark {
2551
+ .sr-snippet mark {
2059
2552
  background: var(--lime);
2060
2553
  color: var(--bg, #0c0c0c);
2061
2554
  padding: 0 2px;
2062
2555
  border-radius: 2px;
2063
2556
  font-weight: 600;
2064
2557
  }
2065
- .search-row-source {
2066
- display: flex;
2067
- align-items: baseline;
2068
- gap: 6px;
2069
- margin-top: 6px;
2070
- font-family: var(--mono);
2071
- font-size: 9.5px;
2072
- }
2073
- .search-row-source-label {
2558
+
2559
+ /* Empty / hint card · only ever rendered inside .search-results
2560
+ so it's tied to the has-results state. Centered block, calm
2561
+ copy. */
2562
+ .search-empty {
2563
+ padding: 40px 0;
2564
+ text-align: center;
2074
2565
  color: var(--text-faint);
2075
- text-transform: uppercase;
2566
+ }
2567
+ .search-empty-kicker {
2568
+ display: block;
2569
+ font-family: var(--mono);
2570
+ font-size: 10px;
2076
2571
  letter-spacing: 0.18em;
2572
+ text-transform: uppercase;
2573
+ color: var(--lime);
2574
+ margin-bottom: 8px;
2077
2575
  }
2078
- .search-row-source-room {
2576
+ .search-empty-msg {
2577
+ font-family: var(--font-human);
2578
+ font-size: 14px;
2079
2579
  color: var(--text-soft);
2080
- font-weight: 600;
2081
- letter-spacing: 0.04em;
2082
- white-space: nowrap;
2083
- overflow: hidden;
2084
- text-overflow: ellipsis;
2085
- max-width: 60%;
2580
+ line-height: 1.5;
2581
+ max-width: 520px;
2582
+ margin: 0 auto;
2583
+ }
2584
+
2585
+ /* Reduced-motion · snap state changes, no transitions. */
2586
+ @media (prefers-reduced-motion: reduce) {
2587
+ .search-hero,
2588
+ .search-card,
2589
+ .search-input,
2590
+ .search-input-icon,
2591
+ .search-input-clear,
2592
+ .sr-title { transition: none; }
2086
2593
  }
2087
- .search-row:hover .search-row-source-room { color: var(--lime); }
2088
2594
 
2089
2595
  /* Search-result jump · in-place keyword pulse + article outline.
2090
2596
  The keyword wraps in a `.search-keyword-flash` span (~2s lime
@@ -2259,7 +2765,7 @@
2259
2765
  spatially, so each fades. */
2260
2766
  .notes-item-passage {
2261
2767
  font-family: var(--font-human);
2262
- font-size: 14px;
2768
+ font-size: 16px;
2263
2769
  line-height: 1.6;
2264
2770
  color: var(--text);
2265
2771
  margin: 0;
@@ -2711,8 +3217,12 @@
2711
3217
  min-width: 22px;
2712
3218
  padding: 0 5px;
2713
3219
  background: var(--panel-3);
2714
- color: var(--text);
2715
- border: 0.5px solid var(--lime-dim);
3220
+ /* Was `var(--text)` (the brightest text token) which read as a
3221
+ second focal point next to the room subject and pulled the
3222
+ eye away from the live header. Drop to `--text-soft` so the
3223
+ chip stays informational (visible but not loud). */
3224
+ color: var(--text-soft);
3225
+ border: 0.5px solid var(--line-bright);
2716
3226
  margin-left: -4px;
2717
3227
  display: inline-flex;
2718
3228
  align-items: center;
@@ -5889,6 +6399,21 @@
5889
6399
  invisible at glance. Bumped to text-dim for legibility while
5890
6400
  still reading as quieter than the name + model in the same line. */
5891
6401
  .msg-tag { color: var(--text-dim); text-transform: lowercase; }
6402
+ /* Excused-from-room marker · the chair removed this director
6403
+ mid-discussion but their past messages still render so the
6404
+ transcript stays coherent. Reads as a muted mono pill in the
6405
+ message header so the reader knows this seat is gone. */
6406
+ .msg-excused {
6407
+ color: var(--text-faint);
6408
+ font-family: var(--mono);
6409
+ font-size: 9.5px;
6410
+ font-weight: 600;
6411
+ letter-spacing: 0.04em;
6412
+ text-transform: lowercase;
6413
+ border: 0.5px dashed var(--line-bright);
6414
+ padding: 1px 6px;
6415
+ cursor: help;
6416
+ }
5892
6417
  /* Subtle pill showing which model produced this message. Reads
5893
6418
  after the agent name; sits in the same monospace meta line
5894
6419
  so it doesn't compete with the conversation. */
@@ -9337,6 +9862,90 @@
9337
9862
  margin: 0 auto;
9338
9863
  padding: 32px 32px;
9339
9864
  width: 100%;
9865
+ /* `isolation: isolate` creates a stacking context here WITHOUT
9866
+ making cmp the containing block for absolute descendants.
9867
+ Result: `.cmp-bg-deco` belongs to cmp's stacking context
9868
+ (so `z-index: -1` puts it just below cmp's static content
9869
+ but ABOVE the lower-layer chat / chat-col panel bgs), while
9870
+ its SPATIAL containing block is `.chat-col` (positioned
9871
+ above) — so the deco can span the full chat-col width
9872
+ without being constrained to cmp's 760px. The two-axis
9873
+ split is what makes the Search-page-style backdrop work
9874
+ inside a max-width-narrower section. */
9875
+ isolation: isolate;
9876
+ }
9877
+ /* 8-bit ambient backdrop · pinned at the top of the composer.
9878
+ Same visual language + position as `.search-bg-deco`. The
9879
+ backdrop ESCAPES `.cmp`'s max-width (760px) so it spans the
9880
+ full main-view width — same `left: 50%; margin-left: -50vw;
9881
+ width: 100vw` pattern the Search page uses. `.main-view`'s
9882
+ own `overflow: hidden` clips it to the visible content area,
9883
+ so "full width" resolves to the room-content rect (sidebar
9884
+ not included). Scene-tuned SVG content is injected by
9885
+ `composerBgDecoSvg(scene)` in app.js · room mode draws a
9886
+ mini boardroom (table + chairs), agent mode draws a row of
9887
+ pixel character heads + speech bubble. */
9888
+ .cmp-bg-deco {
9889
+ /* Containing block is `.chat-col` (set position: relative above)
9890
+ so `top/left/right: 0` snap to the FULL CHAT COLUMN — exactly
9891
+ the right-pane width the user wants. No `100vw` escape · the
9892
+ previous viewport-relative width spilled behind the sidebar
9893
+ because the sidebar lives OUTSIDE this main-view's overflow
9894
+ clip, so the deco rendered under it. Anchoring to `.chat-col`
9895
+ gets us the right pane's actual content rect and nothing
9896
+ beyond it. */
9897
+ position: absolute;
9898
+ top: 0;
9899
+ left: 0;
9900
+ right: 0;
9901
+ height: 280px;
9902
+ pointer-events: none;
9903
+ z-index: -1;
9904
+ -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
9905
+ mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
9906
+ opacity: 0.85;
9907
+ }
9908
+ .cmp-bg-deco svg { display: block; width: 100%; height: 100%; }
9909
+ /* 8-bit deco animations · keep subtle so the field reads as
9910
+ "alive" rather than "demanding attention". All animations are
9911
+ opacity / quantised transforms so the pixel-art register
9912
+ holds (no smooth tweens). Each element gets an inline
9913
+ `animation-delay` from the SVG generator so siblings don't
9914
+ pulse in lockstep — the field shimmers organically. */
9915
+ @keyframes deco-twinkle {
9916
+ 0%, 100% { opacity: 1; }
9917
+ 50% { opacity: 0.35; }
9918
+ }
9919
+ @keyframes deco-shine {
9920
+ 0%, 100% { opacity: 1; }
9921
+ 50% { opacity: 0.55; }
9922
+ }
9923
+ @keyframes deco-spark {
9924
+ 0%, 100% { opacity: 1; transform: scale(1); }
9925
+ 50% { opacity: 0.4; transform: scale(0.7); }
9926
+ }
9927
+ @keyframes deco-bob {
9928
+ 0%, 100% { transform: translateY(0); }
9929
+ 50% { transform: translateY(-1.5px); }
9930
+ }
9931
+ @keyframes deco-blink {
9932
+ 0%, 70%, 100% { opacity: 1; }
9933
+ 80%, 92% { opacity: 0.15; }
9934
+ }
9935
+ .cmp-bg-deco .deco-twinkle { animation: deco-twinkle 4.5s ease-in-out infinite; }
9936
+ .cmp-bg-deco .deco-shine { animation: deco-shine 3.2s ease-in-out infinite; }
9937
+ .cmp-bg-deco .deco-spark { animation: deco-spark 2.4s ease-in-out infinite;
9938
+ transform-box: fill-box; transform-origin: center; }
9939
+ .cmp-bg-deco .deco-bob { animation: deco-bob 3.0s ease-in-out infinite;
9940
+ transform-box: fill-box; transform-origin: center; }
9941
+ .cmp-bg-deco .deco-blink { animation: deco-blink 4.0s ease-in-out infinite; }
9942
+ /* Reduced-motion · stop animation but keep the deco visible. */
9943
+ @media (prefers-reduced-motion: reduce) {
9944
+ .cmp-bg-deco .deco-twinkle,
9945
+ .cmp-bg-deco .deco-shine,
9946
+ .cmp-bg-deco .deco-spark,
9947
+ .cmp-bg-deco .deco-bob,
9948
+ .cmp-bg-deco .deco-blink { animation: none; }
9340
9949
  }
9341
9950
  /* Default composer mode (content fits viewport).
9342
9951
  Toggled by JS via `.chat--composer` (added in
@@ -12399,6 +13008,17 @@
12399
13008
  overflow: hidden;
12400
13009
  background: var(--panel);
12401
13010
  height: 100%;
13011
+ /* `position: relative` makes chat-col the CONTAINING BLOCK
13012
+ for `.cmp-bg-deco` (positioned absolute) so the deco's
13013
+ `left/right: 0` snap to the full chat-col width. NOTE: no
13014
+ `isolation` here · we DON'T want chat-col to also be the
13015
+ stacking context. The deco's `z-index: -1` is meant to
13016
+ paint within `.cmp`'s isolated stacking context (which sits
13017
+ on top of chat / chat-col bgs), not get trapped below
13018
+ chat-col's own panel bg. Splitting the two roles lets the
13019
+ deco stretch wide while still rendering above the chat
13020
+ panel — see `.cmp` + `.cmp-bg-deco` below. */
13021
+ position: relative;
12402
13022
  }
12403
13023
 
12404
13024
  /* Input bar — sits inside the chat column. Live state hosts a
@@ -13478,6 +14098,17 @@
13478
14098
  }
13479
14099
  return;
13480
14100
  }
14101
+ // "search" → cross-room keyword search view. Restores on
14102
+ // refresh same as reports / notes — without this branch
14103
+ // the saved token fell through to the roomId resolver,
14104
+ // failed `appRoomExists("search")`, and bounced the user
14105
+ // back to the new-room composer.
14106
+ if (sub === "search") {
14107
+ if (window.app && typeof window.app.openSearch === "function") {
14108
+ window.app.openSearch();
14109
+ }
14110
+ return;
14111
+ }
13481
14112
  if (!sub || sub === "new") {
13482
14113
  if (window.app && typeof window.app.setComposerMode === "function") {
13483
14114
  window.app.setComposerMode("room");
@@ -13580,9 +14211,9 @@
13580
14211
  // when there's no saved sub-state at all.
13581
14212
  const saved = lsGet(ROOMS_KEY);
13582
14213
  let target;
13583
- if (saved && saved !== "new" && saved !== "reports" && saved !== "notes") {
14214
+ if (saved && saved !== "new" && saved !== "reports" && saved !== "notes" && saved !== "search") {
13584
14215
  target = saved; // explicit room id
13585
- } else if (saved === "reports" || saved === "notes" || saved === "new") {
14216
+ } else if (saved === "reports" || saved === "notes" || saved === "new" || saved === "search") {
13586
14217
  target = saved; // explicit destination preserved on any nav
13587
14218
  } else if (fromTabClick) {
13588
14219
  // No saved sub-state · fresh user clicking the tab. Prefer