@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap');

/* Design tokens lifted from sparked-ignition-app so both products read as one
   brand. Light values are its exact index.css variables; the dark set is
   derived from the same palette (the reference app is light-only). */
:root {
  --background: 30 33% 97%;
  --foreground: 0 0% 10%;
  --card: 0 0% 100%;
  --card-foreground: 0 0% 10%;
  --primary: 26 96% 54%;
  --primary-strong: 13 80% 51%;
  --primary-foreground: 0 0% 100%;
  --secondary: 30 33% 94%;
  --muted: 30 20% 92%;
  --muted-foreground: 0 0% 45%;
  --border: 30 15% 88%;
  --input: 30 15% 92%;
  --ring: 26 96% 54%;
  --radius: 0.625rem;
  --destructive: 0 72% 51%;

  --sparked-yellow: #FFDC64;
  --sparked-cream: #F5E9DE;
  --sparked-blue: #64A6C8;

  /* Inline code in messages. A blue wash rather than the grey --muted so a
     span like `sudo` reads as "this is a literal" the way a terminal or a
     code review does — grey on grey was close to invisible in a paragraph. */
  --code-bg: 204 56% 91%;
  --code-ink: 205 45% 30%;

  --font: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
}

:root[data-theme='dark'] {
  --background: 0 0% 9%;
  --foreground: 30 20% 94%;
  --card: 0 0% 13%;
  --card-foreground: 30 20% 94%;
  --primary: 26 96% 54%;
  --primary-strong: 13 80% 51%;
  --primary-foreground: 0 0% 100%;
  --secondary: 0 0% 17%;
  --muted: 0 0% 17%;
  --muted-foreground: 30 8% 62%;
  --border: 0 0% 22%;
  --input: 0 0% 20%;
  --destructive: 0 72% 60%;
  --code-bg: 206 40% 22%;
  --code-ink: 204 55% 78%;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --background: 0 0% 9%;
    --foreground: 30 20% 94%;
    --card: 0 0% 13%;
    --card-foreground: 30 20% 94%;
    --secondary: 0 0% 17%;
    --muted: 0 0% 17%;
    --muted-foreground: 30 8% 62%;
    --border: 0 0% 22%;
    --input: 0 0% 20%;
    --destructive: 0 72% 60%;
    --code-bg: 206 40% 22%;
    --code-ink: 204 55% 78%;
    }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: hsl(var(--background));
  color: hsl(var(--foreground));
  font-family: var(--font);
  font-size: 15px;
  -webkit-font-smoothing: antialiased;
  /* Stop iOS inflating text in landscape. */
  -webkit-text-size-adjust: 100%;
}

html {
  /* A quick second tap on a button must not zoom the page in on iOS; pinching
     still works. Tapping the critical-alerts button zoomed the whole app and
     left it stuck (Jon 2026-09-30). */
  touch-action: manipulation;
}

body {
  /* Nothing should ever scroll sideways on a phone. */
  overflow-x: hidden;
  overscroll-behavior-y: none;
}

[hidden] { display: none !important; }

/* No invert filter: brands ship a real dark-mode logo, swapped by
   theme-init.js. invert() turned Ecomstat's navy wordmark pale yellow. */
.logo { display: block; }

/* ---------- Login / splash ---------- */

/* Height tracks the VISUAL viewport, as .app does: min-height:100% is the
   layout viewport, which does not shrink when the keyboard opens, so the card
   stayed centred on the full screen and the email field ended up underneath
   it. --vvh is published from visualViewport in app.js; dvh is the fallback.

   Centring against the shrunken viewport is what makes the card ride up with
   the keyboard rather than needing to be scrolled into view. */
.login-screen {
  /* Fixed height, not min-height: this is the scroller now, and a min-height
     box grows with its content instead of scrolling it. */
  height: 100%;
  height: 100dvh;
  height: var(--vvh, 100dvh);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* Flex column with auto margins on the card, rather than grid centring:
     `align-items: center` on an overflowing child clips its TOP and makes it
     unreachable, while `margin: auto` collapses to the start edge instead. So
     the card is centred when it fits and scrolls from the top when it does
     not. */
  display: flex;
  flex-direction: column;
  padding: 24px;
  background:
    radial-gradient(ellipse 80% 50% at 50% -10%, hsl(var(--primary) / 0.13), transparent 70%),
    hsl(var(--background));
}

.login-card {
  /* auto, so the card centres when there is room and sits at the top — fully
     reachable — when the keyboard leaves less than its own height. */
  margin: auto;
  width: 100%;
  max-width: 380px;
  flex: 0 0 auto;
  background: hsl(var(--card));
  border: 1px solid hsl(var(--border));
  border-radius: 16px;
  padding: 40px 32px;
  display: flex;
  flex-direction: column;
  box-shadow: 0 1px 2px hsl(0 0% 0% / 0.04), 0 12px 32px hsl(0 0% 0% / 0.07);
}

.login-logo { width: 148px; margin: 0 auto 8px; }

.login-sub {
  color: hsl(var(--muted-foreground));
  text-align: center;
  margin: 10px 0 28px;
  font-size: 14px;
  line-height: 1.55;
}

.login-card label {
  font-size: 12px;
  font-weight: 600;
  color: hsl(var(--muted-foreground));
  margin-bottom: 7px;
  letter-spacing: 0.01em;
}

.login-card input {
  background: hsl(var(--background));
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
  padding: 12px 14px;
  color: hsl(var(--foreground));
  font: inherit;
  margin-bottom: 18px;
  width: 100%;
}

.login-card input::placeholder { color: hsl(var(--muted-foreground) / 0.7); }

.login-card input:focus,
.composer textarea:focus {
  outline: none;
  border-color: hsl(var(--ring));
  box-shadow: 0 0 0 3px hsl(var(--ring) / 0.16);
}

.login-card button[type='submit'] {
  background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-strong)));
  color: hsl(var(--primary-foreground));
  border: 0;
  border-radius: var(--radius);
  padding: 13px;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.login-card button[type='submit']:hover:not(:disabled) { filter: brightness(1.07); }
.login-card button[type='submit']:disabled { opacity: 0.6; cursor: default; }

.error {
  color: hsl(var(--destructive));
  font-size: 13px;
  margin: 16px 0 0;
  text-align: center;
  line-height: 1.5;
}

.sent-mark { font-size: 36px; text-align: center; margin: 16px 0 12px; }
.sent-title { font-size: 19px; font-weight: 600; text-align: center; margin: 0 0 10px; }

.sent-body {
  color: hsl(var(--muted-foreground));
  font-size: 14px;
  text-align: center;
  margin: 0 0 22px;
  line-height: 1.6;
}

.sent-body strong { color: hsl(var(--foreground)); word-break: break-all; }
.link-btn.centred { display: block; margin: 0 auto; }

/* ---------- App shell ---------- */

/* 100% of <html> is the *layout* viewport, which on mobile sits behind the
   browser chrome and does not shrink when the keyboard opens — the composer
   ends up under the keyboard. dvh tracks the visible area; the 100% line is
   the fallback for browsers without it.

   --vvh is set from visualViewport in app.js and wins where it exists. dvh is
   NOT enough on iOS: it ignores the on-screen keyboard, so Safari compensates
   by scrolling the document to reveal the focused input, which drags the
   topbar and sidebar logo off the top of the screen. Pinning .app to the
   visual viewport keeps the header where it belongs. */
.app {
  display: flex;
  height: 100%;
  height: 100dvh;
  height: var(--vvh, 100dvh);
}

/* Inside the iOS app the keyboard height comes from @capacitor/keyboard, which
   reports it once as the keyboard starts to move. Easing the height over the
   keyboard's own duration and curve makes the composer ride up with the keys
   rather than jumping to its final place. The website is left alone: there the
   height follows visualViewport frame by frame and needs no easing. */
html[data-native-kb] .app,
html[data-native-kb] .login-screen {
  transition: height 0.25s cubic-bezier(0.38, 0.7, 0.125, 1);
}

/* The document must never scroll — .messages is the only scroller in the chat
   shell, and the login card is sized to the visible viewport. Without this,
   iOS still has somewhere to scroll the page to and does so the moment a field
   is focused: in the chat that slid the header away, and on the login screen
   it threw the card off the top as the keyboard opened.

   This was once scoped to :has(#app:not([hidden])) so the login card could
   scroll out from under the keyboard. The card still has to be reachable on a
   short viewport — an iPhone SE in portrait, or any phone in landscape, does
   not have room for the card above the keyboard — but the DOCUMENT is the
   wrong thing to scroll: iOS moves it on focus and the card leaves the screen.
   .login-screen scrolls instead, below, so the card stays put and its own
   contents move. */
html, body {
  overflow: hidden;
  overscroll-behavior: none;
}

.sidebar {
  /* Set by the drag handle and restored from localStorage; 264px is the
     default until then. */
  width: var(--sidebar-w, 264px);
  flex: 0 0 var(--sidebar-w, 264px);
  background: hsl(var(--secondary));
  border-right: 1px solid hsl(var(--border));
  display: flex;
  flex-direction: column;
  /* The list rows ask "how wide is the sidebar?" via @container, so dragging
     the divider — not the viewport — decides what a row can afford to show.
     Width is pinned by the var above, so the inline-size containment this
     implies costs nothing. */
  container: sidebar / inline-size;
}

/*
 * The drag handle between list and thread. Four pixels wide with a wider
 * invisible grab area, because a 4px target is a frustrating thing to hit.
 */
.resizer {
  flex: 0 0 4px;
  cursor: col-resize;
  background: transparent;
  position: relative;
  z-index: 5;
}
.resizer::after {
  content: '';
  position: absolute;
  inset: 0 -4px;          /* 12px of grabbable area, 4px of visible line */
}
.resizer:hover,
.resizer:focus-visible,
body.resizing .resizer { background: hsl(var(--primary) / 0.5); }
.resizer:focus-visible { outline: none; }

/* While dragging, nothing else should react to the pointer — text selection
   and hover states across the whole page make the drag feel broken. */
body.resizing { cursor: col-resize; -webkit-user-select: none; user-select: none; }
body.resizing * { pointer-events: none; }
body.resizing .resizer { pointer-events: auto; }

.sidebar-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Wraps rather than overflowing: logo + five icons is ~320px, wider than a
     264px sidebar, and the spill-over sat on top of the chat title (Jon,
     2026-09-24). On a narrow sidebar the icons drop under the logo. */
  flex-wrap: wrap;
  gap: 10px;
  padding: 16px 14px;
  border-bottom: 1px solid hsl(var(--border));
}

.sidebar-logo { width: 104px; }

/* New bot: the robot with a small + on its corner, so it reads as "add"
   like its neighbour without a second glyph overflowing the square. */
#new-bot { position: relative; }
#new-bot .plus-badge {
  position: absolute;
  top: 1px;
  right: 3px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
}

/* The header buttons sit right of the logo; the view-as popup anchors to
   this group so it drops under its 👤 icon rather than across the list. */
.sidebar-head-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
  margin-left: auto;
  min-width: 0;
  position: relative;
}

/* Desktop floor for the conversation pane: the sidebar gives way before the
   thread does, so neither a dragged divider nor a narrow window can crush the
   chat into a strip. */
@media (min-width: 861px) {
  /* .app > .main: a bare .main lost to the later `.main { min-width: 0 }`, so
     this floor never applied. */
  .app > .main { min-width: 440px; }
  .sidebar { max-width: calc(100vw - 444px); min-width: 200px; }
  /* With a document open beside the chat, the sidebar yields too (down to its
     200px floor) instead of the chat. */
  .app:has(#preview:not([hidden])) .sidebar { flex: 0 1 var(--sidebar-w, 264px); }
  /* Header buttons wrap onto a second row rather than spilling out of a narrow
     pane and over the document beside it. */
  .topbar { flex-wrap: wrap; row-gap: 6px; }
  .topbar .conv-heading { flex: 1 1 180px; min-width: 0; }
  /* The composer asks how wide the PANE is, not the window. */
  .composer-wrap { container: composer / inline-size; }
}


.view-pop {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 30;
  width: max(240px, 100%);
  display: grid;
  gap: 6px;
  padding: 10px 12px;
  background: hsl(var(--background));
  border: 1px solid hsl(var(--border));
  border-radius: 10px;
  box-shadow: 0 8px 24px hsl(0 0% 0% / 0.35);
}

.view-pop label { font-size: 12px; font-weight: 600; }
.view-pop select { width: 100%; min-width: 0; padding: 8px; border: 1px solid hsl(var(--border)); border-radius: 8px; background: hsl(var(--background)); color: inherit; font: inherit; }
.view-pop small { font-size: 11px; opacity: .65; }
.view-pop input { width: 100%; min-width: 0; padding: 8px; border: 1px solid hsl(var(--border)); border-radius: 8px; background: hsl(var(--background)); color: inherit; font: inherit; }
.view-as-list { display: grid; gap: 1px; max-height: min(360px, 55vh); overflow-y: auto; overflow-x: hidden; margin: 0 -4px; }
/* On a desktop the picker opens as a wide panel with the people in columns, so
   the whole list (70+) fits on screen instead of scrolling a 6-row box. It is
   fixed to the viewport because the sidebar it anchors to is far narrower. */
@media (min-width: 900px) {
  #view-as-pop.view-pop {
    position: fixed;
    top: 64px;
    left: 16px;
    right: auto;
    width: min(1100px, calc(100vw - 32px));
    max-height: calc(100vh - 80px);
  }
  #view-as-pop .view-as-list {
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    max-height: calc(100vh - 190px);
    column-gap: 4px;
  }
}
.view-as-item { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; width: 100%; text-align: left; background: none; border: 0; border-radius: 8px; padding: 6px 8px; color: inherit; font: inherit; cursor: pointer; }
.view-as-item:hover, .view-as-item:focus-visible { background: hsl(var(--muted)); outline: none; }
.view-as-item[aria-selected="true"] { background: hsl(var(--primary) / 0.12); }
.view-as-item[aria-selected="true"] .view-as-name::after { content: " ✓"; color: hsl(var(--primary)); }
.view-as-name { font-size: 13.5px; font-weight: 600; }
.view-as-sub { font-size: 11.5px; opacity: .65; }
.view-as-none { font-size: 12.5px; opacity: .7; margin: 4px 8px; }

.ghost-btn {
  background: transparent;
  border: 1px solid hsl(var(--border));
  color: hsl(var(--muted-foreground));
  border-radius: 8px;
  width: 32px;
  height: 32px;
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  flex: 0 0 32px;
}

/* Mouse only. On a touch screen :hover sticks to the last button tapped, so
   a toggle tapped OFF kept its blue outline and looked stuck on (Jon,
   2026-09-25). */
@media (hover: hover) {
  .ghost-btn:hover {
    color: hsl(var(--primary));
    border-color: hsl(var(--primary));
    background: hsl(var(--primary) / 0.07);
  }
}

.conv-list { flex: 1; overflow-y: auto; padding: 8px; min-height: 200px; }

/* On a short window the client chips, search, filters and Order used to take
   the whole height: the tools block shrank under its own content (so "Order"
   sat on top of the list) and the chat list was left a sliver that could not
   scroll (Jon, 2026-09-24). Now the fixed parts keep their size, the list keeps
   at least 200px, and when that does not fit the sidebar scrolls as a whole. */
.sidebar { overflow-y: auto; overflow-x: hidden; }
.sidebar-head, .sidebar-tools, .sidebar-foot { flex-shrink: 0; }

.conv-item {
  /* Row padding scales too, so a larger list gets room rather than just
     bigger text crammed into the same height. */
  display: flex;
  align-items: center;
  gap: 8px;
  padding: calc(9px * var(--list-scale, 1)) 10px;
  border-radius: 8px;
  color: hsl(var(--muted-foreground));
  cursor: pointer;
  font-size: 14px;
  margin-bottom: 2px;
}

.conv-item:hover { background: hsl(var(--card)); color: hsl(var(--foreground)); }

.conv-item.active {
  background: hsl(var(--card));
  color: hsl(var(--foreground));
  font-weight: 500;
  box-shadow: inset 2px 0 0 hsl(var(--primary));
}

.conv-item span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The name is the one span allowed to wrap (to two rows) — at larger list
   text a one-line clamp truncated it to "CLAUD…". Needs to beat the shared
   span rule above. */
.conv-item .conv-name { white-space: normal; }

.conv-del {
  background: none;
  border: 0;
  color: hsl(var(--muted-foreground));
  cursor: pointer;
  opacity: 0;
  font-size: 17px;
  padding: 0 2px;
}

.conv-item:hover .conv-del { opacity: 1; }
.conv-del:hover { color: hsl(var(--destructive)); }

/*
 * On touch the delete button is the action REVEALED by a swipe, so it is
 * pinned to the right edge under the sliding content rather than sitting in
 * the row's flow. Width must match ACTION_W in app.js.
 */
@media (hover: none) and (pointer: coarse) {
  .conv-del {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 84px;
    margin: 0;
    padding: 0;
    border-radius: 0;
    opacity: 1;
    display: grid;
    place-items: center;
    background: hsl(var(--destructive, 0 72% 51%));
    color: #fff;
    font-size: 14px;
    font-weight: 600;
    /* Only reachable once the row is swiped; otherwise a stray tap near the
       edge would delete a conversation. */
    pointer-events: none;
  }
  .conv-item.swiped .conv-del { pointer-events: auto; }
}

/* Three rows: email, then controls, then the build stamp.
   The email gets a row to itself because it cannot share one — a full address
   plus three controls needs more than the ~280px sidebar has, so any
   single-row layout ends in an ellipsis ("jon.trevor@ecoms…"). Stacking is
   what makes the address readable in full. */
/* One row of controls with the address above it.
 *
 * Was a three-column grid with each control pinned to an explicit cell, which
 * meant anything new had nowhere to go and dropped to a row of its own — the
 * footer had grown to four stacked rows. A flex row lets the controls sit
 * along the bottom and use the width that is already there, and wrap only if
 * the sidebar is genuinely too narrow. */
.sidebar-foot {
  border-top: 1px solid hsl(var(--border));
  padding: 10px 14px 12px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  font-size: 13px;
}

/* The two icon toggles sit together on the left; Sign out is pushed to the far
   right by the 1fr gap between them. Previously the theme toggle was adjacent
   to Sign out, which put a harmless control one slip away from a destructive
   one. */
.sidebar-foot #notify-toggle { order: 2; }
/* ☀ and ☾ both have emoji variants, and left to the platform the sun turns
   into a full-colour glyph a size larger than the moon — the row then jitters
   on every toggle. Text presentation keeps both as monochrome ink that takes
   the button's own colour, and a fixed width stops the swap nudging Sign out. */
.sidebar-foot #theme-toggle {
  order: 3;
  font-variant-emoji: text;
  line-height: 1;
  min-width: 1.2em;
  text-align: center;
}
/* Pushed to the far right by the auto margin, so the slack between the
   controls and Sign out is what separates a harmless control from a
   destructive one. */
.sidebar-foot #logout { order: 6; margin-left: auto; }
.sidebar-foot .text-size { order: 4; }
/* The build stamp shares the controls row. It is the least useful thing in
   the footer, so rather than compete with the address for the last few pixels
   it simply goes away once the sidebar is too narrow to hold both. The
   address stays: it tells you which account you are signed in as. */
.sidebar-foot .build-stamp {
  order: 5;
  min-width: 0;
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Viewport, not @container: the build stamp is hidden on small screens, which
   is about the device, not how wide the divider has been dragged. */
@media (max-width: 1100px) {
  .sidebar-foot .build-stamp { display: none; }
}

.build-stamp {
  font-family: var(--mono);
  font-size: 10px;
  color: hsl(var(--muted-foreground) / 0.7);
  text-decoration: none;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.build-stamp:hover { color: hsl(var(--primary)); }

/* Full width, and allowed to wrap rather than ellipse — a truncated address
   is useless for telling which account you are signed in as. overflow-wrap
   (not word-break) so it breaks between characters only when a single run is
   genuinely wider than the sidebar. */
.who {
  /* On the same row as the controls, taking the slack between them and
     shrinking with an ellipsis when there is not enough. It was given a row
     of its own on the theory that an address is too long to share one — but
     the sidebar is wide enough, and a footer that stacks to three rows to
     print an address in full is the worse trade. Hovering shows it whole. */
  order: 1;
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: hsl(var(--muted-foreground));
  min-width: 0;
  line-height: 1.35;
}

.link-btn {
  background: none;
  border: 0;
  color: hsl(var(--muted-foreground));
  cursor: pointer;
  font: inherit;
  padding: 0;
  white-space: nowrap;
}

.link-btn:hover { color: hsl(var(--primary)); }

.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }

.topbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 20px;
  border-bottom: 1px solid hsl(var(--border));
  background: hsl(var(--card));
}

.conv-title {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  font-size: 14px;
}

.pill {
  font-size: 11px;
  padding: 3px 9px;
  border-radius: 999px;
  background: hsl(var(--destructive) / 0.13);
  color: hsl(var(--destructive));
  border: 1px solid hsl(var(--destructive) / 0.3);
  white-space: nowrap;
}

/* ---------- Messages ----------
   Full-bleed rows with an alternating tint, so the transcript uses the whole
   width instead of a narrow centred column. */

.messages { flex: 1; overflow-y: auto; }

.msg {
  display: flex;
  gap: 14px;
  padding: calc(22px * var(--msg-scale, 1)) 32px;
  border-bottom: 1px solid hsl(var(--border) / 0.6);
}

.msg.assistant { background: hsl(var(--card)); }

.msg .role {
  flex: 0 0 28px;
  height: 28px;
  border-radius: 8px;
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 700;
  /* Level with the message, not pinned to its first line. Who spoke is a
     property of the whole bubble, and on the short messages that make up most
     of a conversation a top-aligned badge reads as floating above the text
     rather than labelling it. The read-aloud button on the other side is
     centred for the same reason. */
  align-self: center;
}

/* Pin a message. Same furniture as reply and fork, one slot further left. */
.msg-pin {
  position: absolute;
  top: 4px;
  right: 158px;
  border: 0;
  background: transparent;
  color: hsl(var(--muted-foreground));
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  opacity: 0;
  transition: opacity .12s ease;
}
.msg:hover .msg-pin { opacity: 0.75; }
.msg-pin:hover { opacity: 1; background: hsl(var(--muted)); }

/* ---------- Pinned message bar ----------
   Between the header and the transcript, the way Telegram does it: always the
   same place, never over the text, and quiet enough to ignore when you are
   reading rather than looking for it. */
.pin-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  background: hsl(var(--card));
  border-bottom: 1px solid hsl(var(--border));
}
.pin-glyph { font-size: 14px; flex: none; opacity: 0.85; }
.pin-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-align: left;
  color: inherit;
}
.pin-who {
  font-size: 11.5px;
  font-weight: 700;
  color: hsl(var(--primary));
  letter-spacing: -0.01em;
}
/* One line, always. A pinned message is a pointer to the real one, and a bar
   that grows with its contents would push the transcript around. */
.pin-text {
  font-size: 12.5px;
  color: hsl(var(--muted-foreground));
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pin-count {
  flex: none;
  font-size: 11px;
  color: hsl(var(--muted-foreground));
  opacity: 0.8;
}
.pin-off { flex: none; font-size: 18px; width: 30px; height: 30px; }
/* The message a pin bar just jumped to, so the eye lands on the right one. */
.msg.pin-flash { animation: pin-flash 1.4s ease-out; }
@keyframes pin-flash {
  0%, 25% { background: hsl(var(--primary) / 0.14); }
  100% { background: transparent; }
}

/* An agent's stable number. Deliberately quiet - it is there to be read when
   you go looking for it, not to compete with the name. */
.bot-id-tag {
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
  letter-spacing: 0.02em;
}

/* Recording. Red and gently pulsing: the one control on the page where not
   noticing it is still on has a cost, so it is the loudest state in the
   composer. */
#mic-btn.recording {
  color: hsl(0 75% 60%);
  background: hsl(0 75% 60% / 0.14);
  animation: mic-pulse 1.1s ease-in-out infinite;
}
#mic-btn.recording { position: relative; }
#mic-btn.recording::after {
  content: attr(data-elapsed);
  position: absolute; top: -17px; left: 50%; transform: translateX(-50%);
  padding: 0 6px; border-radius: 8px;
  font-size: 11px; line-height: 16px; font-variant-numeric: tabular-nums;
  color: #fff; background: hsl(0 75% 52%);
}
#mic-btn.working { opacity: 0.6; }
@keyframes mic-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }

/* Auto-play is on: the speaker control is lit rather than merely pressed, so
   a glance tells you whether the next reply is going to start talking. */
#autoplay-btn.on {
  color: hsl(var(--primary));
  border-color: hsl(var(--primary));
  background: hsl(var(--primary) / 0.18);
}

/* Tall enough that centring would strand the badge in the middle of the text:
   back to the top, with the 1px nudge that lines it up optically with the
   first line. See watchMessageHeight() for where .tall comes from. */
.msg.tall .role {
  align-self: flex-start;
  margin-top: 1px;
}

.msg.user .role {
  background: hsl(var(--muted));
  color: hsl(var(--muted-foreground));
}

.msg.assistant .role {
  background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-strong)));
  color: hsl(var(--primary-foreground));
  font-size: 14px;
}

.msg .body {
  flex: 1;
  min-width: 0;
  /* The desktop 15px body size, times the conversation's own text size. */
  font-size: calc(15px * var(--msg-scale, 1));
  line-height: 1.65;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  padding-top: 3px;
  /* Selectable, so one paragraph can be taken on its own instead of the whole
     message. The rest of the row (badge, controls, quoted parent) stays
     unselectable: a drag that starts on furniture should not smear text. */
  -webkit-user-select: text;
  user-select: text;
}

.msg .body pre {
  background: hsl(var(--code-bg) / 0.55);
  border: 1px solid hsl(var(--code-ink) / 0.25);
  border-radius: 8px;
  padding: 13px;
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 13px;
  white-space: pre;
  line-height: 1.5;
}

.msg .body code {
  font-family: var(--mono);
  font-size: calc(13px * var(--msg-scale, 1));
  background: hsl(var(--code-bg));
  color: hsl(var(--code-ink));
  padding: 1.5px 5px;
  border-radius: 4px;
}

.msg .body pre code { background: none; padding: 0; }

/* A run of file-only messages from one speaker, drawn as one card. */
.msg.file-run-head:not(.file-run-last),
.msg.file-run-cont:not(.file-run-last) {
  border-bottom: none;
  padding-bottom: 0;
}
.msg.file-run-cont { padding-top: 6px; }
.msg.file-run-cont .role { visibility: hidden; height: 0; }
.msg.file-run-cont .msg-head { display: none; }
.msg.file-run-head .msg-docs,
.msg.file-run-cont .msg-docs { margin-bottom: 0; }
.file-run-all {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  padding: 6px 12px;
  border-radius: 10px;
  border: 1px solid hsl(var(--border));
  color: hsl(var(--foreground));
  font-weight: 600;
  text-decoration: none;
}
.file-run-all:hover { background: hsl(var(--muted, 220 14% 20%)); }
/* Nothing to read aloud in a file-only message, and six ⋯ buttons stacked
   down one card is noise: each row's menu shows on hover instead (touch keeps
   the long-press menu). */
