/* webapp/app/static/app.css
   Cowork — the G3 Infiniti desk: a private research house, not a terminal.
   Warm paper and deep evergreen grounds, a serif masthead, brass used the way
   a bank uses it -- on the rule, the mark, the thing worth noticing -- and
   mono kept strictly for what is *data*. The grid stays strict, the rules
   stay hairline; the voice changed, not the discipline.

   Everything here is self-hosted. No CDN, no webfont service, no network
   request of any kind leaves the browser for the sake of a page's looks.
   ------------------------------------------------------------------------ */

/* --- typeface -------------------------------------------------------------
   Cormorant Garamond carries the masthead and the report titles -- the same
   serif the G3 site sets its name in, so the app and the house read as one
   thing. Schibsted Grotesk is the working text: a newsroom grotesque, built
   for exactly this kind of reading. IBM Plex Mono stays, for metadata that is
   *data* -- timestamps, channel slugs, agent identifiers, cron expressions,
   counts, tokens. font-display:swap so a cold cache renders in the fallback
   stack immediately rather than showing nothing.                            */

@font-face {
  font-family: "Cormorant Garamond";
  src: url("/static/fonts/cormorant-garamond-latin-600-normal.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Cormorant Garamond";
  src: url("/static/fonts/cormorant-garamond-latin-700-normal.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Cormorant Garamond";
  src: url("/static/fonts/cormorant-garamond-latin-600-italic.woff2") format("woff2");
  font-weight: 600; font-style: italic; font-display: swap;
}
@font-face {
  font-family: "Schibsted Grotesk";
  src: url("/static/fonts/schibsted-grotesk-latin-400-normal.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Schibsted Grotesk";
  src: url("/static/fonts/schibsted-grotesk-latin-500-normal.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Schibsted Grotesk";
  src: url("/static/fonts/schibsted-grotesk-latin-600-normal.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Schibsted Grotesk";
  src: url("/static/fonts/schibsted-grotesk-latin-700-normal.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("/static/fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}

/* --- tokens ---------------------------------------------------------------
   Both themes are first class, and both are the house's: light is warm paper
   under evergreen ink, dark is the evergreen itself with ivory text and brass
   marks -- the G3 site's palette (#0B2625 / #D6B884 / #F4F1EA), extended into
   a working set. Every foreground/background pair below was measured: body
   text >= 4.5:1, control borders and focus rings >= 3:1. The brass is NOT
   reused as light-theme text -- #D6B884 on paper measures under 2:1 -- so
   light resolves --accent-text to a deepened #77602A and keeps the bright
   brass for rules and fills only.                                           */

:root {
  color-scheme: light;

  --bg:            #EFEAE0;   /* recessed: page, sidebar -- warm paper       */
  --surface:       #FAF7F0;   /* raised: reading area, cards                 */
  --surface-2:     #E5DFD1;   /* fills, hovers, data blocks                  */
  --field:         #FFFFFF;   /* input wells -- a raised white on the paper  */
  --text:          #1B2A26;   /* evergreen ink                               */
  --text-muted:    #5C665F;
  --border:        #DAD3C3;   /* hairline separators                         */
  --border-strong: #8B8678;   /* control outlines -- 3:1 against every bg    */
  --primary:       #0F5744;   /* pine-green links and live marks             */
  --primary-solid: #0B2625;   /* the house evergreen, as the solid button    */
  --primary-hover: #133B36;
  --on-primary:    #F4F1EA;
  --accent:        #A8873C;   /* brass: rules and marks                      */
  --accent-text:   #77602A;   /* the same brass, dark enough to read as text */
  --danger:        #A93F2E;
  /* The solid destructive button on the confirm page. --on-danger is a token
     and not #fff because the dark theme's danger is a *light* red, on which
     white measures 2.6:1; each theme names the readable pairing itself. */
  --danger-hover:  #8C2F20;
  --on-danger:     #FAF7F0;
  --focus:         #0F5744;
  --shadow:        0 1px 2px rgba(27, 42, 38, .07),
                   0 8px 24px rgba(27, 42, 38, .09);

  /* type */
  --sans: "Schibsted Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI",
          Roboto, "Helvetica Neue", Arial, sans-serif;
  --display: "Cormorant Garamond", "Iowan Old Style", Georgia, "Times New Roman",
          serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
          "Liberation Mono", monospace;

  --fs-xs:   11px;
  --fs-sm:   12.5px;
  --fs-base: 15px;
  --fs-md:   16px;
  --fs-lg:   19px;
  --fs-xl:   26px;

  /* a 4px grid, used everywhere */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px;

  --radius:    5px;
  --radius-lg: 8px;
  --ease: cubic-bezier(.16, .8, .32, 1);

  /* The four traffic-light colours a report's status and a request's outcome
     are set in. Tokens rather than literals in the component rules, because
     each theme needs its own readable set and a theme that can now be *chosen*
     cannot express that with a media query alone. All pairs verified >= 4.5:1
     against the theme's actual --surface. */
  --st-green:      #237A41;
  --st-dark-green: #1E6B3A;
  --st-amber:      #8A6114;   /* deepened for the light surface              */
  --st-red:        #B23B34;

  /* --- the dark palette, named once ---------------------------------------
     The values live here and nowhere else; the two selectors below are the
     only things that decide *when* they apply -- an explicit choice, or the
     system preference when nothing was chosen. Repeating a value in both
     would be the shadowed-default bug this codebase already learned once.

     The ground was fully green on every surface, which read as "too green"
     and left inputs, cards and buttons without separation. Desaturated to a
     warm near-neutral charcoal that keeps only a whisper of evergreen -- the
     identity lives in the ACCENTS (the rail's active stripe, the agent mark,
     the brass) rather than in every pixel of background.                    */
  --d-bg:            #0C1310;   /* app ground / sidebar                      */
  --d-surface:       #161D1A;   /* reading area, cards                       */
  --d-surface-2:     #222B27;   /* fills, hovers, data blocks                */
  --d-field:         #0D1512;   /* input wells -- recessed below the surface */
  --d-text:          #EAE5D8;   /* warm ivory                               */
  --d-text-muted:    #9BA8A0;
  --d-border:        #2D3833;
  --d-border-strong: #6F7C76;
  --d-primary:       #8CC5AE;   /* sage links -- brass is for marks, not prose */
  --d-primary-solid: #C9A961;   /* brass, as the solid button               */
  --d-primary-hover: #D6B884;
  --d-on-primary:    #14231F;
  --d-accent:        #D6B884;   /* the site's brass, readable on this ground */
  --d-accent-text:   #D6B884;
  --d-danger:        #E8897D;
  --d-danger-hover:  #F0A79D;
  --d-on-danger:     #2B0A0A;
  --d-focus:         #C9A961;
  --d-shadow:        0 1px 2px rgba(0, 0, 0, .4),
                     0 8px 28px rgba(0, 0, 0, .45);
  /* status-amber is remapped in dark, unlike the other three: the light value
     above is deepened for paper and measures only 2.94:1 on the dark surface,
     so dark goes back to the value that already read fine there (5.78:1). */
  --d-st-green:      #4DC27A;
  --d-st-dark-green: #48B374;
  --d-st-amber:      #C8901F;
  --d-st-red:        #E06A61;
}

/* --- which theme is on ----------------------------------------------------
   Two ways in, one palette. `[data-theme]` is written by the pre-paint script
   in base.html from the reader's saved choice; the media query is what a
   reader with no JavaScript -- and the very first paint before the script
   runs -- gets instead. The `:not([data-theme])` guard is what makes the
   explicit choice win: once the script has spoken, the system preference
   stops being consulted, which is the whole point of a manual override.    */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) { color-scheme: dark; }
}

/* The remap. Listed twice because CSS has no way to share one declaration
   block between a media query and a plain selector -- but the *values* are
   single-sourced above, so a colour is still changed in exactly one place. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: var(--d-bg);                     --surface: var(--d-surface);
  --surface-2: var(--d-surface-2);       --field: var(--d-field);
  --text: var(--d-text);                 --text-muted: var(--d-text-muted);
  --border: var(--d-border);             --border-strong: var(--d-border-strong);
  --primary: var(--d-primary);           --primary-solid: var(--d-primary-solid);
  --primary-hover: var(--d-primary-hover); --on-primary: var(--d-on-primary);
  --accent: var(--d-accent);             --accent-text: var(--d-accent-text);
  --danger: var(--d-danger);             --danger-hover: var(--d-danger-hover);
  --on-danger: var(--d-on-danger);       --focus: var(--d-focus);
  --shadow: var(--d-shadow);
  --st-green: var(--d-st-green);         --st-dark-green: var(--d-st-dark-green);
  --st-amber: var(--d-st-amber);         --st-red: var(--d-st-red);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --bg: var(--d-bg);                     --surface: var(--d-surface);
    --surface-2: var(--d-surface-2);       --field: var(--d-field);
    --text: var(--d-text);                 --text-muted: var(--d-text-muted);
    --border: var(--d-border);             --border-strong: var(--d-border-strong);
    --primary: var(--d-primary);           --primary-solid: var(--d-primary-solid);
    --primary-hover: var(--d-primary-hover); --on-primary: var(--d-on-primary);
    --accent: var(--d-accent);             --accent-text: var(--d-accent-text);
    --danger: var(--d-danger);             --danger-hover: var(--d-danger-hover);
    --on-danger: var(--d-on-danger);       --focus: var(--d-focus);
    --shadow: var(--d-shadow);
    --st-green: var(--d-st-green);         --st-dark-green: var(--d-st-dark-green);
    --st-amber: var(--d-st-amber);         --st-red: var(--d-st-red);
  }
}

/* --- reset ---------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--fs-base);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3 { margin: 0; font-weight: 600; line-height: 1.25;
             letter-spacing: -0.011em; }
h1 { font-size: var(--fs-xl); }
h2 { font-size: var(--fs-md); }
h3 { font-size: var(--fs-base); }
p  { margin: 0; }

a { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 2px; }

ul, ol { margin: 0; padding: 0; list-style: none; }

/* One focus treatment, everywhere, and never removed. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 2px;
}

::selection { background: var(--primary); color: var(--on-primary); }

.mono { font-family: var(--mono); font-variant-ligatures: none; }

/* Available to a screen reader, never to the eye. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --- controls ------------------------------------------------------------- */

button, .btn {
  font: 500 var(--fs-sm)/1 var(--sans);
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s2);
  min-height: 32px;
  padding: 0 var(--s3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  transition: background-color 160ms var(--ease), border-color 160ms var(--ease),
              color 160ms var(--ease);
}
button:hover, .btn:hover { background: var(--surface-2); }
button:active, .btn:active { transform: translateY(0.5px); }

button.primary, .btn.primary {
  background: var(--primary-solid);
  border-color: var(--primary-solid);
  color: var(--on-primary);
  min-height: 38px;
  padding: 0 var(--s4);
  font-size: var(--fs-base);
}
button.primary:hover, .btn.primary:hover {
  background: var(--primary-hover); border-color: var(--primary-hover);
}
button.primary.block { width: 100%; }

button.quiet { border-color: var(--border); color: var(--text-muted); }
button.quiet:hover { border-color: var(--border-strong); color: var(--text); }

button.danger { color: var(--danger); }
button.danger:hover { border-color: var(--danger); background: var(--surface-2); }

input, select, textarea {
  font: 400 var(--fs-base)/1.4 var(--sans);
  width: 100%;
  padding: 9px var(--s3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  /* A well, not the surface it sits on. Inputs used to share --surface with
     the card behind them, so a text box was invisible until you clicked it --
     the "the text boxes are green too" report. --field is a distinct tone
     (white on paper, recessed on the dark ground) so a field reads as a
     field at a glance. */
  background: var(--field);
  color: var(--text);
  transition: border-color 160ms var(--ease), box-shadow 160ms var(--ease);
}
input::placeholder, textarea::placeholder { color: var(--text-muted); opacity: 1; }
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 22%, transparent);
}
input[type="checkbox"] { width: auto; min-width: 16px; accent-color: var(--primary-solid); }
input[type="number"] { font-family: var(--mono); }

label { display: block; }

.field { display: block; margin: 0 0 var(--s4); }
.field > .label {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--s2);
}
.field .hint  { display: block; margin-top: var(--s2); }
.field .hint.above { margin: 0 0 var(--s2); }
.field .err   { display: block; margin-top: var(--s2); }

.hint { font-size: var(--fs-sm); color: var(--text-muted); line-height: 1.5; }

/* Errors carry a mark and a word as well as a colour. */
.err {
  font-size: var(--fs-sm);
  color: var(--danger);
  line-height: 1.5;
}
.err::before {
  content: "!";
  font: 600 10px/14px var(--mono);
  display: inline-block; width: 14px; height: 14px;
  margin-right: 6px; vertical-align: 1px;
  text-align: center;
  border: 1px solid currentColor; border-radius: 50%;
}

/* --- the centred pages: login, register, empty ---------------------------- */

.page {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  grid-template-rows: 1fr auto;
  padding: var(--s6) var(--s4) var(--s4);
}

.card {
  width: 100%;
  max-width: 400px;
  margin: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--s6);
  box-shadow: var(--shadow);
}
.card.wide { max-width: 480px; }

/* The wordmark: the house serif over a brass rule -- the one place on a card
   page where the app says whose desk this is. */
.wordmark {
  display: flex; align-items: baseline; gap: var(--s2);
  padding-bottom: var(--s4);
  margin-bottom: var(--s5);
  border-bottom: 1px solid var(--accent);
}
.wordmark .name {
  font-family: var(--display);
  font-size: 28px; font-weight: 700; letter-spacing: .01em; line-height: 1;
}
.wordmark .tag {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted);
  border: 0; padding: 0;
}

.card h1 { font-size: var(--fs-lg); margin-bottom: var(--s2); }
.card .lede { color: var(--text-muted); font-size: var(--fs-sm); margin-bottom: var(--s5); }
.card form { margin: 0; }
.card .foot {
  margin-top: var(--s5); padding-top: var(--s4);
  border-top: 1px solid var(--border);
  font-size: var(--fs-sm); color: var(--text-muted);
}
.card .stack { margin-top: var(--s4); }
.card .stack > * + * { margin-top: var(--s3); }

.page-foot {
  text-align: center;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding-top: var(--s5);
}

/* A banner for one important message on a card page. */
.notice {
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  background: var(--surface-2);
  border-radius: var(--radius);
  padding: var(--s3) var(--s4);
  font-size: var(--fs-sm);
  margin-bottom: var(--s5);
}
.notice.bad    { border-left-color: var(--danger); }
.notice.accent { border-left-color: var(--accent); }
.notice strong { font-weight: 600; }

/* --- the shell ------------------------------------------------------------ */

.shell {
  display: grid;
  /* `auto` rather than a number: the rail is the dock alone on a document
     surface and the dock plus the channel column on a conversation, and it is
     the rail's own children that know which. */
  grid-template-columns: auto minmax(0, 1fr);
  height: 100vh;
  height: 100dvh;
  /* ...and, on a phone with the on-screen keyboard up, the height of what is
     actually still visible. `--vvh` is set only while something is covering
     the page (_composer_js.html); the rest of the time this resolves to the
     line above. Without it the composer -- and its Send button -- sits under
     the keyboard on iOS, where opening one does not change the layout
     viewport at all. */
  height: var(--vvh, 100dvh);
}

