@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%;
}

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

.login-screen {
  min-height: 100%;
  display: grid;
  place-items: center;
  padding: 24px;
  background:
    radial-gradient(ellipse 80% 50% at 50% -10%, hsl(var(--primary) / 0.13), transparent 70%),
    hsl(var(--background));
}

.login-card {
  width: 100%;
  max-width: 380px;
  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);
}

/* While the chat shell is up the document must never scroll — .messages is the
   only scroller. Without this, iOS still has somewhere to scroll the page to
   and the header slides away regardless of how .app is sized.

   Scoped to :has(#app:not([hidden])) rather than applied to body outright: the
   login screen centres a card that genuinely must scroll when the keyboard
   covers it, and locking the document there would strand the submit button. */
html:has(#app:not([hidden])), body:has(#app:not([hidden])) {
  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 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;
  gap: 10px;
  padding: 16px 14px;
  border-bottom: 1px solid hsl(var(--border));
}

.sidebar-logo { width: 104px; }

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

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

.conv-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 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; }

.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. */
.sidebar-foot {
  border-top: 1px solid hsl(var(--border));
  padding: 10px 14px 12px;
  display: grid;
  /* auto / auto / 1fr: the elastic track sits between the icon toggles and
     Sign out, so the slack becomes the gap that separates them. */
  grid-template-columns: auto auto minmax(0, 1fr);
  align-items: center;
  gap: 6px 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 { grid-area: 2 / 1; justify-self: start; }
/* ☀ 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 {
  grid-area: 2 / 2;
  justify-self: start;
  font-variant-emoji: text;
  line-height: 1;
  min-width: 1.2em;
  text-align: center;
}
.sidebar-foot #logout { grid-area: 2 / 3; justify-self: end; }

/* Row 3, under the controls. */
.build-stamp {
  grid-area: 3 / 1 / 3 / -1;
  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 {
  grid-area: 1 / 1 / 1 / -1;
  color: hsl(var(--muted-foreground));
  min-width: 0;
  overflow-wrap: anywhere;
  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: 22px 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;
  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;
  line-height: 1.65;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  padding-top: 3px;
}

.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: 13px;
  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; }
.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; }

/* 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;
  place-items: center;
  padding: 24px;
  cursor: zoom-out;
}

.lightbox img { max-width: 100%; max-height: 100%; border-radius: 8px; }

.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;
  max-height: 220px;
  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;
}

.composer #attach-btn {
  width: 42px;
  height: 42px;
  flex: 0 0 42px;
  font-size: 19px;
  border-radius: 11px;
}

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

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

  .msg { padding: 16px 14px; gap: 10px; }
  .msg .body { line-height: 1.6; }

  /* Long code lines must scroll inside the block, not widen the page. */
  .msg .body pre { font-size: 12.5px; padding: 11px; max-width: 100%; }

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

  /* 16px minimum: anything smaller makes iOS Safari zoom the page on focus,
     and it never zooms back out. This is the single most important mobile
     rule in the file — do not "tidy" it to match the desktop 15px. */
  .composer textarea,
  .login-card input {
    font-size: 16px;
    max-height: 140px;
  }

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

  .login-screen { padding: 16px; align-items: flex-start; padding-top: 8vh; }
  .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 hamburger belonged to the drawer; back navigation replaces it. */
  body[data-view="thread"] #menu-btn { display: none; }
  #back-btn { display: grid; place-items: center; }

  .conv-avatar { flex-basis: 44px; width: 44px; height: 44px; }
}

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

/* Short landscape phones: reclaim the vertical space the splash wastes. */
@media (max-height: 500px) and (orientation: landscape) {
  .login-screen { padding-top: 3vh; }
  .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;
}

.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 {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* 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: 14px;
  letter-spacing: -0.01em;
}
.conv-item.unread .conv-name { font-weight: 700; }

.conv-sub {
  font-size: 11px;
  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 {
  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-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)); }

.admin-view {
  margin-bottom: 10px;
}

.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;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 3px;
}

.msg-sender {
  font-size: 12.5px;
  font-weight: 700;
  color: hsl(var(--who-h, 210) 55% 38%);
  letter-spacing: -0.01em;
  /* A full email is long; let it shrink rather than push the time off. */
  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%); }

/* 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%); }
[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%);
  }
  :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-pill,
.conv-item .conv-time,
.conv-item .conv-moves { flex: 0 0 auto; }

/* 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: 40px;
  height: 40px;
  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-size: 21px;
  font-variant-emoji: emoji;
}
.agent-add-dot.emoji { font-size: 15px; }
.role.emoji { font-size: 17px; }

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

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

/* 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, and a permanently visible control on every message is
   clutter — long-press the message instead is not worth the complexity here,
   so it is simply desktop-only. */