.msg[data-file-only] .msg-speak { display: none; }
@media (hover: hover) {
  .msg.file-run-head .msg-more, .msg.file-run-cont .msg-more { opacity: 0; transition: opacity 0.15s; }
  .msg.file-run-head:hover .msg-more, .msg.file-run-cont:hover .msg-more,
  .msg.file-run-head .msg-more:focus-visible, .msg.file-run-cont .msg-more:focus-visible { opacity: 1; }
}

/* A bot's inline buttons (Telegram inline_keyboard), under its message. */
.msg-buttons { display: grid; gap: 6px; margin-top: 8px; }
.msg-buttons-row { display: flex; gap: 6px; flex-wrap: wrap; }
.msg-button {
  flex: 1 1 0;
  min-width: 96px;
  min-height: 36px;
  padding: 6px 12px;
  border-radius: 10px;
  border: 1px solid hsl(var(--border));
  background: hsl(var(--muted, 220 14% 20%));
  color: hsl(var(--foreground));
  font: inherit;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
.msg-button:disabled { opacity: 0.55; cursor: default; }
.msg-button.pressed { outline: 2px solid hsl(var(--primary, 210 90% 55%)); }
.msg.error .body { color: hsl(var(--destructive)); }
.msg.error .role { background: hsl(var(--destructive)); color: #fff; }

.cursor {
  display: inline-block;
  width: 7px;
  height: 15px;
  background: hsl(var(--primary));
  vertical-align: -2px;
  animation: blink 1s steps(2) infinite;
}

@keyframes blink { 50% { opacity: 0; } }

.empty { text-align: center; color: hsl(var(--muted-foreground)); margin-top: 16vh; padding: 0 24px; }
.empty-logo { width: 132px; margin: 0 auto 20px; opacity: 0.9; }
.empty h2 { margin: 0 0 8px; font-size: 19px; color: hsl(var(--foreground)); font-weight: 600; }
.empty p { margin: 0; font-size: 14px; }

/* ---------- Attachments ---------- */

.composer-wrap { border-top: 1px solid hsl(var(--border)); background: hsl(var(--card)); }

/* @ mention picker, floating above the composer inside the wrap so it scrolls
   with it rather than escaping the chat column. */
.mention-picker {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: calc(100% + 6px);
  max-height: 240px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  background: hsl(var(--popover, var(--card)));
  border: 1px solid hsl(var(--border));
  border-radius: 10px;
  box-shadow: 0 8px 24px hsl(0 0% 0% / 0.18);
  z-index: 30;
}
.mention-picker[hidden] { display: none; }
.composer-wrap { position: relative; }
.mention-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.mention-row.active,
.mention-row:hover { background: hsl(var(--muted)); }
.mention-disc {
  flex: none;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: hsl(var(--muted));
  color: hsl(var(--primary-foreground, white));
  font-size: 12px;
  font-weight: 600;
}
.mention-name {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tray {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  padding: 12px 32px 0;
}

.thumb {
  position: relative;
  width: 68px;
  height: 68px;
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid hsl(var(--border));
  background: hsl(var(--muted));
  flex: 0 0 auto;
}

.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

.thumb.uploading img { opacity: 0.4; }

.thumb .spin {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 11px;
  color: hsl(var(--muted-foreground));
}

.thumb .rm {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 20px;
  height: 20px;
  border-radius: 999px;
  border: 0;
  background: hsl(0 0% 0% / 0.62);
  color: #fff;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  display: grid;
  place-items: center;
}

/* Documents have nothing to preview, so the chip carries the information:
   type, name, and what the server extracted ("12 pages", "3 sheets"). */
.thumb.doc {
  width: auto;
  min-width: 130px;
  max-width: 220px;
  height: auto;
  min-height: 68px;
  background: hsl(var(--card));
}

.doc-face {
  padding: 9px 10px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  height: 100%;
}

.doc-ext {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: hsl(var(--primary));
}

.doc-name {
  font-size: 12px;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.doc-sum { font-size: 10.5px; color: hsl(var(--muted-foreground)); }

.msg-docs { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 8px; }

.doc-chip {
  font-size: 12px;
  padding: 5px 10px;
  border-radius: 999px;
  background: hsl(var(--muted));
  border: 1px solid hsl(var(--border));
  color: hsl(var(--muted-foreground));
}

.thumb.failed { border-color: hsl(var(--destructive)); }
.thumb.failed .spin { color: hsl(var(--destructive)); font-size: 10px; text-align: center; padding: 2px; }

/* Videos inside the transcript: a real player, sized to the video so the
   bubble does not jump when the first frame arrives. */
.msg-audios { display: flex; flex-direction: column; gap: 8px; margin: 0 0 8px; }
.msg-audio { display: flex; flex-direction: column; gap: 4px; max-width: 100%; }
.msg-audio-name { font-size: 13px; opacity: .8; overflow-wrap: anywhere; }
.msg-audio audio { width: 100%; max-width: 320px; }
.msg-videos { display: flex; flex-direction: column; gap: 8px; margin: 0 0 8px; }

.msg-videos video {
  display: block;
  width: min(360px, 100%);
  max-height: 420px;
  border-radius: 10px;
  border: 1px solid hsl(var(--border));
  background: #000;
}

.msg-video-expired {
  font-size: 12px;
  padding: 8px 12px;
  border-radius: 10px;
  background: hsl(var(--muted));
  border: 1px dashed hsl(var(--border));
  color: hsl(var(--muted-foreground));
  width: fit-content;
  max-width: 100%;
}

/* Images inside the transcript. */
.msg-images { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 8px; }

.msg-images img {
  max-width: min(320px, 100%);
  max-height: 260px;
  border-radius: 10px;
  border: 1px solid hsl(var(--border));
  cursor: zoom-in;
  display: block;
}

/* Full-size viewer. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: hsl(0 0% 0% / 0.88);
  display: grid;
  /* A definite 1fr cell, so the image's max-height: 100% has something to
     resolve against. With an auto row it resolved to nothing, the image kept
     its natural height, and in landscape it hung off the bottom of the screen
     instead of fitting and centring (Jon, 2026-09-25). */
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
  place-items: center;
  padding: 24px;
  padding-left: max(24px, env(safe-area-inset-left));
  padding-right: max(24px, env(safe-area-inset-right));
  cursor: zoom-out;
  overflow: hidden;
  /* Every gesture belongs to the viewer: the page must not pinch-zoom or
     scroll underneath it. */
  touch-action: none;
  overscroll-behavior: contain;
}

.lightbox img {
  max-width: 100%;
  max-height: 100%;
  border-radius: 8px;
  transform-origin: center;
  cursor: zoom-in;
  user-select: none;
  -webkit-user-select: none;
}
.lightbox.zoomed img { border-radius: 0; cursor: grab; }
/* In the app the long-press menu is replaced by Save (see openLightbox). */
.lightbox.no-callout img { -webkit-touch-callout: none; }

.lightbox-bar {
  position: absolute;
  top: max(12px, env(safe-area-inset-top));
  right: max(12px, env(safe-area-inset-right));
  display: flex;
  gap: 8px;
}
.lightbox-btn {
  min-height: 40px;
  min-width: 40px;
  padding: 0 14px;
  border: 0;
  border-radius: 999px;
  background: hsl(0 0% 100% / 0.16);
  color: #fff;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}
.lightbox-btn:hover { background: hsl(0 0% 100% / 0.26); }
/* Eased only when settling (a swipe snapping back or a new image); while a
   finger is down the image tracks it with no lag. */
.lightbox img { transition: transform 0.18s ease-out; }
.lightbox img.dragging { transition: none; }
.lightbox-count {
  position: absolute;
  top: max(12px, env(safe-area-inset-top));
  left: max(16px, env(safe-area-inset-left));
  line-height: 40px;
  color: hsl(0 0% 100% / 0.85);
  font-size: 15px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  padding: 0;
  width: 44px;
  height: 44px;
  font-size: 28px;
  line-height: 1;
}
.lightbox-nav.prev { left: max(12px, env(safe-area-inset-left)); }
.lightbox-nav.next { right: max(12px, env(safe-area-inset-right)); }
/* Touch screens swipe; the arrows are for a mouse. */
@media (hover: none) { .lightbox-nav { display: none; } }
.lightbox-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

.drop-hint {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: hsl(var(--primary) / 0.12);
  border: 3px dashed hsl(var(--primary));
  display: grid;
  place-items: center;
  font-weight: 600;
  color: hsl(var(--primary-strong));
  pointer-events: none;
}

/* ---------- Composer ---------- */

/* Border and background live on .composer-wrap so the attachment tray sits
   inside the same surface. */
.composer {
  padding: 14px 32px 18px;
  display: flex;
  gap: 10px;
  align-items: flex-end;
}

.composer textarea {
  flex: 1;
  background: hsl(var(--background));
  border: 1px solid hsl(var(--border));
  border-radius: 12px;
  padding: 12px 15px;
  color: hsl(var(--foreground));
  font: inherit;
  resize: none;
  /* autoGrow() sets the real cap; this only bounds it. */
  max-height: 60vh;
  line-height: 1.5;
}

/* Submit only — the attach button sits in the same row but must stay a quiet
   outline, or the composer shows two identical filled squares that mean
   different things. */
.composer button[type='submit'] {
  background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-strong)));
  color: hsl(var(--primary-foreground));
  border: 0;
  border-radius: 11px;
  width: 42px;
  height: 42px;
  font-size: 17px;
  cursor: pointer;
  flex: 0 0 42px;
}

/* The outline controls in the composer share one size: a row of buttons
   that are nearly the same reads as a mistake, and the mic arrived smaller. */
.composer #attach-btn,
.composer #paste-btn,
.composer #autoplay-btn,
.composer #clear-btn,
.composer #secret-btn,
.composer #mic-btn {
  width: 42px;
  height: 42px;
  flex: 0 0 42px;
  font-size: 19px;
  border-radius: 11px;
}

/* Drag the top edge of the composer to set the box's height; double-click
   resets it to grow-with-text. Desktop only: a phone sizes the box itself. */
.composer-grip { display: none; }
@media (min-width: 861px) {
  .composer-grip {
    display: block;
    height: 9px;
    margin-top: -5px;
    cursor: row-resize;
    position: relative;
    z-index: 3;
    touch-action: none;
  }
  .composer-grip::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 3px;
    width: 36px;
    height: 3px;
    margin-left: -18px;
    border-radius: 3px;
    background: hsl(var(--border));
  }
  .composer-grip:hover::after,
  .composer-grip:focus-visible::after,
  body.resizing-composer .composer-grip::after { background: hsl(var(--primary) / 0.7); }
  .composer-grip:focus-visible { outline: none; }
  body.resizing-composer { cursor: row-resize; user-select: none; }
}

.composer button:hover:not(:disabled) { filter: brightness(1.07); }
.composer button:disabled { opacity: 0.45; cursor: default; }

/* ---------- Admin console ---------- */

/* A centred modal rather than a right-hand drawer. The drawer competed with
   the sidebar for edge space and felt like a side panel on a phone; an admin
   panel is a focused task, so it takes the centre of the screen. */
.console {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  padding: 24px;
  padding-top: max(24px, env(safe-area-inset-top));
  padding-bottom: max(24px, env(safe-area-inset-bottom));
  background: hsl(0 0% 0% / 0.5);
  opacity: 0;
  transition: opacity 0.18s ease;
}

.console > .console-panel {
  width: min(680px, 100%);
  max-height: 100%;
  background: hsl(var(--card));
  border: 1px solid hsl(var(--border));
  border-radius: 16px;
  box-shadow: 0 24px 64px hsl(0 0% 0% / 0.4);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transform: translateY(8px) scale(0.99);
  transition: transform 0.18s ease;
}

.console.open { opacity: 1; }
.console.open > .console-panel { transform: none; }
/* Support and the legal pages open over one another, so they are one size
   with one scroll: a fixed panel whose body scrolls, not a panel that grows
   with its text (Jon, 2026-09-28: "they should both be the same size container
   with the same level of scrolling"). */
#support-modal > .console-panel,
#legal-modal > .console-panel { height: min(760px, 100%); }
.console[hidden] { display: grid !important; visibility: hidden; }
.console.open[hidden] { visibility: visible; }

.console-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 15px 18px;
  border-bottom: 1px solid hsl(var(--border));
  font-size: 15px;
}

.console-body { flex: 1; overflow-y: auto; padding: 6px 18px 24px; }

.console-section { padding: 16px 0; border-bottom: 1px solid hsl(var(--border) / 0.7); }
.console-section:last-child { border-bottom: 0; }

.console-section h3 {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: hsl(var(--muted-foreground));
  margin: 0 0 12px;
  display: flex;
  align-items: center;
  gap: 8px;
}

.count {
  background: hsl(var(--primary));
  color: hsl(var(--primary-foreground));
  border-radius: 999px;
  padding: 1px 7px;
  font-size: 11px;
  letter-spacing: 0;
}

.op-form { display: flex; flex-direction: column; gap: 6px; }

.op-form label {
  font-size: 12px;
  font-weight: 600;
  color: hsl(var(--muted-foreground));
  margin-top: 6px;
}

/* :not([type=checkbox]) matters: a bare `.op-form input` also matched the
   checkboxes inside .check-row and stretched each to width:100%, which ate
   the whole row and pushed the name out of view. */
.op-form select,
.op-form input:not([type='checkbox']):not([type='radio']) {
  background: hsl(var(--background));
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
  padding: 10px 12px;
  color: hsl(var(--foreground));
  font: inherit;
  font-size: 16px; /* 16px: stops iOS zooming the page on focus */
  width: 100%;
}

.op-form button {
  margin-top: 14px;
  background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-strong)));
  color: hsl(var(--primary-foreground));
  border: 0;
  border-radius: var(--radius);
  padding: 12px;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.op-form button:disabled { opacity: 0.5; cursor: default; }
.op-hint { font-size: 12px; color: hsl(var(--muted-foreground)); margin: 10px 0 0; line-height: 1.5; }
.op-hint.err { color: hsl(var(--destructive)); }
/* Success needs to be distinguishable from failure at a glance — and an
   invite link shown here may be the only copy, so it must be selectable. */
.op-hint.ok { color: hsl(142 55% 40%); user-select: all; word-break: break-all; }

/* --- Admin: clients, people, groups --- */

.admin-list { display: flex; flex-direction: column; gap: 8px; }

.admin-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid hsl(var(--border));
  border-radius: 10px;
  background: hsl(var(--background));
  font-size: 13px;
}

.admin-row-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }

/* An agent's name in the members list is the way into its chat, so it reads as
   a label and only reveals itself as a control on hover — same reasoning as the
   fleet table's names. */
.member-open {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.member-open:hover, .member-open:focus-visible { text-decoration: underline; }
.admin-meta {
  color: hsl(var(--muted-foreground));
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tag {
  font-size: 11px;
  padding: 2px 8px;
  border-radius: 999px;
  background: hsl(var(--muted));
  color: hsl(var(--muted-foreground));
  white-space: nowrap;
}
.tag-pending  { background: hsl(38 92% 50% / 0.15); color: hsl(38 92% 38%); }
.tag-accepted { background: hsl(142 55% 40% / 0.15); color: hsl(142 55% 34%); }
.tag-revoked,
.tag-expired  { background: hsl(var(--destructive) / 0.12); color: hsl(var(--destructive)); }

.link-btn.danger { color: hsl(var(--destructive)); font-size: 12px; }

/* <details> keeps the forms out of the way until wanted — three stacked forms
   would otherwise dominate the panel. */
.admin-add { margin-top: 10px; }
.admin-add > summary {
  cursor: pointer;
  font-size: 13px;
  color: hsl(var(--primary));
  padding: 4px 0;
  user-select: none;
}
.admin-add[open] > summary { margin-bottom: 6px; }

.group-card {
  border: 1px solid hsl(var(--border));
  border-radius: 10px;
  background: hsl(var(--background));
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.group-card .admin-row { border: 0; padding: 0; background: none; }

.policy-row { display: flex; align-items: center; gap: 10px; font-size: 13px; }
/* An agent card carries four controls on this row, which on a phone runs off
   the side of the card. Only the rows that need it wrap — a policy row that is
   one label and one control reads better held on a single line. */
.policy-row.wrap-row { flex-wrap: wrap; row-gap: 8px; }
.policy-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: hsl(var(--muted-foreground));
  white-space: nowrap;
}

.policy-select {
  flex: 1;
  min-width: 0;
  padding: 7px 9px;
  border: 1px solid hsl(var(--border));
  border-radius: 8px;
  background: hsl(var(--card));
  color: hsl(var(--foreground));
  font: inherit;
  font-size: 12px;
}
/* A brief confirmation, because the change saves on select with no button. */
.policy-select.saved { border-color: hsl(142 55% 40%); }

.policy-obligations { display: flex; flex-direction: column; gap: 6px; }

.check-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  cursor: pointer;
  line-height: 1.4;
}
.check-row input { flex: none; margin: 0; accent-color: hsl(var(--primary)); }

.req-list { display: flex; flex-direction: column; gap: 10px; }
.req-empty { color: hsl(var(--muted-foreground)); font-size: 13px; margin: 0; }

.req {
  border: 1px solid hsl(var(--border));
  border-radius: 10px;
  padding: 12px;
  background: hsl(var(--background));
  font-size: 13px;
}

/* A pending prod action is the one thing in here that should catch the eye. */
.req.pending { border-color: hsl(var(--primary) / 0.55); }
.req.pending.prod { border-color: hsl(var(--destructive) / 0.6); }

.req-top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; }
.req-op { font-weight: 600; }

.tag {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid hsl(var(--border));
  color: hsl(var(--muted-foreground));
}

.tag.prod { background: hsl(var(--destructive) / 0.14); color: hsl(var(--destructive)); border-color: transparent; }
.tag.dev { background: hsl(var(--muted)); }
.tag.done { background: hsl(140 60% 40% / 0.16); color: hsl(140 50% 34%); border-color: transparent; }
.tag.failed, .tag.denied { background: hsl(var(--destructive) / 0.14); color: hsl(var(--destructive)); border-color: transparent; }
.tag.pending { background: hsl(var(--primary) / 0.16); color: hsl(var(--primary-strong)); border-color: transparent; }

.req-meta { color: hsl(var(--muted-foreground)); font-size: 12px; }

.req-actions { display: flex; gap: 8px; margin-top: 10px; }

.req-actions button {
  flex: 1;
  border-radius: 8px;
  padding: 9px;
  font: inherit;
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
  border: 1px solid hsl(var(--border));
  background: hsl(var(--card));
  color: hsl(var(--foreground));
  min-height: 40px;
}

.req-actions .approve {
  background: hsl(var(--primary));
  color: hsl(var(--primary-foreground));
  border-color: transparent;
}

.req-actions button:disabled { opacity: 0.5; cursor: default; }

.req-out {
  margin: 10px 0 0;
  background: hsl(var(--muted));
  border-radius: 8px;
  padding: 9px;
  font-family: var(--mono);
  font-size: 11.5px;
  max-height: 200px;
  overflow: auto;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 16px;
  height: 16px;
  border-radius: 999px;
  background: hsl(var(--primary));
  color: hsl(var(--primary-foreground));
  font-size: 10px;
  font-weight: 700;
  display: grid;
  place-items: center;
  padding: 0 4px;
}

#console-btn { position: relative; overflow: visible; }

.mobile-only { display: none; }