/* The mobile drawer latch. Off-screen but focusable, so the channel list is
   reachable by keyboard as well as by tap. No `name`, so it posts nothing. */
.nav-latch {
  position: absolute; width: 1px; height: 1px; opacity: 0;
  margin: 0; pointer-events: none;
}
.scrim { display: none; }

/* --- sidebar -------------------------------------------------------------- */

/* The rail: the dock, and beside it the channel column. Laid out here rather
   than by the shell's grid because on a phone the whole thing becomes one
   sliding drawer, and a drawer is one element. */
.rail { display: flex; min-height: 0; }

.sidebar {
  display: flex; flex-direction: column;
  width: 244px;
  min-height: 0;
  background: var(--bg);
  border-right: 1px solid var(--border);
}

/* Option C, in one rule. A document surface -- the watchlist, a report, a
   page, the account -- gets its 244px back; a conversation keeps the list.
   `display: none` and not `visibility` or a width of zero, because the rows
   must leave the accessibility tree along with the picture. They stay in the
   DOM either way: the unread poll, the mark-read forms and the notifier that
   names the channel a message landed in all read their facts out of them, and
   document.querySelector finds an undisplayed element perfectly well. */
.rail-docs .sidebar { display: none; }

/* --- the dock ------------------------------------------------------------
   Six places and the reader's own controls, in 64px. Everything is a real <a>
   or a real <summary>: the three that are one page navigate, the three that
   are a short list open in place. Labels under the glyphs rather than a
   tooltip on hover, because a dock whose icons have to be learned by hovering
   is a dock nobody learns -- 9px of mono is cheap, and it is what makes this
   legible to somebody who opens the app once a week. */

.dock {
  flex: none;
  /* Wide enough for the longest label at its own size, which is "Watchlist":
     the names here are the names of the pages they open, and shortening one to
     fit is the navigation disagreeing with the thing it navigates to. */
  width: 68px;
  display: flex; flex-direction: column; align-items: center;
  gap: 2px;
  padding: var(--s3) 0 var(--s2);
  background: var(--bg);
  border-right: 1px solid var(--border);
  overflow: visible;        /* the panels open out of it */
}

/* The house mark, in the cut each ground can actually carry.

   Light is the default here and takes the TILE -- the mark on its own
   evergreen, cropped tight to itself by make_icons.py and rounded to the
   app's radius so it reads as the tile it is. Dark swaps to the OPEN cut,
   brass on transparency, which needs no tile because the rail is already the
   ground the mark was drawn for. The open one is not simply used for both:
   its gradient runs to #FAEB9A, which against the paper #EFEAE0 measures
   about 1.1:1 and is gone.

   The tile draws at 40px and the open cut at 32px. That is deliberate: the
   tile keeps a margin of ground around the mark and the open cut does not, so
   equal boxes would print an unequal mark. These two numbers are what make
   the frames the same width in both themes.

   Listed twice below for the same reason the palette is: an explicit choice
   (`[data-theme]`, written by base.html before the first paint) has to beat
   the system preference, and CSS cannot share one declaration block between a
   media query and a plain selector. Which is also why this is two <img>s and
   not <picture media>: that element reads the system preference only, and
   would show the wrong cut to anybody who picked a theme by hand.

   The cost, stated rather than hidden: the browser fetches both cuts and
   paints one, so the unused theme's mark is under 7KB nobody looks at. Two
   background-images would have fetched only the matching one -- but app.css is
   a static file with no Jinja in it, so the URL in it could not carry
   asset()'s cache stamp, and an unstamped icon is the stale-asset bug that
   helper exists to stop. A one-time 7KB is the cheaper of the two. */
.dock-brand {
  /* A fixed 40px box, centred, so the 8px the two cuts differ by does not
     shove the whole dock down the page the moment somebody flips the theme. */
  display: flex; align-items: center; justify-content: center;
  height: 40px;
  padding: 2px 0 var(--s3);
  box-sizing: content-box;
}
.dock-brand img { display: block; }
.dock-brand .mark-tile { border-radius: var(--radius); }
.dock-brand .mark-open { display: none; }
.dock-brand:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

:root[data-theme="dark"] .dock-brand .mark-open { display: block; }
:root[data-theme="dark"] .dock-brand .mark-tile { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .dock-brand .mark-open { display: block; }
  :root:not([data-theme]) .dock-brand .mark-tile { display: none; }
}

/* One rule for the links and the disclosure summaries alike: a place you can
   be is a place you can be, and the eye should not be able to tell which of
   the six opens a list. */
.dock-item {
  position: relative;
  box-sizing: border-box;
  width: 56px; min-height: 46px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px;
  border-radius: var(--radius);
  color: var(--text-muted);
  text-decoration: none;
  cursor: pointer;
  list-style: none;          /* it is a <summary> half the time */
  transition: background 120ms var(--ease), color 120ms var(--ease);
}
.dock-item::-webkit-details-marker { display: none; }
.dock-item:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.dock-item:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }

.dock-name {
  font-family: var(--mono);
  font-size: 9px; letter-spacing: .04em; line-height: 1;
}

/* Where you are. The brass fill and the ink are two signals, not one: a fill
   alone is a colour, and this design system does not let a colour carry a
   state on its own. */
.dock-item.on,
.dock-pop.on > .dock-item {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent-text);
}

/* The unread total, on Chat. The only unread signal on a document surface --
   see _sidebar.html's poll, which repaints it. */
.dock-n {
  position: absolute; top: 0; right: 2px;
  min-width: 15px; padding: 0 4px;
  border-radius: 999px;
  background: var(--primary-solid);
  color: var(--on-primary);
  box-shadow: 0 0 0 2px var(--bg);
  font-family: var(--mono); font-size: 9px; font-weight: 700; line-height: 15px;
  text-align: center;
}
.dock-n[hidden] { display: none; }

.dock-face {
  width: 26px; height: 26px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  font-family: var(--mono); font-size: 11.5px; color: var(--text);
}

/* The reader's own control sits at the foot, away from the six places -- it is
   about this person and this device, not about the desk. */
.dock-you { margin-top: auto; }

/* --- a dock panel --------------------------------------------------------- */

.dock-pop { position: relative; }

.dock-panel {
  position: absolute; z-index: 40;
  top: 0; left: calc(100% + 6px);
  width: 250px;
  padding: var(--s2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}
/* Anchored to the bottom, because it opens from the bottom of the rail and a
   250px panel hanging downward from there would go off the screen. */
.dock-panel-you { top: auto; bottom: 0; }

.dock-head {
  margin: var(--s2) var(--s2) var(--s1);
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-muted);
}
.dock-who {
  margin: var(--s1) var(--s2) var(--s2);
  font-size: var(--fs-sm); color: var(--text); font-weight: 500;
}

.dock-list { list-style: none; margin: 0; padding: 0; }
.dock-list a {
  display: block;
  padding: 6px var(--s2);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: 13.5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dock-list a:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.dock-list li.on > a { color: var(--text); font-weight: 500; background: var(--surface-2); }
.dock-tick {
  font-family: var(--mono); font-size: var(--fs-xs);
  color: var(--accent-text); margin-right: 5px;
}

.dock-list-end {
  margin-top: var(--s2);
  padding-top: var(--s2);
  border-top: 1px solid var(--border);
}

/* The one thing a person comes to this panel to *start* rather than navigate
   to, and it leads for that reason -- at the bottom of a list it is a feature
   nobody finds. */
.dock-cta {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  padding: 7px var(--s3);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  color: var(--accent-text);
  font-size: 13px; font-weight: 500;
}
.dock-cta:hover {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent-text); text-decoration: none;
}
.dock-cta svg { flex: none; }

/* The three device controls, which used to sit at the foot of the rail
   competing with the channel list. In here they are what the panel is for. */
.dock-panel .rail-theme,
.dock-panel .rail-notify { padding: var(--s2) var(--s2) 0; }
.dock-panel .rail-notify:last-of-type { padding-bottom: var(--s1); }

@media (hover: none) {
  .dock-item { min-height: 52px; }
  .dock-cta { min-height: 44px; }
  .dock-list a { min-height: 40px; display: flex; align-items: center; }
}

.sidebar .brand {
  display: flex; align-items: baseline; gap: var(--s2);
  padding: var(--s4) var(--s4) var(--s3);
  border-bottom: 1px solid var(--border);
}
.sidebar .brand .name {
  font-family: var(--display);
  font-size: 23px; font-weight: 700; letter-spacing: .01em; line-height: 1;
  color: var(--text);
}

.rail-label {
  font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--text-muted);
  padding: var(--s4) var(--s4) var(--s2);
}

/* --- collapsible rail sections -------------------------------------------
   Four lists in one column -- channels, direct messages, pages, research --
   is more than a rail can show at once on a laptop, so each is a <details>
   the reader can shut. <details> and not a scripted panel: the browser owns
   the disclosure, it works with no JavaScript, and the chevron is already
   the vocabulary the agent roster taught. Script only remembers the choice
   across navigations (see _sidebar.html).

   The label keeps its own voice exactly -- same size, weight, tracking and
   colour -- so shutting a section is a new affordance on the rail and not a
   new kind of thing on it.                                                */

.rail-sec > summary {
  display: flex; align-items: center; gap: var(--s2);
  list-style: none; cursor: pointer;
  /* The label's own padding, less a little at the left for the chevron to sit
     in, so the section names stay in the column they were already in. */
  padding-left: var(--s2);
  transition: color 140ms var(--ease);
}
.rail-sec > summary::-webkit-details-marker { display: none; }
.rail-sec > summary:hover { color: var(--text); }
.rail-sec > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.rail-sec-name { flex: 1 1 auto; min-width: 0; }

.rail-chev {
  flex: none;
  transition: transform 160ms var(--ease);
}
.rail-sec[open] > summary .rail-chev { transform: rotate(90deg); }

/* What is waiting inside a shut section. Hidden while the section is open,
   where every row states its own count and an aggregate would just be the
   same news twice. Mono and boxed, because it is the same kind of fact as
   the per-channel badge and this rail sets counts one way. */
.rail-sec-n {
  flex: none;
  min-width: 19px; padding: 0 5px;
  font-family: var(--mono); font-size: var(--fs-xs); font-weight: 400;
  line-height: 17px; text-align: center; letter-spacing: 0;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
}
.rail-sec-n[hidden] { display: none; }
.rail-sec[open] > summary .rail-sec-n { display: none; }

/* A shut section still owes the next one air; an open one already has the
   list's own bottom padding. */
.rail-sec:not([open]) { margin-bottom: var(--s2); }

/* The "+" in the Direct messages summary keeps its place at the end of the
   row now that the name has taken the space between them. */
.rail-sec > summary.dm-head { justify-content: flex-start; }

/* A thumb needs more than a 26px label to aim at. */
@media (hover: none) {
  .rail-sec > summary { min-height: 44px; }
}

/* The rail's one call to action: start a research request. A brass-outlined
   pill, so it reads as a control among a column of navigation links rather
   than as one more place to go. Not solid-filled -- a filled brass block in
   the sidebar would outshout the channel a reader is actually in (the active
   row is the accent) -- but outlined and brass-lettered, it is unmistakably
   the button. Hover fills it. */

/* The direct-messages heading carries the only control in the rail that makes
   something: "start a conversation". It sits on the label rather than under
   the list because that is where the eye already is when it decides there is
   nobody in the list to talk to. */
.rail-label.dm-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s2);
}
.rail-new {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 3px; line-height: 0;
  color: var(--text-muted);
  border: 1px solid transparent; border-radius: var(--radius);
  transition: color 140ms var(--ease), border-color 140ms var(--ease);
}
.rail-new:hover { color: var(--accent-text); border-color: var(--border); }
.rail-new:focus-visible { color: var(--accent-text); }

/* A person's name is prose, not data: sans, sentence case, no hash. Sized to
   match the mono slug above it so the two lists share one rhythm. */
.channel-list .person {
  font-size: 13.5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dm-list li.unread .person { font-weight: 600; }

/* Nobody to show. A line of instruction rather than an empty box -- the whole
   feature is invisible until you have used it once. */
.channel-list li.rail-none { padding: 0; }
.channel-list li.rail-none a {
  display: block; width: 100%;
  padding: 7px var(--s2) 7px 10px;
  font-size: var(--fs-sm); color: var(--text-muted);
}
.channel-list li.rail-none a:hover { color: var(--accent-text); text-decoration: none; }

/* Two lists now -- channels, then direct messages -- and one scroller around
   both. The rail scrolls as one column rather than as two independently
   scrolling halves, which is what happens if each <ul> keeps its own overflow
   and is the sort of thing you only notice once you have nine channels and
   four conversations. */
.rail-scroll { flex: 1; min-height: 0; overflow-y: auto; }
.channel-list { padding: 0 var(--s2); }
.channel-list:last-child { padding-bottom: var(--s4); }

/* The row, not the link, carries the state: the unread count and the
   mark-read control sit beside the link and have to share its fill, its rule
   and its hover. */
.channel-list li {
  display: flex; align-items: center; gap: var(--s1);
  padding-right: 6px;
  border-left: 2px solid transparent;
  border-radius: 0 var(--radius) var(--radius) 0;
  transition: background-color 160ms var(--ease);
}
.channel-list li:hover { background: var(--surface-2); }

.channel-list a {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: baseline; gap: var(--s2);
  padding: 7px var(--s2) 7px 10px;
  color: var(--text-muted);
  transition: color 160ms var(--ease);
}
.channel-list a:hover { color: var(--text); text-decoration: none; }
.channel-list .slug { font-family: var(--mono); font-size: 13.5px;
                      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.channel-list .hash { color: var(--text-muted); }

/* Current channel: a filled row AND an accent rule AND full-strength text --
   three signals, so colour is never carrying it alone. */
.channel-list li.active {
  background: var(--surface-2);
  border-left-color: var(--accent);
}
.channel-list li.active > a { color: var(--text); font-weight: 500; }
.channel-list li.active .hash { color: var(--accent-text); }

.channel-list .kind {
  margin-left: auto;
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-muted);
}

/* --- unread --------------------------------------------------------------
   Same discipline as the current-channel row: never one signal. An unread
   channel is set in 600 at full text strength (weight and contrast, both
   legible with no colour at all) and states its count in mono. The badge is
   the number, not the news. */
.channel-list li.unread > a { color: var(--text); }
.channel-list li.unread .slug { font-weight: 600; }
.channel-list li.unread .hash { color: var(--text); }

.unread-count {
  flex: none;
  min-width: 21px;
  padding: 0 5px;
  font-family: var(--mono); font-size: var(--fs-xs);
  line-height: 18px; text-align: center;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
}
.channel-list li.active .unread-count { background: var(--surface); }
.unread-count[hidden] { display: none; }

/* --- mentioned -----------------------------------------------------------
   Somebody typed this reader's name in here. A different fact from "this
   channel is busy", so a different mark rather than a bigger number -- and,
   as everywhere else in this rail, never one signal: a glyph, a rule down the
   left edge, and the row set at full strength.

   The glyph is the `@` itself, in mono, because that is the character that was
   typed. It is aria-hidden and a visually-hidden clause in the link says the
   words, which is the same split the unread badge makes.

   Deliberately not on the active row's border, which is the accent and means
   "you are here": being told where you are outranks being told where to look
   next, and the flag still shows. */
.channel-list li.mentioned > a { color: var(--text); }
.channel-list li.mentioned .slug,
.channel-list li.mentioned .person { font-weight: 600; }
.channel-list li.mentioned:not(.active) { border-left-color: var(--primary); }
/* A mention can arrive in a channel with no unread count at all -- #logs,
   which the counts leave out on purpose -- so the row needs its own way to be
   marked read. */
.channel-list li.mentioned .mark-read { display: block; }

/* Sits between the link and the unread badge, so the `#` on every row in the
   list stays in the same column whether or not anybody has been named. */
.at-flag {
  flex: none; order: 1;
  min-width: 14px; text-align: center;
  font-family: var(--mono); font-size: 13.5px; font-weight: 700;
  line-height: 1;
  color: var(--primary);
}
.channel-list .unread-count { order: 2; }
.channel-list .mark-read { order: 3; }
.at-flag[hidden] { display: none; }

/* Mark one channel read without opening it. Present only on a row that has
   something to mark, and quiet until the row is hovered or the button itself
   is focused -- the same treatment the message action cluster uses, and for
   the same reason: this is a control for the row you are pointing at, not
   furniture for the nine you are scanning past. It keeps its space either
   way, so revealing it never shifts the list. */
.channel-list .mark-read { display: none; flex: none; }
.channel-list li.unread .mark-read { display: block; }
.channel-list .mark-read button {
  display: block;
  padding: 3px; border: 0; background: none; cursor: pointer;
  line-height: 0; color: var(--text-muted);
  border-radius: var(--radius);
  opacity: 0;
  transition: opacity 140ms var(--ease), color 140ms var(--ease);
}
.channel-list li:hover .mark-read button { opacity: 1; }
.channel-list .mark-read button:hover { color: var(--accent-text); }
.channel-list .mark-read button:focus-visible { opacity: 1; }
/* No hover on a touch screen, so the control simply stands. */
@media (hover: none) {
  .channel-list .mark-read button { opacity: 1; }
}

/* Mark everything read. Quiet, in the rail's own label voice, and absent
   entirely when there is nothing to mark. */
.rail-all { padding: 0 var(--s2) var(--s3); }
.rail-all[hidden] { display: none; }
.rail-all button {
  /* justify-content, not only text-align: the shared button rule is an
     inline-flex that centres its content, and a control that lines up with
     nothing else in the rail reads as an ornament. This sits under
     "Channels", so it starts where "Channels" starts. */
  width: 100%; text-align: left; justify-content: flex-start;
  padding: 6px 10px; border: 0; background: none; cursor: pointer;
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-muted);
  border-radius: var(--radius);
  transition: background-color 160ms var(--ease), color 160ms var(--ease);
}
.rail-all button:hover { background: var(--surface-2); color: var(--text); }

