/* webapp/app/static/app.css
   Cowork — "research desk": a strict grid, hairline rules, generous space in
   reading areas, controlled density in data areas, one accent used sparingly.

   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 -------------------------------------------------------------
   IBM Plex Sans for UI and prose; IBM Plex Mono 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: "IBM Plex Sans";
  src: url("/static/fonts/ibm-plex-sans-latin-400-normal.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("/static/fonts/ibm-plex-sans-latin-500-normal.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("/static/fonts/ibm-plex-sans-latin-600-normal.woff2") format("woff2");
  font-weight: 600; 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. Every foreground/background pair below was
   measured: body text >= 4.5:1, control borders and focus rings >= 3:1.
   #1E40AF is deliberately NOT reused in dark -- on #0F172A it measures 1.9:1,
   which is unreadable -- so the dark theme resolves --primary to #60A5FA.   */

:root {
  color-scheme: light;

  --bg:            #F1F3F7;   /* recessed: page, sidebar                     */
  --surface:       #FFFFFF;   /* raised: reading area, cards                 */
  --surface-2:     #E6EAF1;   /* fills, hovers, data blocks                  */
  --text:          #0F172A;
  --text-muted:    #535E75;
  --border:        #DCE1E9;   /* hairline separators                         */
  --border-strong: #7C8595;   /* control outlines -- 3:1 against every bg    */
  --primary:       #1E40AF;
  --primary-solid: #1E40AF;
  --primary-hover: #17348F;
  --on-primary:    #FFFFFF;
  --accent:        #059669;   /* rules and marks                             */
  --accent-text:   #046C4E;   /* the same idea, dark enough to read as text  */
  --danger:        #B42318;
  /* 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:  #92140C;
  --on-danger:     #FFFFFF;
  --focus:         #1E40AF;
  --shadow:        0 1px 2px rgba(15, 23, 42, .06),
                   0 8px 24px rgba(15, 23, 42, .08);

  /* type */
  --sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
          Roboto, "Helvetica Neue", Arial, sans-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:   24px;

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

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

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;

    --bg:            #0F172A;
    --surface:       #162035;
    --surface-2:     #1F2B45;
    --text:          #E7ECF5;
    --text-muted:    #9AA6BC;
    --border:        #26324B;
    --border-strong: #697894;
    --primary:       #60A5FA;
    --primary-solid: #2563EB;
    --primary-hover: #1D4ED8;
    --on-primary:    #FFFFFF;
    --accent:        #34D399;
    --accent-text:   #34D399;
    --danger:        #F87171;
    --danger-hover:  #FCA5A5;
    --on-danger:     #2B0A0A;
    --focus:         #60A5FA;
    --shadow:        0 1px 2px rgba(0, 0, 0, .4),
                     0 8px 28px rgba(0, 0, 0, .45);
  }
}

/* --- 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);
  background: var(--surface);
  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: a hairline rule under it, nothing more. */
.wordmark {
  display: flex; align-items: baseline; gap: var(--s2);
  padding-bottom: var(--s4);
  margin-bottom: var(--s5);
  border-bottom: 1px solid var(--border);
}
.wordmark .name { font-size: var(--fs-lg); font-weight: 600; letter-spacing: -.015em; }
.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;
  grid-template-columns: 260px 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 -------------------------------------------------------------- */

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

.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-size: var(--fs-md); font-weight: 600; letter-spacing: -.015em;
  color: var(--text);
}
.sidebar .brand .tag {
  font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted);
  border: 0; padding: 0;
}

.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);
}

/* 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);
}

.sidebar .me {
  border-top: 1px solid var(--border);
  padding: var(--s3) var(--s4);
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  font-size: var(--fs-sm);
}
.sidebar .me .who { font-weight: 500; overflow: hidden; text-overflow: ellipsis;
                    white-space: nowrap; }
.sidebar .me a { color: var(--text-muted); font-size: var(--fs-sm); }
.sidebar .me a:hover { color: var(--text); }

/* --- 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; }
.main > header h1 {
  font-family: var(--mono);
  font-size: var(--fs-md); font-weight: 400; letter-spacing: -.01em;
}
.main > header h1 .hash { color: var(--text-muted); }
.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. */
.msg.lead { margin-top: var(--s4); }
.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; }

/* --- 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;
  }
  .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. A file is a hairline card with the name in
   sans and the facts in mono underneath -- the same split the byline uses.
   An image gets a small thumbnail in the same card rather than a lightbox: the
   point is to recognise which chart it is, not to look at it here.          */

.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. */
.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);
}

.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: mono byline + an AGENT label + an accent rule down the left.
   Three carriers, so this survives greyscale, and colour blindness. */
.msg.agent { border-left-color: var(--accent); }
.msg.agent .who { font-family: var(--mono); font-weight: 400; font-size: 13.5px; }
.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);
}

/* --- 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; }

/* 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; }

.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(--surface); }
.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;
}

/* --- 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(--surface);
  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; }

/* --- 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. */
.page-list .person { font-size: var(--fs-sm); }

/* --- 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; }

/* --- 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 sidebar becomes a drawer. It is never removed from the document:
     hiding it outright made switching channels impossible on a phone. */
  .shell { grid-template-columns: minmax(0, 1fr); }

  .sidebar {
    position: fixed; inset: 0 auto 0 0; z-index: 30;
    width: min(84vw, 300px);
    background: var(--bg);
    border-right: 1px solid var(--border);
    box-shadow: var(--shadow);
    transform: translateX(-101%);
    transition: transform 200ms var(--ease);
  }
  .nav-latch:checked ~ .sidebar { transform: none; }

  .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; }
  .sidebar .me { min-height: 52px; }
  .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); }
}

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

/* The arrow on a rail link that leaves Cowork. Muted until the row is hovered
   or focused: it is a note about where the link goes, not a decoration that
   should compete with the name beside it. */
.rail-away {
  margin-left: auto; padding-left: var(--s2);
  color: var(--text-muted); font-size: var(--fs-xs);
}
.page-list a:hover .rail-away,
.page-list a:focus-visible .rail-away { color: var(--text); }

/* --- 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);
}