/* Dims the page behind the drawer and gives a tap-anywhere-to-close target. */
.scrim {
  position: fixed;
  inset: 0;
  z-index: 19;
  background: hsl(0 0% 0% / 0.45);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.scrim.open { opacity: 1; pointer-events: auto; }

@media (max-width: 860px) {
  .mobile-only { display: grid; place-items: center; }

  /* Conversation rows: a touch target, not a desktop list. Avatar 40→48,
     name 14→16, second line 11→12.5, more padding — "increase a bit" (Jon,
     2026-09-18), not a redesign, so proportions stay as they were.

     Every size here multiplies by --list-scale. This block sits AFTER the
     base rules and so overrode them: the - / + moved the preview line but
     left the NAME alone at any window under 860px, which is the width the
     sidebar is actually used at. */
  .conv-item {
    padding: calc(12px * var(--list-scale, 1)) 12px;
    gap: calc(12px * var(--list-scale, 1));
    font-size: calc(15px * var(--list-scale, 1));
    border-radius: 10px;
    margin-bottom: 3px;
  }
  .conv-avatar {
    width: calc(48px * var(--list-scale, 1));
    height: calc(48px * var(--list-scale, 1));
    font-size: calc(15px * var(--list-scale, 1));
  }
  .conv-avatar.group { border-radius: 14px; }
  .conv-avatar.emoji { font-size: calc(25px * var(--list-scale, 1)); }
  .conv-kind { width: calc(19px * var(--list-scale, 1)); height: calc(19px * var(--list-scale, 1)); }
  .conv-name { font-size: calc(16px * var(--list-scale, 1)); }
  .conv-sub { font-size: calc(12.5px * var(--list-scale, 1)); }
  .conv-time { font-size: calc(12px * var(--list-scale, 1)); }
  .badge { min-width: 18px; height: 18px; font-size: 11px; }

  /* Footer: the address and build stamp are desk information. On a phone the
     row is the theme toggle and Sign out, and the height goes to the list. */
  .sidebar-foot .who, .sidebar-foot .build-stamp { display: none; }
  .sidebar-foot { padding: 8px 14px calc(8px + env(safe-area-inset-bottom)); }

  /* Jump to latest — floats over the transcript, above the composer. */
  .main { position: relative; }
  .jump-latest {
    position: absolute;
    right: 14px;
    bottom: 74px;
    z-index: 5;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 1px solid hsl(var(--border));
    background: hsl(var(--card));
    color: hsl(var(--primary));
    font-size: 20px;
    line-height: 1;
    box-shadow: 0 4px 14px hsl(0 0% 0% / 0.25);
    cursor: pointer;
  }
  .jump-latest[hidden] { display: none; }
  /* Draggable: the finger moves it, so the page must not scroll under it. */
  .jump-latest { touch-action: none; user-select: none; -webkit-user-select: none; }
  .jump-latest.dragging { transform: scale(1.12); box-shadow: 0 8px 24px hsl(0 0% 0% / 0.35); }

  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 20;
    width: min(84vw, 300px);
    flex-basis: auto;
    transform: translateX(-100%);
    transition: transform 0.22s ease;
    box-shadow: 0 0 40px hsl(0 0% 0% / 0.3);
    /* Clear the notch and the home indicator. */
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
  }

  .sidebar.open { transform: none; }

  /* The wordmark shares its line with the camera cutout on an iPhone, and hard
     against the left edge it reads as pushed into the corner rather than
     placed. Nudged in so it sits in the gap beside the cutout instead of
     starting at the very edge of it. Only the logo moves — the buttons are
     held to the right by `space-between`. */
  .sidebar-logo { margin-left: 10px; }

  .topbar { padding: 11px 14px; padding-top: calc(11px + env(safe-area-inset-top)); }
  .conv-title { font-size: 15px; }

  /* ---- Phone typography, sized against Telegram ----
     The desktop 15px body is a reading size for a screen at arm's length. On
     a phone held one-handed, often in motion, it is too small to skim - which
     is the complaint. Telegram's iOS message text is 17px and its chat list
     titles 17px with a 15px preview, and that is the target here: the same
     text, bigger, rather than a different layout. Everything in this block is
     a size, so nothing below can be broken by changing one. */
  /* --msg-scale is the same step as --list-scale, set from the user's saved
     size. Padding and the gap scale too: at the larger steps the text grew
     but the space between messages did not, so they ran together. */
  /* Sides: a smaller avatar and a tighter left edge give the text back the
     width the avatar column took. The right padding is the text's LEFT inset
     (10px edge + 26px avatar + the gap), so the text sits the same distance
     from both edges of the screen at every text size (Jon, 2026-09-24: "at
     least aligned on both sides"; 20px on the right still read as too tight). */
  .msg {
    padding: calc(16px * var(--msg-scale, 1)) calc(36px + 8px * var(--msg-scale, 1))
             calc(16px * var(--msg-scale, 1)) 10px;
    gap: calc(8px * var(--msg-scale, 1));
  }
  .msg .body { font-size: calc(17px * var(--msg-scale, 1)); line-height: 1.5; }
  .msg .body pre { font-size: calc(14px * var(--msg-scale, 1)); padding: 11px; max-width: 100%; }
  .msg .body code { font-size: calc(15px * var(--msg-scale, 1)); }
  .msg-sender { font-size: calc(14px * var(--msg-scale, 1)); }
  .msg-time { font-size: calc(13px * var(--msg-scale, 1)); }
  /* The badge grows with the text beside it, or it turns into a full stop. */
  .msg .role { flex: 0 0 26px; width: 26px; height: 26px; font-size: 10px; }
  .msg.assistant .role { font-size: 13px; }
  .role.emoji { font-size: calc(15px * var(--msg-scale, 1)); }

  /* Chat list: Telegram's proportions - a title you can read at a glance and
     a preview big enough to be worth reading at all. */
  /* --list-scale is set on <html> from the user's saved preference. The
     avatar scales with the text: growing the names alone leaves a row whose
     picture no longer matches it, and the row reads as broken rather than
     bigger. */
  .conv-avatar {
    width: calc(54px * var(--list-scale, 1));
    height: calc(54px * var(--list-scale, 1));
    font-size: calc(17px * var(--list-scale, 1));
    flex-basis: calc(54px * var(--list-scale, 1));
  }
  .conv-avatar.emoji { font-size: calc(28px * var(--list-scale, 1)); }
  .conv-name { font-size: calc(17px * var(--list-scale, 1)); font-weight: 600; }
  .conv-sub { font-size: calc(14.5px * var(--list-scale, 1)); }
  .conv-time { font-size: calc(13px * var(--list-scale, 1)); }
  .conv-kind { width: 21px; height: 21px; }
  .pin-who { font-size: 13px; }
  .pin-text { font-size: 14.5px; }
  .pin-count { font-size: 12.5px; }
  .badge { min-width: 20px; height: 20px; font-size: 12px; }
  .doc-name { font-size: 13px; }
  .doc-chip { font-size: 13px; padding: 5px 10px; }

  .composer {
    padding: 10px 12px;
    padding-bottom: calc(10px + env(safe-area-inset-bottom));
    gap: 8px;
  }

  /* The bottom inset reserves room for the gesture bar. The keyboard covers
     that bar, so while it is up the reserved strip is a band of dead space
     between the composer and the keys. data-kb is set from the focused element
     in app.js; see the note there for why focus and not viewport height. */
  html[data-kb='open'] .composer {
    padding-bottom: 10px;
  }

  /*
   * 16px minimum on EVERY focusable control, not just the composer.
   *
   * iOS Safari zooms the page in when a control smaller than 16px takes
   * focus, and it never zooms back out: the whole app is left oversized and
   * scrolled sideways, which is exactly what tapping the sidebar's 🔍 did -
   * that input was 13px. The composer had the rule and nothing else did, so
   * the bug was one tap away in every other field on the page. A blanket
   * rule is the only version of this that cannot be missed again.
   *
   * Do not "tidy" any of this back to the desktop sizes.
   */
  input, select, textarea { font-size: 16px; }

  .composer textarea,
  .login-card input {
    font-size: 16px;
    max-height: 50vh;
  }

  /* Only the submit button gets the filled treatment. Without this selector
     the attach button inherits it too and the composer shows two identical
     orange squares with different meanings. */
  .composer button[type='submit'] { width: 44px; height: 44px; flex-basis: 44px; }
  .composer #attach-btn,
  .composer #paste-btn,
  .composer #autoplay-btn,
  .composer #clear-btn,
  .composer #secret-btn,
  .composer #mic-btn { width: 44px; height: 44px; flex-basis: 44px; font-size: 20px; }

  /* The tray must share the composer's horizontal padding, or thumbnails sit
     inset from the controls below them. */
  .tray { padding: 10px 12px 0; gap: 8px; }

  .thumb { width: 60px; height: 60px; }
  .thumb.doc { width: auto; min-width: 116px; max-width: 62vw; height: auto; min-height: 60px; }
  .doc-name { font-size: 11.5px; }

  /* Cap image height so one tall screenshot does not fill the whole screen
     and push the conversation out of view. */
  .msg-images img { max-width: 100%; max-height: 200px; }
  .doc-chip { font-size: 11.5px; padding: 4px 9px; }

  /* Full-bleed viewer with room for a thumb to dismiss. */
  .lightbox { padding: 12px; }

  /* Centred, not top-pinned: the card is short enough to sit in the middle on
     any modern phone, and flush to the top reads like a rendering fault.
     align-items is restored to centre rather than left at flex-start, and the
     safe-area inset keeps it off the notch on a wrapped (Capacitor) build.

     The keyboard case that top-pinning was guarding is handled below, where a
     genuinely short viewport falls back to top alignment — so a keyboard can
     still never strand the submit button off-screen. */
  /* Centring is the card's `margin: auto`, not align-items — see the base
     rule. Only the padding changes here. */
  .login-screen {
    padding: 16px;
    padding-top: max(16px, env(safe-area-inset-top));
  }
  .login-card { padding: 32px 22px; }
  .login-logo { width: 132px; }

  .empty { margin-top: 8vh; }
  .empty-logo { width: 112px; }

  /* Conversation rows: comfortable thumb targets. Delete is revealed by
     swiping the row left — see the (hover: none) block above. */
  .conv-item { padding: 12px 10px; }

  /* No draggable boundary on a phone: the list and the thread are separate
     views, and the sidebar spans the full width in list view. */
  .resizer { display: none; }
  body[data-view="list"] .sidebar { width: 100%; flex-basis: auto; }

  /* flex-wrap was here and did nothing — the footer is a grid. */
  .sidebar-foot { padding: 12px 14px; gap: 6px 12px; }

  /*
   * List-first. The phone shows ONE pane at a time: the chat list, or the
   * thread you tapped — the shape every messaging app has.
   *
   * The old model made the thread the landing view with the list as an
   * off-canvas drawer, so the app opened on an empty chat window and the list
   * of conversations was behind a hamburger. Inverted here.
   *
   * The drawer rules above are left in place rather than deleted: they are
   * harmless once .sidebar is statically positioned, and removing them risks
   * the cascade in ways this layout does not need to take on.
   */
  body[data-view="list"] .sidebar {
    position: static;
    transform: none;
    width: 100%;
    flex-basis: auto;
    box-shadow: none;
  }
  body[data-view="list"] .main,
  body[data-view="list"] .scrim { display: none; }

  body[data-view="thread"] .sidebar { display: none; }
  body[data-view="thread"] .main { display: flex; }

  /*
   * The chat's NAME is the most important thing in this bar, and it was
   * losing to a row of six icons - "CODEX LOCAL..." with the bot unreadable.
   * The tools fold away here and come back on demand: ⋯ keeps the action
   * sheet, and + unfolds a second row holding the tools (find, the text-size
   * pair, brain, catch-up, fresh, members, activity, console — Jon
   * 2026-09-26: "another plus or something that shows the other options …
   * like show more show less buttons"). They stay in the DOM and keep their
   * handlers; unfolding only changes their layout box.
   */
  #find-btn,
  #mute-btn,
  .chat-text-size,
  #brain-btn,
  #catchup-btn,
  #fresh-btn,
  #activity-btn,
  #members-btn,
  #console-btn { display: none !important; }
  #more-btn { display: grid; place-items: center; }
  #unfold-btn { display: grid; place-items: center; font-size: 17px; }
  /* The title takes what the three remaining controls do not. */
  .conv-heading { min-width: 0; flex: 1 1 auto; }

  /* The unfolded tools row. The bar's own flex-wrap is refused above, so the
     unfold re-enables it, and the bar's own ::after — a real flex item — is
     the full-width line break between the name row and the tools: everything
     default-ordered stays up top, the break comes next, the tools (order 2)
     land on one clean second line regardless of how wide the name is. */
  .topbar.unfolded { flex-wrap: wrap !important; row-gap: 4px; }
  .topbar.unfolded::after {
    content: '';
    flex-basis: 100%;
    height: 0;
    order: 1;
  }
  .topbar.unfolded #find-btn,
  .topbar.unfolded .chat-text-size,
  .topbar.unfolded #brain-btn,
  .topbar.unfolded #catchup-btn,
  .topbar.unfolded #fresh-btn,
  .topbar.unfolded #activity-btn,
  .topbar.unfolded #members-btn,
  .topbar.unfolded #console-btn {
    display: grid !important;
    place-items: center;
    order: 2;
  }
  .topbar.unfolded .chat-text-size { display: inline-flex !important; }
  /* Unfolded, the ⋯ sheet is the same nine buttons as this row — it only
     repeated what was now visible, and its button wrapped onto a middle line
     of its own (three rows, Jon 2026-09-26: "supposed to be two lines max").
     It steps aside unfolded; 🔈 joins the tools so nothing is lost. */
  .topbar.unfolded #more-btn { display: none !important; }
  /* The × joins the tools row and comes last on it (Jon 2026-09-26: "the x
     should always be the last one") — one extra line, not two. */
  .topbar.unfolded #unfold-btn { order: 3; }
  /* The two-line clamp belongs to the folded bar; unfolded, the name owns
     its full two lines above the tools row without a hard cap fighting the
     wrap. */
  .topbar.unfolded .conv-heading { max-height: none; }

  /* The hamburger belonged to the drawer; back navigation replaces it. */
  body[data-view="thread"] #menu-btn { display: none; }
  #back-btn { display: grid; place-items: center; }

  /* 48, not 44: the list rows were enlarged the same day (788b2e6) and this
     line, later in the block, was quietly winning over that. */
  .conv-avatar { flex-basis: 48px; width: 48px; height: 48px; }

  /* Conversation header on a phone: ONE row. Back arrow, title (ellipsis),
     then the tools pushed to the right at 30px each. The subtitle
     ("Connected · Telegram linked") is desk information and goes; every pixel
     of height here is a pixel of transcript. Wrapping is refused outright so
     no later rule can grow this back into two rows. */
  .topbar {
    flex-wrap: nowrap !important;
    gap: 5px;
    padding: 7px 10px;
    padding-top: calc(7px + env(safe-area-inset-top));
    min-height: 0;
  }
  .topbar .conv-heading { flex: 1 1 auto; min-width: 0; padding: 0 4px; margin-left: 0; }
  /* Up to two lines, not one ellipsised line: with the header buttons beside
     it a bot's full name ("🥶CLAUDE ANSIBLE 3🥶 · Opus 5.5 MAX") was cut to
     its first word or two, which is the part that tells you least. */
  .topbar .conv-title {
    font-size: 15px;
    line-height: 1.22;
    white-space: normal;
    overflow-wrap: break-word;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
  }
  .topbar .conv-subtitle { display: none; }
  /* The model and fleet-number tags follow the name in the same run of text
     rather than on a line of their own (Jon, 2026-09-25), so a name that
     wraps carries its tags with it and the header stays two lines at most. */
  .topbar .conv-heading { display: block; max-height: calc(2 * 1.22 * 15px + 4px); overflow: hidden; }
  .topbar .conv-title-row,
  .topbar .conv-meta-row { display: inline; margin: 0; }
  .topbar .conv-title { display: inline; -webkit-line-clamp: unset; line-clamp: none; }
  /* vertical-align: text-top put the tags' tops on the text's cap line while
     their pill boxes sit lower — next to a short one-line name they read as
     hanging from the top (the #45 / Coding plan look, Jon 2026-09-26).
     middle hangs them on the x-height midpoint of the surrounding run, so a
     one-line header centres them against the name instead. */
  .topbar .conv-meta-row .model-tag { display: inline-block; vertical-align: middle; margin-left: 5px; font-size: 10px; line-height: 1.4; }
  /* Inline-block plus the meta row's own line-height rode the dot below the
   text baseline, so it looked bottom-hung next to the #41 tag. Line-height 0
   stops the glyph box stretching the run; the dot now centres on the tags. */
  .topbar .conv-meta-row .conv-light { display: inline-block; vertical-align: middle; margin-left: 7px; line-height: 0; }
  .topbar .ghost-btn { width: 30px; height: 30px; flex: 0 0 30px; font-size: 15px; border-radius: 7px; }
  .topbar #back-btn { width: 30px; height: 30px; flex-basis: 30px; }
  .topbar .pill { font-size: 10px; padding: 2px 6px; }
  .topbar .badge { top: -5px; right: -5px; min-width: 15px; height: 15px; font-size: 9.5px; }

  /* No fleet strip inside a conversation on a phone. Jon, 2026-09-18: "the
     fleet waiting can go away entirely when in a message or group chat". On a
     phone the thread is the only place the strip could show (the list view
     hides .main), so this removes it from phones altogether; the fold-by-
     default from earlier stays for tablets between 700 and 860px. Desktop
     keeps the strip. */
  body[data-view="thread"] #fleet-bar { display: none !important; }
}

/* Desktop keeps both panes, so the back arrow has nothing to go back to. */
@media (min-width: 861px) {
  .jump-latest { display: none !important; }
  #back-btn { display: none; }
}

/* Short landscape phones: reclaim the vertical space the splash wastes. The
   card's auto margins already handle the alignment. */
@media (max-height: 500px) and (orientation: landscape) {
  .login-screen { padding-top: 12px; }
  .login-card { padding: 20px 22px; }
  .login-logo { width: 108px; margin-bottom: 2px; }
  .login-sub { margin: 6px 0 14px; }
  .empty { margin-top: 3vh; }
  .empty-logo { width: 88px; margin-bottom: 10px; }
}


/* The product name under the wordmark on the login card. */
.brand-lockup {
  text-align: center;
  margin: 2px 0 0;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: hsl(var(--muted-foreground));
}

/* Attachment chips are links to the stored file, so they need to look
   clickable rather than like inert labels. */
.doc-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: 100%;
  padding: 7px 11px;
  border: 1px solid hsl(var(--border));
  border-radius: 10px;
  background: hsl(var(--background));
  color: inherit;
  text-decoration: none;
  font-size: 12.5px;
}

.doc-chip:hover {
  border-color: hsl(var(--primary));
  background: hsl(var(--primary) / 0.06);
}

.doc-chip-ext {
  flex: none;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 2px 6px;
  border-radius: 5px;
  background: hsl(var(--primary) / 0.15);
  color: hsl(var(--primary));
}

.doc-chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-chip-sum { flex: none; color: hsl(var(--muted-foreground)); font-size: 11.5px; }


/* --- Messaging: grouped sidebar, senders, people pickers --- */

.conv-section {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: hsl(var(--muted-foreground) / 0.75);
  padding: 12px 10px 5px;
}

/* Unread chip on, nothing unread: a clear answer at the top of the list. */
.conv-caught-up {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 8px 10px 10px;
  padding: 12px 14px;
  border: 1px solid hsl(142 50% 40% / 0.45);
  border-radius: 10px;
  background: hsl(142 50% 40% / 0.1);
  font-size: 13px;
  color: hsl(var(--muted-foreground));
}
.conv-caught-up strong { color: hsl(142 45% 42%); font-size: 14px; }

/* "+ N more" under the capped Recently read section. */
.conv-more { display: block; padding: 6px 10px 10px; font-size: 13px; font-weight: 600; }

.conv-empty {
  color: hsl(var(--muted-foreground));
  font-size: 13px;
  padding: 14px 10px;
  margin: 0;
}

.conv-label { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.conv-name {
  /* Wraps to at most two rows: when the list text is scaled up a one-line
     name collapsed to "CLAUD…" and you could no longer tell chats apart. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
  /* The row is muted so its second line recedes; the NAME is what you scan
     for, and inheriting that muting made the whole list one grey wash. */
  color: hsl(var(--foreground));
  font-weight: 600;
  font-size: calc(14px * var(--list-scale, 1));
  letter-spacing: -0.01em;
}
.conv-item.unread .conv-name { font-weight: 700; }

.conv-sub {
  font-size: calc(11px * var(--list-scale, 1));
  color: hsl(var(--muted-foreground));
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Another person's message: a distinct badge, so a group transcript reads as
   a conversation between people rather than a single voice. */
.role.other {
  background: hsl(var(--primary) / 0.16);
  color: hsl(var(--primary));
  font-size: 11px;
  font-weight: 700;
}

/*
 * An agent's badge. The hue is set inline per agent (stored on the identity,
 * not derived in the client) so several agents in one group stay visually
 * distinct — the same trick Telegram uses for group participants.
 */
.role.agent,
.role.person {
  border-radius: 50%;
  color: #fff;
  background: hsl(var(--primary));
  letter-spacing: 0.02em;
}

/* Who spoke, above the text. Groups only — see addMessage().
 *
 * Coloured from --who-h, the per-speaker hue set on the row. Every sender used
 * to render in the one accent colour, so a transcript with four voices in it
 * looked like one voice with four paragraph breaks. */
.people-search {
  margin-bottom: 6px;
}

.tag + .tag { margin-left: 4px; }

.fleet-bar {
  border-bottom: 1px solid hsl(var(--border));
  background: hsl(var(--card));
  padding: 8px 20px;
}

.fleet-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 6px;
}

.fleet-table th[data-sort] { cursor: pointer; user-select: none; }
.fleet-table th[data-sort]:hover { color: hsl(var(--foreground)); }
.fleet-table th.sorted { color: hsl(var(--foreground)); }
.fleet-table th.sorted::after { content: ' \25B4'; }
.fleet-table th.sorted[data-dir="desc"]::after { content: ' \25BE'; }

.fleet-detail { margin-top: 8px; }

.fleet-stamp {
  font-weight: 400;
  font-size: 11.5px;
  color: hsl(var(--muted-foreground));
  margin-left: 6px;
}

.fleet-fold {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
  padding: 8px 10px;
  border: 1px solid hsl(var(--border, 0 0% 85%));
  border-radius: 8px;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.fleet-fold[hidden] { display: none; }
.fleet-fold-chev { color: hsl(var(--muted-foreground)); font-size: 14px; }
.fleet-tiles[hidden] { display: none; }

.fleet-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
  gap: 8px;
  margin-bottom: 10px;
}

.fleet-tile {
  border: 1px solid hsl(var(--border, 0 0% 85%));
  border-radius: 8px;
  padding: 8px 10px;
  background: transparent;
  color: inherit;
  text-align: left;
  font: inherit;
  cursor: pointer;
}

.fleet-tile:hover { border-color: hsl(var(--primary, 210 90% 55%) / 0.6); }
.fleet-tile[aria-pressed="true"] {
  border-color: hsl(var(--primary, 210 90% 55%));
  box-shadow: inset 0 0 0 1px hsl(var(--primary, 210 90% 55%) / 0.35);
}

.fleet-tile-value { font-size: 19px; font-weight: 700; font-variant-numeric: tabular-nums; }
.fleet-tile-label { font-size: 11px; color: hsl(var(--muted-foreground)); margin-top: 1px; }
.fleet-tile.good .fleet-tile-value { color: hsl(140 55% 40%); }
.fleet-tile.warn .fleet-tile-value { color: hsl(38 85% 45%); }
.fleet-tile.bad  .fleet-tile-value { color: hsl(0 70% 50%); }

.fleet-waiting { margin-bottom: 10px; display: grid; gap: 4px; }

.fleet-waiting-row {
  display: flex;
  gap: 8px;
  align-items: baseline;
  font-size: 12px;
  padding: 5px 8px;
  border-radius: 6px;
  background: hsl(0 70% 50% / 0.08);
}

.fleet-waiting-age { font-weight: 700; font-variant-numeric: tabular-nums; color: hsl(0 70% 45%); }
.fleet-waiting-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fleet-waiting-who { color: hsl(var(--muted-foreground)); }

/* The Not-mirrored lens. Reuses the waiting rows' shape — same list, different
   question — with a heading between the two directions, because "reaching us
   and dropped" and "here with no mirror" must not read as one pile. */
.fleet-unmirrored { display: grid; gap: 4px; padding-bottom: 4px; }
.fleet-section-head {
  margin-top: 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: hsl(var(--muted-foreground));
}
.fleet-section-head:first-child { margin-top: 0; }
.fleet-note { font-size: 12px; margin: 2px 0 4px; line-height: 1.45; max-width: 62ch; }
/* In this list the count is neutral — it is how chatty the chat is, not how
   late an answer is — so it must not wear the waiting rows' red. */
.fleet-unmirrored .fleet-waiting-age {
  color: hsl(var(--muted-foreground));
  min-width: 34px;
}
.fleet-unmirrored .fleet-open { flex: 1; min-width: 0; }

/* The table is wider than a phone; let it scroll rather than crushing it. */
/* Scrolls in both directions now. The height is the draggable one — see
   .fleet-resizer — and 15 bots in a 260px box is exactly the case that needs
   it: the table scrolls, the tiles and the foot stay put. */
.fleet-scroll { overflow: auto; max-height: var(--fleet-h, 260px); }

/* The handle between the fleet panel and the chat under it. Same idea as
   .resizer, turned ninety degrees: a thin strip that only shows itself when
   you are near it, so it is not a permanent line across the app. */
.fleet-resizer {
  height: 7px;
  margin: 4px -20px -8px;
  cursor: row-resize;
  background: transparent;
  position: relative;
}
.fleet-resizer::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 2px;
  width: 46px;
  height: 3px;
  border-radius: 2px;
  transform: translateX(-50%);
  background: hsl(var(--border));
}
.fleet-resizer:hover,
.fleet-resizer:focus-visible,
body.resizing-fleet .fleet-resizer { background: hsl(var(--primary) / 0.35); }
.fleet-resizer:focus-visible { outline: none; }
/* While dragging, nothing else should react to the pointer crossing it. */
body.resizing-fleet { cursor: row-resize; user-select: none; }
body.resizing-fleet * { pointer-events: none; }
body.resizing-fleet .fleet-resizer { pointer-events: auto; }
/* On a phone the fleet panel is already hidden; nothing to resize. */
@media (max-width: 700px) {
  .fleet-resizer { display: none; }
}

.fleet-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.fleet-table th {
  text-align: left;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: hsl(var(--muted-foreground));
  padding: 4px 8px 4px 0;
  white-space: nowrap;
}
.fleet-table td { padding: 5px 8px 5px 0; border-top: 1px solid hsl(var(--border, 0 0% 85%) / 0.6); white-space: nowrap; }
.fleet-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.fleet-table .name { max-width: 210px; overflow: hidden; text-overflow: ellipsis; }
.fleet-table .ok { color: hsl(140 55% 38%); }
.fleet-table .bad { color: hsl(0 70% 50%); }
.fleet-table .muted { color: hsl(var(--muted-foreground)); }

/* No margin of its own: the client pills' bottom padding plus the search's
   top padding already make the gap. The 10px margin stacked on both left
   24px of empty band under the pills (Jon, 2026-09-25). */
.admin-view {
  margin-bottom: 0;
}