/* Notifications. Two opt-ins and a sentence about the limit, in the rail's
   own label voice -- deliberately the quietest thing in the sidebar. This is
   an offer, not an announcement: a permission control that shouts is a
   permission control that gets denied, and denial is permanent.

   `hidden` until the script unhides it (see _notify_js.html), so a page with
   no JavaScript shows no button that could not do anything. */
.rail-notify { padding: 0 var(--s2) var(--s3); display: flex; flex-wrap: wrap;
               gap: var(--s1); }
.rail-notify[hidden] { display: none; }
.rail-notify button {
  /* Same shape as "Mark all read" above it, because it sits directly under it
     and two near-identical controls that differ by three pixels read as a
     mistake. Not full width: these are two controls, side by side, and the
     second is a toggle rather than a queue of actions. */
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 10px; border: 0; background: none; cursor: pointer;
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-muted);
  border-radius: var(--radius);
  transition: background-color 160ms var(--ease), color 160ms var(--ease);
}
.rail-notify button[hidden] { display: none; }
.rail-notify button:hover { background: var(--surface-2); color: var(--text); }
.rail-notify button svg { flex: none; }
/* On is a state, so it carries colour and weight, not only a word. Off keeps
   the muted voice: the resting state of this feature is off, and the rail
   should not look like something is wrong when it is. */
.rail-notify button.on { color: var(--accent-text); }
/* One icon, two states: the speaker keeps its wave when sound is on and its
   cross when it is off, so the control says which it is without being read. */
.rail-notify button .wave { display: none; }
.rail-notify button.on .wave { display: inline; }
.rail-notify button.on .mute { display: none; }
.notify-note {
  flex-basis: 100%;
  margin: 2px 0 0; padding: 0 10px;
  font-size: var(--fs-xs); line-height: 1.45; color: var(--text-muted);
}

/* --- the theme control ----------------------------------------------------
   Three segments in one hairline group, sitting just above the reader's own
   name at the foot of the rail -- the same corner every other "about you"
   control lives in. Quiet by construction: this is a preference, not an
   action, so it carries no fill until one of the three is on, and the one
   that is on is marked by ground AND weight AND aria-pressed rather than by
   colour alone. Icon-only, but each button carries a visually-hidden word and
   a title, which is what makes it announceable and hoverable. */
.rail-theme { padding: 0 var(--s2) var(--s3); }
.rail-theme[hidden] { display: none; }
.theme-set {
  display: inline-flex;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
}
.theme-set button {
  min-height: 26px; padding: 0 10px;
  border: 0; border-radius: 999px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  line-height: 0;
  transition: background-color 140ms var(--ease), color 140ms var(--ease);
}
.theme-set button:hover { color: var(--text); background: var(--surface-2); }
.theme-set button.on {
  background: var(--surface-2);
  color: var(--accent-text);
  box-shadow: inset 0 0 0 1px var(--border-strong);
}
/* Thumb-sized on a phone, where this sits in the drawer. */
@media (hover: none) { .theme-set button { min-height: 36px; padding: 0 var(--s3); } }

/* --- main column ---------------------------------------------------------- */

.main {
  display: flex; flex-direction: column;
  min-width: 0; min-height: 0;
  background: var(--surface);
}

.main > header {
  display: flex; align-items: flex-start; gap: var(--s3);
  padding: var(--s4) var(--s6);
  border-bottom: 1px solid var(--border);
}
.main > header .titles { min-width: 0; }
/* The channel name is the page's title now, not a slug in a log: working
   sans, semibold, with the hash standing back. The slug voice survives where
   the string is data -- the rail, the mention chips, #logs. */
.main > header h1 {
  font-size: var(--fs-lg); font-weight: 600; letter-spacing: -.014em;
}
.main > header h1 .hash { color: var(--accent-text); font-weight: 500; }
.main > header .purpose {
  margin-top: 2px; font-size: var(--fs-sm); color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.main > header .spacer { margin-left: auto; }
/* the drawer button, mobile only */
.nav-btn {
  display: none;
  align-items: center; justify-content: center;
  width: 44px; height: 44px; margin: -4px 0 -4px -10px;
  border-radius: var(--radius);
  color: var(--text);
  cursor: pointer; flex: none;
}
.nav-btn:hover { background: var(--surface-2); }
.nav-latch:focus-visible ~ .main .nav-btn {
  outline: 2px solid var(--focus); outline-offset: 2px;
}

/* --- the agent roster -----------------------------------------------------
   Who is in this channel and what each one is for, under the header and above
   the conversation. A <details>, closed by default: the desk reads these
   channels all day, and six rows of agent standing permanently between the
   title and the first message would cost more than the discoverability it
   buys. Closed it is one 28px line carrying the count.

   No script anywhere in this block. The roster is the path by which somebody
   finds out an agent exists at all, so opening it cannot be the thing that
   needs JavaScript. app/templates/_mention_js.html adds click-to-insert on top
   and marks the element .can-insert when it has; everything below that is not
   under .can-insert is what a reader with no scripting gets.               */

.roster { flex: none; border-bottom: 1px solid var(--border); }

.roster > summary {
  display: flex; align-items: baseline; gap: var(--s2);
  padding: var(--s2) var(--s6);
  list-style: none; cursor: pointer;
  font-size: var(--fs-xs); color: var(--text-muted);
}
.roster > summary::-webkit-details-marker { display: none; }
.roster > summary:hover { background: var(--surface-2); color: var(--text); }
.roster-chev {
  flex: none; align-self: center;
  transition: transform 160ms var(--ease);
}
.roster[open] > summary .roster-chev { transform: rotate(90deg); }
.roster-label {
  font-weight: 600; letter-spacing: .07em; text-transform: uppercase;
}
/* The count is data, so it is set in mono like every other count in this app. */
.roster-count { font-family: var(--mono); color: var(--text); }
.roster-hint { min-width: 0; overflow: hidden; text-overflow: ellipsis;
               white-space: nowrap; }

.roster-list {
  /* An open roster is a list, not a panel: it can be scrolled past rather than
     pushing the conversation off a short screen. */
  max-height: 42vh; overflow-y: auto;
  max-width: 76ch;
  padding: var(--s1) var(--s6) var(--s3);
}
/* Air between agents, none inside one. Two lines that belong together and a
   gap before the next pair is what makes this read as four entries rather than
   as eight lines. */
.roster-agent + .roster-agent { margin-top: var(--s2); }

/* Slug and name on one line, purpose on the next and flush with the slug. Not
   indented under the name: `@research-analyst` and `@scout` are ten characters
   apart, so an indent measured from the name would step in and out on every
   row and the block would read as ragged rather than as a list.

   The purpose is *on the page* and not in a title= -- a tooltip is nothing on
   a phone and nothing to somebody skimming, and "show their descriptions" was
   half of what was asked for. */
.roster-row {
  display: grid;
  grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
  column-gap: var(--s3);
  align-items: baseline;
  width: 100%;
  padding: var(--s1) var(--s2);
  margin-left: calc(var(--s2) * -1);
  border-radius: var(--radius);
  text-align: left;
}
.roster-slug {
  font-family: var(--mono); font-size: var(--fs-sm); color: var(--accent-text);
}
.roster-who { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
.roster-purpose {
  grid-column: 1 / -1; font-size: var(--fs-sm); color: var(--text-muted);
  line-height: 1.45;
}

/* The scripted half. A real <button>, swapped in for the span, so it is
   focusable and keyboard-operable for free -- which means undoing the button
   treatment this stylesheet gives every other button. */
.roster-insert {
  min-height: 0;
  border: 0; background: none; color: inherit; font: inherit;
  cursor: pointer;
}
.roster.can-insert .roster-insert:hover { background: var(--surface-2); }
.roster.can-insert .roster-insert:hover .roster-slug { color: var(--accent-text); }

/* --- messages ------------------------------------------------------------- */

.messages {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: var(--s5) var(--s6) var(--s6);
  overflow-wrap: anywhere;
}

/* Reading measure. Prose does not get to run the width of a 27" display. */
.messages > * { max-width: 76ch; }

.msg { padding: 3px 0 3px var(--s4); border-left: 2px solid transparent; }
/* The first message of a run gets air above it; a follow-on sits tight.
   More air than the old desk gave: with agents writing briefs, the gap
   between two authors is what makes a channel scannable at all. */
.msg.lead { margin-top: var(--s5); }
.messages > .msg:first-child { margin-top: 0; }

.msg .byline {
  display: flex; align-items: baseline; gap: var(--s2);
  margin-bottom: 1px;
  flex-wrap: wrap;
}
.msg .who { font-weight: 600; font-size: 14.5px; color: var(--text); }
.msg .at {
  font-family: var(--mono); font-size: var(--fs-xs);
  color: var(--text-muted); letter-spacing: .02em;
}
/* The body is block markup now, not one run of pre-wrapped text: app/markdown.py
   renders the small subset of markdown the agents write in. A line break comes
   through as an explicit <br>, so the container goes back to `normal` --
   pre-wrap on the *container* would add a break for every newline in the
   generated markup as well as the ones somebody typed.

   pre-wrap moves down onto the leaves instead, because this stylesheet had it
   before markdown existed and messages already stored rely on it: an agent
   that lined up a small table with spaces would have it collapsed by `normal`.
   The renderer emits no whitespace between tags, so putting it here preserves
   exactly the runs a writer typed and nothing else. */
.msg .body { white-space: normal; }
.msg .body p, .msg .body li, .msg .body h3, .msg .body h4 { white-space: pre-wrap; }

/* --- markdown inside a message -------------------------------------------
   The rule this whole section is written against: a brief is a *message*, not
   a web page. Structure is worth exactly as much emphasis as it takes to scan
   the thing -- so the type scale barely moves, nothing is centred, nothing is
   boxed unless it is code, and the vertical rhythm stays on the 4px grid the
   rest of the app uses. A headline in a message is 16px and semibold, not
   24px: it has to sit under a 14.5px byline without outranking it.          */

/* Block spacing. Only *between* blocks -- the first and last hug the row, or a
   one-line message would carry margin nobody asked for. */
.msg .body > * + * { margin-top: var(--s2); }

/* A message with more than one block in it is a document, and needs to be told
   apart from the next one. In a run from a single author the byline is
   collapsed and the follow-on row sits 3px under the one above -- which is
   *less* air than the 8px between two paragraphs inside one brief, so two
   briefs in a row read as one long brief. Seen the moment a real agent posted
   twice. A row carrying block structure takes the lead spacing back.
   :has() is a progressive enhancement here: where it is unsupported the
   spacing is exactly what it was before markdown existed. */
.msg:has(.body > * + *) { margin-top: var(--s4); }

.msg .body strong { font-weight: 600; }
.msg .body em { font-style: italic; }

/* A heading in a message. Two levels, both quiet; see app/markdown.py for why
   there is no h1 or h2 on this path. */
.msg .body h3, .msg .body h4 {
  font-weight: 600; line-height: 1.35; letter-spacing: -0.006em;
  color: var(--text);
}
.msg .body h3 { font-size: var(--fs-md); }
.msg .body h4 { font-size: var(--fs-base); }
.msg .body > h3 + *, .msg .body > h4 + * { margin-top: var(--s1); }
.msg .body > * + h3, .msg .body > * + h4 { margin-top: var(--s4); }

/* Lists. The global reset strips markers from every ul in the app, so they are
   put back here and nowhere else. The indent is 1.25em rather than the
   browser's 40px: a bullet that starts a third of the way across a chat column
   reads as an outline, not as a sentence with a dot in front of it. */
.msg .body ul, .msg .body ol {
  padding-left: 1.25em;
  list-style-position: outside;
}
.msg .body ul { list-style: disc; }
.msg .body ol { list-style: decimal; }
.msg .body li::marker { color: var(--text-muted); }
.msg .body ol li::marker { font-family: var(--mono); font-size: var(--fs-sm); }
.msg .body li + li { margin-top: var(--s1); }
/* A nested list is part of the item above it, not a new block. */
.msg .body li > ul, .msg .body li > ol { margin-top: var(--s1); }

/* A quote. One hairline rule and a step back in colour -- the same vocabulary
   the agent stripe and the tombstone already use, so a quoted paragraph reads
   as "not this author's words" without a box or a serif or an oversized mark. */
.msg .body blockquote {
  margin-left: 0;
  padding-left: var(--s3);
  border-left: 2px solid var(--border);
  color: var(--text-muted);
}
.msg .body blockquote > * + * { margin-top: var(--s2); }

/* Code. Inline stays on the text baseline at the same size the timestamps and
   channel slugs use; a block gets the one filled panel in this stylesheet that
   sits inside a message, and scrolls sideways rather than forcing the column
   wider or breaking a line of shell in the middle of a flag. */
.msg .body code {
  font-family: var(--mono); font-variant-ligatures: none;
  font-size: 0.86em;
  background: var(--surface-2);
  border-radius: 3px;
  padding: 0.1em 0.3em;
  overflow-wrap: break-word;
}
.msg .body pre {
  margin: 0;
  padding: var(--s2) var(--s3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow-x: auto;
  white-space: pre;
  -webkit-overflow-scrolling: touch;
}
.msg .body pre code {
  background: none; padding: 0; border-radius: 0;
  font-size: var(--fs-sm); line-height: 1.5;
}

/* A link the reader can see is a link, in a paragraph of prose. Underlined
   here rather than only on hover: inside a body the colour alone is doing the
   work of distinguishing it from bold, and those are different things. */
.msg .body a {
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
}
.msg .body a:hover { text-decoration-color: currentColor; }

/* A system line is mono and small; its code needs to stop being mono-in-mono
   and just be quoted, or a #logs entry turns into a grey mosaic. */
.msg.system .body code { background: none; padding: 0; font-size: 1em; }

/* --- the clamp -------------------------------------------------------------
   An agent's brief can run three screens; a channel of them cannot. Script
   (_clamp_js.html) measures each body after render and clamps the ones past
   the threshold behind a fade and a "Show the full brief" control -- so the
   channel reads as a stack of openings, and any one of them is a click from
   whole. Progressive enhancement in the strict sense: with no script every
   message simply stands at its full height, which is exactly the behaviour
   this replaces. The fade is a mask, not a painted gradient, so it is right
   in both themes and on the thread view's tinted root panel for free.      */

.msg .body.clamped {
  max-height: 30em;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 72px), transparent);
          mask-image: linear-gradient(to bottom, black calc(100% - 72px), transparent);
}
.clamp-toggle {
  display: inline-flex; align-items: center; gap: var(--s2);
  margin-top: var(--s1);
  min-height: 26px; padding: 0 var(--s2);
  border: 0; background: none; cursor: pointer;
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--accent-text);
  border-radius: var(--radius);
}
.clamp-toggle:hover { background: var(--surface-2); }
.clamp-toggle::before { content: "\2026\0020"; }         /* an ellipsis: there is more */
.clamp-toggle.open::before { content: none; }
@media (hover: none) { .clamp-toggle { min-height: 36px; } }