@media (hover: none) and (pointer: coarse) {
  .msg-fork, .msg-reply, .msg-delete { display: none; }
}

/* Long-press action menu (touch). A translucent sheet over the transcript;
   the actions are the very same handlers the hover buttons run. */
.msg { -webkit-touch-callout: none; }
.msg-menu {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 16px;
  gap: 8px;
  background: hsl(0 0% 0% / 0.35);
}
.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)); }
.msg-menu-item.danger { color: hsl(0 70% 45%); }
.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; }

/*
 * Reorder controls, one pair per row. Always visible: the list never reflows
 * under the pointer, so the row you reach for is the row you get.
 *
 * The ▲/▼ arrows are real buttons with real hit areas (20x14px, not a 13px
 * glyph): one tap, one slot, the move that cannot miss. The handler is wired
 * once on the list (wireListMoves), so these elements are only the targets it
 * matches.
 */
.conv-moves {
  display: flex;
  flex-direction: column;
  gap: 0;
  flex: none;
}
.conv-move {
  border: 0;
  background: transparent;
  color: hsl(var(--muted-foreground));
  font-size: 10px;
  line-height: 1;
  width: 20px;
  height: 14px;
  padding: 0;
  cursor: pointer;
  border-radius: 4px;
}
.conv-move:hover { color: hsl(var(--foreground)); background: hsl(var(--muted)); }
/* Touch gets the same arrows with a larger hit area. */
@media (hover: none) and (pointer: coarse) {
  .conv-move { width: 26px; height: 20px; font-size: 12px; }
}

/* 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. */
.stats-sheet { max-height: 80vh; overflow-y: auto; }
.stats-head {
  font-weight: 700;
  font-size: 16px;
  padding: 4px 2px 8px;
}
.stats-row {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 7px 2px;
  border-bottom: 1px solid hsl(var(--muted));
  font-size: 14px;
}
.stats-k { color: hsl(var(--muted-foreground)); }
.stats-v { font-variant-numeric: tabular-nums; text-align: right; }
.chat-menu.sheet .stats-row {
  border-radius: 8px;
  background: hsl(var(--card));
  border-bottom: 0;
  padding: 10px 14px;
}

/* 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. */
  flex: 0 1 auto;
  min-width: 0;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 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: 12px;
  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: 11px;
  color: hsl(var(--muted-foreground));
  font-variant-numeric: tabular-nums;
}

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

/* 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: 11px;
  color: hsl(var(--muted-foreground));
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.conv-subtitle:empty { display: none; }

.archive-toggle { margin: 8px 12px; align-self: flex-start; font-size: 12px; }
/* The two list-pruning controls share a line: they are one idea, and a second
   full-width row of link text above the list would cost more than it says. */
/* 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; }
/* Narrow sidebar: the name wins, as it does for the kind pill. */
@media (max-width: 420px) {
  .conv-stats { display: none; }
}

.list-tools { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin: 0 12px; }
.list-tools .archive-toggle { margin: 8px 0; }

/* Selection mode.
   ---------------------------------------------------------------------------
   A checkbox per row, and one bar under the list saying how many are picked
   and what will happen to them. The bar is the only place the action is
   spelled out, which is why it sits under the rows rather than in the tools
   block: you read the count and the verb in the same glance as the ticks. */
.conv-check {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: hsl(var(--primary));
  cursor: pointer;
}
/* Picked rows read like the open one — same surface, without the left bar,
   which stays the "you are looking at this" mark. */
.conv-list.selecting .conv-item.picked {
  background: hsl(var(--primary) / 0.10);
  color: hsl(var(--foreground));
}
/* While picking, a row is a target, not a door. */
.conv-list.selecting .conv-item { user-select: none; }

.select-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-top: 1px solid hsl(var(--border));
  background: hsl(var(--card));
}
.select-count { flex: 1; font-size: 12px; color: hsl(var(--muted-foreground)); }
.select-apply {
  border: 1px solid hsl(var(--primary) / 0.5);
  background: hsl(var(--primary));
  color: hsl(var(--primary-foreground));
  border-radius: 8px;
  padding: 5px 12px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.select-apply:disabled { opacity: 0.45; cursor: default; }

/* 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: 10px;
  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. */
@media (max-width: 420px) {
  .conv-pill { 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;
}

/* 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: 12px;
  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; }
#archive-chat { flex-shrink: 0; font-size: 12px; min-height: 44px; }
.chat-notice { display: flex; align-items: center; gap: 12px; padding: 0 20px; border-bottom: 1px solid hsl(var(--border)); }
.chat-notice #archive-chat { margin-left: auto; }

/* The open chat's own numbers, filling the strip that held only the Hide link.
   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 {
  width: min(460px, 40vw);
  flex: none;
  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; }

#preview-download { font-size: 17px; text-decoration: none; flex: none; }

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