.admin-view > summary {
  cursor: pointer;
  list-style: none;
  user-select: none;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: hsl(var(--muted-foreground));
  padding: 6px 2px;
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Safari draws its own triangle through ::-webkit-details-marker. */
.admin-view > summary::-webkit-details-marker { display: none; }

.admin-view > summary::before {
  content: '\25B8';
  font-size: 10px;
  transition: transform 0.12s ease;
}

.admin-view[open] > summary::before { transform: rotate(90deg); }

.admin-view > summary:hover { color: hsl(var(--foreground)); }

.admin-view > summary:focus-visible {
  outline: 2px solid hsl(var(--ring, 210 90% 55%));
  outline-offset: 2px;
  border-radius: 4px;
}

.msg-head {
  display: flex;
  /* The name comes first and keeps its full width: when it does not fit
     beside the model tag and time, those wrap to the line below instead of
     cutting the name to "R..." (Jon, 2026-09-25). */
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 10px;
  row-gap: 2px;
  margin-bottom: 3px;
}

.msg-sender {
  font-size: 12.5px;
  font-weight: 700;
  color: hsl(var(--who-h, 210) 55% 38%);
  letter-spacing: -0.01em;
  /* Never shrunk for the tag or time; only a name longer than the whole
     line (a long email) is cut, and then with an ellipsis. */
  flex: 0 0 auto;
  max-width: 100%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Right-aligned whether or not a name sits beside it. */
.msg-time {
  margin-left: auto;
  flex: none;
  font-size: 11.5px;
  color: hsl(var(--muted-foreground));
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  opacity: 0.8;
}

/* A stripe down the edge in the speaker's colour. Cheaper to scan than a name:
   you can see who is talking without reading anything. */
.msg[style*="--who-h"] { box-shadow: inset 3px 0 0 hsl(var(--who-h) 60% 50%), inset -3px 0 0 hsl(var(--who-h) 60% 50%); }
/* Your own messages carry the stripe too (both edges), in a fixed dark blue, so
   every row in a thread is edged and yours are still told apart at a glance. */
.msg.user:not(.from-other) { box-shadow: inset 3px 0 0 hsl(221 83% 40%), inset -3px 0 0 hsl(221 83% 40%); }

/* Your own messages, marked as yours. Without this the only difference between
   your line and someone else's was the word in the badge. */
.msg.user:not(.from-other) { background: hsl(var(--primary) / 0.05); }
.msg.user:not(.from-other) .role {
  background: hsl(var(--primary) / 0.18);
  color: hsl(var(--primary));
}

[data-theme="dark"] .msg-sender { color: hsl(var(--who-h, 210) 80% 72%); }
[data-theme="dark"] .msg[style*="--who-h"] { box-shadow: inset 3px 0 0 hsl(var(--who-h) 65% 58%), inset -3px 0 0 hsl(var(--who-h) 65% 58%); }
[data-theme="dark"] .msg.user:not(.from-other) { box-shadow: inset 3px 0 0 hsl(221 83% 53%), inset -3px 0 0 hsl(221 83% 53%); }
[data-theme="dark"] .msg.user:not(.from-other) { background: hsl(var(--primary) / 0.09); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .msg-sender { color: hsl(var(--who-h, 210) 80% 72%); }
  :root:not([data-theme="light"]) .msg[style*="--who-h"] {
    box-shadow: inset 3px 0 0 hsl(var(--who-h) 65% 58%), inset -3px 0 0 hsl(var(--who-h) 65% 58%);
  }
  :root:not([data-theme="light"]) .msg.user:not(.from-other) {
    box-shadow: inset 3px 0 0 hsl(221 83% 53%), inset -3px 0 0 hsl(221 83% 53%);
  }
  :root:not([data-theme="light"]) .msg.user:not(.from-other) { background: hsl(var(--primary) / 0.09); }
}

/* --- Agent roster (admin console) --- */

/* Connected or not. A bot that never polls cannot answer, and without this
   that is indistinguishable from the app being broken. */
.bot-dot {
  flex: 0 0 8px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-top: 7px;
  background: hsl(var(--muted-foreground) / 0.5);
}
.bot-dot.on { background: hsl(142 70% 42%); }

.bot-token-box {
  margin-top: 10px;
  padding: 12px;
  border: 1px solid hsl(var(--primary) / 0.35);
  border-radius: 10px;
  background: hsl(var(--primary) / 0.06);
}
.bot-token-warn {
  margin: 0 0 8px;
  font-size: 12px;
  font-weight: 600;
  color: hsl(var(--primary));
}
.bot-token-snippet {
  margin: 0 0 8px;
  padding: 10px;
  border-radius: 8px;
  background: hsl(var(--background));
  border: 1px solid hsl(var(--border));
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  /* The token is long and must not be truncated — it is shown exactly once. */
  white-space: pre-wrap;
  word-break: break-all;
  /* Prefixed first: tapping to select the token is the fallback when the
     clipboard API is unavailable, and Safari is where that happens. */
  -webkit-user-select: all;
  user-select: all;
}

.people-picker {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 260px;
  overflow-y: auto;
  /* Never sideways: a long address must ellipse, not push the row out and
     add a horizontal scrollbar to the dialog. */
  overflow-x: hidden;
  min-width: 0;
  border: 1px solid hsl(var(--border));
  border-radius: 10px;
  padding: 8px 10px;
  background: hsl(var(--background));
}

.people-picker .check-row {
  padding: 6px 2px;
  gap: 10px;
  min-width: 0;
}

/* The email takes the free space, so the checkbox stays beside it and the
   tag sits at the right edge. Without this the span shrank to its content
   and margin-left:auto on the tag pushed the whole row over. */
.people-picker .person-email {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.people-picker .admin-meta { flex: none; font-size: 11px; }
.people-picker .tag { flex: none; }


/* --- Pasted address lists --- */

/* A textarea in the admin forms needs the same treatment as the inputs; the
   :not() on the input rule does not reach it. */
.op-form textarea {
  background: hsl(var(--background));
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
  padding: 10px 12px;
  color: hsl(var(--foreground));
  font: inherit;
  font-size: 16px; /* stops iOS zooming the page on focus */
  width: 100%;
  resize: vertical;
  min-height: 44px;
}

.op-form textarea:focus {
  outline: none;
  border-color: hsl(var(--ring));
  box-shadow: 0 0 0 3px hsl(var(--ring) / 0.16);
}

/* What the parser found, so it is clear before sending which addresses were
   picked out of the pasted text. */
.found-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
}

.found-count {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: hsl(var(--muted-foreground));
  /* Own line: with many addresses the count otherwise gets stranded mid-row. */
  flex-basis: 100%;
  margin-bottom: 2px;
}

.found-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 100%;
  padding: 3px 4px 3px 9px;
  border-radius: 999px;
  background: hsl(var(--primary) / 0.13);
  color: hsl(var(--primary));
  font-size: 12px;
}

.found-chip > span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The admin forms style every button as a full-width primary button, which
   turned these into large orange circles overlapping the address. Reset the
   inherited sizing explicitly rather than relying on specificity. */
.found-chips .found-x {
  flex: none;
  width: 16px;
  height: 16px;
  min-width: 16px;
  margin: 0;
  border: 0;
  border-radius: 50%;
  background: hsl(var(--primary) / 0.22);
  color: inherit;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  line-height: 16px;
  text-align: center;
  cursor: pointer;
  padding: 0;
}

.found-chips .found-x:hover { background: hsl(var(--destructive)); color: #fff; }

/* --- Emoji picker --- */

/* The trigger sits beside the field rather than inside it: an absolutely
   positioned button inside the input would need padding hacks that break
   when the placeholder is long. */
.name-row { display: flex; gap: 8px; align-items: stretch; }
.name-row input { flex: 1; min-width: 0; }

/* Inside an admin card the picker button would otherwise be as tall as the
   field it sits beside is short, and the row would stretch the card. The name
   takes a line to itself: it is the longest control on the card and sharing
   one with three others left it a few characters wide on a phone. */
.rename-row { margin: 6px 0 0; flex: 1 1 100%; }
.rename-row .emoji-btn { width: 38px; font-size: 16px; }

.emoji-btn {
  flex: none;
  width: 44px;
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
  background: hsl(var(--background));
  color: inherit;
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}

.emoji-btn:hover { border-color: hsl(var(--primary)); background: hsl(var(--primary) / 0.07); }
.emoji-btn[aria-expanded='true'] { border-color: hsl(var(--primary)); background: hsl(var(--primary) / 0.12); }

.emoji-panel {
  margin-top: 8px;
  border: 1px solid hsl(var(--border));
  border-radius: 12px;
  background: hsl(var(--card));
  box-shadow: 0 8px 28px hsl(0 0% 0% / 0.28);
  overflow: hidden;
}

.emoji-tabs {
  display: flex;
  gap: 2px;
  padding: 7px 8px;
  border-bottom: 1px solid hsl(var(--border));
  overflow-x: auto;
}

/* Overrides the .op-form button rule, which would otherwise make each tab a
   full-width orange gradient button. */
.emoji-tabs .emoji-tab {
  flex: none;
  margin: 0;
  padding: 5px 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: hsl(var(--muted-foreground));
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.emoji-tabs .emoji-tab:hover { background: hsl(var(--muted)); color: hsl(var(--foreground)); }
.emoji-tabs .emoji-tab.active { background: hsl(var(--primary) / 0.15); color: hsl(var(--primary)); }

.emoji-grid {
  display: grid;
  /* Fixed 9 columns rather than auto-fill: auto-fill packed as many as would
     fit and left a single emoji stranded on the last row. */
  grid-template-columns: repeat(9, 1fr);
  gap: 3px;
  padding: 9px;
  max-height: 190px;
  overflow-y: auto;
}

.emoji-grid .emoji-cell {
  margin: 0;
  padding: 0;
  height: 36px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: inherit;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}

.emoji-grid .emoji-cell:hover { background: hsl(var(--primary) / 0.15); }

/* Group names carry emoji, so the sidebar and header must not clip them. */
.conv-name, .conv-title { font-variant-emoji: emoji; }

/* --- Chat list rows (Telegram-shaped) --- */

/*
 * `.conv-item span { flex: 1 }` (further up) targets every span in the row and
 * stretched the avatar into an oval and the unread badge into a bar. These
 * three need their intrinsic size, so they opt out explicitly — the shared
 * rule cannot simply be dropped, because .conv-label still depends on it.
 */
.conv-item .conv-avatar,
.conv-item .conv-avatar-wrap,
.conv-item .conv-badge,
.conv-item .conv-members,
.conv-item .conv-time { flex: 0 0 auto; }

/* The GROUP / DIRECT pill is the one child allowed to give way when the row
   is tight. It repeats what the avatar's shape already says, while the name
   beside it is what you actually scan for — so at a larger text size the
   name takes the width and the pill yields it. */
.conv-item .conv-pill { flex: 0 1 auto; min-width: 0; overflow: hidden; }

/* The label is the only child that should absorb the leftover width. Without
   this the shared `.conv-item span { flex: 1 }` splits the row evenly between
   the avatar wrapper and the label, leaving the name ~97px in a 264px sidebar
   and ellipsising every title. */
.conv-item .conv-label { flex: 1 1 auto; min-width: 0; }

.conv-avatar {
  width: calc(40px * var(--list-scale, 1));
  height: calc(40px * var(--list-scale, 1));
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 13px;
  font-weight: 700;
  color: #fff;
  background: hsl(var(--primary));
  overflow: hidden;
}
/* A group reads as a rounded square, a person or agent as a circle — the
   shape difference is deliberately large enough to see without reading. */
.conv-avatar.group { border-radius: 12px; }

/* An emoji is doing the job initials did, so it is sized to fill the circle
   rather than sit in the middle of it at caption size. `font-variant-emoji`
   asks for the colour form: several of these have a monochrome text
   presentation that a browser will otherwise pick, and a flat grey glyph on a
   coloured disc is not what anyone meant by an emoji. */
.conv-avatar.emoji,
.agent-add-dot.emoji,
.role.emoji {
  font-variant-emoji: emoji;
}
/* The list avatars follow the list stepper; .role.emoji is a MESSAGE avatar
   and follows --msg-scale with the thread it sits in. */
.conv-avatar.emoji,
.agent-add-dot.emoji { font-size: calc(21px * var(--list-scale, 1)); }
.role.emoji { font-size: calc(21px * var(--msg-scale, 1)); }
.agent-add-dot.emoji { font-size: calc(15px * var(--list-scale, 1)); }
.role.emoji { font-size: calc(17px * var(--msg-scale, 1)); }

.conv-avatar-wrap { position: relative; flex: 0 0 auto; display: block; }

/* The kind badge, tucked on the avatar's corner. Shape alone is a weak signal
   at a glance; a glyph says "group" or "agent" outright. */
.conv-kind {
  position: absolute;
  right: -3px;
  bottom: -3px;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 9px;
  line-height: 1;
  background: hsl(var(--secondary));
  /* Ring in the row's own colour so the badge reads as sitting on top of the
     avatar rather than merging into it. */
  box-shadow: 0 0 0 2px hsl(var(--background));
}
.conv-item:hover .conv-kind,
.conv-item.active .conv-kind { box-shadow: 0 0 0 2px hsl(var(--card)); }

/* Not over an emoji, though. The badge exists to say what a disc of initials
   belongs to; an emoji already fills the disc, so the badge lands on top of it
   and the two glyphs fight. The kind is still written out in words on the row's
   second line, so nothing is lost by dropping it. */
.conv-avatar.emoji + .conv-kind { display: none; }

/* Except when it is carrying a state — then it is not decoration, it is the
   only thing on the row saying the agent is stuck or offline. It stays, as the
   light on its own: the glyph goes and the ring is the whole signal. */
.conv-avatar.emoji + .conv-kind[data-bot-state] {
  display: grid;
  width: 12px;
  height: 12px;
  font-size: 0;
}

/* Branch from a message. Sits on the message, not inside .body — the text div
   must stay .body's last child for in-place edits to target the right node. */
.msg { position: relative; }
.msg-fork {
  position: absolute;
  top: 4px;
  right: 8px;
  border: 0;
  background: transparent;
  color: hsl(var(--muted-foreground));
  cursor: pointer;
  font-size: 22px;
  line-height: 1;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  opacity: 0;
  transition: opacity .12s ease;
}
.msg:hover .msg-fork { opacity: 0.75; }
.msg-fork:hover { opacity: 1; background: hsl(var(--muted)); }

/* The message menu, opened from the ⋯ button. Touch-only: on a desktop the
   hover row already offers every one of these actions, and long-press does not
   exist there. On a phone it is the ONLY way to reach reply/pin/fork/delete and
   Copy, now that long-press selects text instead of opening a menu. */
.msg-more { display: none; }

/* Reply to a message. Sits beside the fork button, same hover behaviour. */
.msg-reply {
  position: absolute;
  top: 4px;
  right: 46px;
  border: 0;
  background: transparent;
  color: hsl(var(--muted-foreground));
  cursor: pointer;
  font-size: 22px;
  line-height: 1;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  opacity: 0;
  transition: opacity .12s ease;
}
.msg:hover .msg-reply { opacity: 0.75; }
.msg-reply:hover { opacity: 1; background: hsl(var(--muted)); }

/* Read the message out loud. Same furniture as its neighbours, one slot
   further left on the desktop row - and, unlike them, it survives onto touch
   (see the coarse-pointer block below), because being usable one-handed in a
   car is the entire reason it exists. */
.msg-speak {
  position: absolute;
  top: 4px;
  right: 122px;
  /* A ring all the way round, so it reads as a button rather than a glyph
     that happens to be sitting on the transcript. */
  border: 1px solid hsl(var(--border));
  background: transparent;
  color: hsl(var(--muted-foreground));
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  opacity: 0;
  transition: opacity .12s ease;
}
.msg:hover .msg-speak { opacity: 0.75; }
.msg-speak:hover { opacity: 1; background: hsl(var(--muted)); }
/* Fetching the audio: a slow pulse, so a tap always visibly does something
   even when the synthesis takes a second. */
.msg-speak.loading { opacity: 1; animation: msg-speak-pulse 1s ease-in-out infinite; }
/* Playing: solid and tinted. The glyph itself is swapped in JS rather than
   overlaid with ::after - a second grid item does not centre where the first
   one did, which is why the stop square sat low and to the right. */
.msg-speak.playing { opacity: 1; color: hsl(var(--primary)); background: hsl(var(--muted)); }
@keyframes msg-speak-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* Delete a message. Left of the reply button, same hover behaviour; red on
   hover because what it does does not come back. */
.msg-delete {
  position: absolute;
  top: 4px;
  right: 84px;
  border: 0;
  background: transparent;
  color: hsl(var(--muted-foreground));
  cursor: pointer;
  font-size: 20px;
  line-height: 1;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  opacity: 0;
  transition: opacity .12s ease;
}
.msg:hover .msg-delete { opacity: 0.75; }
.msg-delete:hover { opacity: 1; background: hsl(0 70% 50% / 0.15); color: hsl(0 70% 45%); }

/* The quoted parent, above the message text inside .body. Coloured from the
   row's --who-h so it reads as part of THIS message, and kept to one line so
   a long parent cannot crowd out the reply itself. */
.msg-quote {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1px;
  margin: 2px 0 6px;
  padding: 5px 8px;
  border: 0;
  border-left: 2px solid hsl(var(--who-h, 210) 55% 45%);
  border-radius: 6px;
  background: hsl(var(--muted) / 0.6);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  max-width: 100%;
}
.msg-quote-name {
  font-size: 12px;
  font-weight: 600;
  color: hsl(var(--who-h, 210) 55% 45%);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-quote-text {
  font-size: 13px;
  color: hsl(var(--muted-foreground));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The picked reply target, above the composer. Same visual language as the
   in-thread quote: accent edge, muted preview, one line. */
.reply-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: hsl(var(--secondary));
}
.reply-bar[hidden] { display: none; }
.reply-glyph { color: hsl(var(--muted-foreground)); font-size: 14px; }
.reply-meta {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  border-left: 2px solid hsl(var(--primary) / 0.6);
  padding-left: 8px;
}
.reply-name { font-size: 12px; font-weight: 600; color: hsl(var(--foreground)); }
.reply-text {
  font-size: 13px;
  color: hsl(var(--muted-foreground));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* No hover on touch, so the row of desktop controls is hidden and the ⋯ button
   below is the single visible affordance for the actions. */
@media (hover: none) and (pointer: coarse) {
  .msg-fork, .msg-reply, .msg-delete, .msg-pin { display: none; }
  /* The exception: reading a message aloud is meant to be hit with a thumb at a
     red light, so it stays visible - but inside the message, clear of the
     timestamp it was sitting on top of. */
  /* ▶ and ⋯ ride on the header line, level with the timestamp, instead of
     standing as a column down the right of every message: that column kept
     ~90px of the text width empty for the whole height of a long reply. The
     header makes room for them; the text below runs the full width. */
  .msg-speak, .msg-more {
    position: absolute;
    top: calc(16px * var(--msg-scale, 1) - 7px);
    width: 32px;
    height: 32px;
    margin: 0;
    opacity: 0.85;
  }
  .msg-speak { right: 52px; font-size: 15px; }
  .msg-speak.playing { opacity: 1; }
  .msg-head { padding-right: 84px; min-height: 18px; margin-bottom: 8px; }
  .msg-more {
    display: grid;
    place-items: center;
    right: 14px;
    font-size: 18px;
    border: 1px solid hsl(var(--border));
    border-radius: 999px;
    background: transparent;
    color: hsl(var(--muted-foreground));
    line-height: 1;
    cursor: pointer;
  }
  .msg-more:active { background: hsl(var(--muted)); }
}

/* Long-press action menu (touch). A translucent sheet over the transcript;
   the actions are the very same handlers the hover buttons run. */
/* The callout is LEFT ON now that text selects: suppressing it fought the new
   select-a-paragraph feature, and iOS shows its own Copy / Look Up strip for a
   selection the moment you let go. Long-press still reaches the menu - the
   handlers below open it on the row, and a drag across the text selects instead. */
.msg { -webkit-touch-callout: default; }

/* Copy a selection, from a button we draw ourselves.
   The OS selection menu is not dependable for this: iOS shows one, Android
   often does not, and a browser without a secure context offers nothing at
   all. This appears whenever the selection is inside a message, sits above
   whatever was picked out, and copies exactly the range rather than the whole
   message - which is the entire point of being able to select a paragraph. */
.sel-copy {
  position: absolute;
  z-index: 61;
  border: 1px solid hsl(var(--primary) / 0.5);
  background: hsl(var(--card));
  color: hsl(var(--foreground));
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 7px 14px;
  border-radius: 999px;
  box-shadow: 0 4px 16px hsl(0 0% 0% / 0.3);
  cursor: pointer;
}
.sel-copy:active { background: hsl(var(--muted)); }

.msg-menu {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 16px;
  background: hsl(0 0% 0% / 0.35);
}
.msg-menu-panel { display: flex; flex-direction: column; gap: 8px; }

/* Desktop: a compact menu at the pointer, not the phone sheet. The overlay
   stays (clear) only to catch the click that dismisses it. */
.msg-menu.popover { background: transparent; padding: 0; display: block; }
.msg-menu.popover .msg-menu-panel {
  position: fixed;
  min-width: 190px;
  max-width: 320px;
  gap: 0;
  padding: 4px;
  border: 1px solid hsl(var(--border));
  border-radius: 10px;
  background: hsl(var(--card));
  box-shadow: 0 8px 28px hsl(0 0% 0% / 0.3);
}
.msg-menu.popover .msg-menu-item {
  box-shadow: none;
  border-radius: 6px;
  font-size: 14px;
  padding: 7px 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-menu.popover .msg-menu-item:hover,
.msg-menu.popover .msg-menu-item:focus-visible {
  background: hsl(var(--muted));
  outline: none;
}
.msg-menu-item {
  border: 0;
  border-radius: 12px;
  background: hsl(var(--card));
  color: inherit;
  font: inherit;
  font-size: 17px;
  text-align: left;
  padding: 18px 20px;
  cursor: pointer;
  box-shadow: 0 4px 16px hsl(0 0% 0% / 0.25);
}
.msg-menu-item:active { background: hsl(var(--muted)); }
/* Dimmed text only. Opacity on the whole row took the background down with
   it and the transcript showed through the sheet (Jon 2026-09-26). */
.msg-menu-item:disabled { color: hsl(var(--muted-foreground)); }
.msg-menu-item.danger { color: hsl(0 70% 45%); }
/* The question a choice sheet asks (report reason, blocked people): drawn
   as a quiet row of the sheet, not a button. */
.msg-menu-item.msg-menu-title {
  color: hsl(var(--muted-foreground));
  font-size: 14px;
  text-align: center;
  padding: 12px 20px;
  cursor: default;
}
.msg-menu-cancel {
  border: 0;
  border-radius: 12px;
  background: transparent;
  color: hsl(var(--primary-foreground, white));
  font: inherit;
  font-size: 17px;
  padding: 18px;
  cursor: pointer;
}
:root:not([data-theme="dark"]) .msg-menu-cancel { color: white; }

/* Something the APP did, not something anyone said. Dimmed and italic so it
   reads as a margin note rather than a message. */
.msg.sys-note { opacity: 0.75; }
.msg.sys-note .body { font-style: italic; font-size: 13px; }
.msg.sys-note .role { background: transparent; color: hsl(var(--muted-foreground)); }

/* The "older messages exist" affordance at the top of a paged transcript. */
.older-loader { display: flex; justify-content: center; padding: 8px 0 4px; }
.older-btn {
  border: 1px solid hsl(var(--border));
  border-radius: 999px;
  background: hsl(var(--card));
  color: hsl(var(--muted-foreground));
  font: inherit;
  font-size: 12px;
  padding: 5px 14px;
  cursor: pointer;
}
.older-btn:hover:not(:disabled) { color: hsl(var(--foreground)); border-color: hsl(var(--primary) / 0.5); }
.older-btn:disabled { opacity: 0.6; cursor: default; }

/* --- Find in the open conversation --- */

.find-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  border-bottom: 1px solid hsl(var(--border));
  background: hsl(var(--secondary));
}
.find-input {
  flex: 1;
  min-width: 0;
  padding: 6px 10px;
  border: 1px solid hsl(var(--border));
  border-radius: 8px;
  background: hsl(var(--background));
  color: inherit;
  font-size: 13px;
}
.find-input:focus { outline: none; border-color: hsl(var(--primary) / 0.6); }
.find-count {
  flex: 0 0 auto;
  font-size: 11px;
  color: hsl(var(--muted-foreground));
  font-variant-numeric: tabular-nums;
  min-width: 34px;
  text-align: right;
}

/* Every hit is tinted; the one you are on is stronger, so stepping through a
   long transcript is followable. */
mark.find-hit {
  background: hsl(38 92% 50% / 0.30);
  color: inherit;
  border-radius: 2px;
}
mark.find-hit.current {
  background: hsl(38 92% 50% / 0.85);
  color: #000;
}

@media (max-width: 860px) {
  /* The count and arrows matter more than a wide box on a phone. */
  .find-bar { padding: 6px 8px; }
  .find-input { font-size: 16px; }  /* 16px or iOS zooms the page on focus */
}

/* --- Search --- */

.search-wrap { padding: 6px 10px 2px; }

.search-input {
  width: 100%;
  padding: 7px 10px;
  border: 1px solid hsl(var(--border));
  border-radius: 9px;
  background: hsl(var(--background));
  color: inherit;
  /* 16px on touch or iOS zooms the whole page on focus. */
  font-size: 13px;
}
.search-input:focus { outline: none; border-color: hsl(var(--primary) / 0.6); }

.search-item {
  padding: 8px 10px;
  border-radius: 8px;
  cursor: pointer;
  margin-bottom: 2px;
}
.search-item:hover { background: hsl(var(--card)); }

.search-head { display: flex; align-items: baseline; gap: 6px; }
.search-conv {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-who { font-size: 11px; color: hsl(var(--muted-foreground)); }

/* Section headers, only shown when the search returned both kinds of hit. */
.search-group-label {
  padding: 8px 10px 2px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: hsl(var(--muted-foreground));
}
/* Same mark as a snippet hit, so a name match reads the same way. */
.search-conv mark {
  background: hsl(38 92% 50% / 0.28);
  color: inherit;
  border-radius: 2px;
  padding: 0 1px;
}
.search-snippet {
  font-size: 12px;
  color: hsl(var(--muted-foreground));
  /* Two lines of context, then clip — a long match should not push the next
     result off the screen. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 2px;
}
.search-snippet mark {
  background: hsl(38 92% 50% / 0.28);
  color: inherit;
  border-radius: 2px;
  padding: 0 1px;
}

/* The message a search result jumped to. Fades so it draws the eye once and
   then gets out of the way. */
@keyframes msg-hit {
  0%   { background: hsl(38 92% 50% / 0.22); }
  100% { background: transparent; }
}
.msg.hit { animation: msg-hit 2.2s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .msg.hit { animation: none; box-shadow: inset 3px 0 0 hsl(38 92% 50%); }
}

/* --- Live agent state --------------------------------------------------
 *
 * An agent that has gone quiet could be thinking, running a long tool, stuck,
 * or dead with a broken login. These make the difference visible without
 * having to read anything: colour on the 🤖 badge, and a coloured status line
 * in the header.
 */

/* The kind badge doubles as the health light. */
.conv-kind.agent[data-bot-state="thinking"],
.conv-kind.agent[data-bot-state="tool"] {
  box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(38 92% 50% / 0.85);
}
.conv-kind.agent[data-bot-state="stalled"] {
  box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(0 72% 51%);
}
.conv-kind.agent[data-bot-state="offline"] {
  opacity: 0.45;
  filter: grayscale(1);
}
/* A notifier and an unused chat are not faults, so they are dimmed but not
   greyed out — the greyscale is what reads as "this is broken". */
.conv-kind.agent[data-bot-state="notify"],
.conv-kind.agent[data-bot-state="unused"] { opacity: 0.7; }
/* An alerts-only bot never goes "offline" in any way worth reporting, so its
   badge never dims for state — the 🔔 already says what it is. */
.conv-kind.agent.notify { opacity: 0.85; }

/* Working states pulse; a stuck one does not — a steady red reads as "wrong"
   where a pulse reads as "in progress". */
@keyframes bot-working {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}
.conv-kind.agent[data-bot-state="thinking"],
.conv-kind.agent[data-bot-state="tool"] { animation: bot-working 1.6s ease-in-out infinite; }

/* The agent roster's meta line, coloured by state like the header is. */
.admin-meta[data-bot-state="thinking"],
.admin-meta[data-bot-state="tool"] { color: hsl(38 92% 50%); }
.admin-meta[data-bot-state="stalled"] { color: hsl(0 72% 55%); font-weight: 600; }
.admin-meta[data-bot-state="offline"] { font-style: italic; }

/* The watch-key input sits with the row's other controls. */
input.policy-select {
  min-width: 120px;
  padding: 4px 8px;
  border: 1px solid hsl(var(--border));
  border-radius: 8px;
  background: hsl(var(--background));
  color: inherit;
  font-size: 12px;
}
input.policy-select.saved { border-color: hsl(142 70% 42%); }

/* The header status line takes its colour from the same states. */
.conv-subtitle[data-bot-state="thinking"],
.conv-subtitle[data-bot-state="tool"] { color: hsl(38 92% 50%); }
.conv-subtitle[data-bot-state="stalled"] { color: hsl(0 72% 55%); font-weight: 600; }
.conv-subtitle[data-bot-state="offline"] { color: hsl(var(--muted-foreground)); font-style: italic; }

@media (prefers-reduced-motion: reduce) {
  .conv-kind.agent[data-bot-state] { animation: none; }
}

/* --- Adding an agent to a group --- */

.agent-picker { display: flex; flex-direction: column; gap: 6px; }

.agent-add-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  border: 1px solid hsl(var(--border));
  border-radius: 10px;
  background: hsl(var(--background));
  color: inherit;
  cursor: pointer;
  text-align: left;
  font-size: 13px;
}
.agent-add-row:hover:not(:disabled) { border-color: hsl(var(--primary) / 0.6); }
/* Where Enter would land. Same weight as hover, because it means the same
   thing — "this is the one" — just arrived at from the keyboard. */
.agent-add-row.active {
  border-color: hsl(var(--primary) / 0.6);
  background: hsl(var(--primary) / 0.1);
}
/* The picker's own search. Sits between the explanation and the list, which is
   where you look when the list is too long to read. */
.agent-search { margin-bottom: 8px; }
/* Long lists scroll inside the section rather than pushing Add-someone off the
   bottom of the dialog. */
.agent-picker { max-height: 46vh; overflow-y: auto; }
.agent-add-row:disabled { opacity: 0.5; cursor: default; }

.agent-add-dot {
  flex: 0 0 28px;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 10px;
  font-weight: 700;
  color: #fff;
  background: hsl(var(--primary));
}
.agent-add-name { flex: 1; min-width: 0; font-weight: 600; }
.agent-add-cta { flex: 0 0 auto; font-size: 12px; color: hsl(var(--primary)); font-weight: 600; }

/* An agent among the human members. */
.tag.agent-tag {
  background: hsl(var(--primary) / 0.14);
  color: hsl(var(--primary));
}

/*
 * A group's member count. It lives on the SECOND line beside the preview, not
 * next to the name: in a 264px sidebar a chip on the title row steals the
 * space the name needs and every row ends in an ellipsis.
 */
.conv-members {
  flex: 0 0 auto;
  font-size: 10px;
  font-weight: 600;
  line-height: 1;
  color: hsl(var(--muted-foreground));
  font-variant-numeric: tabular-nums;
  opacity: 0.8;
}
.conv-members::before { content: '👥'; margin-right: 2px; font-size: 9px; }

.conv-item { gap: 10px; padding: 8px 10px; }

/*
 * Swipe-to-delete. The row content lives in .conv-slide and translates left
 * over .conv-del, which sits pinned to the right edge underneath — the same
 * mechanic as the mail and chat apps this list is modelled on.
 */
.conv-item { position: relative; overflow: hidden; }


/* The chat's right-click menu. At the pointer on desktop, a bottom sheet on
   touch — reusing the message menu's sheet styling so one gesture language
   covers both. */
.chat-menu { display: none; }
.chat-menu.sheet {
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 60;
  flex-direction: column;
  justify-content: flex-end;
  padding: 16px;
  gap: 8px;
  background: hsl(0 0% 0% / 0.35);
}
@media (min-width: 721px) {
  .chat-menu.at-pointer {
    display: flex;
    position: fixed;
    flex-direction: column;
    gap: 2px;
    inset: auto;
    min-width: 210px;
    padding: 6px;
    border-radius: 10px;
    background: hsl(var(--card));
    box-shadow: 0 6px 24px hsl(0 0% 0% / 0.3);
    z-index: 60;
  }
}
.chat-menu-item {
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 14px;
  text-align: left;
  padding: 10px 14px;
  cursor: pointer;
}
.chat-menu.at-pointer .chat-menu-item:hover,
.chat-menu.sheet .chat-menu-item:active { background: hsl(var(--muted)); }
.chat-menu.sheet .chat-menu-item {
  border-radius: 12px;
  background: hsl(var(--card));
  font-size: 17px;
  padding: 18px 20px;
  box-shadow: 0 4px 16px hsl(0 0% 0% / 0.25);
}

/* Stats, in the sheet: label/value pairs, one glance each. */
/* The little confirmation for retag/connection actions. */
.chat-toast {
  position: fixed;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  z-index: 70;
  background: hsl(var(--card));
  color: hsl(var(--foreground));
  border: 1px solid hsl(var(--muted));
  border-radius: 10px;
  padding: 10px 16px;
  font-size: 14px;
  max-width: min(520px, 90vw);
  box-shadow: 0 4px 16px hsl(0 0% 0% / 0.25);
  cursor: pointer;
}

.conv-slide {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  min-width: 0;
  /* Its own layer, so dragging repaints only this row. */
  will-change: transform;
  background: inherit;
}

/* The row's own background must be opaque, or the delete action shows through
   the content sliding over it. */
.conv-item { background: hsl(var(--background)); }
.conv-item:hover { background: hsl(var(--card)); }
.conv-item.active { background: hsl(var(--card)); }

/*
 * An opaque background is not on its own enough to hide the action, and this
 * is the rest of it. Painting order does not follow the DOM: .conv-del is
 * positioned and .conv-slide is not, so the action paints ON TOP of the
 * content covering it and every row showed a permanent red Delete panel —
 * which is how it looked in Edge on iOS. Giving both an explicit stacking
 * order is what puts the content over the action.
 *
 * The row's padding moves inward at the same time, all four sides of it. The
 * action is pinned to the row's edges while the content is only as big as the
 * content, so any padding left on .conv-item is a strip of red showing past
 * the end of what is supposed to be covering it — down the right edge from the
 * horizontal padding, and along the top and bottom from the vertical.
 *
 * The corners go square for the same reason. Clipping the row to a radius is
 * antialiased, and those part-covered pixels blend the content with what is
 * behind it rather than being covered by it, so a pink arc of the action
 * traced every corner. Rounding the action to match does not help — it is the
 * row's own clip that leaks. Square is also what the list apps this is
 * modelled on do at full width.
 *
 * Kept here rather than beside the other touch rules further up the file
 * because .conv-item's padding is set again, unmediated, just above — an
 * earlier override loses to it whatever the media query says.
 */
@media (hover: none) and (pointer: coarse) {
  .conv-item { padding: 0; border-radius: 0; }
  .conv-slide {
    position: relative;
    z-index: 1;
    padding: 8px 10px;
  }
  .conv-del { z-index: 0; }
}

/* Title and time on one line, preview and unread badge on the next. */
.conv-top, .conv-bottom {
  display: flex;
  align-items: baseline;
  gap: 6px;
  /* Without min-width:0 a flex child refuses to shrink below its content, so
     the title pushed the row wide instead of ellipsising. */
  min-width: 0;
  width: 100%;
}
.conv-top .conv-name {
  /* The name does not stretch: the subject beside it takes the slack, so a
     short name like "Sparked Agent" is not padded out to the full width.
     Like .conv-name below it, it wraps to two rows rather than ellipsising —
     at larger text sizes a one-line clamp left only "CLAUD…". */
  flex: 0 1 auto;
  min-width: 0;
  font-size: calc(14px * var(--list-scale, 1));
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}

/* What a thread is ABOUT, next to who it is with. Only plain threads need it —
   several of them share the built-in assistant, so the name alone cannot tell
   them apart. Dimmed and lighter, so the row still reads as "who" first. */
.conv-subject {
  flex: 1 1 auto;
  min-width: 0;
  font-size: calc(12px * var(--list-scale, 1));
  font-weight: 400;
  color: hsl(var(--muted-foreground));
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.conv-subject::before { content: '· '; }
.conv-bottom .conv-sub { min-width: 0; }

.conv-time {
  flex: 0 0 auto;
  font-size: calc(11px * var(--list-scale, 1));
  color: hsl(var(--muted-foreground));
  font-variant-numeric: tabular-nums;
}
/* In a row, the time belongs on the right edge, not trailing the name: a
   short title left it floating mid-row, out of line with the fleet figures
   under it and the pill beyond them. Pinned right, it shares the edge the
   stats end on, and the whole right side of the row reads as one column. */
.conv-top .conv-time { margin-left: auto; padding-left: 6px; }

.conv-bottom .conv-sub { flex: 1; }

/* What was last said: two lines at most, quieter than the name. Scoped under
   .conv-item to outrank the shared `.conv-item span` rule, whose nowrap would
   otherwise cut it to one line. */
.conv-item .conv-preview {
  flex: none;
  white-space: normal;
  text-overflow: clip;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
  margin-top: 2px;
  font-size: calc(12px * var(--list-scale, 1));
  line-height: 1.35;
  color: hsl(var(--muted-foreground));
}
.conv-item .conv-preview-who { flex: none; white-space: normal; color: hsl(var(--foreground)); opacity: 0.8; }

/* Admin-only, and the least important thing in the row — kept to one line and
   dimmed so it does not compete with the message preview. */
.conv-tenant {
  font-size: 10px;
  color: hsl(var(--muted-foreground));
  opacity: 0.6;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The real number. A dot could not tell 1 unread from 99. */
.conv-badge {
  flex: 0 0 auto;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: hsl(var(--primary));
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.conv-item.unread .conv-name { font-weight: 600; color: hsl(var(--foreground)); }

/* Header: name over the member count, as a group chat shows. */
.conv-heading {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0;
  flex: 1;
  /* Room to show a hover state without the text shifting when it appears. */
  padding: 2px 6px;
  margin-left: -6px;
  border-radius: 8px;
  text-align: left;
}
/* Only in a group, where there is a membership to open. The cue is deliberately
   quiet until you are on it: this is the title of the thing you are reading,
   and it should not look like a button all day. */
.conv-heading.clickable { cursor: pointer; }
.conv-heading.clickable:hover { background: hsl(var(--card)); }
.conv-heading.clickable:hover .conv-title { text-decoration: underline; }
.conv-heading.clickable:focus-visible {
  outline: 2px solid hsl(var(--primary));
  outline-offset: 1px;
}
.conv-subtitle {
  font-size: calc(11px * var(--list-scale, 1));
  color: hsl(var(--muted-foreground));
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.conv-subtitle:empty { display: none; }

/* The order picker. A select rather than another row of chips: there are eight
   orders and only one can be on, which is what a select is for — and the chips
   above it already own the "several independent narrowings" shape. */
.chat-order { display: flex; align-items: center; gap: 8px; margin: 8px 12px 0; }
.chat-order label { font-size: 11px; color: hsl(var(--muted-foreground)); }
.chat-order select { flex: 1; min-width: 0; font-size: 12px; }

/* The fleet numbers on a chat row: a status dot and two or three small figures,
   pinned right so the name and the preview keep the left edge and the column
   of numbers lines up down the list. */
.conv-item .conv-stats {
  /* Specificity, not style: the shared `.conv-item span { flex: 1 }` further
     up would otherwise stretch this and squeeze the preview out. */
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: auto;
  padding-left: 6px;
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  color: hsl(var(--muted-foreground));
}
.conv-item .conv-stat, .conv-item .conv-dot { flex: 0 0 auto; }
.conv-stat { white-space: nowrap; }
/* The unit is the label, and it must not shout over the figure it labels. */
.conv-unit { opacity: 0.65; margin-left: 1px; }
.conv-stat.bad { color: hsl(var(--destructive)); font-weight: 600; }
.conv-dot { font-size: 9px; line-height: 1; }
.conv-dot.ok { color: hsl(142 65% 42%); }
.conv-dot.warn { color: hsl(38 92% 50%); }
.conv-dot.bad { color: hsl(var(--destructive)); }
.conv-dot.muted { color: hsl(var(--muted-foreground) / 0.6); }
/* The badge is the one thing on this line that must never be pushed off, and
   it needs air: sat tight against the figures it read as a third number. */
.conv-bottom .conv-badge { flex: 0 0 auto; margin-left: 4px; }
/* The pin mark, just after a pinned chat's name. */
.conv-pin { flex: 0 0 auto; align-self: center; display: inline-flex; color: hsl(var(--primary)); }
.conv-pin .i { width: 14px; height: 14px; transform: rotate(45deg); }
/* Narrow sidebar: the name wins, as it does for the kind pill. @container, not
   @media — the sidebar shrinks by dragging the divider while the viewport
   stays put, so a viewport query never fires when the rows actually run out
   of room. */
@container sidebar (max-width: 420px) {
  .conv-stats { display: none; }
}

/* Filter chips. One row, so turning the list down to groups costs no more
   vertical space than the control itself. */
/* What kind of thing this row is, in a word. Muted by default: it labels the
   row, it is not the point of it — except for Alerts, which is the one that
   changes what you can expect from the chat. */
.conv-pill {
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid hsl(var(--border));
  font-size: calc(10px * var(--list-scale, 1));
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: hsl(var(--muted-foreground));
  white-space: nowrap;
}
.conv-pill.group {
  border-color: hsl(var(--primary) / 0.45);
  background: hsl(var(--primary) / 0.12);
  color: hsl(var(--primary));
}
.conv-pill.alert {
  border-color: hsl(38 92% 50% / 0.5);
  background: hsl(38 92% 50% / 0.14);
  color: hsl(38 80% 42%);
}
[data-theme="dark"] .conv-pill.alert { color: #f5bf66; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .conv-pill.alert { color: #f5bf66; }
}
/* On a narrow sidebar the name matters more than the label. Same @container
   reasoning as the stats above: the divider, not the viewport, decides. */
@container sidebar (max-width: 420px) {
  .conv-pill { display: none; }
}
/* Divider dragged down near its floor: even the timestamp goes, and the name
   gets the whole row. */
@container sidebar (max-width: 300px) {
  .conv-top .conv-time { display: none; }
}

/* The roster's per-row client picker. Narrow on purpose: it is a label, not
   the subject of the row. */
.client-pick {
  flex: 0 0 auto;
  max-width: 140px;
  padding: 3px 6px;
  border: 1px solid hsl(var(--border));
  border-radius: 6px;
  background: hsl(var(--background));
  color: inherit;
  font: inherit;
  font-size: 11px;
}
.client-pick:disabled { opacity: 0.4; }

.chat-filter { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 10px 0; }
/* The client pills reuse these chip styles by carrying the same container
   class, but they sit inside the inset .inbox-view rather than as their own
   block, so the block-level padding is dropped. flex-wrap keeps a long
   roster readable instead of overflowing the sidebar. */
#client-filter.chat-filter { padding: 0; }
.chat-filter .chip {
  flex: 1 1 auto;
  min-width: 58px;
  padding: 5px 6px;
  border: 1px solid hsl(var(--border));
  border-radius: 999px;
  background: transparent;
  color: hsl(var(--muted-foreground));
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.chat-filter .chip:hover { background: hsl(var(--card)); }
.chat-filter .chip[aria-pressed="true"] {
  background: hsl(var(--primary) / 0.14);
  border-color: hsl(var(--primary) / 0.55);
  color: inherit;
  font-weight: 600;
}

/* The "No client" pill. Not a client at all — it is the bucket for unfiled
   agents (image, data analysis, presentation), so it reads violet, distinct
   from every real client's orange, and says "agents" before you have learned
   the roster. Its pressed state keeps the same hue rather than falling back
   to the client primary. */
#client-filter .chip.no-client {
  border-color: hsl(262 60% 55% / 0.5);
  color: hsl(262 65% 45%);
}
#client-filter .chip.no-client:hover { background: hsl(262 60% 55% / 0.09); }
#client-filter .chip.no-client[aria-pressed="true"] {
  background: hsl(262 60% 55% / 0.16);
  border-color: hsl(262 60% 55% / 0.6);
  color: hsl(262 65% 45%);
}
[data-theme="dark"] #client-filter .chip.no-client,
[data-theme="dark"] #client-filter .chip.no-client[aria-pressed="true"] { color: #c4b0f5; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) #client-filter .chip.no-client,
  :root:not([data-theme="light"]) #client-filter .chip.no-client[aria-pressed="true"] { color: #c4b0f5; }
}

/* "A new version is ready" -- see checkForNewBuild(). Bottom centre, above
   the composer, and a real button so it can be tapped on a phone. */
.update-bar {
  position: fixed;
  left: 50%;
  bottom: calc(96px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 60;
  padding: 9px 16px;
  border-radius: 999px;
  border: 1px solid hsl(var(--border));
  background: hsl(var(--primary));
  color: hsl(var(--primary-foreground));
  font-size: 13px;
  font-weight: 600;
  box-shadow: 0 6px 20px hsl(0 0% 0% / 0.25);
  cursor: pointer;
  max-width: calc(100vw - 32px);
}

/* --- Text size stepper (sidebar footer) --- */

.text-size {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
.text-size .link-btn {
  /* Fixed square, so - and + are the same size and the pair does not jump
     as the labels change width. */
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border-radius: 6px;
  font-size: 15px;
  line-height: 1;
}
.text-size .link-btn:hover:not(:disabled) { background: hsl(var(--muted)); }

/* The conversation's own pair, in its top bar. "A−" / "A+" so it reads as
   text size among the other icons there. */
/* Same spacing as the other top-bar buttons, whatever the bar uses at this width. */
.chat-text-size { gap: inherit; flex: 0 0 auto; }
.chat-text-size .ghost-btn { font-size: 13px; font-weight: 600; letter-spacing: -0.02em; padding: 0 6px; }
.chat-text-size .ghost-btn:disabled { opacity: 0.3; cursor: default; }
.text-size .link-btn:disabled {
  /* At the end of the range. Dimmed rather than hidden: a control that
     vanishes at the limit makes the pair jump about. */
  opacity: 0.3;
  cursor: default;
}

/* --- Starred clients ---
   A star inside the filter pill, so the accounts an admin works on sort to
   the front of the roster. The star is its own hit target within the pill:
   tapping it stars, tapping the rest of the pill still filters. */
#client-filter .chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
}

.chip-star {
  /* Dimmed until hovered or starred: a row of bright stars on an unstarred
     roster reads as though everything is already prioritised. */
  opacity: 0.35;
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  /* A wider tap target than the glyph, without making the pill taller. */
  padding: 2px;
  margin: -2px -1px -2px 0;
  transition: opacity 0.12s ease;
}
.chip-star:hover,
.chip-star:focus-visible { opacity: 1; }
.chip.starred .chip-star { opacity: 1; color: hsl(38 92% 50%); }
/* The starred pill itself carries a little weight, so the priority group is
   legible as a group rather than only by its stars. */
.chip.starred { font-weight: 600; }

/* "n more" opens the rest of the roster. Deliberately quieter than a client
   pill and without a border: it is not a filter, and a pill that looks like
   the others invites being pressed as though it were one. */
#client-filter .chip.chip-more {
  border-color: transparent;
  background: none;
  color: hsl(var(--muted-foreground));
  font-size: 12px;
}
#client-filter .chip.chip-more:hover {
  color: hsl(var(--foreground));
  text-decoration: underline;
}

@media (max-width: 760px) {
  /* Bigger target on a phone, where the star sits inside another control. */
  .chip-star { font-size: 14px; padding: 4px; margin: -4px -2px -4px 0; }
}

/* Folded away by the header toggle. display:none rather than a height
   animation: the block contains a <select>, and a half-collapsed one is still
   focusable and still opens. */
.sidebar-tools.collapsed { display: none; }
.conv-sub.conv-warning, .conv-warning {
  color: #a35c00;
  font-size: calc(12px * var(--list-scale, 1));
  font-weight: 600;
}
.conv-warning::before { content: '⚠ '; }
[data-theme="dark"] .conv-warning { color: #f5bf66; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .conv-warning { color: #f5bf66; }
}
.conv-bottom .conv-sub.conv-warning { white-space: normal; line-height: 1.4; }
#conv-warning { white-space: normal; line-height: 1.4; }
.chat-notice { display: flex; align-items: center; gap: 12px; padding: 0 20px; border-bottom: 1px solid hsl(var(--border)); }

/* The open chat's own numbers.
   Chips rather than a table: this is a glance, and it shares a row with a
   warning that must still be the loudest thing on it. */
.chat-stats { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
.chat-stat {
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid hsl(var(--border));
  font-size: 11px;
  font-weight: 600;
  color: hsl(var(--muted-foreground));
  white-space: nowrap;
  cursor: default;
}
.chat-stat.ok {
  border-color: hsl(142 70% 40% / 0.45);
  background: hsl(142 70% 40% / 0.12);
  color: hsl(142 60% 32%);
}
.chat-stat.bad {
  border-color: hsl(var(--destructive) / 0.45);
  background: hsl(var(--destructive) / 0.12);
  color: hsl(var(--destructive));
}
.chat-stat.muted { opacity: 0.7; }
[data-theme="dark"] .chat-stat.ok { color: #6ee7a8; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .chat-stat.ok { color: #6ee7a8; }
}
/* On a phone the header is already two lines; the numbers are a desk thing. */
@media (max-width: 700px) {
  .chat-stats { display: none; }
}

/* The Type column. Deliberately NOT a flex cell: display:flex on a td drops it
   out of the table box model and the columns stop lining up. Inline pills with
   a margin between them do the same job and leave the table a table. */
.fleet-table td.type { white-space: nowrap; }
.fleet-table td.type .conv-pill + .conv-pill { margin-left: 4px; }
/* The chat list drops its pills on a narrow screen because the name matters
   more there. This column IS the label, so it keeps them. */
@media (max-width: 420px) {
  .fleet-table td.type .conv-pill { display: inline; }
}

/* A bot's name on the fleet table is the way into its chat. Styled as text
   until you point at it: a table of twelve buttons reads as a toolbar. */
.fleet-open {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.fleet-open:hover, .fleet-open:focus-visible { color: hsl(var(--primary)); text-decoration: underline; }
@media (max-width: 700px) {
  .topbar { flex-wrap: wrap; gap: 8px; }
  .topbar .conv-heading { flex-basis: calc(100% - 48px); }
}
.conv-del:focus-visible { opacity: 1; outline: 2px solid hsl(var(--primary)); }
@media (hover: hover) {
  .conv-del { position: absolute; right: 8px; bottom: 6px; font-size: 11px; opacity: 1; padding: 0; }
  .conv-tenant { padding-right: 55px; }
}

/* Back arrow: hidden on desktop, where both panes are always on screen. */
#back-btn { font-size: 26px; line-height: 1; }

/* --- Scrollbars --- */

/* Untouched until now, so every scrolling panel showed the OS default — a
   light grey track with a chunky arrow-capped thumb, glaringly wrong against
   a dark sidebar. color-scheme alone fixes the native widget in Firefox and
   modern Chromium; the -webkit rules style it properly everywhere else.
   Both are driven by the theme tokens, so they follow the brand palette. */
:root { color-scheme: light; }
:root[data-theme='dark'] { color-scheme: dark; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) { color-scheme: dark; }
}

* {
  scrollbar-width: thin;
  scrollbar-color: hsl(var(--border)) transparent;
}

*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }

*::-webkit-scrollbar-thumb {
  background: hsl(var(--muted-foreground) / 0.32);
  border-radius: 999px;
  /* Inset via a transparent border so the thumb reads as a slim pill rather
     than filling the gutter edge to edge. */
  border: 2px solid transparent;
  background-clip: content-box;
}

*::-webkit-scrollbar-thumb:hover { background: hsl(var(--muted-foreground) / 0.55); background-clip: content-box; }
*::-webkit-scrollbar-corner { background: transparent; }

/* --- Live delivery --- */

/* A conversation with something new in it. A dot rather than a count: the
   server does not track read state yet, so a number would be a guess and a
   wrong number is worse than no number. */
.conv-item.unread .conv-name { font-weight: 600; color: hsl(var(--foreground)); }

/* The unread dot lived here. It is gone because .conv-badge now shows the
   actual count in the row — two indicators for one fact, and the dot was the
   one that could not tell 1 from 99. */

.typing-row .typing-dots {
  color: hsl(var(--muted-foreground));
  font-size: 13px;
  letter-spacing: 0.06em;
}

/* --- File preview panel --- */

/* A third column beside the transcript, not an overlay: the point is to read
   the file and ask about it at the same time. */
/* The agent activity feed. Same shape as the file preview beside it — one
   column on the right that shares the row rather than covering the chat, which
   is the point: you watch what the agent is doing WHILE you talk to it. */
.activity {
  width: min(380px, 34vw);
  flex: none;
  display: flex;
  flex-direction: column;
  border-left: 1px solid hsl(var(--border));
  background: hsl(var(--card));
  min-width: 0;
}
.activity-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 14px;
  border-bottom: 1px solid hsl(var(--border));
}
.activity-head strong { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.activity-note {
  margin: 0;
  padding: 10px 14px;
  font-size: 11.5px;
  color: hsl(var(--muted-foreground));
  border-bottom: 1px solid hsl(var(--border) / 0.6);
}
.activity-list { flex: 1; overflow: auto; padding: 6px 0 14px; }

/* One event. The rail down the left makes a run of tool calls read as a
   sequence rather than a stack of unrelated lines. */
.act {
  position: relative;
  padding: 7px 14px 7px 30px;
  font-size: 12.5px;
  line-height: 1.45;
}
.act::before {
  content: '';
  position: absolute;
  left: 15px;
  top: 14px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: hsl(var(--muted-foreground) / 0.55);
}
.act::after {
  content: '';
  position: absolute;
  left: 18px;
  top: 21px;
  bottom: -7px;
  width: 1px;
  background: hsl(var(--border));
}
.act:last-child::after { display: none; }
.act.tool::before    { background: hsl(38 92% 50%); }
.act.thinking::before{ background: hsl(var(--primary)); }
.act.stalled::before { background: hsl(0 72% 51%); }
.act.offline::before { background: hsl(var(--muted-foreground) / 0.35); }
.act-what { color: hsl(var(--foreground)); overflow-wrap: anywhere; }
.act-what .act-tool { font-weight: 600; }
.act-meta { font-size: 11px; color: hsl(var(--muted-foreground)); margin-top: 1px; }
.act.stalled .act-meta { color: hsl(0 72% 55%); font-weight: 600; }

/* Below this the chat itself is only just wide enough; a third column would
   leave nothing for the conversation. */
@media (max-width: 1100px) {
  .activity { position: absolute; inset: 0 0 0 auto; width: min(380px, 92vw); z-index: 40; }
}

.preview {
  /* Wide enough for a page of a Word document or a few spreadsheet columns,
     but it gives way before the conversation does: a fixed 48vw left the chat
     a 30px sliver beside a wide sidebar (Jon 2026-10-02). */
  width: auto;
  flex: 0 1 760px;
  min-width: 320px;
  display: flex;
  flex-direction: column;
  border-left: 1px solid hsl(var(--border));
  background: hsl(var(--card));
  min-width: 0;
}

.preview-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 14px;
  border-bottom: 1px solid hsl(var(--border));
}

.preview-title { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.preview-title strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.preview-body { flex: 1; overflow: auto; padding: 14px 16px; }

.preview-text {
  margin: 0;
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.65;
  /* pre-wrap, not pre: extracted document text has no meaningful line length,
     and a horizontal scrollbar makes it unreadable. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: hsl(var(--foreground));
}

.preview-image { max-width: 100%; border-radius: 8px; display: block; }

/* A labelled button, not a bare ↓: on a phone it is the only way to keep
   the file, and a lone arrow read as decoration (Jon 2026-09-27). */
.preview-download {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px;
  border-radius: 10px;
  background: hsl(var(--primary));
  color: hsl(var(--primary-foreground, 0 0% 100%));
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
}

.preview-copy {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px;
  border-radius: 10px;
  border: 1px solid hsl(var(--border));
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.preview-copy:hover { background: hsl(var(--muted, var(--border)) / 0.5); }

/* A chip is a button now, so it needs the button reset the .doc-chip anchor
   styling did not have to worry about. */
.msg-docs .doc-chip {
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  margin: 0;
}

/* On a phone there is no room for a third column — the preview takes over. */
@media (max-width: 860px) {
  .preview {
    position: fixed;
    inset: 0;
    width: auto;
    z-index: 25;
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
  }
}

/* --- Rewrite bar --- */

.rewrite-bar {
  display: flex;
  gap: 8px;
  align-items: flex-end;
  padding: 10px 12px;
  border-top: 1px solid hsl(var(--border));
  background: hsl(var(--background));
}

.rewrite-bar textarea {
  flex: 1;
  min-width: 0;
  max-height: 96px;
  padding: 9px 11px;
  border: 1px solid hsl(var(--border));
  border-radius: 10px;
  background: hsl(var(--card));
  color: hsl(var(--foreground));
  font: inherit;
  font-size: 16px; /* stops iOS zooming on focus */
  resize: none;
}

.rewrite-bar textarea:focus {
  outline: none;
  border-color: hsl(var(--ring));
  box-shadow: 0 0 0 3px hsl(var(--ring) / 0.16);
}

.rewrite-bar button {
  flex: none;
  padding: 10px 14px;
  border: 0;
  border-radius: 10px;
  background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-strong)));
  color: hsl(var(--primary-foreground));
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.rewrite-bar button:disabled { opacity: 0.55; cursor: default; }

.inbox-view { padding: 8px 14px; display: grid; gap: 6px; }
.inbox-view[hidden] { display: none; }
/* 4 + the search's 6px top = the same 10px as between search and the chips
   below it. */
#client-view { padding-bottom: 4px; }
.inbox-view label { font-size: 12px; font-weight: 600; }
.inbox-view select, .inbox-view input[type="search"] { width: 100%; min-width: 0; padding: 8px; border: 1px solid hsl(var(--border)); border-radius: 8px; background: hsl(var(--background)); color: inherit; font: inherit; }
.inbox-view small { font-size: 11px; opacity: .65; }

/* Native-agent toggle: green while on, matching the .saved confirmation
   colour the rest of the admin controls use. */
button.native-on {
  border-color: hsl(142 55% 40%);
  color: hsl(142 55% 40%);
}


/*
 * iOS zoom, properly this time.
 *
 * The earlier version of this rule was `input, select, textarea` inside the
 * phone block, and it lost: `.search-input` and `.find-input` are class
 * selectors that also come later in the file, so a bare element selector
 * never had a chance. The page still zoomed on the sidebar search, which is
 * exactly what it was written to stop.
 *
 * Last block in the file, and specific enough to win. Anything focusable that
 * is added later needs to be listed here too - under 16px, iOS zooms the page
 * in on focus and never zooms back out.
 */
@media (max-width: 860px) {
  input,
  select,
  textarea,
  .search-input,
  .find-input,
  .chat-order select,
  .view-pop select,
  .view-pop input,
  .inbox-view select,
  .inbox-view input,
  .inbox-view input[type="search"],
  #agent-search,
  .composer textarea {
    font-size: 16px;
  }
}

/* ---------- Contact support ---------- */

.op-form textarea {
  background: hsl(var(--background));
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
  padding: 10px 12px;
  color: hsl(var(--foreground));
  font: inherit;
  font-size: 16px; /* stops iOS zooming the page on focus */
  width: 100%;
  resize: vertical;
  min-height: 7em;
}
.support-thumbs { display: flex; gap: 8px; flex-wrap: wrap; }
.support-thumbs:empty { display: none; }
/* "Close my account" / "Delete my data" in the Support form: what happens
   next, above the request. */
.support-close-account { display: grid; gap: 8px; }
.support-close-account[hidden] { display: none; }
.support-legal { margin-top: 4px; }
.support-legal a { color: hsl(var(--primary)); }
/* The legal pages, read inside the app. */
.legal-body { line-height: 1.55; max-width: 72ch; }
.legal-body h1 { font-size: 1.3em; margin: 0 0 8px; }
.legal-body h2 { font-size: 1.05em; margin: 18px 0 6px; }
.legal-body p, .legal-body li { color: hsl(var(--foreground)); }
.legal-body a { color: hsl(var(--primary)); }
.legal-body img { max-width: 100%; max-height: 48px; width: auto; }
.support-thumbs figure { position: relative; margin: 0; }
.support-thumbs img {
  width: 72px; height: 72px; object-fit: cover;
  border-radius: var(--radius); border: 1px solid hsl(var(--border));
}
.support-thumbs button {
  position: absolute; top: -6px; right: -6px;
  width: 22px; height: 22px; padding: 0; border-radius: 50%;
  line-height: 20px; font-size: 14px;
}
.support-context summary {
  cursor: pointer; font-size: 12px; color: hsl(var(--muted-foreground));
  margin-top: 6px;
}
.support-context pre {
  white-space: pre-wrap; word-break: break-word;
  font-size: 11px; line-height: 1.45; max-height: 30vh; overflow: auto;
  background: hsl(var(--muted)); border-radius: var(--radius);
  padding: 8px 10px; margin: 6px 0 0;
}

/* Another person in a group: white initials on their speaker colour (set
   inline), the same as an agent. At the end so it beats .msg.user .role,
   which would otherwise grey the text on the coloured circle. */
.msg .role.person {
  background: hsl(var(--primary));
  color: #fff;
}

/* A document shown in the preview panel fills it edge to edge. */
.preview-body.framed { padding: 0; overflow: hidden; display: flex; }
.preview-frame { flex: 1; width: 100%; height: 100%; border: 0; background: #fff; }
/* A phone's document: its pages as pictures, one under another. */
.preview-body.paged { padding: 10px; background: hsl(var(--muted)); display: flex; flex-direction: column; gap: 10px; }
.preview-page { width: 100%; height: auto; display: block; background: #fff; border-radius: 4px; box-shadow: 0 1px 4px hsl(0 0% 0% / 0.15); }

/* The model an agent is running on: beside its name in the header (now) and
   on each of its messages (the model that wrote that one). Quiet on purpose --
   it answers "what is this" when you look, and stays out of the way otherwise. */
.conv-title-row {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.conv-title-row .conv-title { flex: 0 1 auto; min-width: 0; }
/* Under the name: the model and fleet-number tags, then the status. Its own
   line so the name never loses width to them. Collapses to nothing when every
   part is hidden or empty (a plain chat). */
.conv-meta-row {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  margin-top: 2px;
}
.conv-meta-row:not(:has(> :not([hidden]):not(:empty))) { display: none; }
.conv-meta-row .conv-subtitle { flex: 1 1 auto; min-width: 0; }
/* The agent's number in a chat-list row: a small grey tag, the same as the
   #45 in the chat header, so it never reads as part of the name above it.
   Scoped under .conv-item to outrank the shared .conv-item span { flex: 1 },
   which otherwise stretched the tag into a bar across half the row. */
.conv-item .conv-num {
  flex: none;
  font-size: 10.5px;
  font-weight: 500;
  line-height: 1.5;
  padding: 0 6px;
  margin-right: 6px;
  border-radius: 999px;
  color: hsl(var(--muted-foreground));
  background: hsl(var(--muted));
  border: 1px solid hsl(var(--border));
  white-space: nowrap;
}

/* The brain pill in a list row. Same amber family as the header's switch, so
   a bot left on the coding plan is noticed in the list, not only in the chat.
   It rides the TITLE line (Jon 2026-09-26: on the status line it squeezed the
   status text out to "C…" — "things getting hidden"), tight after the name;
   the time keeps the right edge via its own margin-left:auto. */
.conv-item .conv-top .conv-brain { margin-right: 0; }
.conv-item .conv-brain.fallback {
  color: hsl(32 90% 38%);
  border-color: hsl(32 90% 50% / 0.55);
  background: hsl(32 90% 50% / 0.12);
}
[data-theme="dark"] .conv-item .conv-brain.fallback { color: hsl(36 95% 65%); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .conv-item .conv-brain.fallback { color: hsl(36 95% 65%); }
}
.conv-item .conv-brain.pending { opacity: 0.75; font-style: italic; }

.conv-light {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: hsl(142 65% 42%);
  box-shadow: 0 0 0 3px hsl(142 65% 42% / 0.18);
}
.conv-light[data-tone="busy"] { background: hsl(38 92% 50%); box-shadow: 0 0 0 3px hsl(38 92% 50% / 0.18); }
.conv-light[data-tone="bad"] { background: hsl(var(--destructive)); box-shadow: 0 0 0 3px hsl(var(--destructive) / 0.18); }
.conv-light[data-tone="off"] { background: hsl(var(--muted-foreground) / 0.6); box-shadow: none; }
/* The brain switch: a model-tag that is a button. Amber while on the coding
   plan, so a bot left on the fallback is noticed rather than forgotten. */
.model-tag.brain-tag {
  font: inherit;
  font-size: 10.5px;
  font-weight: 600;
  cursor: pointer;
  color: hsl(var(--foreground));
}
.model-tag.brain-tag.fallback {
  color: hsl(32 90% 38%);
  border-color: hsl(32 90% 50% / 0.55);
  background: hsl(32 90% 50% / 0.12);
}
[data-theme="dark"] .model-tag.brain-tag.fallback { color: hsl(36 95% 65%); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .model-tag.brain-tag.fallback { color: hsl(36 95% 65%); }
}
#brain-btn.fallback {
  border-color: hsl(32 90% 50% / 0.7);
  background: hsl(32 90% 50% / 0.16);
}
.model-tag.brain-tag.pending { opacity: 0.75; font-style: italic; }
.model-tag {
  flex: none;
  font-size: 10.5px;
  font-weight: 500;
  line-height: 1.5;
  padding: 0 7px;
  border-radius: 999px;
  color: hsl(var(--muted-foreground));
  background: hsl(var(--muted));
  border: 1px solid hsl(var(--border));
  white-space: nowrap;
  max-width: 40vw;
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-model { font-size: 10px; opacity: 0.9; }

/* A reply from a different model than the same agent's previous one. Loud on
   purpose: the whole row, not just the tag, so a brain change mid-conversation
   is unmistakable. Placed last so it wins over .msg.assistant's card colour. */
.msg.model-changed,
.msg.assistant.model-changed {
  background: hsl(var(--destructive) / 0.18);
  box-shadow: inset 4px 0 0 hsl(var(--destructive)), inset -4px 0 0 hsl(var(--destructive));
}
.msg.model-changed .msg-model {
  color: #fff;
  background: hsl(var(--destructive));
  border-color: hsl(var(--destructive));
  font-weight: 600;
  max-width: none;
}

/* --- Markdown tables in assistant messages --- */
/* render() turns a |---| block into this. The wrap lets a wide table scroll
   sideways inside the bubble instead of stretching the chat column. */
.msg .body .tbl-wrap { overflow-x: auto; max-width: 100%; margin: 6px 0; }
.msg .body table.md-table {
  border-collapse: collapse;
  font-size: calc(13px * var(--msg-scale, 1));
  line-height: 1.45;
  white-space: normal;
  max-width: 100%;
}
.msg .body .md-table th,
.msg .body .md-table td {
  border: 1px solid hsl(var(--border));
  padding: 5px 10px;
  text-align: left;
  vertical-align: top;
}
.msg .body .md-table th { background: hsl(var(--muted) / 0.6); font-weight: 600; }
.msg .body .md-table tbody tr:nth-child(even) { background: hsl(var(--muted) / 0.3); }

/* The kind pill lives on the title line now. It was a column of its own down
   the right, which on a phone was mostly empty space and squeezed the name
   and the stats into the middle (Jon 2026-09-26). All three words show at
   every width: Direct was briefly hidden on phones as "the default", and Jon
   wanted it back the same day. */
.conv-item .conv-top .conv-kind-word {
  flex: 0 0 auto;
  align-self: center;
  margin-left: 2px;
  padding: 1px 6px;
}

/* ==========================================================================
   The Telegram pass (Jon 2026-09-26: "make our UI more slick — like the real
   telegram"). Last in the file on purpose: the list rules above are layered
   and several set the same property unmediated, so this section wins by
   position rather than by an arms race of specificity.
   ========================================================================== */

/* --- Tabs: one swipeable row each for clients and kinds ------------------ */

/* Two rows of pills that scroll sideways instead of wrapping into six. A
   twenty-client roster now costs one line of height, like Telegram's folders. */
.chat-filter,
#client-filter.chat-filter {
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  gap: 6px;
}
.chat-filter::-webkit-scrollbar { display: none; }
.chat-filter .chip { flex: 0 0 auto; min-width: 0; padding: 6px 12px; white-space: nowrap; }
#client-view.inbox-view { padding: 8px 12px 0; }
#client-view.inbox-view #client-filter { margin: 0 -12px; padding: 0 12px; }
.sidebar-tools > .chat-filter { padding: 8px 12px 8px; }

/* The kind row reads as Telegram's folder tabs: words, no outlines, the
   chosen one underlined in the accent colour. */
.sidebar-tools > .chat-filter .chip {
  border: 0;
  border-radius: 0;
  background: transparent;
  padding: 6px 8px 7px;
  font-size: 14px;
  font-weight: 500;
  box-shadow: inset 0 -2px 0 transparent;
}
.sidebar-tools > .chat-filter .chip[aria-pressed="true"] {
  background: transparent;
  color: hsl(var(--primary));
  font-weight: 600;
  box-shadow: inset 0 -2px 0 hsl(var(--primary));
}

/* Order, as the last tab: a ⇅ with the native select laid invisibly over it,
   so a tap opens the platform's own picker. */
.sidebar-tools .chat-filter .chat-order {
  position: relative;
  flex: 0 0 auto;
  margin: 0 0 0 auto;
  display: grid;
  place-items: center;
  width: 34px;
  color: hsl(var(--muted-foreground));
  font-size: 16px;
}
.sidebar-tools .chat-filter .chat-order select {
  position: absolute;
  inset: 0;
  width: 100%;
  opacity: 0;
  font-size: 16px;
  cursor: pointer;
}

/* Only the search folds away now; the tabs always show. */
.sidebar-tools.collapsed { display: block; }
.sidebar-tools.collapsed .search-wrap { display: none; }
.sidebar-tools { border-bottom: 1px solid hsl(var(--border) / 0.7); }

/* --- Rows ----------------------------------------------------------------- */

.sidebar { background: hsl(var(--background)); }
.conv-list { padding: 0 0 8px; }
.conv-item,
.conv-item:hover,
.conv-item.active { border-radius: 0; margin: 0; }
.conv-item { background: hsl(var(--background)); }
/* Pinned rows: a light blue band, so the pinned block reads as its own group
   at the top. It was a faint grey, and Jon took the pins (kept in pin order)
   for a broken "most recent" sort (2026-10-04). Blue, not the orange of the
   open chat, so the two never look alike. Hover and the open chat still win.
   Mixed, not translucent: the row must stay opaque over its swipe actions. */
.conv-item { --pinned-bg: color-mix(in srgb, hsl(199 89% 48%) 14%, hsl(var(--background))); }
:root[data-theme="dark"] .conv-item { --pinned-bg: color-mix(in srgb, hsl(199 89% 48%) 20%, hsl(var(--background))); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .conv-item { --pinned-bg: color-mix(in srgb, hsl(199 89% 48%) 20%, hsl(var(--background))); }
}
.conv-item.pinned { background: var(--pinned-bg); }
@media (hover: hover) { .conv-item:hover { background: hsl(var(--muted) / 0.5); } }
.conv-item.active { background: hsl(var(--primary) / 0.12); }
.conv-slide { position: relative; padding: 9px 12px; gap: 12px; }

/* A hairline between rows that starts where the text does, not under the
   avatar — the detail that most makes a list read as Telegram's. */
.conv-slide::after {
  content: '';
  position: absolute;
  left: calc(12px + 12px + 54px * var(--list-scale, 1));
  right: 0;
  bottom: 0;
  height: 1px;
  background: hsl(var(--border) / 0.75);
}
.conv-item:last-child .conv-slide::after { display: none; }

/* Avatars: bigger, and all round — a group's square corners are one more
   thing to decode, and the badge on the avatar already says "group". */
.conv-avatar,
.conv-avatar.group {
  width: calc(54px * var(--list-scale, 1));
  height: calc(54px * var(--list-scale, 1));
  border-radius: 50%;
  font-size: calc(17px * var(--list-scale, 1));
}
.conv-avatar.emoji { font-size: calc(28px * var(--list-scale, 1)); }

.conv-label { gap: 2px; }
.conv-top { align-items: center; gap: 6px; }
.conv-item .conv-top .conv-name {
  font-size: calc(16px * var(--list-scale, 1));
  font-weight: 600;
  /* Two rows, so a long name is read in full rather than cut to "LOCAL…"
     beside empty space (Jon, 2026-09-29: "the full name of the bots should be
     visible, even if it goes over two lines"). */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  white-space: normal;
  color: hsl(var(--foreground));
}
/* One line when everything fits: name, number, brain pill, time at the right.
   Only when it cannot (a long name, a narrow phone) do the tags wrap to a
   second line under the name (Jon, 2026-09-29: "it should only spill over if
   it has to"). */
.conv-item .conv-top { flex-wrap: wrap; gap: 2px 6px; }
.conv-item .conv-top .conv-name { max-width: 100%; }
.conv-item .conv-top:has(.conv-pin, .conv-desk) .conv-name { max-width: calc(100% - 28px); }
/* `.conv-item span { flex: 1; overflow: hidden }` outranks the plain .conv-pin
   rule, and once the tags group grew to fill the line it squeezed the pin to a
   sliver (Jon, 2026-09-29: "the pin is getting hidden"). */
.conv-item .conv-top .conv-pin,
.conv-item .conv-top .conv-desk { flex: 0 0 auto; overflow: visible; }
/* Kind, brain pill and time as one group: on the name's line when it all fits,
   else dropped under the name as a whole, the time at the right edge. */
.conv-item .conv-top .conv-tags {
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  gap: 6px;
  min-width: 0;
}
.conv-item .conv-top .conv-time {
  font-size: calc(13px * var(--list-scale, 1));
  margin-left: auto;
}
.conv-item.unread .conv-top .conv-time { color: hsl(var(--primary)); }

/* The kind: a faint word, not a pill. */
.conv-item .conv-top .conv-kind-word {
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  background: none;
  font-size: calc(11px * var(--list-scale, 1));
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: none;
  color: hsl(var(--muted-foreground));
  opacity: 0.75;
  margin-left: 0;
}

/* Preview and badge share the second line; the badge sits level with it. */
.conv-item .conv-bottom { align-items: center; gap: 8px; }
.conv-item .conv-bottom .conv-preview {
  flex: 1 1 auto;
  min-width: 0;
  margin-top: 0;
  font-size: calc(14.5px * var(--list-scale, 1));
  line-height: 1.3;
}
.conv-item .conv-preview.empty { font-style: italic; opacity: 0.7; }
.conv-item .conv-badge {
  align-self: center;
  min-width: calc(22px * var(--list-scale, 1));
  height: calc(22px * var(--list-scale, 1));
  line-height: calc(22px * var(--list-scale, 1));
  border-radius: 999px;
  padding: 0 7px;
  font-size: calc(12.5px * var(--list-scale, 1));
}
.conv-item .conv-badge.quiet { background: hsl(var(--muted-foreground) / 0.55); }

/* Live status: only while a bot works or something is wrong. */
.conv-item .conv-sub {
  flex: none;
  font-size: calc(12.5px * var(--list-scale, 1));
  color: hsl(var(--muted-foreground));
}
.conv-item .conv-sub[hidden] { display: none; }
.conv-item .conv-sub[data-bot-state="thinking"],
.conv-item .conv-sub[data-bot-state="tool"] { color: hsl(38 92% 50%); }
.conv-item .conv-sub[data-bot-state="stalled"] { color: hsl(0 72% 55%); font-weight: 600; }

/* Touch feedback: a brief highlight while the finger is down. */
@media (hover: none) {
  .conv-item:active .conv-slide { background: hsl(var(--muted) / 0.6); }
}

/* --- Swipe actions ------------------------------------------------------ */

.conv-actions { display: none; }
@media (hover: none) and (pointer: coarse) {
  .conv-actions {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 0;
    display: flex;
  }
  .conv-action {
    width: 78px;
    border: 0;
    color: #fff;
    font: inherit;
    font-size: 14px;
    font-weight: 600;
  }
  .conv-action.read { background: hsl(var(--primary)); }
  .conv-action.more { background: hsl(215 12% 50%); }
  /* Not painted until the row is being swiped or is open. The slide covering
     them is its own compositing layer, and iOS WebKit sometimes shows a frame
     without it — worst under the tab bar's backdrop blur — so the grey ⋯ block
     flickered through at the right edge of the list (Jon, 2026-09-27). */
  .conv-actions { visibility: hidden; }
  .conv-item.swiping .conv-actions,
  .conv-item.swiped .conv-actions { visibility: visible; }
  .conv-slide {
    background: hsl(var(--background));
    transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  .conv-item.pinned .conv-slide { background: var(--pinned-bg); }
}

/* --- Header, tab bar and settings sheet on a phone ---------------------- */

.tabbar, .foot-scrim { display: none; }

@media (max-width: 860px) {
  /* The 🔍 and + moved down to the tab bar, where the thumb is. */
  .sidebar-head #tools-toggle,
  .sidebar-head #new-chat { display: none; }
  .sidebar-head { padding: 10px 14px; border-bottom: 0; }

  body:is([data-view="list"], [data-split]) .tabbar {
    display: flex;
    position: fixed;
    left: 14px;
    right: 14px;
    bottom: calc(10px + env(safe-area-inset-bottom));
    z-index: 45;
    height: 62px;
    padding: 0 6px;
    border-radius: 31px;
    background: hsl(var(--card) / 0.82);
    -webkit-backdrop-filter: blur(18px) saturate(1.4);
    backdrop-filter: blur(18px) saturate(1.4);
    border: 1px solid hsl(var(--border) / 0.8);
    box-shadow: 0 8px 30px hsl(0 0% 0% / 0.35);
    align-items: center;
    justify-content: space-around;
  }
  .tab {
    position: relative;
    flex: 1;
    height: 52px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    border: 0;
    border-radius: 26px;
    background: transparent;
    color: hsl(var(--muted-foreground));
    font: inherit;
    cursor: pointer;
  }
  .tab.active { color: hsl(var(--primary)); background: hsl(var(--primary) / 0.12); }
  .tab-icon { font-size: 20px; line-height: 1; }
  .tab-label { font-size: 11px; font-weight: 600; }
  .tab-badge {
    position: absolute;
    top: 3px;
    left: calc(50% + 6px);
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    border-radius: 10px;
    background: hsl(0 78% 55%);
    color: #fff;
    font-size: 11.5px;
    font-weight: 700;
    line-height: 20px;
    text-align: center;
    font-variant-numeric: tabular-nums;
  }
  .tab-badge[hidden] { display: none; }

  /* Room under the last row for the floating bar. */
  body:is([data-view="list"], [data-split]) .conv-list { padding-bottom: calc(92px + env(safe-area-inset-bottom)); }

  /* The footer becomes the Settings sheet. */
  body:is([data-view="list"], [data-split]) .sidebar-foot { display: none; }
  body:is([data-view="list"], [data-split]) .sidebar.foot-open .sidebar-foot {
    display: flex;
    position: fixed;
    left: 14px;
    right: 14px;
    bottom: calc(84px + env(safe-area-inset-bottom));
    z-index: 44;
    padding: 16px 18px;
    gap: 14px 18px;
    border: 1px solid hsl(var(--border));
    border-radius: 20px;
    background: hsl(var(--card));
    box-shadow: 0 10px 34px hsl(0 0% 0% / 0.4);
    font-size: 16px;
  }
  body:is([data-view="list"], [data-split]) .sidebar.foot-open .sidebar-foot .who { display: block; flex-basis: 100%; order: 0; font-size: 13px; color: hsl(var(--muted-foreground)); }
  .foot-scrim:not([hidden]) {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 43;
    background: hsl(0 0% 0% / 0.35);
  }
}
/* The slide carries the row's padding now, at every width. */
.conv-item { padding: 0; }
/* Every row says what it is, at every width (the older container rule hid
   the kind below a 420px sidebar — i.e. on most phones). As a faint word it
   costs a few characters, not a pill's worth. */
.sidebar .conv-item .conv-top .conv-kind-word { display: inline; color: hsl(var(--muted-foreground)); }
.sidebar .conv-item .conv-top .conv-kind-word.alert { color: hsl(38 70% 50% / 0.9); }

/* Search with its Cancel, as Telegram: the box takes the row, Cancel sits at
   its end. Cancel shows only where the box can be folded again — on a phone,
   where the bottom bar's Search opened it. */
.search-wrap { display: flex; align-items: center; gap: 10px; }
.search-wrap .search-input { flex: 1; min-width: 0; }
.search-cancel { display: none; flex: none; font-size: 15px; color: hsl(var(--primary)); }
@media (max-width: 860px) {
  .search-cancel { display: inline-block; }
}

/* Hide-keyboard roundal: floats just above the composer's right edge while
   something is being typed on a touch screen, and nowhere else. */
.kb-down { display: none; }
@media (hover: none) and (pointer: coarse) {
  html[data-kb="open"] .kb-down {
    display: grid;
    place-items: center;
    position: absolute;
    /* Left: the jump-to-latest arrow owns the right-hand corner. */
    left: 12px;
    top: -52px;
    z-index: 6;
    width: 42px;
    height: 42px;
    padding: 0 0 6px;
    border: 1px solid hsl(var(--border));
    border-radius: 50%;
    background: hsl(var(--card) / 0.92);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    box-shadow: 0 4px 14px hsl(0 0% 0% / 0.3);
    color: hsl(var(--foreground));
    font-size: 26px;
    line-height: 1;
    cursor: pointer;
  }
}

/* The viewer's Save / × were white on 16% white: over a light screenshot that
   filled the screen they vanished (Jon 2026-09-26, light mode). A dark disc
   with a light ring reads over any image; and the image now starts below the
   bar rather than under it. */
.lightbox-btn {
  background: hsl(0 0% 0% / 0.6);
  border: 1px solid hsl(0 0% 100% / 0.35);
  box-shadow: 0 2px 10px hsl(0 0% 0% / 0.35);
}
.lightbox-btn:hover { background: hsl(0 0% 0% / 0.75); }
.lightbox { padding-top: calc(max(12px, env(safe-area-inset-top)) + 52px); }
.lightbox-count {
  padding: 0 12px;
  border-radius: 999px;
  background: hsl(0 0% 0% / 0.6);
  color: #fff;
}
/* ==========================================================================
   The Telegram pass, part 2: the message view (Jon 2026-09-26). Bubbles
   instead of full-width rows, runs per speaker, time inside the bubble, a
   patterned background. Scoped under #messages so it outranks the layered
   row rules above without an arms race; last in the file for the same reason.
   ========================================================================== */

:root {
  --chat-bg: 210 22% 88%;
  --bubble-in: 0 0% 100%;
  --bubble-in-fg: var(--foreground);
  --bubble-out: 212 90% 52%;
  --bubble-out-fg: 0 0% 100%;
  --chat-doodle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' fill='none' stroke='%23000' stroke-opacity='.07' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='22' cy='24' r='9'/%3E%3Cpath d='M70 14l4 8 9 1-7 6 2 9-8-5-8 5 2-9-7-6 9-1z'/%3E%3Cpath d='M118 20h26a6 6 0 0 1 6 6v14a6 6 0 0 1-6 6h-16l-8 7v-7h-2a6 6 0 0 1-6-6V26a6 6 0 0 1 6-6z'/%3E%3Cpath d='M16 86c8-12 22-12 30 0'/%3E%3Cpath d='M20 98h22'/%3E%3Crect x='66' y='72' width='26' height='20' rx='4'/%3E%3Cpath d='M66 78h26'/%3E%3Cpath d='M126 80a12 12 0 1 0 24 0a12 12 0 1 0-24 0zM138 74v6l4 3'/%3E%3Cpath d='M26 132l10 10 18-20'/%3E%3Cpath d='M84 124c0-6 10-6 10 0 0 6-10 12-10 12s-10-6-10-12c0-6 10-6 10 0z'/%3E%3Cpath d='M124 130h28M138 116v28'/%3E%3C/svg%3E");
}
[data-theme="dark"] {
  --chat-bg: 216 28% 8%;
  --bubble-in: 215 20% 17%;
  --bubble-in-fg: 0 0% 96%;
  --bubble-out: 217 70% 45%;
  --bubble-out-fg: 0 0% 100%;
  --chat-doodle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' fill='none' stroke='%23fff' stroke-opacity='.045' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='22' cy='24' r='9'/%3E%3Cpath d='M70 14l4 8 9 1-7 6 2 9-8-5-8 5 2-9-7-6 9-1z'/%3E%3Cpath d='M118 20h26a6 6 0 0 1 6 6v14a6 6 0 0 1-6 6h-16l-8 7v-7h-2a6 6 0 0 1-6-6V26a6 6 0 0 1 6-6z'/%3E%3Cpath d='M16 86c8-12 22-12 30 0'/%3E%3Cpath d='M20 98h22'/%3E%3Crect x='66' y='72' width='26' height='20' rx='4'/%3E%3Cpath d='M66 78h26'/%3E%3Cpath d='M126 80a12 12 0 1 0 24 0a12 12 0 1 0-24 0zM138 74v6l4 3'/%3E%3Cpath d='M26 132l10 10 18-20'/%3E%3Cpath d='M84 124c0-6 10-6 10 0 0 6-10 12-10 12s-10-6-10-12c0-6 10-6 10 0z'/%3E%3Cpath d='M124 130h28M138 116v28'/%3E%3C/svg%3E");
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --chat-bg: 216 28% 8%;
    --bubble-in: 215 20% 17%;
    --bubble-in-fg: 0 0% 96%;
    --bubble-out: 217 70% 45%;
    --bubble-out-fg: 0 0% 100%;
    --chat-doodle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' fill='none' stroke='%23fff' stroke-opacity='.045' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='22' cy='24' r='9'/%3E%3Cpath d='M70 14l4 8 9 1-7 6 2 9-8-5-8 5 2-9-7-6 9-1z'/%3E%3Cpath d='M118 20h26a6 6 0 0 1 6 6v14a6 6 0 0 1-6 6h-16l-8 7v-7h-2a6 6 0 0 1-6-6V26a6 6 0 0 1 6-6z'/%3E%3Cpath d='M16 86c8-12 22-12 30 0'/%3E%3Cpath d='M20 98h22'/%3E%3Crect x='66' y='72' width='26' height='20' rx='4'/%3E%3Cpath d='M66 78h26'/%3E%3Cpath d='M126 80a12 12 0 1 0 24 0a12 12 0 1 0-24 0zM138 74v6l4 3'/%3E%3Cpath d='M26 132l10 10 18-20'/%3E%3Cpath d='M84 124c0-6 10-6 10 0 0 6-10 12-10 12s-10-6-10-12c0-6 10-6 10 0z'/%3E%3Cpath d='M124 130h28M138 116v28'/%3E%3C/svg%3E");
  }
}

/* --- The transcript ------------------------------------------------------ */

#messages {
  background-color: hsl(var(--chat-bg));
  background-image: var(--chat-doodle);
  background-size: 160px 160px;
  padding: 8px 0 10px;
}

#messages .msg,
#messages .msg.assistant,
#messages .msg.user:not(.from-other) {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 6px;
  padding: 1px 10px;
  border: 0;
  background: transparent;
  box-shadow: none;
}
#messages .msg:not(.run-cont) { padding-top: 6px; }
#messages .msg.run-last { padding-bottom: 3px; }

/* Yours on the right. */
#messages .msg.user:not(.from-other) { flex-direction: row-reverse; }

/* The day, as a small floating pill. */
#messages .msg-day {
  flex: 0 0 100%;
  display: flex;
  justify-content: center;
  margin: 8px 0 10px;
  order: -1;
}
#messages .msg-day span {
  padding: 4px 11px;
  border-radius: 999px;
  background: hsl(0 0% 0% / 0.28);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
}

/* Avatars: only in groups, only beside the last bubble of someone else's run
   (the slot is kept on the others so the bubbles line up). */
#messages .msg .role { display: none; }
#messages.in-group .msg.from-other .role {
  display: grid;
  visibility: hidden;
  flex: 0 0 32px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  align-self: flex-end;
  margin: 0;
  font-size: 12px;
}
#messages.in-group .msg.from-other .role.emoji { font-size: 18px; }
#messages.in-group .msg.from-other.run-last .role { visibility: visible; }

/* The bubble. */
#messages .msg .body {
  flex: 0 1 auto;
  min-width: 0;
  max-width: min(84%, 640px);
  display: flex;
  flex-direction: column;
  padding: 6px 11px 5px;
  border-radius: 18px;
  background: hsl(var(--bubble-in));
  color: hsl(var(--bubble-in-fg));
  box-shadow: 0 1px 1.5px hsl(0 0% 0% / 0.16);
  font-size: calc(16px * var(--msg-scale, 1));
  line-height: 1.42;
}
#messages .msg.user:not(.from-other) .body {
  background: hsl(var(--bubble-out));
  color: hsl(var(--bubble-out-fg));
}
/* Run shape: the corner facing the speaker's side tightens where the run
   continues, and the last bubble keeps the round tail corner. */
#messages .msg.run-cont .body { border-top-left-radius: 6px; }
#messages .msg:not(.run-last) .body { border-bottom-left-radius: 6px; }
#messages .msg.run-last .body { border-bottom-left-radius: 4px; }
#messages .msg.user:not(.from-other).run-cont .body { border-top-left-radius: 18px; border-top-right-radius: 6px; }
#messages .msg.user:not(.from-other):not(.run-last) .body { border-bottom-left-radius: 18px; border-bottom-right-radius: 6px; }
#messages .msg.user:not(.from-other).run-last .body { border-bottom-left-radius: 18px; border-bottom-right-radius: 4px; }

/* Inside the bubble: name, media, quote, text, then the footer. */
#messages .msg .body > .msg-head { order: 1; padding: 0; margin: 0 0 2px; min-height: 0; }
#messages .msg .body > * { order: 5; }
#messages .msg .body > .msg-text { order: 10; }
#messages .msg .body > .msg-foot { order: 50; }
#messages .msg .body > .msg-buttons { order: 60; }
#messages .msg .msg-head:empty,
#messages .msg.run-cont .msg-head { display: none; }
#messages .msg-sender { font-size: 14px; }
#messages .msg .body a { color: #1a6fd1; text-decoration: underline; overflow-wrap: anywhere; }
:root[data-theme='dark'] #messages .msg .body a { color: #7ab8ff; }
#messages .msg.user:not(.from-other) .body a { color: inherit; text-decoration: underline; }
#messages .msg .body pre { max-width: 100%; }
#messages .msg-images, #messages .msg-videos, #messages .msg-audios, #messages .msg-docs { margin: 2px 0 4px; }
#messages .msg-quote { margin-bottom: 4px; }

/* The footer: ▶ ⋯ … model · time, small, in the bottom-right corner. */
#messages .msg-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 6px;
  margin: 1px -3px 0 0;
  font-size: 12px;
  line-height: 1;
  opacity: 0.72;
}
#messages .msg-foot .msg-time {
  margin: 0;
  font-size: 12px;
  color: inherit;
  opacity: 1;
}
#messages .msg-foot .msg-model {
  font-size: 11px;
  padding: 1px 6px;
  border-radius: 999px;
  border: 0;
  background: hsl(0 0% 50% / 0.18);
  color: inherit;
}
#messages .msg-foot .msg-speak,
#messages .msg-foot .msg-more {
  position: static;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid currentColor;
  border-radius: 999px;
  background: transparent;
  color: inherit;
  opacity: 0.85;
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
}
/* ▶ then ⋯ on the left, model and time pushed to the right. */
#messages .msg-foot .msg-speak { order: 1; margin: 0; }
#messages .msg-foot .msg-more { order: 2; margin: 0 auto 0 0; }
#messages .msg-foot .msg-model { order: 3; }
#messages .msg-foot .msg-time { order: 4; }
#messages .msg-foot .msg-speak.playing { opacity: 1; background: hsl(0 0% 100% / 0.18); }