/* --- threads --------------------------------------------------------------
   A reply is not in the channel flow -- it is in exactly one read path, the
   thread view -- so this control is the only sign the channel gives that
   anything is under a message. It was a caption saying "3 in thread" and
   nothing to click, which meant an agent could answer in a thread and no
   person could read the answer. It is a link now, and it is on every message:
   with replies it states the count, with none it is the way to start one,
   which a human could not do at all before this milestone.               */

.msg { position: relative; }

.replies {
  display: inline-block; margin-top: var(--s1);
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .04em;
  color: var(--primary);
}
.replies::after { content: " \203A"; }      /* it goes somewhere */
.replies:hover { text-decoration: underline; text-underline-offset: 3px; }

/* --- the action cluster ---------------------------------------------------
   React, edit, delete, and -- while a message has no replies -- the way to
   start a thread. One hairline strip at the top right of the reading measure,
   invisible until the row is hovered or something inside it takes focus.

   The reasoning is the one the reply control arrived with and this
   generalises: a hundred messages each captioned "Reply · React · Edit ·
   Delete" is noise on the one page whose job is to be scannable, and the
   controls only ever apply to the row being looked at. It is opaque, so on a
   follow-on row with no byline it occludes the text under it instead of
   colliding with it, and it is absolutely positioned so that revealing it
   never moves a single line of the conversation.

   pointer-events is off while it is invisible; keyboard focus ignores that,
   which is what makes :focus-within enough to reveal the whole strip for a
   reader who is tabbing rather than pointing.                              */

.actions {
  position: absolute; top: 2px; right: 0; z-index: 2;
  display: flex; align-items: center; gap: 1px;
  padding: 1px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  opacity: 0; pointer-events: none;
  transition: opacity 120ms var(--ease);
}
.msg:hover > .actions,
.msg:focus-within > .actions { opacity: 1; pointer-events: auto; }
/* An open picker outlives the pointer leaving the row. :focus-within already
   covers the usual case -- clicking a <summary> focuses it -- but a browser
   that disagrees would otherwise close the menu the moment the mouse moved
   towards it. */
.actions:has(details[open]) { opacity: 1; pointer-events: auto; }

/* One treatment for the three kinds of thing in the strip: a link, a
   <summary>, and the reply link. They must read as one control each and as one
   row together, so the shared rule is the whole of it. */
.actions > .act,
.actions > .replies.zero,
.actions .pick > summary {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 22px;
  padding: 0 var(--s2);
  border: 0; border-radius: 3px;
  background: none;
  font-family: var(--mono); font-size: var(--fs-xs); letter-spacing: .04em;
  color: var(--text-muted);
  text-decoration: none;
  cursor: pointer;
  transition: background-color 120ms var(--ease), color 120ms var(--ease);
}
.actions > .act:hover,
.actions > .replies.zero:hover,
.actions .pick > summary:hover {
  background: var(--surface-2); color: var(--text); text-decoration: none;
}
/* Deletion is the one control here that cannot be undone. It says so on
   hover, and only on hover: a permanently red word in every row would read as
   an alert rather than as a control. */
.actions > .act.danger:hover { color: var(--danger); }

/* Inside the strip the reply control is just another item: no floating of its
   own, no chevron (everything here goes somewhere). */
.actions > .replies.zero {
  position: static; opacity: 1; pointer-events: auto; margin: 0;
}
.replies.zero::after { content: ""; }

/* --- the reaction picker --------------------------------------------------
   <details>, so the menu is a popover that costs no layout while it is closed
   and needs no script to open. That is not a nicety: reacting has to work with
   scripting off, and a picker that does not open without JS would make the
   whole feature keyboard-and-JS-only for anything not already reacted to.   */

.pick { position: relative; display: flex; }
.pick > summary { list-style: none; }
.pick > summary::-webkit-details-marker { display: none; }
.pick[open] > summary { background: var(--surface-2); color: var(--text); }

/* A single row, not a labelled list. Two reasons, and the second is the one
   that would have shown up as a bug report: five labelled rows is 160px of
   popover hanging below a message inside a scrolling container, which the
   reader has to scroll to see whenever the message is near the foot of the
   page. A 28px strip clears that, and it is the more restrained thing anyway.
   The words are not lost -- each button carries its label as its accessible
   name and as its tooltip. */
.pick-menu {
  position: absolute; top: calc(100% + 5px); right: -1px; z-index: 20;
  display: flex; gap: 1px;
  padding: var(--s1);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.pick-item {
  min-height: 24px;
  padding: 0 5px;
  border: 0; border-radius: 3px;
  background: none;
}
.pick-item:hover { background: var(--surface-2); }
.pick-item .glyph { font-size: 15px; line-height: 1; }

/* --- reactions ------------------------------------------------------------
   Only the ones somebody pressed, in a compact row under the message. The
   count is the label; who is behind it lives in the title, on hover, because
   a research note with five names printed under it is a note nobody reads.  */

.reactions {
  display: flex; flex-wrap: wrap; gap: var(--s1);
  max-width: 76ch;
  margin: var(--s2) 0 0;
}
.react {
  gap: 5px;
  min-height: 22px;
  padding: 0 7px;
  border: 1px solid var(--border);
  border-radius: 11px;
  background: var(--surface);
  color: var(--text-muted);
  font: 400 var(--fs-xs)/1 var(--mono);
}
.react .glyph { font-size: 13px; line-height: 1; }
.react .n { font-variant-numeric: tabular-nums; letter-spacing: .02em; }
.react:hover:not(:disabled) {
  background: var(--surface-2); border-color: var(--border-strong);
  color: var(--text);
}
/* An archived channel, or a tombstone: the counts stand, the toggle does not. */
.react:disabled { cursor: default; opacity: .8; }

/* The ones you pressed. Three carriers, not one, because the design rule here
   is that nothing is signalled by colour alone: a filled ground, a ring that
   reads as double-weight in greyscale, and the count in a heavier face. */
.react.mine {
  background: var(--surface-2);
  border-color: var(--primary);
  box-shadow: inset 0 0 0 1px var(--primary);
  color: var(--text);
  font-weight: 500;
}
.react.mine:hover:not(:disabled) { border-color: var(--primary); }

/* --- edited, and deleted --------------------------------------------------
   Both are the same idea: a message is not only its text, it is also what has
   happened to it since. Neither is loud, and neither is optional.           */

/* Next to the timestamp, in the same mono the timestamp uses, because it is
   the same kind of fact. `.trailing` is the follow-on row's placement -- no
   byline there to sit in. */
.edited {
  font-family: var(--mono); font-size: var(--fs-xs);
  color: var(--text-muted); letter-spacing: .02em;
}
.edited.trailing { display: block; margin-top: 2px; }

/* The tombstone. Quiet and mono -- the same treatment a system message gets,
   because it is the app talking rather than a person -- and the left rule
   goes hairline so a deleted agent message stops flying the accent stripe for
   content that is no longer there. */
.msg.deleted { border-left-color: var(--border); }
.msg.deleted .who { color: var(--text-muted); font-weight: 500; }
.msg.deleted .tag { color: var(--text-muted); }
.body.tombstone {
  font-family: var(--mono); font-size: var(--fs-sm);
  color: var(--text-muted);
}
.body.tombstone::before { content: "\2014\00a0"; }

/* Nothing to hover on a phone, so the strip stops floating and takes its row.
   One quiet line per message is the price of being able to react to, edit or
   thread a message at all from a device with no pointer.

   It also stops being a strip. Floating over the text, the hairline box is
   what separates the controls from the message they belong to; standing in
   the flow on every single row, the same box turns a reading surface into a
   toolbar -- which is the thing this desk is not. So the container gives up
   its border, its fill and its padding, and the controls go back to being
   what the reply link was before any of this: quiet mono words on a line. */
@media (hover: none) {
  .actions {
    position: static;
    opacity: 1; pointer-events: auto;
    /* inline-flex rather than flex, so the strip is content-width on its own
       line without the message row having to become a flex column. */
    display: inline-flex;
    max-width: 100%;
    margin-top: var(--s1);
    flex-wrap: wrap;
    gap: var(--s4);
    padding: 0;
    border: 0;
    background: none;
    /* The reply count follows the strip in the source and, with the strip
       standing in the flow rather than floating, now sits on the same line as
       it. Without this the row reads "Delete2 replies". */
    margin-right: var(--s4);
  }
  /* 22px is a fine target for a mouse and a poor one for a thumb. The words
     do not change size -- only the box around them, which is invisible. */
  .actions > .act,
  .actions > .replies.zero,
  .actions .pick > summary,
  .react {
    min-height: 30px;
    padding-left: 0; padding-right: 0;
  }
  /* On a phone the strip stands under every message, so it whispers: a step
     smaller and a step lighter than the desktop hover strip, and Delete --
     one mistap from a confirm page -- lighter still until pressed. The words
     stay words; the reading surface just stops competing with them. */
  .actions > .act,
  .actions > .replies.zero,
  .actions .pick > summary {
    font-size: 10px;
    color: color-mix(in srgb, var(--text-muted) 72%, transparent);
  }
  .actions > .act.danger { color: color-mix(in srgb, var(--text-muted) 55%, transparent); }
  .react { padding: 0 var(--s2); }
  .reactions { gap: var(--s2); margin-top: var(--s2); }
  /* Right-anchoring a popover that is now on the left of the screen would
     push it off the edge on the narrowest phones. */
  .pick-menu { right: auto; left: -1px; }
  .pick-item { min-height: 32px; padding: 0 var(--s2); }
}

/* The rule between a thread's message and its answers. The daymark's shape --
   a hairline with a mono label sitting on it -- because it does the same job:
   it says what the next run of rows is. */
.thread-rule {
  display: flex; align-items: center; gap: var(--s3);
  margin: var(--s5) 0 var(--s2);
}
.thread-rule::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.thread-rule span {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted);
}

/* The message a thread hangs from: the subject of the page, not the first row
   of a list. A panel rather than a heavier rule, so an agent's accent stripe
   and a system message's grey one survive being the root. */
.messages.thread > .msg:first-child {
  padding: var(--s3) var(--s4);
  background: var(--surface-2);
  border-radius: 0 var(--radius) var(--radius) 0;
}

/* --- attachments ----------------------------------------------------------
   A research desk, not a gallery -- but on this desk the image IS the work:
   an agent's daily chart, a screenshot of a term sheet. So an image renders
   inline at reading width, bounded, with its filename as a quiet caption and
   the whole figure a link to the original. A non-image file keeps the
   hairline card: name in sans, facts in mono, the byline's own split.       */

.files {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--s1);
  margin-top: var(--s2);
}
.file { max-width: 100%; }

.file-link {
  display: inline-flex; align-items: center; gap: var(--s3);
  max-width: 100%;
  padding: var(--s2) var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  transition: border-color 160ms var(--ease), background-color 160ms var(--ease);
}
.file-link:hover {
  text-decoration: none;
  border-color: var(--border-strong);
  background: var(--surface-2);
}

.file-icon {
  display: inline-flex; flex: none;
  color: var(--text-muted);
}
.file-link:hover .file-icon { color: var(--primary); }

/* Fixed box, object-fit: cover -- a column of thumbnails that all agree on
   their size reads as a list; one that does not reads as a mess. (Kept for
   surfaces that still card an image -- search hits, old markup in the live
   stream's cache.) */
.thumb {
  flex: none;
  width: 44px; height: 44px;
  object-fit: cover;
  border-radius: 2px;
  /* A chart is usually white. Without the hairline it has no edge at all on a
     light theme, and the card looks like it failed to load an image. */
  border: 1px solid var(--border);
  background: var(--surface-2);
}

/* The inline figure. Bounded at 420px tall so three charts are three glances,
   not three screens; width follows the reading measure. The white ground is
   literal, not a token: a matplotlib chart is white in both themes, and a
   transparent PNG on the dark surface would be unreadable exactly when it
   matters. */
.file.image { width: 100%; }
.img-link { display: block; max-width: 560px; }
.img-link:hover { text-decoration: none; }
.img-inline {
  display: block;
  max-width: 100%; max-height: 420px;
  width: auto; height: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #FFFFFF;
  transition: border-color 160ms var(--ease);
}
.img-link:hover .img-inline { border-color: var(--border-strong); }
.img-caption {
  display: block; margin-top: 3px;
  font-family: var(--mono); font-size: var(--fs-xs);
  color: var(--text-muted); letter-spacing: .02em;
}