/* The desktop hover row is folded into the ⋯ menu: floating over a bubble it
   covered the text, and ⋯ (and right-click) reach every one of them. */
#messages .msg-reply,
#messages .msg-pin,
#messages .msg-fork,
#messages .msg-delete { display: none; }

/* The brain changed since this agent's previous reply: ring the bubble. */
#messages .msg.model-changed .body { box-shadow: 0 0 0 2px hsl(0 75% 55%); }
#messages .msg.model-changed .msg-foot .msg-model { background: hsl(0 75% 55%); color: #fff; }

/* Notes and errors stay centred and quiet. */
#messages .msg.sys-note .body {
  margin: 0 auto;
  background: hsl(0 0% 0% / 0.25);
  color: #fff;
  font-size: 13px;
  box-shadow: none;
}
#messages .msg.error .body { background: hsl(0 70% 50% / 0.18); color: hsl(var(--destructive)); }

/* A file run keeps its grouping; the bubble just follows along. */
#messages .msg.file-run-cont .role { visibility: hidden; }

/* --- Header and composer on a phone -------------------------------------- */

@media (max-width: 860px) {
  .topbar {
    background: hsl(var(--card) / 0.86);
    -webkit-backdrop-filter: blur(18px) saturate(1.4);
    backdrop-filter: blur(18px) saturate(1.4);
    border-bottom: 1px solid hsl(var(--border) / 0.6);
    padding: 8px 12px;
    /* Clear the status bar and notch. The shorthand above dropped the
       safe-area inset the earlier phone rule added, and the chat's name sat
       under the clock (Jon 2026-09-26). */
    padding-top: calc(8px + env(safe-area-inset-top));
    gap: 8px;
  }
  .topbar .ghost-btn { border-radius: 999px; }
  #back-btn { width: 38px; height: 38px; flex-basis: 38px; font-size: 24px; }
  .conv-heading { align-items: center; text-align: center; }
  .conv-title-row, .conv-meta-row { justify-content: center; }

  .composer-wrap {
    border-top: 0;
    background: hsl(var(--chat-bg));
  }
  .composer { padding: 6px 10px calc(8px + env(safe-area-inset-bottom)); gap: 8px; align-items: flex-end; }
  html[data-kb="open"] .composer { padding-bottom: 8px; }
  .composer textarea {
    border-radius: 22px;
    padding: 10px 16px;
    background: hsl(var(--card));
    border-color: hsl(var(--border) / 0.8);
  }
  .composer #attach-btn,
  .composer #paste-btn,
  .composer #autoplay-btn,
  .composer #clear-btn,
  .composer #secret-btn,
  .composer #mic-btn,
  .composer button[type='submit'] {
    width: 44px;
    height: 44px;
    flex: 0 0 44px;
    border-radius: 50%;
    background: hsl(var(--card));
  }
  .composer button[type='submit'] {
    background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-strong)));
  }
}

/* Time on the text's last line when it fits, as Telegram: the bubble is a
   wrapping row, every part but the text and footer takes a full line, and the
   footer sits at the end — beside a short message, under a long one. */
#messages .msg .body { flex-flow: row wrap; align-items: flex-end; column-gap: 10px; }
#messages .msg .body > * { flex: 0 0 100%; min-width: 0; }
#messages .msg .body > .msg-text { flex: 0 1 auto; }
#messages .msg .body > .msg-foot { flex: 0 0 auto; margin-left: auto; padding-top: 2px; }

/* ==========================================================================
   Floating composer and action sheets (Jon 2026-09-26: the message bar should
   float like Telegram's home-screen bar; the ⋯ pop-up was out of line with
   the rest of the app).
   ========================================================================== */
@media (max-width: 860px) {
  body[data-view="thread"] .main { position: relative; }
  body[data-view="thread"] .composer-wrap {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 5;
    background: transparent;
    border: 0;
    pointer-events: none;
  }
  body[data-view="thread"] .composer-wrap > * { pointer-events: auto; }
  body[data-view="thread"] #messages {
    padding-bottom: calc(var(--composer-h, 72px) + 6px);
    scroll-padding-bottom: var(--composer-h, 72px);
  }
  /* Glass, like the list's tab bar: each control floats on its own. */
  body[data-view="thread"] .composer textarea,
  body[data-view="thread"] .composer #attach-btn,
  body[data-view="thread"] .composer #paste-btn,
  body[data-view="thread"] .composer #autoplay-btn,
  body[data-view="thread"] .composer #clear-btn,
  body[data-view="thread"] .composer #secret-btn,
  body[data-view="thread"] .composer #mic-btn {
    background: hsl(var(--card) / 0.8);
    -webkit-backdrop-filter: blur(18px) saturate(1.4);
    backdrop-filter: blur(18px) saturate(1.4);
    border: 1px solid hsl(var(--border) / 0.8);
    box-shadow: 0 6px 22px hsl(0 0% 0% / 0.28);
  }
  body[data-view="thread"] .composer button[type='submit'] {
    box-shadow: 0 6px 22px hsl(0 0% 0% / 0.28);
  }
  body[data-view="thread"] .reply-bar,
  body[data-view="thread"] .tray {
    margin: 0 10px;
    border-radius: 16px;
    background: hsl(var(--card) / 0.9);
    -webkit-backdrop-filter: blur(18px);
    backdrop-filter: blur(18px);
    border: 1px solid hsl(var(--border) / 0.8);
  }
  /* The jump arrow sits above the floating bar, not behind it. */
  body[data-view="thread"] .jump-latest { z-index: 6; }
}