/* A document card: the first page of a PDF next to its name. Wider padding
   than the plain file row and a real thumbnail, because the point of it is
   that you can tell one brief from another without opening either. The page
   image is top-anchored -- a document is recognised by its masthead, never by
   its middle -- and sits on white, since a rendered page always is. */
.file.doc { max-width: 100%; }
.doc-link {
  display: inline-flex; align-items: flex-start; gap: var(--s3);
  max-width: 100%;
  padding: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  transition: border-color 160ms var(--ease), background-color 160ms var(--ease);
}
.doc-link:hover {
  text-decoration: none;
  border-color: var(--border-strong);
  background: var(--surface-2);
}
.doc-page {
  flex: none;
  width: 116px; height: 150px;
  object-fit: cover; object-position: top center;
  border: 1px solid var(--border);
  border-radius: 2px;
  background: #FFFFFF;
}
.doc-link:hover .doc-page { border-color: var(--border-strong); }
/* Optically level with the top of the page image rather than flush with it. */
.file.doc .file-meta { padding-top: 2px; }

.file-meta { display: flex; flex-direction: column; min-width: 0; }
.file-name {
  font-size: var(--fs-sm); font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.file-facts {
  font-family: var(--mono); font-size: var(--fs-xs);
  color: var(--text-muted); letter-spacing: .02em;
}

/* A label, not a badge: uppercase mono, hairline box, no fill. */
.tag {
  font-family: var(--mono);
  font-size: 9.5px; line-height: 14px;
  letter-spacing: .12em; text-transform: uppercase;
  padding: 0 5px;
  border: 1px solid currentColor;
  border-radius: 2px;
}

/* An agent: a brass-named byline + an AGENT label + a brass rule down the
   left. Three carriers, so this survives greyscale, and colour blindness.
   The name stopped being mono: an agent that writes half the desk's reading
   is an author, and its name is set like one -- the brass and the label are
   what say it is not a person. */
.msg.agent { border-left-color: var(--accent); }
.msg.agent .who { color: var(--accent-text); }
.msg.agent .tag { color: var(--accent-text); }

/* The machine talking to itself: quiet, mono throughout, secondary. */
.msg.system { border-left-color: var(--border-strong); }
.msg.system .who { font-family: var(--mono); font-weight: 400; font-size: 13px;
                   color: var(--text-muted); }
.msg.system .tag { color: var(--text-muted); }
.msg.system .body {
  font-family: var(--mono); font-size: var(--fs-sm); color: var(--text-muted);
}

/* Day divider: a rule with the date sitting on it, in mono. */
.daymark {
  display: flex; align-items: center; gap: var(--s3);
  margin: var(--s6) 0 var(--s2);
}
.daymark::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.daymark span {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted);
}
.messages > .daymark:first-child { margin-top: 0; }
/* ...and so can the pagination control, which then owns the top of the list. */
.messages > .older:first-child + .daymark { margin-top: 0; }

/* Where the reader left off: the day rule's twin, in the accent, with the
   label set in a pill so it reads as a marker rather than as another date.
   Deliberately the same shape as .daymark -- these two are the only rules in
   the flow, and a reader who has learned one has learned the other. The
   accent is the second signal, not the only one: the words say what it is. */
.newmark {
  display: flex; align-items: center; gap: var(--s3);
  margin: var(--s5) 0 var(--s3);
}
.newmark::after { content: ""; flex: 1; height: 1px; background: var(--accent); }
.newmark span {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--accent-text);
  border: 1px solid var(--accent);
  border-radius: 999px;
  padding: 1px var(--s2);
}
.messages > .newmark:first-child { margin-top: 0; }

.empty-thread {
  color: var(--text-muted); font-size: var(--fs-sm);
  padding: var(--s5) 0 var(--s5) var(--s4);
  border-left: 2px solid var(--border);
}

/* --- the @mention chip ----------------------------------------------------
   Who a message was addressed to, visible at a glance in a wall of prose.
   Built by app/markdown.py from a literal and a slug the grammar has already
   constrained to [A-Za-z0-9-]; the live stream re-checks the exact class
   strings below before adopting one (_stream_js.html).

   Mono, because a slug is a machine identifier and this stylesheet sets those
   in mono everywhere else -- it is the string you type, not somebody's name.

   The two kinds are told apart by more than colour: the agent chip carries the
   accent this app reserves for marks *and* a full outline in it, because an
   agent mention is a thing that happens -- it starts a model run that costs
   money -- while a person mention is a thing that is read. */
.mention {
  font-family: var(--mono);
  font-size: .93em;
  padding: 0 4px;
  border-radius: 3px;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border);
  white-space: nowrap;
}
.mention-person { color: var(--primary); }
.mention-agent {
  color: var(--accent-text);
  box-shadow: inset 0 0 0 1px var(--accent);
}
/* The agent chip is a link to that agent's DM, and a body link is underlined
   (see .msg .body a). Not this one: the outline already says "this is a
   thing", and a chip with an underline through its monospace reads as a typo.
   The hover state does the link's work instead. */
.msg .body a.mention, .msg .body a.mention:hover, a.mention, a.mention:hover {
  text-decoration: none;
}
a.mention-agent:hover { box-shadow: inset 0 0 0 1px var(--accent-text); }

/* --- composer ------------------------------------------------------------- */

.composer-wrap {
  /* Also the positioning parent for the @-mention menu below, which hangs
     above the composer rather than pushing it down the page. */
  position: relative;
  border-top: 1px solid var(--border);
  padding: var(--s3) var(--s6) var(--s4);
  padding-bottom: max(var(--s4), env(safe-area-inset-bottom));
  background: var(--surface);
}
/* Bottom-aligned, so a textarea that has grown to eight lines keeps its Send
   button and its paperclip on the last line -- beside where the caret is,
   rather than floating level with the first line of a paragraph the writer
   has already finished. */
.composer { display: flex; align-items: flex-end; gap: var(--s2); max-width: 76ch; }
.composer input { min-height: 42px; }
/* One line to start with and up to about ten before it scrolls; the height
   between those is set by script, which measures rather than counts, so a
   wrapped line takes the two lines it actually occupies. `field-sizing` does
   the same thing natively where a browser has it, which makes the no-script
   case grow too -- and where it does not, `rows="1"` still opens at one line.
   No resize handle: the box sizes itself, and a handle that fights it is a
   control that puts the conversation in the wrong place. */
.composer textarea {
  min-height: 42px;
  max-height: 40vh;
  resize: none;
  field-sizing: content;
  line-height: 1.5;
  /* `auto`, not `hidden`, because this is also the no-script value: a box that
     stops at max-height and hides what is under it is a box you cannot read
     what you typed in. The script switches it to `hidden` while the content
     fits, so the scrollbar only appears when there is something to scroll. */
  overflow-y: auto;
}
.composer button { min-height: 42px; flex: none; padding: 0 var(--s5); }
/* The keyboard contract, unhidden by script once it is true. Set in mono
   because <kbd> names keys, which are machine identifiers like every other
   thing this stylesheet sets in mono. */
.composer-keys { white-space: nowrap; }
.composer-keys[hidden] { display: none; }
.composer-note kbd {
  font-family: var(--mono); font-size: inherit;
  padding: 0 3px;
  border: 1px solid var(--border);
  border-radius: 3px;
  color: var(--text);
}
.composer-note {
  max-width: 76ch;
  margin-top: var(--s2);
  font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-muted);
}


/* --- the @-mention menu ---------------------------------------------------
   Filled by script from the roster's data block; empty and hidden without it,
   which is the whole of its no-JavaScript behaviour. Typing `@analyst` out in
   full has always worked and still does, so this is a shortcut and never a
   requirement.

   It floats above the composer rather than sitting between it and the
   conversation: a menu that pushes the input down the page moves the thing the
   reader is typing into, mid-keystroke.                                     */

.mention-menu {
  position: absolute; z-index: 25;
  left: var(--s6); bottom: calc(100% + 4px);
  width: min(44ch, calc(100% - var(--s6) * 2));
  max-height: min(40vh, 300px); overflow-y: auto;
  padding: var(--s1);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.mention-menu[hidden] { display: none; }

/* Three columns now: the slug, the name, and which kind of thing this is.
   The third one is not decoration -- picking an agent starts a model run and
   picking a colleague sends them a notification, and a menu that made those
   look identical would be a menu that spends money by accident. */
.mention-item {
  display: grid;
  grid-template-columns: minmax(0, max-content) minmax(0, 1fr) max-content;
  column-gap: var(--s2);
  align-items: baseline;
  padding: var(--s2);
  border-radius: 3px;
  cursor: pointer;
}
.mention-kind {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-muted);
}
/* Colour follows the chip the mention will become, so the menu row and the
   thing it inserts are recognisably the same object. Never colour alone: the
   word beside it says which it is. */
.mention-item.is-person .mention-slug { color: var(--primary); }
/* Hover is a hint; the selection is a commitment -- it is the one Enter will
   complete -- so it carries a second signal as well as the fill. The rule is
   inset, not a border, so nothing shifts by a pixel as the selection moves. */
.mention-item:hover { background: var(--surface-2); }
.mention-item[aria-selected="true"] {
  background: var(--surface-2);
  box-shadow: inset 2px 0 0 var(--primary);
}
.mention-slug {
  font-family: var(--mono); font-size: var(--fs-sm); color: var(--accent-text);
}
.mention-who { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
/* Flush with the slug, not indented under the name -- the same argument as the
   roster: the slugs are ten characters apart in width, so an indent measured
   from the name steps in and out on every row. */
.mention-purpose {
  grid-column: 1 / -1; font-size: var(--fs-xs); color: var(--text-muted);
  line-height: 1.4;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The paperclip. A label, because the input it drives is off-screen -- so it
   carries the button treatment by hand rather than inheriting it. Square and
   flex:none, so choosing a file never resizes the text input beside it. */
.composer .attach {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none;
  width: 42px; min-height: 42px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
  transition: background-color 160ms var(--ease), border-color 160ms var(--ease),
              color 160ms var(--ease);
}
.composer .attach:hover { background: var(--surface-2); color: var(--text); }
/* The off-screen input still takes focus; the label has to show it, or the
   control is invisible to anyone tabbing through the composer. A general
   sibling combinator, not an adjacent one: there is a <noscript> between the
   two in the markup, and `+` silently stopped matching when it was added. */
.composer input[type="file"]:focus-visible ~ .attach {
  outline: 2px solid var(--focus); outline-offset: 2px;
}

/* Occupies the hint's row rather than a row of its own: picking a file must
   not shift the composer up the page. */
.composer-chosen {
  display: flex; align-items: baseline; gap: var(--s3);
  max-width: 76ch;
  margin-top: var(--s2);
  font-family: var(--mono); font-size: var(--fs-xs); color: var(--text);
}
.composer-chosen[hidden] { display: none; }
.chosen-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chosen-name::before { content: "attached: "; color: var(--text-muted); }
.chosen-clear {
  flex: none;
  min-height: 0; padding: 0;
  border: 0; background: none;
  font: inherit; color: var(--text-muted);
  text-decoration: underline; text-underline-offset: 2px;
}
.chosen-clear:hover { background: none; color: var(--danger); }

/* A refused upload, said where the person is looking. */
.composer-error {
  max-width: 76ch;
  margin-bottom: var(--s3);
  padding: var(--s2) var(--s3);
  border-left: 3px solid var(--danger);
  background: var(--surface-2);
  font-size: var(--fs-sm); color: var(--text);
}

/* --- the edit and delete pages --------------------------------------------
   Both reuse the composer's foot: same rule above it, same padding, same
   button sizes, so acting on a message happens where writing one happens. */

/* The Cancel that sits beside Save and beside Delete. `.btn` is 32px tall by
   default and would read as a half-height sibling of a 42px composer button. */
.composer .btn { flex: none; min-height: 42px; padding: 0 var(--s4); }
.composer.confirm { justify-content: flex-end; }

.confirm-note {
  max-width: 76ch;
  margin-bottom: var(--s3);
  font-size: var(--fs-sm); color: var(--text-muted);
}
.confirm-note strong { color: var(--text); font-weight: 600; }
.composer-wrap > .notice { max-width: 76ch; margin-bottom: var(--s3); }

/* The one destructive submit in the app, and the only place a solid red
   button is warranted: it is the single action of a page that exists to ask
   "are you sure". --on-danger is a token rather than #fff because the dark
   theme's danger is a light red, on which white text measures 2.6:1. */
button.primary.danger-solid {
  background: var(--danger); border-color: var(--danger); color: var(--on-danger);
}
button.primary.danger-solid:hover {
  background: var(--danger-hover); border-color: var(--danger-hover);
}

.archived-note {
  border-top: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  background: var(--surface-2);
  padding: var(--s4) var(--s6);
  font-size: var(--fs-sm); color: var(--text-muted);
}
.archived-note strong { color: var(--text); font-weight: 600; }

/* --- admin ---------------------------------------------------------------- */

.admin { background: var(--bg); min-height: 100vh; }

.admin-head {
  position: sticky; top: 0; z-index: 5;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.admin-head .bar {
  max-width: 1120px; margin: 0 auto;
  padding: var(--s4) var(--s5) var(--s3);
  display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
}
.admin-head h1 { font-size: var(--fs-lg); }
.admin-head .back { margin-left: auto; font-size: var(--fs-sm); }
.admin-head .whoami {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted);
}

.admin-nav {
  max-width: 1120px; margin: 0 auto;
  padding: 0 var(--s5) var(--s3);
  display: flex; gap: var(--s1); flex-wrap: wrap;
}
.admin-nav a {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-muted);
  padding: 6px var(--s2);
  border-radius: var(--radius);
}
.admin-nav a:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }

.admin-body { max-width: 1120px; margin: 0 auto; padding: var(--s6) var(--s5) var(--s8); }

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  margin-bottom: var(--s5);
  scroll-margin-top: 108px;
}
.panel > header {
  display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--border);
}
.panel > header h2 { font-size: var(--fs-md); }
.panel > header .count {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .08em; color: var(--text-muted);
}
.panel > header .note { margin-left: auto; font-size: var(--fs-sm); color: var(--text-muted); }

.rows > li {
  padding: var(--s3) var(--s5);
  border-bottom: 1px solid var(--border);
}
.rows > li:last-child { border-bottom: 0; }
.rows > li.none { color: var(--text-muted); font-size: var(--fs-sm); }

.row-head {
  display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap;
  margin-bottom: var(--s1);
}
.row-head:last-child { margin-bottom: 0; }
.row-head .title { font-weight: 600; }
.row-head .id {
  font-family: var(--mono); font-size: var(--fs-sm); color: var(--text-muted);
}
.row-actions { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s3); }
/* One reversible action belongs on the row it acts on, not on a line of its
   own: five people used to mean five rows of button. */
.row-actions.inline { margin: 0 0 0 auto; align-self: center; }
.row-actions form { display: inline; }
/* The retime control is three fields and a button rather than a lone button,
   so it needs to be its own flex row: inline would let the two inputs and the
   button break apart from one another when the actions row wraps, and a stray
   "Retime" under someone else's Delete is an easy way to retime the wrong
   schedule. min-width:0 lets the inputs shrink instead of forcing the row wider
   than a phone. */
.row-actions form.inline-set { display: flex; gap: var(--s1); flex-wrap: nowrap; }
.row-actions form.inline-set input { min-width: 0; }

/* A row's facts, as a label/value grid rather than a wall of <br>. */
.facts {
  display: grid;
  grid-template-columns: 118px minmax(0, 1fr);
  gap: 3px var(--s3);
  margin-top: var(--s2);
  font-size: var(--fs-sm);
}
.facts dt {
  font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-muted);
  padding-top: 3px;
}
.facts dd { margin: 0; overflow-wrap: anywhere; }
.facts dd.mono { font-family: var(--mono); font-size: var(--fs-sm); }

/* A flat mono label: a fact about a row, not a status light. */
.chip {
  font-family: var(--mono); font-size: var(--fs-xs); line-height: 16px;
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-muted);
  border: 1px solid transparent; border-radius: 2px;
  padding: 0 5px;
}
/* Only the fact worth noticing gets a box. */
.chip.strong { color: var(--text); border-color: var(--border-strong); }

/* Status words, always words -- the dot is a second carrier, not the only one. */
.state {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-muted);
  display: inline-flex; align-items: center; gap: 5px;
}
.state::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: none;
}
.state.ok   { color: var(--accent-text); }
.state.warn { color: var(--danger); }
.state.off  { color: var(--text-muted); }
.state.off::before { background: none; box-shadow: inset 0 0 0 1px currentColor; }
/* A run that looked and chose silence: the accent of a success, the hollow
   dot of nothing posted. Between "ok" and "off" on purpose, because that is
   what it is. */
.state.quiet { color: var(--accent-text); }
.state.quiet::before { background: none; box-shadow: inset 0 0 0 1px currentColor; }

.subrows {
  margin-top: var(--s3);
  border-top: 1px solid var(--border);
}
.subrows > li {
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--border);
  font-size: var(--fs-sm);
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
}
.subrows > li:last-child { border-bottom: 0; }
.subrows .meta { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-muted); }
.subrows form { margin-left: auto; }

/* the create-form footer of a panel */
.panel-form {
  padding: var(--s5);
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.panel-form h3 {
  font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: .09em; text-transform: uppercase; color: var(--text-muted);
  margin-bottom: var(--s4);
}
.panel-form h3.sep { margin-top: var(--s6); padding-top: var(--s5);
                     border-top: 1px solid var(--border); }
.panel-form .form-note { margin-top: var(--s3); max-width: 62ch; }
.panel-form input, .panel-form select { background: var(--field); }
.pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--s4);
}
.subform {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--s4);
}
.subform h4 {
  margin: 0 0 var(--s4);
  font-size: var(--fs-sm); font-weight: 600; color: var(--text);
}
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0 var(--s4);
}
.grid .field.span2 { grid-column: 1 / -1; }
.form-go { margin-top: var(--s2); }

fieldset {
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: var(--s3) var(--s4) var(--s4);
  margin: 0 0 var(--s4);
}
legend {
  font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted);
  padding: 0 var(--s2);
}
.checks { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); }
.checks label {
  display: inline-flex; align-items: center; gap: var(--s2);
  font-family: var(--mono); font-size: var(--fs-sm);
  min-height: 28px;
}

/* Agent channel chips: the detach lives on the row, like every other row
   action on this page. The button is a real button with a label, because
   a bare multiplication sign is not an announcement. */
.agent-chans { display: flex; flex-wrap: wrap; gap: var(--s2); }
.agent-chan { display: inline-flex; align-items: center; gap: 4px;
              border: 1px solid var(--border); border-radius: 999px;
              padding: 0 4px 0 8px; }
.agent-chan form { display: inline; }
.chan-detach { border: 0; background: none; color: var(--text-muted);
               cursor: pointer; font-size: var(--fs-sm); padding: 2px 4px; }
.chan-detach:hover { color: var(--danger); }
/* The hands_to picker sits inside the same <dd> as the current list: a short
   multi-select and a Save button side by side, the footprint of the channel
   chips one row up. Mono, because the options are slugs. */
.hands-to { display: flex; align-items: flex-start; gap: var(--s2); margin-top: var(--s2); }
.hands-to select { font-family: var(--mono); font-size: var(--fs-sm); }

/* --- the one-time secrets ------------------------------------------------
   Shown exactly once. Losing one costs an admin real time, so these get the
   loudest treatment in the app: full-width, accent rule, a heading that says
   it will not be shown again, and a select-all block.                       */

.reveal {
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: var(--s5);
  margin-bottom: var(--s5);
  box-shadow: var(--shadow);
}
.reveal .kicker {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--accent-text);
  margin-bottom: var(--s2);
}
.reveal h2 { font-size: var(--fs-md); margin-bottom: var(--s2); }
.reveal p { font-size: var(--fs-sm); color: var(--text-muted); max-width: 68ch; }
.reveal .value {
  margin: var(--s4) 0 0;
  padding: var(--s4);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  font-family: var(--mono); font-size: 13px; line-height: 1.6;
  white-space: pre-wrap; word-break: break-all;
  -webkit-user-select: all;
  user-select: all;                 /* one click selects the whole credential */
}
.reveal .after {
  margin-top: var(--s2);
  font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-muted);
  letter-spacing: .06em; text-transform: uppercase;
}

.flash {
  border: 1px solid var(--border);
  border-left: 3px solid var(--primary);
  background: var(--surface);
  border-radius: var(--radius);
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s5);
  font-size: var(--fs-sm);
}

/* --- the rail's search box ------------------------------------------------
   A GET form in the sidebar, above the channel list: the thing you reach for
   when you know a word and not a place.                                     */

.rail-search {
  display: flex; gap: var(--s2);
  padding: var(--s3) var(--s4) 0;
}
.rail-search input {
  min-height: 32px;
  padding: 5px var(--s3);
  font-size: var(--fs-sm);
  background: var(--field);
  border-color: var(--border);
}
.rail-search input:hover { border-color: var(--border-strong); }
.rail-search input:focus { border-color: var(--focus); }
/* Safari draws its own clear affordance on type=search; the app draws none. */
.rail-search input::-webkit-search-decoration { -webkit-appearance: none; }
.rail-search button {
  flex: none; min-height: 32px; width: 32px; padding: 0;
  border-color: var(--border);
  color: var(--text-muted);
}
.rail-search button:hover { border-color: var(--border-strong); color: var(--text); }

/* --- search results -------------------------------------------------------- */

.results {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: var(--s5) var(--s6) var(--s8);
}
.results > * { max-width: 76ch; }

.search-form {
  display: flex; gap: var(--s2); align-items: stretch;
  margin-bottom: var(--s5);
}
.search-form .q { flex: 1 1 auto; min-width: 0; }
.search-form .scope { flex: 0 1 190px; min-width: 0; }
.search-form input, .search-form select { min-height: 38px; }
.search-form select { font-family: var(--mono); font-size: var(--fs-sm); }
.search-form button { flex: none; }

/* The two honest empty states: nothing typed, and nothing found. Same shape,
   because they are the same page in two moods. */
.search-guide {
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: var(--s5);
}
.search-guide h2 { font-size: var(--fs-md); margin-bottom: var(--s2); }
.search-guide .hint + .hint { margin-top: var(--s3); }
.search-guide .facts { margin-top: var(--s4); grid-template-columns: 108px minmax(0, 1fr); }
.search-guide .facts dd .mono { font-size: var(--fs-sm); }

.hits > li { padding: var(--s4) 0; border-bottom: 1px solid var(--border); }
.hits > li:first-child { padding-top: 0; }
.hits > li:last-child { border-bottom: 0; }

/* The whole metadata line is the link: channel, author and time together are
   the answer to "which message is this", so they are one target, not three. */
.hit-head {
  display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap;
  color: var(--text);
}
.hit-head:hover { text-decoration: none; }
.hit-head:hover .chan { text-decoration: underline; text-underline-offset: 2px; }
.hit-head .chan {
  font-family: var(--mono); font-size: 13.5px; color: var(--primary);
}
.hit-head .chan .hash { color: var(--text-muted); }
/* A DM is named after a person, so it is set as a name -- sans, not the mono
   a slug is set in -- with a mono "DM" standing where the "#" would be. */
.hit-head .chan.dm { font-family: var(--sans); }
.hit-head .chan.dm .hash {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .08em; margin-right: 5px;
}
.hit-head .who { font-weight: 600; font-size: 14.5px; }
.hit-head .at {
  font-family: var(--mono); font-size: var(--fs-xs);
  color: var(--text-muted); letter-spacing: .02em;
}
.hit.agent .who { font-family: var(--mono); font-weight: 400; font-size: 13.5px; }
.hit.system .who { font-family: var(--mono); font-weight: 400; font-size: 13px;
                   color: var(--text-muted); }

.snippet {
  margin-top: var(--s1);
  overflow-wrap: anywhere;
  color: var(--text);
}
.hit.system .snippet {
  font-family: var(--mono); font-size: var(--fs-sm); color: var(--text-muted);
}

/* The matched run. Never markup from a body -- app/messages.py hands the
   template a list of runs and only this element is the template's own. */
mark {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  color: var(--text);
  border-radius: 2px;
  padding: 0 2px;
}

/* --- the person picker ------------------------------------------------------
   /dm. A list, not a dialog: one row per colleague, each row a real form, and
   a filter above it that only ever hides rows. Reuses .results for the column
   so the page has the same measure and the same scroll as search.           */

.people-filter { margin-bottom: var(--s4); }
.people-filter input { min-height: 38px; }

.people > li {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--border);
}
.people > li:last-child { border-bottom: 0; }
.people > li[hidden] { display: none; }
.people .who {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap;
}
.people .name { font-weight: 500; }
/* The handle is data -- it is what @mentions and the admin page call them --
   so it is set in mono, quietly, beside the name a person actually goes by. */
.people .handle { font-size: var(--fs-sm); color: var(--text-muted); }
.people form { flex: none; }
.people .btn, .people button { flex: none; }
.people li.none { color: var(--text-muted); font-size: var(--fs-sm); }
.people li.none[hidden] { display: none; }
/* The two lists on /dm -- People, then Agents -- under a small heading each.
   An agent's row carries two more lines than a person's (what it is for, and
   when it posts on its own), so they go full-width under the name rather than
   fighting the handle for the baseline. The AGENT mark is the rail's. */
.people-head {
  margin: var(--s5) 0 var(--s2);
  font-size: var(--fs-xs); font-family: var(--mono);
  letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted);
}
.people .kind {
  margin-left: var(--s2);
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted);
}
.people .agent-purpose, .people .agent-rhythm {
  flex-basis: 100%; font-size: var(--fs-sm); color: var(--text-muted);
}
.people .agent-rhythm { font-style: italic; }

/* --- history pagination ---------------------------------------------------
   A server-rendered link, because this is the only route to anything older
   than one page and it has to work with scripting off.                      */

.older {
  display: flex; align-items: center; gap: var(--s3);
  max-width: 76ch;
  margin-bottom: var(--s5);
}
.older .btn {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-muted);
  border-color: var(--border);
}
.older .btn:hover {
  text-decoration: none; color: var(--text); border-color: var(--border-strong);
}
.older::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.older.start span {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted);
}
.older.bottom { margin: var(--s5) 0 0; }

/* "you are reading old history", in the header, next to the channel name. */
.to-latest {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .08em; text-transform: uppercase;
  white-space: nowrap;
}
/* Two words on two lines read as two things. */
.main > header .state { white-space: nowrap; }

/* --- the message a link landed on ----------------------------------------
   A search result points at one message in a page of a hundred. Three
   carriers again: a tinted panel, a solid rule down the left, and -- where
   motion is welcome -- one slow fade of the tint so the eye is drawn to it.
   The tint stays afterwards, so this survives reduced motion and a reload. */

.msg { scroll-margin: var(--s5) 0; }

.msg:target {
  background: color-mix(in srgb, var(--primary) 10%, transparent);
  border-left-color: var(--primary);
  border-radius: 0 var(--radius) var(--radius) 0;
  animation: land 1200ms var(--ease) 1;
}
@keyframes land {
  from { background: color-mix(in srgb, var(--primary) 34%, transparent); }
  to   { background: color-mix(in srgb, var(--primary) 10%, transparent); }
}

/* --- motion --------------------------------------------------------------- */

@keyframes rise {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
.msg.live { animation: rise 180ms var(--ease) both; }

/* --- narrow --------------------------------------------------------------- */

/* --- pages ----------------------------------------------------------------
   A page is a document the whole desk reads, rendered by a vendored component
   in a same-origin frame (app/routes/page_routes.py explains why a frame).

   The panel is dark in *both* Cowork themes, deliberately. The component's
   palette is dark and its hazard-hatch fills assume a dark ground, so the one
   thing its brief tells us not to do is re-theme it halfway. The chrome around
   it stays in the reader's chosen theme; the diagram sits on its own surface
   inside it, the way a photograph does.                                     */

.page-frame {
  flex: 1; min-height: 0; min-width: 0;
  /* The component's own --scf-field. Named literally rather than through a
     token because it is not this design system's colour -- it is the frame's,
     and it is here so the panel does not flash white before the frame paints. */
  background: #0F1518;
}
.page-frame iframe {
  display: block; width: 100%; height: 100%; border: 0;
}

/* The pointer to the caveat. The full disclaimer is the last thing inside the
   frame, where the supplier requires it to stay; this says enough that nobody
   quotes a mid-point as a disclosed figure without having scrolled that far. */
.page-note {
  flex: none;
  padding: var(--s3) var(--s6);
  border-top: 1px solid var(--border);
  background: var(--surface);
  font-size: var(--fs-xs); color: var(--text-muted);
  max-width: none;
}

/* A page in the rail has no unread count -- there is no "new" to count -- so
   the row is just a name, and the title carries it rather than a mono slug. */

/* --- the page editor ------------------------------------------------------ */

/* A few hundred lines of JSON. Mono, no wrapping, and tall enough to see a
   node and its neighbours at once. */
textarea.code {
  width: 100%;
  font-family: var(--mono); font-size: 13px; line-height: 1.55;
  background: var(--surface);
  white-space: pre; overflow: auto; resize: vertical;
  tab-size: 2;
}

/* A charter is prose, not JSON: a thousand words of English in paragraphs
   somebody wrote to be read. So it wraps -- `white-space: pre` above would put
   a horizontal scrollbar under every paragraph -- while keeping the monospace
   face, which is what makes a stray leading dash or a lost blank line visible.
   `.code.prose` rather than a class of its own so the two stay one control
   that differs in one property. */
textarea.code.prose { white-space: pre-wrap; overflow-wrap: break-word; }

/* A flash that says "not saved". Same shape as the ordinary one, and the only
   difference is the rule down its left edge -- which is why the word "Not
   saved" is in the sentence too: the colour is the second signal, not the
   only one. */
.flash.bad { border-left-color: var(--danger); }

/* An archived version, shown in place. Bounded, because one of these is 28 kB
   and an open <details> would otherwise be the whole page. */
.rows details { margin-top: var(--s2); font-size: var(--fs-sm); }
.rows details > summary {
  cursor: pointer; color: var(--text-muted);
  padding: var(--s1) 0;
}
.rows details > summary:hover { color: var(--text); }
.rows details .value {
  margin-top: var(--s2);
  max-height: 320px; overflow: auto;
  padding: var(--s3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--mono); font-size: 12px; line-height: 1.5;
  white-space: pre;
}
/* An archived charter is prose, not JSON. It wraps, for the same reason
   `textarea.code.prose` above does: paragraphs of English behind a horizontal
   scrollbar are unreadable, and reading one is how an admin decides whether to
   restore it. Placed after the rule it overrides, not before. */
.rows details .value.prose { white-space: pre-wrap; overflow-wrap: break-word; }

/* One <details> per desk on /admin/prompts. Collapsed unless there is only
   one desk -- fifteen souls under fifteen open lists is the same wall of
   text the split off the admin index exists to avoid. */
.rows-group summary { cursor: pointer; padding: var(--s2) 0; font-weight: 600; }

/* --- operations dashboard --------------------------------------------------
   Dense machine values: eleven columns of counts, money and durations. Three
   decisions carry the whole thing.

   Numbers are mono and right-aligned, always, so a column of figures reads as
   a column -- the digits line up, and a four-figure bill cannot be mistaken
   for a three-figure one at a glance.

   The table scrolls inside its own wrapper rather than pushing the page
   sideways. A dense table that widens the document breaks every other panel on
   the page to show one more column.

   And below 720px it stops being a table at all: rows become blocks and each
   cell prints its own label from data-label. Eleven columns cannot be made
   readable on a 375px screen by shrinking them -- they have to stop being
   columns.                                                                  */

.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.optable {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}
.optable caption { text-align: left; }
.optable th, .optable td {
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--border);
  text-align: left;
  white-space: nowrap;
  vertical-align: baseline;
}
.optable thead th {
  font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-strong);
  position: sticky; top: 0; background: var(--surface); z-index: 1;
}
.optable tbody tr:last-child td { border-bottom: 0; }
.optable td.none { color: var(--text-muted); white-space: normal; }
/* The row of buttons under a schedule on /admin. It belongs to the row above
   it, so it carries no rule of its own between them. */