/* Action sheets (message ⋯ and chat long-press) as one grouped card with
   hairlines between the actions and Cancel as its own card below, on a dimmed
   and blurred backdrop — the iOS / Telegram sheet. */
.msg-menu:not(.popover),
.chat-menu.sheet {
  padding: 12px 10px calc(10px + env(safe-area-inset-bottom));
  gap: 0;
  background: hsl(0 0% 0% / 0.45);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.msg-menu:not(.popover) .msg-menu-panel {
  gap: 0;
  border-radius: 16px;
  overflow: hidden;
  background: hsl(var(--card));
  box-shadow: 0 10px 34px hsl(0 0% 0% / 0.4);
}
.msg-menu:not(.popover) .msg-menu-item,
.chat-menu.sheet .chat-menu-item {
  border-radius: 0;
  box-shadow: none;
  background: hsl(var(--card));
  padding: 15px 20px;
  font-size: 17px;
  text-align: left;
}
.msg-menu:not(.popover) .msg-menu-item + .msg-menu-item,
.chat-menu.sheet .chat-menu-item + .chat-menu-item {
  border-top: 1px solid hsl(var(--border) / 0.7);
}
.msg-menu:not(.popover) .msg-menu-item:active,
.chat-menu.sheet .chat-menu-item:active { background: hsl(var(--muted)); }
.chat-menu.sheet .chat-menu-item:first-of-type { border-top-left-radius: 16px; border-top-right-radius: 16px; }
.chat-menu.sheet .chat-menu-item:nth-last-child(2) { border-bottom-left-radius: 16px; border-bottom-right-radius: 16px; }
.msg-menu:not(.popover) .msg-menu-cancel,
.chat-menu.sheet .msg-menu-cancel {
  margin-top: 8px;
  border-radius: 16px;
  background: hsl(var(--card));
  color: hsl(var(--primary));
  font-weight: 600;
  padding: 15px;
  box-shadow: 0 10px 34px hsl(0 0% 0% / 0.4);
}
.msg-menu:not(.popover) .msg-menu-cancel:active,
.chat-menu.sheet .msg-menu-cancel:active { background: hsl(var(--muted)); }

/* Phone chat header, tidied (Jon 2026-09-26: "top of the screen still looks
   weird … the connected needs to be at least aligned"). The name gets its own
   centred line; under it ONE centred row holds the status dot, the brain, the
   model and the fleet number, all the same height and centred on each other,
   in that order. */
@media (max-width: 860px) {
  .topbar .conv-heading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    max-height: none;
    overflow: visible;
    padding: 0 4px;
  }
  .topbar .conv-title-row {
    display: flex;
    justify-content: center;
    max-width: 100%;
  }
  .topbar .conv-title {
    display: block;
    font-size: 16px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    -webkit-line-clamp: unset;
  }
  .topbar .conv-meta-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
    margin-top: 3px;
    max-width: 100%;
  }
  .topbar .conv-meta-row .model-tag,
  .topbar .conv-meta-row .brain-tag {
    display: inline-flex;
    align-items: center;
    height: 20px;
    margin: 0;
    padding: 0 7px;
    font-size: 11px;
    line-height: 1;
    vertical-align: middle;
  }
  .topbar .conv-meta-row .conv-light { order: 1; margin: 0 2px 0 0; }
  .topbar .conv-meta-row #conv-brain { order: 2; }
  .topbar .conv-meta-row #conv-model { order: 3; }
  .topbar .conv-meta-row #conv-num { order: 4; }
}

/* A bubble with a picture, video, file or quote sizes to its widest part.
   The inline-time row layout summed every part's width into one line's worth
   and left a photo floating in a bubble twice its size. */
#messages .msg .body:has(> .msg-images, > .msg-videos, > .msg-audios, > .msg-docs, > .msg-quote) {
  flex-flow: column nowrap;
  align-items: stretch;
}
#messages .msg .body:has(> .msg-images, > .msg-videos, > .msg-audios, > .msg-docs, > .msg-quote) > .msg-foot {
  align-self: flex-end;
}
/* When the row is wider than the heading, overflow to the right rather than
   pushing the dot under the back button. */
@media (max-width: 860px) {
  .topbar .conv-title-row, .topbar .conv-meta-row { justify-content: safe center; overflow: hidden; }
}

/* Header buttons the same size as the composer's (Jon 2026-09-26: "top icons
   need to be the same sizes as bottom icons"): 44px discs, top and bottom. */
@media (max-width: 860px) {
  .topbar .ghost-btn,
  .topbar #back-btn {
    width: 44px;
    height: 44px;
    flex: 0 0 44px;
    border-radius: 50%;
    font-size: 19px;
    display: grid;
    place-items: center;
    padding: 0;
    background: hsl(var(--card));
  }
  .topbar .ghost-btn[hidden] { display: none; }
  .topbar #back-btn { font-size: 26px; }
}

/* Reactions (Jon 2026-09-26): chips under the text, above the time. */
.msg-reactions { display: flex; flex-wrap: wrap; gap: 4px; margin: 4px 0 2px; }
.msg-reactions[hidden] { display: none; }
#messages .msg .body > .msg-reactions { order: 45; flex: 0 0 100%; }
.msg-reaction {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 28px;
  padding: 0 9px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: hsl(0 0% 50% / 0.2);
  color: inherit;
  font: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.msg-reaction.mine { background: hsl(var(--primary) / 0.28); border-color: hsl(var(--primary) / 0.7); }
#messages .msg.user:not(.from-other) .msg-reaction { background: hsl(0 0% 100% / 0.2); }
#messages .msg.user:not(.from-other) .msg-reaction.mine { background: hsl(0 0% 100% / 0.38); border-color: hsl(0 0% 100% / 0.7); }
.msg-reaction-count { font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
/* Who reacted: overlapping roundels after the emoji, Telegram style. */
.msg-reaction-faces { display: inline-flex; align-items: center; padding: 2px 0; }
.msg-reaction-face {
  width: 20px; height: 20px; border-radius: 50%;
  display: inline-grid; place-items: center;
  font-size: 9px; font-weight: 700; color: #fff; line-height: 1;
  border: 1.5px solid hsl(var(--card)); overflow: hidden;
}
.msg-reaction-faces .msg-reaction-face + .msg-reaction-face { margin-left: -6px; }
.msg-reaction-faces .msg-reaction-count { margin-left: 3px; }
/* The "who reacted" list. */
.reactors-panel { min-width: 220px; max-height: min(60vh, 420px); overflow-y: auto; }
.reactor-row { display: flex; align-items: center; gap: 10px; padding: 8px 14px; }
.reactor-row .msg-reaction-face { width: 28px; height: 28px; font-size: 12px; flex: 0 0 auto; }
.reactor-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reactor-emoji { font-size: 18px; }

/* The emoji row at the top of the message menu. */
.msg-menu-reacts {
  display: flex;
  justify-content: space-between;
  gap: 2px;
  padding: 8px 10px;
  background: hsl(var(--card));
  border-bottom: 1px solid hsl(var(--border) / 0.7);
}
.msg-menu-react {
  flex: 1;
  height: 44px;
  border: 0;
  border-radius: 12px;
  background: transparent;
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
}
.msg-menu-react:active, .msg-menu-react:hover { background: hsl(var(--muted)); }
.msg-menu.popover .msg-menu-reacts { padding: 4px; border-radius: 6px; }
.msg-menu.popover .msg-menu-react { height: 34px; font-size: 20px; }

/* ==========================================================================
   Icons, big emoji and motion (Jon 2026-09-27: "the icons don't look as slick
   as Telegram or WhatsApp"). Every control used to be a typed character or an
   emoji, each drawn by a different font at its own weight and baseline. They
   are now one SVG family (Lucide, sprite in index.html, icon() in app.js):
   one grid, one stroke, and the button's own colour.
   ========================================================================== */
.i {
  width: 1.2em;
  height: 1.2em;
  display: inline-block;
  vertical-align: -0.24em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* Clicks land on the button, so handlers that test e.target still match. */
  pointer-events: none;
  transition: transform 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
button > .i:only-child { display: block; margin: auto; }
/* Pressed: the icon dips and springs back. On the icon, not the button,
   because several buttons are already placed with transform. */
button:active > .i { transform: scale(0.82); transition-duration: 0.07s; }

.topbar #back-btn .i { width: 26px; height: 26px; stroke-width: 2.2; }
.composer button[type='submit'] .i { width: 22px; height: 22px; transform: translateX(1px); }
.composer button[type='submit']:active .i { transform: translateX(1px) scale(0.82); }
.tab-icon .i { width: 24px; height: 24px; }
.tab.active .tab-icon .i { fill: currentColor; fill-opacity: 0.18; }
.emoji-btn .i { width: 20px; height: 20px; }

/* Menu rows: a fixed-width icon column, so the labels line up. */
.msg-menu-item, .chat-menu-item { display: flex; align-items: center; gap: 14px; }
.msg-menu-item .i, .chat-menu-item .i { width: 20px; height: 20px; opacity: 0.8; }
/* :only-child ignores the label's text node, so the icon-button rule above
   would centre these with auto margins and shove the label right. */
.msg-menu-item > svg.i, .chat-menu-item > svg.i, .sel-copy > svg.i { display: inline-block; margin: 0; }
/* Eight reactions must fit the row (320px in the popover): no button padding. */
.msg-menu-react { padding: 0; min-width: 0; }
.msg-menu.popover .msg-menu-item .i, .chat-menu.at-pointer .chat-menu-item .i { width: 17px; height: 17px; }
.sel-copy { display: inline-flex; align-items: center; gap: 8px; }
.sel-copy .i { width: 16px; height: 16px; }

/* Emoji in the reaction row grow under the finger, as Telegram's do. */
.msg-menu-react { transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.15s; }
.msg-menu-react:active { transform: scale(1.3); }
@media (hover: hover) { .msg-menu-react:hover { transform: scale(1.2); } }

/* A reaction that just arrived pops in; the menu slides up rather than
   appearing. */
@keyframes pop-in {
  0%   { transform: scale(0.3); opacity: 0; }
  55%  { transform: scale(1.18); opacity: 1; }
  100% { transform: scale(1); }
}
.msg-reaction.pop { animation: pop-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes sheet-up { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes menu-in { from { transform: scale(0.94); opacity: 0; } to { transform: none; opacity: 1; } }
.msg-menu:not(.popover) .msg-menu-panel,
.msg-menu:not(.popover) .msg-menu-cancel { animation: sheet-up 0.26s cubic-bezier(0.2, 0.9, 0.3, 1.1); }
.msg-menu.popover .msg-menu-panel { transform-origin: top right; animation: menu-in 0.16s ease-out; }

/* One to three emoji and nothing else: large, with no bubble (see
   syncJumbo). The time keeps a small pill of its own so it stays readable on
   the wallpaper in both themes. */
#messages .msg.jumbo .body,
#messages .msg.user.jumbo:not(.from-other) .body {
  background: transparent;
  box-shadow: none;
  padding: 0 2px;
}
#messages .msg.jumbo .msg-text { font-size: 52px; line-height: 1.12; letter-spacing: 0.04em; }
#messages .msg.jumbo[data-emoji="2"] .msg-text { font-size: 44px; }
#messages .msg.jumbo[data-emoji="3"] .msg-text { font-size: 38px; }
#messages .msg.jumbo .msg-text p { margin: 0; }
#messages .msg.jumbo .msg-foot,
#messages .msg.jumbo .msg-foot > * { color: hsl(var(--muted-foreground)); }
#messages .msg.jumbo .msg-time {
  background: hsl(0 0% 0% / 0.38);
  color: #fff;
  border-radius: 999px;
  padding: 1px 8px;
}

@media (prefers-reduced-motion: reduce) {
  .i, .msg-menu-react { transition: none; }
  .msg-reaction.pop, .msg-menu-panel, .msg-menu-cancel { animation: none; }
}

/* Desktop: the conversation is a centred column, as Telegram Web's is (Jon
   2026-09-27: on a wide monitor every bubble sat against the left edge with
   the rest of the screen empty). The padding does the centring rather than a
   max-width, so the scrollbar and the wallpaper still span the whole pane. */
@media (min-width: 861px) {
  :root { --chat-col: 980px; }
  #messages { padding-inline: max(0px, calc((100% - var(--chat-col)) / 2)); }
  .composer { padding-inline: max(32px, calc((100% - var(--chat-col)) / 2 + 10px)); }
  .tray { padding-inline: max(32px, calc((100% - var(--chat-col)) / 2 + 10px)); }
  .reply-bar { margin-inline: max(0px, calc((100% - var(--chat-col)) / 2)); }
}


/* --- A list sized to the person (see listDensity in app.js) ---------------- */

/* Few chats: the rows alone. Filter tabs, order and search are for a fleet. */
body[data-density="few"] #sidebar-tools,
body[data-density="single"] #sidebar-tools,
body[data-density="few"] #tools-toggle,
body[data-density="single"] #tools-toggle,
body[data-density="few"] #tab-search,
body[data-density="single"] #tab-search { display: none; }

/* One chat: no list. The sidebar is about that chat instead. */
body[data-density="single"] #conv-list,
body[data-density="single"] #search-results { display: none; }

.room-panel {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 8px 16px 24px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.room-panel[hidden] { display: none; }

.room-card { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 6px; padding: 18px 0 6px; }
.room-card-av {
  width: 76px; height: 76px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 30px; font-weight: 600; color: #fff;
  background: hsl(var(--primary));
}
.room-card-av.emoji { font-size: 40px; }
.room-card h2 { margin: 6px 0 0; font-size: 18px; line-height: 1.25; overflow-wrap: anywhere; }
.room-card p { margin: 0; font-size: 13px; color: hsl(var(--muted-foreground)); }

.room-section h3 {
  display: flex; align-items: baseline; gap: 8px;
  margin: 0 0 6px; font-size: 11px; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: hsl(var(--muted-foreground));
}
.room-manage { margin-left: auto; font-size: 12px; text-transform: none; letter-spacing: 0; }
.room-empty { margin: 0; font-size: 13px; line-height: 1.45; color: hsl(var(--muted-foreground)); }

.room-row {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 7px 8px; margin: 0 -8px;
  border: 0; border-radius: 10px; background: none;
  color: inherit; font: inherit; text-align: left;
}
button.room-row { cursor: pointer; }
@media (hover: hover) { button.room-row:hover { background: hsl(var(--muted) / 0.5); } }
.room-av {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 14px; font-weight: 600; color: #fff;
  background: hsl(var(--muted-foreground) / 0.5);
}
.room-av.emoji { font-size: 18px; background: hsl(var(--muted)); }
.room-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.room-title, .room-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.room-title { font-size: 14px; }
.room-sub { font-size: 12px; color: hsl(var(--muted-foreground)); }
.room-tag { flex: 0 0 auto; font-size: 11px; color: hsl(var(--muted-foreground)); }

/* Phone only: the panel stands in for the thread there, so it offers the way back. */
.room-open {
  display: none; margin-top: 10px; padding: 10px 22px; border: 0; border-radius: 999px;
  background: hsl(var(--primary)); color: #fff; font: inherit; font-weight: 600; cursor: pointer;
}
@media (max-width: 860px) { .room-open { display: inline-block; } }

/* Admin preview banner (View as). Floats over whatever layout the previewed
   person gets, so the way back is never inside a panel that layout hides. */
.view-as-banner {
  position: fixed; z-index: 60; left: 50%; transform: translateX(-50%);
  /* Under the chat header, over the top of the thread: clear of every button. */
  top: calc(env(safe-area-inset-top, 0px) + 84px);
  display: flex; align-items: center; gap: 10px;
  max-width: calc(100vw - 24px);
  padding: 6px 6px 6px 14px; border-radius: 999px;
  background: hsl(36 95% 50%); color: #1a1300;
  font-size: 13px; font-weight: 600;
  box-shadow: 0 4px 16px hsl(0 0% 0% / 0.3);
}
.view-as-banner[hidden] { display: none; }
.view-as-banner span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.view-as-exit {
  flex: 0 0 auto; border: 0; border-radius: 999px; padding: 6px 12px;
  background: #1a1300; color: #fff; font: inherit; cursor: pointer;
}
/* On a phone it would sit over the chat header; drop it to the bottom edge,
   clear of the floating tab bar and composer. */
@media (max-width: 860px) {
  .view-as-banner { top: auto; bottom: calc(env(safe-area-inset-bottom, 0px) + 84px); }
}

/* View as a non-admin: nothing admin-only, whatever script revealed it. */
body.previewing:not(.admin-ui) #fleet-bar,
body.previewing:not(.admin-ui) #admin-view { display: none !important; }
/* Read-only preview: the composer cannot send as the admin. */
body.previewing .composer, body.previewing #composer { opacity: 0.55; pointer-events: none; }

/* --- Chat stats card ------------------------------------------------------ */
.chat-menu.sheet.stats-overlay { justify-content: flex-end; align-items: center; }
@media (min-width: 721px) { .chat-menu.sheet.stats-overlay { justify-content: center; } }
.stats-card {
  width: min(440px, 100%); max-height: 85vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 14px;
  padding: 18px; border-radius: 18px;
  background: hsl(var(--card)); color: hsl(var(--foreground));
  box-shadow: 0 16px 48px hsl(0 0% 0% / 0.45);
}
.stats-card .stats-head { display: flex; align-items: flex-start; gap: 12px; padding: 0; border: 0; }
.stats-card .stats-head > div { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.stats-title { font-size: 17px; overflow-wrap: anywhere; }
.stats-sub { font-size: 12px; color: hsl(var(--muted-foreground)); }
.stats-x { flex: 0 0 auto; }
.stats-tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.stats-tile {
  display: flex; flex-direction: column; gap: 2px; padding: 12px 10px;
  border-radius: 12px; background: hsl(var(--muted) / 0.6);
}
.stats-num { font-size: 24px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.stats-tile .stats-k { font-size: 12px; line-height: 1.3; }
.stats-card .stats-rows { display: flex; flex-direction: column; border-radius: 12px; background: hsl(var(--muted) / 0.35); }
.stats-card .stats-row {
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  padding: 10px 12px; margin: 0; border: 0; background: none; font-size: 14px;
}
.stats-card .stats-row + .stats-row { border-top: 1px solid hsl(var(--border) / 0.6); }
.stats-card .stats-v { font-weight: 600; display: inline-flex; align-items: center; gap: 6px; }
.stats-card .stats-row.warn .stats-v { color: hsl(0 75% 60%); }
.stats-label { margin-bottom: -8px; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: hsl(var(--muted-foreground)); }
.stats-dot { width: 8px; height: 8px; border-radius: 50%; background: hsl(var(--muted-foreground)); }
.stats-row.ok .stats-dot { background: hsl(142 70% 45%); }
.stats-row.warn .stats-dot { background: hsl(0 75% 55%); }
.stats-card .stats-k { color: hsl(var(--muted-foreground)); }
.stats-card .stats-v { font-variant-numeric: tabular-nums; text-align: right; }

/* Arrive, rather than appear: a slide up from the bottom on a phone, a soft
   scale-in where it sits centred on a desktop. */
.stats-card { animation: sheet-up 0.24s cubic-bezier(0.2, 0.9, 0.3, 1.05); }
@media (min-width: 721px) { .stats-card { animation: menu-in 0.16s ease-out; } }
.chat-menu.sheet.stats-overlay { animation: fade-in 0.16s ease-out; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .stats-card, .chat-menu.sheet.stats-overlay { animation: none; } }

/* --- The agent health light, every state coloured -------------------------
   A listening bot had no colour at all, so on an emoji avatar (where the badge
   shrinks to a bare light) it read as a dark square (Jon, 2026-09-27). */
.conv-kind.agent[data-bot-state="idle"] {
  box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(142 70% 42%);
}
.conv-kind.agent[data-bot-state="offline"] {
  box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(0 72% 51% / 0.8);
}
/* On an emoji avatar the badge IS the light: fill it. */
.conv-avatar.emoji + .conv-kind.agent[data-bot-state="idle"] { background: hsl(142 70% 42%); box-shadow: 0 0 0 2px hsl(var(--background)); }
.conv-avatar.emoji + .conv-kind.agent[data-bot-state="thinking"],
.conv-avatar.emoji + .conv-kind.agent[data-bot-state="tool"] { background: hsl(38 92% 50%); box-shadow: 0 0 0 2px hsl(var(--background)); }
.conv-avatar.emoji + .conv-kind.agent[data-bot-state="stalled"] { background: hsl(0 72% 51%); box-shadow: 0 0 0 2px hsl(var(--background)); }
.conv-avatar.emoji + .conv-kind.agent[data-bot-state="offline"] { background: hsl(0 72% 51%); opacity: 0.6; filter: none; box-shadow: 0 0 0 2px hsl(var(--background)); }
.conv-avatar.emoji + .conv-kind.agent[data-bot-state="notify"],
.conv-avatar.emoji + .conv-kind.agent[data-bot-state="unused"] { background: hsl(var(--muted-foreground) / 0.6); box-shadow: 0 0 0 2px hsl(var(--background)); }
.conv-item:hover .conv-avatar.emoji + .conv-kind.agent[data-bot-state],
.conv-item.active .conv-avatar.emoji + .conv-kind.agent[data-bot-state] { box-shadow: 0 0 0 2px hsl(var(--card)); }

/* The avatar wrapper is a span, so the row-wide `.conv-item span` truncation
   rule gave it overflow:hidden — and the health light, which sits a little
   past the avatar's corner, was cut into a square (Jon, 2026-09-27). */
.conv-item .conv-avatar-wrap { overflow: visible; }
.conv-avatar.emoji + .conv-kind[data-bot-state] { width: 14px; height: 14px; right: -2px; bottom: -2px; }

/* Groups wear the light of their bots (groupLight): the same colours. */
.conv-kind.group[data-bot-state="idle"] { box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(142 70% 42%); }
.conv-kind.group[data-bot-state="partial"] { box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(38 92% 50%); }
.conv-kind.group[data-bot-state="offline"] { box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(0 72% 51% / 0.8); }
.conv-avatar.emoji + .conv-kind.group[data-bot-state="idle"] { background: hsl(142 70% 42%); box-shadow: 0 0 0 2px hsl(var(--background)); }
.conv-avatar.emoji + .conv-kind.group[data-bot-state="partial"] { background: hsl(38 92% 50%); box-shadow: 0 0 0 2px hsl(var(--background)); }
.conv-avatar.emoji + .conv-kind.group[data-bot-state="offline"] { background: hsl(0 72% 51%); opacity: 0.6; box-shadow: 0 0 0 2px hsl(var(--background)); }
.conv-item:hover .conv-avatar.emoji + .conv-kind.group[data-bot-state],
.conv-item.active .conv-avatar.emoji + .conv-kind.group[data-bot-state] { box-shadow: 0 0 0 2px hsl(var(--card)); }

/* Support desk mark beside a group's name. */
.conv-desk { flex: 0 0 auto; font-size: 0.85em; line-height: 1; opacity: 0.85; }

/* --- 🚨 Critical alerts ---------------------------------------------------
   The header button counts what is broken right now (a site down). It stays
   visible with nothing open, so its absence never has to be interpreted. */
.critical-btn { position: relative; }
.critical-btn.hot {
  background: hsl(0 72% 51%);
  border-color: hsl(0 72% 51%);
  color: #fff;
  animation: critical-pulse 1.6s ease-in-out infinite;
}
@keyframes critical-pulse {
  0%, 100% { box-shadow: 0 0 0 0 hsl(0 72% 51% / 0.55); }
  50% { box-shadow: 0 0 0 6px hsl(0 72% 51% / 0); }
}
@media (prefers-reduced-motion: reduce) { .critical-btn.hot { animation: none; } }
.critical-count {
  position: absolute;
  top: -6px;
  right: -6px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: #fff;
  color: hsl(0 72% 45%);
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  box-shadow: 0 1px 4px hsl(0 0% 0% / 0.35);
}
.critical-count[hidden] { display: none; }

.critical-sheet {
  position: fixed;
  inset: 0;
  z-index: 65;
  display: grid;
  place-items: center;
  padding: 16px;
  padding-top: max(16px, env(safe-area-inset-top));
  padding-bottom: max(16px, env(safe-area-inset-bottom));
  background: hsl(0 0% 0% / 0.5);
}
.critical-sheet[hidden] { display: none; }
.critical-card {
  touch-action: pan-y;
  width: min(560px, 100%);
  max-height: min(80vh, 720px);
  display: flex;
  flex-direction: column;
  border: 1px solid hsl(var(--border));
  border-radius: 16px;
  background: hsl(var(--card));
  color: hsl(var(--foreground));
  box-shadow: 0 12px 40px hsl(0 0% 0% / 0.45);
  animation: sheet-up 0.24s cubic-bezier(0.2, 0.9, 0.3, 1.05);
}
.critical-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px 10px;
  border-bottom: 1px solid hsl(var(--border));
}
.critical-head h2 { margin: 0; font-size: 17px; }
.critical-list { overflow-y: auto; padding: 8px 8px 12px; }
.critical-section {
  margin: 10px 8px 4px;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: hsl(var(--muted-foreground));
}
.critical-section-row { display: flex; align-items: center; justify-content: space-between; }
.critical-dismiss-all, .critical-dismiss {
  border: 0; background: transparent; color: hsl(var(--muted-foreground));
  font: inherit; cursor: pointer; border-radius: 8px;
}
.critical-dismiss-all { padding: 4px 8px; text-transform: none; letter-spacing: 0; font-weight: 600; }
.critical-dismiss { flex: none; width: 40px; align-self: stretch; font-size: 16px; margin: 2px 0; }
.critical-dismiss-all:hover, .critical-dismiss:hover { background: hsl(var(--muted)); color: inherit; }
.critical-dismiss:disabled, .critical-dismiss-all:disabled { opacity: 0.5; }
.critical-row { display: flex; align-items: stretch; gap: 4px; }
.critical-row .critical-item { flex: 1; min-width: 0; }
.critical-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  text-align: left;
  padding: 10px 12px;
  margin: 2px 0;
  border: 0;
  border-left: 4px solid hsl(0 72% 51%);
  border-radius: 8px;
  background: hsl(0 72% 51% / 0.1);
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.critical-item.resolved { border-left-color: hsl(142 60% 40%); background: hsl(142 60% 40% / 0.08); }
.critical-item:active { filter: brightness(1.2); }
.critical-key { font-weight: 700; font-size: 15px; overflow-wrap: anywhere; }
.critical-meta { font-size: 12.5px; color: hsl(var(--muted-foreground)); }
.critical-empty { padding: 24px 12px; text-align: center; color: hsl(var(--muted-foreground)); }

.support-done { display: grid; gap: 10px; text-align: center; padding: 24px 8px; justify-items: center; }
.support-done[hidden] { display: none; }
.support-done-title { font-size: 18px; }
.support-done-ticket { margin: 0; font-size: 16px; font-weight: 600; }
.support-done-ticket[hidden] { display: none; }
.support-done .op-hint { margin: 0; }

/* --- Offline: banner and queued messages --- */

.offline-bar {
  margin: 0;
  padding: 7px 14px;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.35;
  text-align: center;
  color: hsl(38 92% 20%);
  background: hsl(43 96% 62%);
}
.offline-bar[hidden] { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .offline-bar { color: hsl(43 96% 80%); background: hsl(38 70% 22%); }
}
:root[data-theme="dark"] .offline-bar { color: hsl(43 96% 80%); background: hsl(38 70% 22%); }

.msg.queued .body { opacity: 0.72; }
.msg.queue-held .body { opacity: 0.6; }
.queue-note {
  order: 20;
  margin-top: 4px;
  font-size: 11.5px;
  color: inherit;
  opacity: 0.85;
}
.msg.queue-failed .queue-note,
.msg.queue-held .queue-note,
.msg.queued .queue-note {
  cursor: pointer;
}
.msg.queue-failed .queue-note,
.msg.queue-held .queue-note {
  opacity: 1;
  font-weight: 700;
  text-decoration: underline;
  cursor: pointer;
}

.conv-muted { width: 14px; height: 14px; flex: 0 0 auto; margin-left: 4px; opacity: 0.55; }
#mute-btn[aria-pressed="true"] { color: hsl(var(--primary)); }

/* Footer link to the desktop download page. */
#desktop-link:not([hidden]) { display: inline-flex; align-items: center; gap: 4px; text-decoration: none; }
#desktop-link .i { width: 14px; height: 14px; }

/* ==========================================================================
   Phone layout, tightened (Jon 2026-10-01, from Android screenshots): the
   group name was cut to "AICO…" by the buttons beside it, the messages sat
   behind a wide empty right gutter, and the one-line message box was squeezed
   between four round buttons. Last in the file on purpose: these win over the
   earlier phone blocks. iOS runs the same web UI, so it gets them too.
   ========================================================================== */
@media (max-width: 860px) {
  /* Messages use the width: the right gutter was sized to mirror the avatar
     column, which made every incoming bubble stop 44px short of the edge. */
  .msg { padding-right: 10px; }
  #messages .msg .body { max-width: 94%; }
  #messages.in-group .msg.from-other .body { max-width: calc(100% - 46px); }

  /* Header: back arrow, the name across all the room left, and ⋯ on the right.
     Nothing else lives in the bar (Jon 2026-10-01): read-aloud and /new are in
     the ⋯ sheet. A status pill, when there is one, wraps to a row under it. */
  .topbar { flex-wrap: wrap !important; row-gap: 0; column-gap: 8px; }
  .topbar::after { content: ''; order: 4; flex: 0 0 100%; height: 0; }
  .topbar > #menu-btn,
  .topbar > #back-btn { order: 1; }
  .topbar > .conv-heading { order: 2; flex: 1 1 0; min-width: 0; }
  .topbar > #more-btn { order: 3; }
  .topbar > .pill { order: 5; margin-top: 4px; }
  #unfold-btn { display: none !important; }
  /* The name wraps freely and sits centred on its own; the group/bot number
     and the model/brain tags are one centred row under it (Jon 2026-10-03:
     "the group number should be on the next line so we're centre aligned").
     Reverses the 2026-10-01 same-line rule. */
  .topbar .conv-heading { display: block; text-align: center; max-height: none; overflow: visible; }
  .topbar .conv-title-row { display: block; margin: 0; overflow: visible; }
  .topbar .conv-meta-row {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
    gap: 5px; margin: 3px 0 0; overflow: visible;
  }
  .topbar .conv-title {
    display: inline;
    white-space: normal;
    overflow: visible;
    overflow-wrap: anywhere;
    -webkit-line-clamp: unset;
    line-clamp: none;
    text-align: center;
  }
  .topbar .conv-meta-row .model-tag,
  .topbar .conv-meta-row .brain-tag {
    display: inline-flex;
    align-items: center;
    margin: 0;
  }
  .topbar .conv-meta-row .conv-light { display: inline-block; margin: 0 2px 0 0; }
  .topbar .conv-meta-row [hidden] { display: none; }

  /* Composer: the box takes the whole row and a bar of options sits under it
     (Jon 2026-10-02): attach, paste and read-aloud on the left, dictate and
     send on the right. */
  .composer {
    display: flex;
    flex-wrap: wrap;
    column-gap: 8px;
    row-gap: 6px;
    align-items: center;
  }
  .composer #attach-btn,
  .composer #paste-btn,
  .composer #autoplay-btn,
  .composer #clear-btn,
  .composer #secret-btn,
  .composer #mic-btn,
  .composer button[type='submit'] { width: 40px; height: 40px; flex: none; }
  .composer textarea { order: -1; flex: 1 1 100%; min-width: 0; min-height: 64px; box-sizing: border-box; }
  .composer #mic-btn:not([hidden]) { margin-left: auto; }
  .composer #mic-btn[hidden] + button[type='submit'] { margin-left: auto; }
}

/* Narrow pane (a document open beside the chat): the box takes the full row
   and the buttons sit in a row beneath it, instead of the box being squeezed
   to a one-letter-wide column between four buttons. */
@media (min-width: 861px) {
@container composer (max-width: 520px) {
  .composer { flex-wrap: wrap; padding-left: 16px; padding-right: 16px; }
  .composer textarea { order: -1; flex: 1 1 100%; min-width: 0; }
  .composer #mic-btn:not([hidden]) { margin-left: auto; }
  .composer #mic-btn[hidden] + button[type='submit'] { margin-left: auto; }
}
}

/* Delivery state of your own messages (Jon, 2026-10-02: a message that had
   landed looked unsent, so it was sent twice). A clock until the server has
   named the row, a tick after. */
#messages .msg.user:not(.from-other):not([data-message-id]) .msg-time::after { content: " \1F552"; }
#messages .msg.user:not(.from-other)[data-message-id] .msg-time::after { content: " \2713"; }
/* Seen by someone else (read receipts, paintSeen): two ticks, brighter. */
#messages .msg.user:not(.from-other)[data-message-id].seen .msg-time::after {
  content: " \2713\2713"; letter-spacing: -0.28em; padding-right: 0.28em;
}

/* The agent took this message and never answered (paintNoReply). */
#messages .msg .no-reply {
  margin-top: 6px; font-size: calc(13px * var(--msg-scale, 1)); opacity: 0.95;
}
#messages .msg .no-reply button.link {
  background: none; border: 0; padding: 0 0 0 4px; color: inherit;
  font: inherit; font-weight: 600; text-decoration: underline; cursor: pointer;
}

/* A chat named inside a message opens that chat. */
#messages .msg .body .chat-ref {
  cursor: pointer; text-decoration: underline dotted; text-underline-offset: 3px;
  color: #1a6fd1;
}
:root[data-theme='dark'] #messages .msg .body .chat-ref { color: #7ab8ff; }
#messages .msg .body .chat-ref:hover { text-decoration-style: solid; }

.thread-note { margin: 40px auto; padding: 0 24px; max-width: 420px; text-align: center; color: hsl(var(--muted-foreground)); }

/* ==========================================================================
   Unfolded foldable (iPhone Duo inner display, about 626 CSS px) and iPad
   portrait: the phone's touch design, but the list and the chat side by side
   instead of one at a time. Apple's guidance is two size classes - compact
   width (one pane) and regular width (two panes) - so this switches on width
   alone, never on the device. app.js sets body[data-split] from the same media
   query, so folding or unfolding is just a CSS change: the open chat, its
   scroll position, the draft and the keyboard are never rebuilt.
   Left and right safe areas are padded separately, because on the Duo they
   differ (the controls strip sits on one side).
   ========================================================================== */
body[data-split] .app {
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
body[data-split] .sidebar {
  display: flex;
  position: static;
  transform: none;
  box-shadow: none;
  width: var(--split-list-w);
  flex: 0 0 var(--split-list-w);
}
body[data-split] .main { display: flex; min-width: 0; }
body[data-split] .scrim,
body[data-split] .resizer,
body[data-split] #back-btn,
body[data-split] #menu-btn { display: none; }
body[data-split] .tabbar {
  right: auto;
  left: calc(14px + env(safe-area-inset-left));
  width: calc(var(--split-list-w) - 28px);
}
body[data-split] .sidebar.foot-open .sidebar-foot {
  right: auto;
  left: calc(14px + env(safe-area-inset-left));
  width: calc(var(--split-list-w) - 28px);
  box-sizing: border-box;
}
body[data-split] { --split-list-w: clamp(284px, 42vw, 340px); }

/* Two tiers (Jon, 2026-10-04): an alerts-only bot's message is a plain log
   card, not a chat bubble — square corners, a slate edge, a tinted panel and
   slightly smaller text — so an agent talking to you never looks like a
   monitor posting. They also arrive without sound (src/index.js, silent). */
#messages .msg.assistant.alert-msg .body {
  border-radius: 6px;
  background: var(--alert-bg);
  color: hsl(var(--foreground));
  box-shadow: inset 3px 0 0 var(--alert-edge);
  padding-left: 13px;
  font-size: calc(14.5px * var(--msg-scale, 1));
  max-width: min(92%, 680px);
}
#messages .msg.assistant.alert-msg .msg-model { display: none; }

/* The alerts colour, a cool slate grey that sits with the blue scheme (Jon,
   2026-10-04: the light orange "doesn't really work"): the message cards
   above and the alerts chats' rows in the list. Pinned (light blue) and the
   open chat still win in the list. Phone banners keep their orange bell. */
:root { --alert-edge: hsl(215 16% 52%); --alert-bg: color-mix(in srgb, hsl(215 16% 52%) 14%, hsl(var(--background))); }
:root[data-theme="dark"] { --alert-bg: color-mix(in srgb, hsl(215 16% 62%) 16%, hsl(var(--background))); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --alert-bg: color-mix(in srgb, hsl(215 16% 62%) 16%, hsl(var(--background))); }
}
.conv-item.alerts:not(.pinned):not(.active) { background: var(--alert-bg); }
@media (hover: none) and (pointer: coarse) {
  .conv-item.alerts:not(.pinned):not(.active) .conv-slide { background: var(--alert-bg); }
}
@media (hover: hover) { .conv-item.alerts:not(.pinned):not(.active):hover { background: hsl(var(--muted) / 0.5); } }

/* The chat's avatar standing in for the ⋯ at the top right (paintHeaderAvatar). */
#more-btn.as-avatar { padding: 0; background: none; border-color: transparent; }
#more-btn .head-avatar { width: 36px; height: 36px; font-size: 13px; }
#more-btn .head-avatar.emoji { font-size: 19px; }
#more-btn .head-avatar.group { border-radius: 10px; }

/* "Draft:" in a chat row's preview: unsent text typed there. */
.conv-preview-draft { color: hsl(var(--destructive)); font-weight: 600; }

/* Forward's chat picker (chooseChat): a search box over a scrolling list. */
.chat-picker .msg-menu-panel { display: flex; flex-direction: column; max-height: min(80vh, 640px); }
.chat-picker-search {
  margin: 4px 12px 8px; padding: 9px 12px; border-radius: 10px; font: inherit;
  border: 1px solid hsl(var(--border)); background: hsl(var(--background)); color: hsl(var(--foreground));
}
.chat-picker-list { overflow-y: auto; min-height: 0; flex: 1 1 auto; }
.chat-picker-row { display: flex; align-items: center; gap: 10px; }
.chat-picker-row .conv-avatar { width: 32px; height: 32px; font-size: 12px; flex: none; }
.chat-picker-row .conv-avatar.emoji { font-size: 17px; }
.chat-picker-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-picker-empty { padding: 12px 16px; opacity: 0.7; }

/* A message its author edited (Edit in the message menu). */
#messages .msg.edited .msg-time::before { content: "edited "; opacity: 0.8; }

/* Multi-select (startSelect): selected rows tinted, and the action bar over
   the composer. */
body.selecting #messages .msg[data-message-id] { cursor: pointer; }
body.selecting #messages .msg.selected { background: hsl(var(--primary) / 0.16); }
.select-bar {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(16px + env(safe-area-inset-bottom)); z-index: 60;
  display: flex; align-items: center; gap: 6px; padding: 6px 8px;
  max-width: calc(100vw - 32px); box-sizing: border-box;
  border-radius: 16px; background: hsl(var(--card)); color: hsl(var(--foreground));
  border: 1px solid hsl(var(--border)); box-shadow: 0 6px 24px hsl(0 0% 0% / 0.25);
}
.select-count { padding: 0 6px; font-weight: 600; white-space: nowrap; }
.select-action {
  border: 0; border-radius: 10px; padding: 8px 12px; font: inherit; cursor: pointer;
  background: hsl(var(--muted)); color: inherit;
}
.select-action.danger { color: hsl(var(--destructive)); }

/* Send a secret (openSecretSheet). */
.secret-sheet .msg-menu-panel { padding-bottom: 8px; }
.secret-field { display: flex; flex-direction: column; gap: 4px; margin: 6px 14px; font-size: 13px; opacity: 0.95; }
.secret-field input {
  padding: 10px 12px; border-radius: 10px; font: inherit; font-size: 16px;
  border: 1px solid hsl(var(--border)); background: hsl(var(--background)); color: hsl(var(--foreground));
}
.secret-note { margin: 6px 14px 10px; font-size: 12.5px; opacity: 0.75; line-height: 1.4; }
.secret-send { margin: 4px 14px; padding: 11px; border-radius: 12px; border: 0; font: inherit; font-weight: 600;
  background: hsl(var(--primary)); color: hsl(var(--primary-foreground, 0 0% 100%)); cursor: pointer; }
.secret-send:disabled { opacity: 0.5; }

/* An agent's file or secret for you to download once (paintShare). */
#messages .msg .share-dl {
  margin-top: 8px; align-self: flex-start; padding: 8px 12px; border-radius: 10px; cursor: pointer;
  border: 1px solid hsl(var(--border)); background: hsl(var(--background)); color: hsl(var(--foreground));
  font: inherit; font-weight: 600; font-size: calc(14px * var(--msg-scale, 1));
}
#messages .msg .share-dl:disabled { opacity: 0.6; }

/* A revealed text share (revealShare). */
.reveal-text {
  margin: 6px 14px; padding: 10px 12px; max-height: 45vh; overflow: auto; white-space: pre-wrap;
  word-break: break-all; border-radius: 10px; font: 13px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: hsl(var(--background)); border: 1px solid hsl(var(--border)); user-select: text; -webkit-user-select: text;
}
.reveal-dl { margin: 2px 14px 6px; align-self: flex-start; }

/* A choice sheet (chooseFrom: delete?, report reason…): its answers centred,
   like Cancel under them, rather than one stranded on the left. */
.msg-menu.choice-sheet .msg-menu-item:not(.msg-menu-title) { justify-content: center; text-align: center; }
/* Desktop: sheets are a compact box in the middle of the window, not a bar
   across the whole screen (Jon, 2026-10-05). Phones keep the bottom sheet. */
@media (min-width: 861px) {
  .msg-menu:not(.popover) { justify-content: center; align-items: center; }
  .msg-menu:not(.popover) .msg-menu-panel,
  .msg-menu:not(.popover) > .msg-menu-cancel { width: min(440px, 100%); box-sizing: border-box; }
}