.optable tr.row-extra td { border-bottom: 1px solid var(--border);
                           white-space: normal; padding-top: 0; }
.optable tr.row-extra .hint { max-width: 78ch; }
.optable tr.row-extra .row-actions { margin-top: var(--s2); }

/* Every figure: mono, right-aligned, tabular so the digits stack. */
.optable .num {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-variant-ligatures: none;
}
.optable td.strong { color: var(--text); font-weight: 500; }
/* One cell's value, when it is more than one thing -- a timestamp and the
   outcome badge beside it. Kept as a single flex item so the two wrap together
   rather than the badge being pushed out of the cell. */
.optable td .v {
  display: inline-flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--s2); min-width: 0;
}
.optable tfoot th, .optable tfoot td {
  border-top: 1px solid var(--border-strong);
  border-bottom: 0;
  font-weight: 600;
}
.optable tfoot th { font-size: var(--fs-sm); letter-spacing: 0; }

/* The window selector: three links, one of them current. Sits where a panel's
   .note does, and says which range every figure below it covers. */
.window { margin-left: auto; display: flex; gap: var(--s1); }
.window a {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-muted);
  padding: 4px var(--s2);
  border: 1px solid transparent; border-radius: var(--radius);
}
.window a:hover { background: var(--surface-2); color: var(--text);
                  text-decoration: none; }
.window a.on {
  color: var(--text); border-color: var(--border-strong);
  background: var(--surface-2);
}

/* A sentence under a panel, saying what its figures do and do not claim. */
.panel-note {
  padding: var(--s3) var(--s5) var(--s4);
  font-size: var(--fs-sm); color: var(--text-muted);
  max-width: 78ch;
}

/* The health panel. `.state` already prints a word beside its dot, so the
   colour is the second carrier here and never the only one. */
.rows.health .row-head { gap: var(--s3); }
.rows.health .hint { max-width: 86ch; overflow-wrap: anywhere; }

/* Housekeeping: label over figure, on a grid that reflows. One hairline per
   item and no cell fill -- a grid drawn with a background showing through 1px
   gaps leaves the last row's unused columns as a grey slab, which reads as a
   broken cell rather than as empty space. Twelve bordered cards would also be
   twelve boxes competing with the panel they already sit in. */
.statgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  margin: 0;
}
.statgrid > div {
  padding: var(--s3) var(--s5);
  border-top: 1px solid var(--border);
}
.statgrid dt {
  font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted);
}
.statgrid dd {
  margin: 2px 0 0;
  font-size: var(--fs-md);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* The one nav item that leaves this page. */
.admin-nav a.away { color: var(--primary); }

@media (max-width: 900px) {
  .main > header { padding: var(--s4); }
  .messages { padding: var(--s4) var(--s4) var(--s5); }
  .composer-wrap { padding-left: var(--s4); padding-right: var(--s4); }
  .archived-note { padding-left: var(--s4); padding-right: var(--s4); }
}

@media (max-width: 720px) {
  /* The rail becomes a drawer -- the dock with it, not a strip left standing
     beside it: 64px is a sixth of a phone, and the dock's job on a phone is
     done by the hamburger that is already in every header. It is never removed
     from the document: hiding it outright made switching channels impossible
     on a phone. */
  .shell { grid-template-columns: minmax(0, 1fr); }

  .rail {
    position: fixed; inset: 0 auto 0 0; z-index: 30;
    width: min(88vw, 320px);
    background: var(--bg);
    box-shadow: var(--shadow);
    transform: translateX(-101%);
    transition: transform 200ms var(--ease);
  }
  .nav-latch:checked ~ .rail { transform: none; }

  /* Inside the drawer the column is always there, whatever surface opened it.
     The reason it is hidden on a document surface is that it costs 244px of
     reading width -- and inside a drawer it costs nothing, while being the
     only quick way to a channel from a report on a phone. */
  .rail-docs .sidebar { display: flex; }
  .sidebar { flex: 1; min-width: 0; width: auto; border-right: 0; }

  /* Out of a 320px drawer there is no room to the right for a 250px panel, so
     it opens over the column instead of beside the dock. */
  .dock-panel { left: calc(100% + 4px); width: min(240px, calc(88vw - 76px)); }

  .scrim {
    display: block; position: fixed; inset: 0; z-index: 20;
    background: rgba(15, 23, 42, .45);
    opacity: 0; pointer-events: none;
    transition: opacity 200ms var(--ease);
  }
  .nav-latch:checked ~ .scrim { opacity: 1; pointer-events: auto; }

  .nav-btn { display: inline-flex; }

  /* Thumb-sized targets in the drawer. */
  .channel-list a { min-height: 44px; align-items: center; padding-top: 0; padding-bottom: 0; }
  .channel-list li.rail-none a { display: flex; align-items: center; }
  /* "Start a direct message" is a 14px glyph on a desktop hover row; on a
     phone it has to be a thumb target. The negative margin buys the height
     from the label's own padding rather than making the row taller. */
  .rail-new { min-width: 40px; min-height: 40px; margin: -10px 0; }
  /* The bottom of the drawer, where the two things a person presses *on
     purpose* on a phone live: catch up on everything, and turn the noise off
     because a meeting just started. A 26px row is a hit-or-miss tap, and
     missing the sound toggle in a quiet room is the worst possible miss. */
  .rail-all button, .rail-notify button { min-height: 40px; }

  .messages > *, .composer, .composer-note { max-width: none; }
  /* The roster keeps the page's phone padding, and its rows become thumb
     targets -- a 26px row is a hit-or-miss tap and this one inserts text. */
  .roster > summary { padding-left: var(--s4); padding-right: var(--s4); }
  .roster-list { max-width: none; padding-left: var(--s4); padding-right: var(--s4); }
  .roster.can-insert .roster-insert { min-height: 44px; align-content: center; }
  .mention-menu {
    left: var(--s4);
    width: calc(100% - var(--s4) * 2);
  }
  .mention-item { min-height: 44px; align-content: center; }
  /* The frame pans horizontally by itself (the canvas is a fixed 1436px and
     scales); the note under it is the only thing here with page padding. */
  .page-note { padding: var(--s3) var(--s4); }
  .results { padding: var(--s4) var(--s4) var(--s7); }
  .results > *, .older { max-width: none; }
  /* The box, the scope and the button will not sit on one line on a phone;
     stacked, each of them keeps a thumb-sized target. */
  .search-form { flex-wrap: wrap; }
  .search-form .q { flex-basis: 100%; }
  .search-form .scope { flex: 1 1 auto; }
  .search-form input, .search-form select { min-height: 44px; }
  .search-guide { padding: var(--s4); }
  .search-guide .facts { grid-template-columns: minmax(0, 1fr); }
  .rail-search input, .rail-search button { min-height: 40px; }
  .rail-search button { width: 40px; }
  /* A phone header already carries a drawer button, a channel name and its
     purpose. "Jump to latest" says the same thing as the "older history" chip
     and is the one you can act on, so the chip stands down here. */
  .main > header .older-state { display: none; }
  /* Same argument on a thread page. "Back to channel" is the actionable half;
     the chip and the "thread on X's message, Sun 23 Aug" line both restate
     what the panel at the top of the list already says, and on 375px they
     squeeze the channel name down to nothing between them. */
  .main > header .thread-state, .main > header .thread-sub { display: none; }
  /* Thumbs, not cursors: the composer clears 44px on a phone too. */
  .composer input, .composer button { min-height: 46px; }
  .composer button { padding: 0 var(--s4); }
  .composer .attach { width: 46px; min-height: 46px; }
  /* A filename is the longest thing in the composer on a phone; give it the
     whole row and let the Clear control sit at the end of it. */
  .composer-chosen { justify-content: space-between; }
  .chosen-clear { min-height: 0; padding: 0; }

  .admin-head .bar, .admin-nav, .admin-body { padding-left: var(--s4); padding-right: var(--s4); }
  .admin-body { padding-top: var(--s5); }
  .panel > header, .rows > li, .panel-form { padding-left: var(--s4); padding-right: var(--s4); }
  .facts { grid-template-columns: minmax(0, 1fr); gap: 0 0; }
  .facts dt { padding-top: var(--s2); }
  .grid { grid-template-columns: minmax(0, 1fr); }
  .grid .field.span2 { grid-column: auto; }
  .subrows form { margin-left: 0; }
  /* A wrapped action reads as belonging to the row when it starts where the
     row starts, not floated off to the right of nothing. */
  .row-actions.inline { margin: var(--s2) 0 0; width: 100%; }
  .panel > header .note { flex-basis: 100%; margin-left: 0; }
  .card { padding: var(--s5); }
  .page { padding: var(--s5) var(--s3) var(--s3); }
  /* Every tappable control clears 44px on a phone. */
  button, .btn { min-height: 44px; padding: 0 var(--s4); }
  .row-actions { gap: var(--s2); }

  /* Eleven columns of machine values cannot be shrunk into 375px; they stop
     being columns. Each row becomes a block and each cell prints the header it
     belonged to, so the table is read down instead of across and nothing is
     dropped to make it fit. */
  .optable, .optable thead, .optable tbody, .optable tfoot,
  .optable tr, .optable th, .optable td { display: block; }
  .optable thead { position: absolute; width: 1px; height: 1px;
                   overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .optable tr {
    border-bottom: 1px solid var(--border);
    padding: var(--s3) 0;
  }
  .optable tbody tr:last-of-type { border-bottom: 0; }
  .optable td, .optable tfoot th {
    border-bottom: 0;
    padding: 2px 0;
    white-space: normal;
    display: flex; align-items: baseline; gap: var(--s3);
  }
  .optable td::before {
    content: attr(data-label);
    flex: 0 0 45%;
    font-size: var(--fs-xs); font-weight: 600;
    letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted);
  }
  /* Right-aligning inside a 55% column just puts the figures in an arbitrary
     place; against the label is where they read. */
  .optable .num { text-align: left; }
  /* A machine value must not break across lines: "Sun 23 Aug," on one line and
     "07:26" on the next, with a status badge wedged between them, is two
     readings of one timestamp. The badge wraps instead. */
  .optable td .mono { white-space: nowrap; }
  /* The first cell is the row's identity, not one of its fields. Labelling it
     "AGENT: @analyst" above ten real fields spends a line saying nothing; it
     reads as the heading it is. */
  .optable td.rowkey { display: block; margin-bottom: var(--s1); }
  .optable td.rowkey::before { content: none; }
  .optable td.rowkey .mono { font-size: var(--fs-md); font-weight: 500; }
  .optable tr.row-extra { padding-top: 0; border-bottom: 0; }
  .optable tr.row-extra td { padding: 0; display: block; }
  .optable tr.row-extra td::before { content: none; }
  /* The rule that separates the workspace total from the agents belongs to
     the row, once. Left on the cells it draws a line above every one of the
     eleven stacked figures and the total reads as eleven totals. */
  .optable tfoot tr { border-top: 1px solid var(--border-strong); }
  .optable tfoot th, .optable tfoot td { border-top: 0; }
  .optable tfoot th { font-size: var(--fs-md); font-weight: 600;
                      margin-bottom: var(--s1); }
  .optable td.none::before { content: none; }
  /* The wrapper has nothing left to scroll. */
  .table-wrap { overflow-x: visible; }

  .window { flex-basis: 100%; margin-left: 0; }
  .window a { min-height: 40px; align-content: center; padding: 0 var(--s3); }
  .panel-note { padding-left: var(--s4); padding-right: var(--s4); }
  .statgrid > div { padding-left: var(--s4); padding-right: var(--s4); }
}

/* Reports — the archive list and the reader. Colours for stance reuse the
   reaction/positive-negative story the app already tells; the four status
   colours are SOC's traffic light, carried verbatim so a migrated report
   reads as it always did. */
/* The archive list scrolls inside the main column, like .messages and
   .results -- without this the list overflows the fixed-height shell and the
   rows past the fold are clipped rather than reachable. The width cap moves
   onto the children so the scrollbar sits at the window edge, not floating at
   780px. The same three-line pattern is applied to .report and .account
   below, and it is the one every in-shell scrolling surface uses. */
.report-list {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: var(--s5);
}
.report-list > * { max-width: 780px; }
.report-filter { display: flex; gap: var(--s2); margin-bottom: var(--s4); }
.report-filter input, .report-filter select { min-height: 36px; }
.report-row {
  display: flex; gap: var(--s3); align-items: baseline;
  padding: var(--s3) var(--s2); border-bottom: 1px solid var(--border);
  text-decoration: none; color: inherit;
}
.report-row:hover { background: var(--bg-hover, rgba(127,127,127,.06)); }
.report-tk { font-family: var(--mono, ui-monospace, monospace); font-weight: 600; min-width: 5.5ch; }
.report-subject { flex: 1 1 auto; }
.report-date { color: var(--text-muted); font-size: var(--fs-sm); white-space: nowrap; }
.report-none { color: var(--text-muted); padding: var(--s4) var(--s2); }

/* The archive's own rows (`.rich` -- the requests queue keeps the flat flex
   shape above). Three lines that read as one entry: the company with its
   ticker beside it, the report's one-sentence verdict, and the facts. The
   verdict is what makes this an archive rather than a filename list; it is
   clamped to two lines so forty reports stay one page. */
.report-row.rich {
  /* A row of two: the cover, where there is one, and the three stacked lines
     of the entry itself (.report-entry keeps that stack, so a row without a
     cover reads exactly as it did before covers existed). */
  flex-direction: row; align-items: flex-start; gap: var(--s3);
  padding: var(--s4) var(--s2);
}
.report-entry {
  display: flex; flex-direction: column; align-items: stretch; gap: 3px;
  flex: 1 1 auto; min-width: 0;
}
/* Small on purpose: fifty of these are a list to scan, not a gallery. Top-
   anchored, because a document is recognised by its masthead; on white,
   because a rendered page always is. */
.report-thumb {
  flex: none;
  width: 58px; height: 76px;
  object-fit: cover; object-position: top center;
  border: 1px solid var(--border); border-radius: 3px;
  background: #FFFFFF;
}
.report-row.rich:hover .report-thumb { border-color: var(--border-strong); }
.report-row.rich:hover .report-title { color: var(--accent-text); }
.report-name {
  display: flex; align-items: baseline; gap: var(--s3); min-width: 0;
}
.report-title {
  font-weight: 600; font-size: var(--fs-md); letter-spacing: -.011em;
  transition: color 140ms var(--ease);
}
.report-name .report-tk { min-width: 0; font-weight: 400; color: var(--text-muted); }
.report-line {
  color: var(--text-muted); font-size: var(--fs-sm); line-height: 1.5;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.report-facts { display: flex; align-items: baseline; gap: var(--s3); margin-top: 2px; }

.stance { font-size: var(--fs-xs); padding: 1px 8px; border-radius: 999px;
          border: 1px solid var(--border); white-space: nowrap; }
.stance-constructive { color: var(--st-green); border-color: var(--st-green); }
.stance-cautious     { color: var(--st-red);   border-color: var(--st-red); }
.stance-neutral      { color: var(--text-muted); }

/* The reader scrolls inside the main column too -- a long report (this is the
   surface where they are longest) was overflowing the shell and getting cut
   off at the fold. Width cap on the children, scroll on the wrapper. */
.report {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: var(--s5);
}
.report > * { max-width: 780px; }
.report-masthead { border-bottom: 2px solid var(--rp-ac, var(--accent)); padding-bottom: var(--s4); margin-bottom: var(--s5); }
.report-firm { font-size: var(--fs-xs); letter-spacing: .12em; text-transform: uppercase; color: var(--accent-text); }
/* The headline: the one place inside the app where the house serif runs at
   display size -- a report is the desk's finished work, and it should open
   like the PDF does. */
.report-headline {
  font-family: var(--display);
  font-size: 34px; font-weight: 700; line-height: 1.1; letter-spacing: .005em;
  margin: var(--s2) 0 var(--s3);
}
/* Section heads take the serif too, a step down, so the reader's eye finds
   the seams of the document without the seams shouting. */
.report-sec > h2 {
  font-family: var(--display);
  font-size: 23px; font-weight: 600; letter-spacing: .005em;
  padding-bottom: var(--s1);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--s3);
}
.report-cover { max-width: 100%; border-radius: 6px; margin: var(--s3) 0; }
.report-idline { display: flex; gap: var(--s3); align-items: baseline; flex-wrap: wrap; }
.report-class { font-size: var(--fs-xs); padding: 1px 8px; border-radius: 999px;
                border: 1px solid var(--rp-ac, currentColor); color: var(--rp-ac, inherit); }
.report-status::before { content: "●"; margin-right: 4px; }
.status-green      { color: var(--st-green); }
.status-dark-green { color: var(--st-dark-green); }
.status-amber      { color: var(--st-amber); }
.status-red        { color: var(--st-red); }
.report-stanceline { margin: var(--s2) 0 0; color: var(--text-muted); }
.report-actions { display: flex; gap: var(--s2); margin-top: var(--s3); }
.report-requested { margin-top: var(--s3); font-size: var(--fs-sm); color: var(--text-muted); font-style: italic; }
.report-sec { margin: var(--s5) 0; }
.report-snapshot { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
                   gap: var(--s3); margin: var(--s4) 0; }
.report-snapshot dt { font-size: var(--fs-xs); color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; }
.report-snapshot dd { margin: 0; font-variant-numeric: tabular-nums; }
.report-chart { width: 100%; min-height: 280px; }
.report-quads { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); }
.report-quads-3 { grid-template-columns: 1fr 1fr 1fr; }
.report-quad { border: 1px solid var(--border); border-radius: 6px; padding: var(--s3); }
.report-quad h3 { margin-top: 0; font-size: var(--fs-sm); }
.report-table th, .report-table td { padding: var(--s1) var(--s2); border-bottom: 1px solid var(--border); text-align: left; }
.report-fig img { max-width: 100%; }
.report-fig figcaption { color: var(--text-muted); font-size: var(--fs-sm); }
.report-cites { color: var(--text-muted); font-size: var(--fs-sm); border-top: 1px solid var(--border); padding-top: var(--s3); }
.report-sources { word-break: break-all; }
.report-glossary summary { cursor: pointer; color: var(--text-muted); }

/* Account — the one form on the page, in a reading column that matches the
   report reader's measure so a settings page and a document page share one
   left edge. The form is capped narrower than the prose: a password field
   the width of a 27" display is a target nothing, not more usable. */
.account {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: var(--s5) var(--s6) var(--s8);
}
.account > * { max-width: 760px; }
.account-sec > h2 {
  font-family: var(--display);
  font-size: 23px; font-weight: 600; letter-spacing: .005em;
  padding-bottom: var(--s1);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--s4);
}
.account-form { max-width: 400px; margin-top: var(--s4); }
.account-form .field .hint { margin-top: var(--s2); }
.account-form button.primary { margin-top: var(--s2); }
.account-note {
  max-width: 400px; margin-top: var(--s5);
  font-size: var(--fs-sm); color: var(--text-muted); line-height: 1.5;
}
@media (max-width: 900px) { .account { padding: var(--s4) var(--s4) var(--s6); } }

/* Requests — the queue and one run's timeline. Status colours reuse the
   report stance/status story: tokens where the theme decides, literals
   only where both themes were measured. */
.req-form { display: grid; gap: var(--s3); max-width: 560px; }
.req-form textarea, .req-form select { font: inherit; padding: var(--s2); }
.req-err { color: var(--st-red); }
.req-note { color: var(--text-muted); font-size: var(--fs-sm); margin-top: var(--s4); }
.req-status { font-size: var(--fs-xs); padding: 1px 8px; border-radius: 999px;
              border: 1px solid var(--border); white-space: nowrap; }
.req-queued  { color: var(--text-muted); }
.req-running { color: var(--primary); border-color: var(--primary); }
.req-failed  { color: var(--st-red);   border-color: var(--st-red); }
.req-done    { color: var(--st-green); border-color: var(--st-green); }
.req-who { color: var(--text-muted); font-size: var(--fs-sm); white-space: nowrap; }
.req-class { color: var(--text-muted); font-size: var(--fs-sm); white-space: nowrap; }
/* The queue row is a <div> now, not a single <a>: the row's own click
   target and the report link are two genuine anchors, and one cannot
   nest inside the other without a browser closing the outer at the
   inner -- the defect this shape replaces. display:contents keeps
   req-main's spans acting as this row's own flex children (the layout
   .report-row was built for) while req-main still exists as one real
   anchor wrapping them, so the row stays a single click target and the
   report link stays a second, independent one beside it. */
.report-row .req-main { display: contents; color: inherit; text-decoration: none; }
/* The asked question is the subject of this page, so it is set like a report's
   headline -- the house serif, quoted by the design rather than by punctuation
   the writer has to type. */
.req-ask {
  font-family: var(--display);
  font-size: 30px; font-weight: 700; line-height: 1.15; letter-spacing: .005em;
  margin: 0 0 var(--s3);
}
/* Facts about the run, separated by the rule rather than by typed middots so
   an absent classification cannot leave a dangling separator. */
.req-meta {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2) var(--s3);
  color: var(--text-muted); font-size: var(--fs-sm);
}
.req-meta > span + span::before {
  content: "·"; margin-right: var(--s3); color: var(--border-strong);
}

.req-head {
  font-family: var(--display);
  font-size: 21px; font-weight: 600; letter-spacing: .005em;
  margin: var(--s6) 0 var(--s2);
  padding-bottom: var(--s1);
  border-bottom: 1px solid var(--border);
}

/* --- the run's timeline --------------------------------------------------
   A live monitor, so it reads down a spine: one hairline rule with a mark per
   event, the time in the mono this app sets every machine value in, and the
   payload as a sentence rather than as the JSON the engine posted. A
   milestone -- the stage reached, the classification settled on -- takes the
   brass mark and full-strength text; the hundred activities under it stay
   quiet, because the point of the page is to see the shape of the run at a
   glance and only then read a line of it.                                  */

.req-timeline {
  list-style: none; margin: 0; padding: 0;
  border-left: 1px solid var(--border);
}
.req-ev {
  position: relative;
  display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
  padding: 7px 0 7px var(--s5);
  font-size: var(--fs-sm);
}
/* The mark on the spine. A ring for an activity, filled brass for a
   milestone -- shape as well as colour, so the two are told apart in
   greyscale like everything else in this app. */
.req-ev::before {
  content: "";
  position: absolute; left: -4px; top: 13px;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--surface);
  box-shadow: inset 0 0 0 1.5px var(--border-strong);
}
.req-ev.milestone::before {
  background: var(--accent);
  box-shadow: inset 0 0 0 1.5px var(--accent);
}

.req-ts {
  flex: none;
  font-family: var(--mono); font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums; font-variant-ligatures: none;
  color: var(--text-muted);
}
.req-body { flex: 1 1 22ch; min-width: 0; }
/* Who is acting. The role is the one thing worth scanning down the column. */
.req-lead { font-weight: 600; color: var(--text); }
.req-lead + .req-detail { margin-left: var(--s2); }
/* What they did. `anywhere`, not `break-all`: an action carries a search
   query with real words in it, and break-all hyphenates mid-word for no
   reason. */
.req-detail { color: var(--text-muted); overflow-wrap: anywhere; }
.req-ev.milestone .req-lead {
  font-family: var(--mono); font-size: var(--fs-xs); font-weight: 400;
  letter-spacing: .1em; text-transform: uppercase; color: var(--accent-text);
}
.req-ev.milestone .req-detail { color: var(--text); font-weight: 500; }

/* Which desk. A quiet label at the end of the row, in the .chip voice. */
.req-dept {
  flex: none; margin-left: auto;
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .06em; color: var(--text-muted);
  white-space: nowrap;
}
.req-none { padding: var(--s3) 0 var(--s3) var(--s5); color: var(--text-muted); }

@media (max-width: 720px) {
  /* The desk name stops fighting the action for the row and takes its own
     line under it. */
  .req-dept { margin-left: 0; }
  .req-body { flex-basis: 100%; }
}
/* The stance, status and request-outcome colours used to be restated here
   per theme; they are --st-* tokens now, remapped once with the rest of the
   dark palette, so they follow a *chosen* theme and not only the system one. */

/* A document's accent was chosen against PDF white; on the dark surface it has
   no contrast floor (the default institutional navy measures 1.57:1). Dark
   declines the suggestion -- `initial` makes the custom property
   guaranteed-invalid, so every var(--rp-ac, ...) fallback stands in. The
   !important is load-bearing: the accent arrives as an inline style attribute,
   which outranks any class rule in the cascade, and important is the one lever
   that outranks it back. Both selectors, for the same reason the palette has
   both: a reader who chose dark must get this too. */
:root[data-theme="dark"] .report {
  --rp-ac: initial !important; --rp-ac2: initial !important;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .report {
    --rp-ac: initial !important; --rp-ac2: initial !important;
  }
}

@media (max-width: 720px) {
  .report, .report-list { padding: var(--s4); }
  .report-quads, .report-quads-3 { grid-template-columns: minmax(0, 1fr); }
  .report-filter { flex-wrap: wrap; }
  .report-filter input, .report-filter select, .report-filter button { min-height: 44px; }
  /* .optable's mobile treatment turns .table-wrap scrolling off globally;
     a report table keeps its columns, so it keeps its scrollbar. */
  .report .table-wrap { overflow-x: auto; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}


/* --- ticker marks and the hover card ------------------------------------
   A ticker in a message body is marked by app/markdown.py as
   `<span class="tk">`. The mark has to be findable without being loud: a
   channel is mostly prose about a handful of names, and underlining every
   one of them turns a paragraph into a rash. A dotted underline that firms
   up on hover says "there is more here" and nothing else.

   No colour change. Colour in this app means state -- a mention chip, an
   error, a link -- and a ticker is none of those. */
.tk {
  border-bottom: 1px dotted var(--border-strong);
  cursor: help;
}
.tk:hover,
.tk:focus {
  border-bottom-style: solid;
  border-bottom-color: var(--accent);
}
.tk:focus {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 2px;
}

.tkcard {
  position: absolute;
  z-index: 60;
  max-width: 20rem;
  padding: .55rem .7rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius, 6px);
  box-shadow: var(--shadow);
  font-size: var(--fs-sm, 13px);
  line-height: 1.45;
  /* Pointer events off: the card opens *below* the word it describes, and a
     card that can take the pointer would steal the hover it depends on and
     flicker itself in and out. */
  pointer-events: none;
}
.tkcard[hidden] { display: none; }

.tkcard-head {
  display: flex;
  align-items: baseline;
  gap: .45rem;
  font-family: var(--mono);
}
.tkcard-px { font-variant-numeric: tabular-nums; }
.tkcard-ch { font-variant-numeric: tabular-nums; font-size: var(--fs-xs, 11px); }
.tkcard-ch.up { color: var(--accent-text); }
.tkcard-ch.dn { color: var(--danger); }

.tkcard-name { margin-top: .15rem; }
.tkcard-meta {
  margin-top: .1rem;
  color: var(--text-muted);
  font-size: var(--fs-xs, 11px);
}
/* The provenance line, in the quietest style the card has and never omitted.
   A price shown without saying it is a live print is how a tooltip becomes
   the thing somebody quotes as a close. */
.tkcard-src {
  margin-top: .35rem;
  padding-top: .3rem;
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: var(--fs-xs, 11px);
}

/* The idea footer under a message: one line of tags and small buttons, the
   same route the /ideas page posts to. Buttons sized like the reaction chips
   and the action strip beside them. */
.idea-foot { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center;
             margin-top: var(--s1); }
.idea-foot form.inline { display: inline-flex; gap: var(--s1); margin: 0; }
.idea-foot button { min-height: 22px; padding: 0 var(--s2); font-size: var(--fs-xs); }
.idea-foot .status-taken { color: var(--primary); }
.idea-foot .status-killed, .idea-foot .status-expired { color: var(--text-muted); }

/* The optional one-line note beside the Taken/Kill buttons on /ideas. */
.idea-note { min-width: 0; width: 16ch; margin-right: var(--s1); }
