/* ThinkOtter brand skin for the Prompt-Buddy thin client.
   -----------------------------------------------------------------------
   Design source: Otter-Marketing/thinkotter-brand-style-guide.html
     - warm paper backgrounds, ONE orange accent, blue demoted to links only
     - Baloo 2 (display) + Plus Jakarta Sans (body), both self-hosted below
   Theme contract (app.js applyTheme + theme-init.js, both write data-theme
   explicitly now):
     :root / [data-theme="dark"]  -> DARK  ("ThinkOtter after dark")
     [data-theme="light"]         -> LIGHT (the warm-paper brand look)
     the prefers-color-scheme block below is the NO-JS fallback only
   Every token NAME from the previous stylesheet is kept, so no selector or
   JS call site depends on a renamed variable. New tokens are additive.
   -----------------------------------------------------------------------
   Per-agent identity colour: JS sets ONE inline custom property, --agent.
   All painting (ring, name, bubble edge, chips) is done here from that
   single value, which is what lets dark mode lighten it via color-mix
   instead of JS having to know about themes. */

/* ---- Brand fonts (self-hosted variable woff2, latin subset) --------------
   Self-hosted rather than <link>ed from fonts.googleapis.com so the station
   renders identically with no network — it is a localhost-only tool. */
@font-face {
  font-family: "Baloo 2";
  src: url("assets/fonts/baloo2.woff2") format("woff2-variations");
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Plus Jakarta Sans";
  src: url("assets/fonts/plus-jakarta-sans.woff2") format("woff2-variations");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

/* ---- Tokens: DARK (default, no data-theme) ------------------------------
   Black-first. This WAS "ThinkOtter after dark" — the warm brown ink family
   inverted, --bg #17110c through --card #241b14. Reported as eye-straining on
   the phone, and the measurement agreed: every neutral was warm (bg +11, card
   +16, card-2 +20, border +26, text +22, as R-B), so there was no neutral for
   the eye to rest against and the whole UI read as a colour cast.

   Replaced on product direction ("dark mode should mean as much black as
   possible"). --bg and --card are both pure black, so a card stops being a lit
   panel; the neutrals go grey; and the accent is demoted from a large FILL to
   an accent on controls (--accent-solid). --text is deliberately held off
   #ffffff, which is 21:1 on black and visibly blooms on OLED — #e4e4e8 is
   16.6:1, far above AA, without the halation.

   Kept in lockstep with frontend/lib/core/presentation/theme/app_theme.dart
   (orangeDarkTheme). The phone and the station must not drift apart.
   Light mode below is UNCHANGED. */
:root {
  --bg: #000000;
  --bg-2: #0b0b0d;
  --card: #000000;
  --card-2: #131316;
  --border: #242429;
  --border-soft: #1e1e22;
  --text: #e4e4e8;
  --muted: #95959d;
  --faint: #7e7e86;
  --accent: #fb923c;
  --accent-press: #f97316;
  --accent-tint: rgba(251, 146, 60, 0.14);
  /* Filled accent surfaces (primary buttons, own-message bubbles, pills). Kept
     separate from --accent because a colour that is legible AS text on the page
     background is not automatically legible UNDER text. On dark the bright
     accent already carries dark ink at high contrast, so they coincide. */
  --accent-solid: #fb923c;
  /* The tonal container behind the viewer's OWN message. Split out from
     --accent-solid because a chat bubble is a large block: at full accent it
     was 8.3:1 against the ground and the brightest thing on the screen. This
     sits at 2.3:1 — clearly the user's, not a light source. Buttons keep
     --accent-solid. */
  /* Warmed from #4a2a12 (1.63:1 against the ground) — the old value read as
     dull brown rather than as the brand's orange. Kept in lockstep with the
     phone's _primaryContainerOrangeDark; the two surfaces show the same bubble
     and must not drift. This is the ceiling: the app's own contrast test
     asserts the fill stays under 3.0:1 against the ground so it reads as a
     panel and not a light source, and the next step up (#8a4a16, 3.07:1) fails
     it — that is also where it starts competing with --accent, which is
     reserved for controls. */
  --own-bubble: #6e3a15;
  --on-own-bubble: #f0e7de;
  /* On dark the accent is already 9.3:1 as text on black — no separate step. */
  --accent-ink: #fb923c;
  --green: #4ade80;
  --red: #f87171;
  --radius: 14px;
  --radius-sm: 10px;
  --space: 14px;

  --on-accent: #20120a;
  --overlay: rgba(0, 0, 0, 0.9);
  --overlay-soft: rgba(255, 255, 255, 0.28);
  --warn: #fbbf24;
  /* The initial-glyph avatar is filled with --agent-ink, which dark mode lifts
     toward cream — white text on that is ~3:1 and unreadable, so the glyph
     takes the same dark ink the accent surfaces use (4.8-6.0:1 across the
     palette). Light mode keeps white on the saturated 700-step fill. */
  --avatar-fg: #20120a;
  --font-sm: 13px;
  --font-lg: 20px;
  /* The plate a CLI logo sits on. Stays light in BOTH themes: the Claude and
     ChatGPT marks are dark artwork and vanish on a dark backing. Separate from
     --avatar-fg, which is the initial-glyph text colour. */
  --logo-plate: #fff;
  /* Native controls, scrollbars, autofill and datalist follow this. */
  color-scheme: dark;

  --display: "Baloo 2", "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, sans-serif;
  --body: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --sh: 0 6px 18px -8px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.25);
  --sh-sm: 0 2px 6px rgba(0, 0, 0, 0.28);
  /* Agent identity colours are authored for warm paper; on dark they are
     lifted toward cream so they keep AA contrast on --card. 64% was enough
     against a brown ground; against pure black it is not, so the lift goes
     harder. At 48% the worst hue in the roster (indigo #4338ca -> #a593cd)
     still clears 7.65:1. Matches AppSemanticColors.darkBlack.agentLift. */
  --agent-lift: 48%;
}

/* ---- Tokens: LIGHT — the ThinkOtter brand palette ----------------------- */
[data-theme="light"] {
  --bg: #fdf8f0;
  --bg-2: #fbf0e0;
  --card: #ffffff;
  --card-2: #fdf8f0;
  --border: #ecdcc4;
  --border-soft: #f4e9d8;
  --text: #271809;
  --muted: #7a6446;
  --faint: #846d4b;
  --accent: #ea580c;
  /* orange-800. The press step MUST be darker than the base: it was #f97316,
     LIGHTER than --accent, so hovering a filled control moved white text
     TOWARD the background (2.80:1) instead of away from it. */
  --accent-press: #9a3412;
  --accent-tint: #fdecd8;
  /* Fills that carry white text. orange-600 gives white only 3.56:1. */
  --accent-solid: #c2410c;
  /* Light keeps the accent AS the own-message fill, exactly as before —
     only the black theme splits them apart. */
  --own-bubble: #c2410c;
  --on-own-bubble: #ffffff;
  /* Accent as FOREGROUND on paper. The brand guide specifies orange-600 for
     26px display CTAs, not for 12px UI labels — at that size it is 3.56:1, so
     small accent text steps down to orange-800 (7.06:1). Fills and borders
     keep the brand --accent. */
  --accent-ink: #9a3412;
  --green: #15803d;
  --red: #c0392b;
  --on-accent: #ffffff;
  --overlay: rgba(39, 24, 9, 0.62);
  --overlay-soft: rgba(255, 255, 255, 0.45);
  --warn: #b45309;
  --avatar-fg: #ffffff;
  --logo-plate: #fff;
  color-scheme: light;
  --sh: 0 6px 18px -8px rgba(120, 70, 20, 0.22), 0 2px 6px rgba(120, 70, 20, 0.08);
  --sh-sm: 0 2px 6px rgba(120, 70, 20, 0.07);
  --agent-lift: 100%;
}

@media (prefers-color-scheme: light) {
  /* Pre-JS default only — the JS-applied [data-theme] always wins once app.js runs. */
  :root:not([data-theme]) {
    --bg: #fdf8f0;
    --bg-2: #fbf0e0;
    --card: #ffffff;
    --card-2: #fdf8f0;
    --border: #ecdcc4;
    --border-soft: #f4e9d8;
    --text: #271809;
    --muted: #7a6446;
    --faint: #846d4b;
    --accent: #ea580c;
    /* orange-800. The press step MUST be darker than the base: it was #f97316,
       LIGHTER than --accent, so hovering a filled control moved white text
       TOWARD the background (2.80:1) instead of away from it. */
    --accent-press: #9a3412;
    --accent-tint: #fdecd8;
    /* Fills that carry white text. orange-600 gives white only 3.56:1. */
    --accent-solid: #c2410c;
    /* Light keeps the accent AS the own-message fill, exactly as before —
       only the black theme splits them apart. */
    --own-bubble: #c2410c;
    --on-own-bubble: #ffffff;
    /* Accent as FOREGROUND on paper. The brand guide specifies orange-600 for
       26px display CTAs, not for 12px UI labels — at that size it is 3.56:1, so
       small accent text steps down to orange-800 (7.06:1). Fills and borders
       keep the brand --accent. */
    --accent-ink: #9a3412;
    --green: #15803d;
    --red: #c0392b;
    --on-accent: #ffffff;
    --overlay: rgba(39, 24, 9, 0.62);
    --overlay-soft: rgba(255, 255, 255, 0.45);
    --warn: #b45309;
    --avatar-fg: #ffffff;
    --logo-plate: #fff;
    color-scheme: light;
    --sh: 0 6px 18px -8px rgba(120, 70, 20, 0.22), 0 2px 6px rgba(120, 70, 20, 0.08);
    --sh-sm: 0 2px 6px rgba(120, 70, 20, 0.07);
    --agent-lift: 100%;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  background-image: radial-gradient(130% 90% at 12% 0%, var(--bg) 0%, var(--bg-2) 62%, var(--bg) 100%);
  background-attachment: fixed;
  color: var(--text);
  font: 15px/1.55 var(--body);
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
}

h1, h2, h3, .group-title, .btn-primary, .thread-sender-name, .thread-send-btn {
  font-family: var(--display);
  letter-spacing: -0.005em;
}

/* ---- Containers ---------------------------------------------------------
   .wrap  = the form-shaped areas (Home / Agents / Setup & Keys).
   .wrap-wide = the Group Chat workspace: 90% of the viewport on landscape
   screens, which is what app.js swaps in for the chat view. */
.wrap {
  max-width: 880px;
  margin: 0 auto;
  padding: 28px 20px 64px;
}

/* The chat shell is a full-height flex column so the workspace fills exactly
   the space left under the header/nav — measuring it with a calc() guess left
   dead space under the panes whenever the header wrapped to a second line.
   Vertical padding is deliberately tiny here: message rows are the scarce
   resource in this view, and every 8px of chrome is a line of conversation. */
.wrap-wide {
  width: 98vw;
  max-width: none;
  margin: 0 auto;
  padding: 8px 0 8px;
  height: 100vh;
  display: flex;
  flex-direction: column;
}
.wrap-wide > #app-view {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}
/* Create-group, the join-code success view and thread errors mount as bare
   cards straight into #app-view — they are forms, not workspaces, and at 98vw
   a name field would stretch across the whole monitor. The shell keeps its
   width (applyShellWidth only swaps on area CHANGE), so constrain the form
   instead of the container. Matches .wrap's measure. */
.wrap-wide > #app-view > .card {
  width: 100%;
  max-width: 880px;
  margin-left: auto;
  margin-right: auto;
}

/* Compact chrome in the chat view only — title, strapline and nav were a
   ~100px stack above a pane whose whole job is showing messages. app.js
   dockNav() moves #app-nav INTO .head here, so it all collapses to one row.
   The form areas (.wrap) keep the full-size header untouched. */
.wrap-wide .head { align-items: center; gap: 14px; flex: none; }
.wrap-wide .head-title { flex: none; }
.wrap-wide .head h1 { font-size: 18px; margin: 0; white-space: nowrap; }
.wrap-wide .sub { display: none; }
.wrap-wide #app-nav { margin: 0; flex: 1; min-width: 0; }
.wrap-wide #app-nav .btn { padding: 6px 13px; font-size: 13px; }

/* ---- App header + nav --------------------------------------------------- */
.head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.head-title { flex: 1; }
.head h1 { display: flex; align-items: center; gap: 10px; white-space: nowrap; margin: 0 0 2px; font-size: 27px; font-weight: 800; color: var(--text); }
.hero-icon { height: 1.3em; width: auto; flex: none; }
.sub { margin: 0 0 8px; color: var(--muted); font-size: 13.5px; }

#app-header-actions { display: flex; align-items: center; gap: 8px; }
#app-user-chip {
  font-size: 13px; font-weight: 600; color: var(--muted);
  background: var(--card); border: 1px solid var(--border); border-radius: 999px; padding: 5px 13px;
}
#app-nav { margin-top: 14px; gap: 6px; flex-wrap: wrap; }
#app-nav .btn { border-radius: 999px; }
#app-nav .btn.active,
#app-nav .btn[aria-current="page"] {
  background: var(--accent-tint); border-color: var(--accent); color: var(--accent-ink); font-weight: 700;
}
#theme-toggle { white-space: nowrap; }

/* ---- Surfaces ----------------------------------------------------------- */
.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px;
  margin-top: 16px;
  box-shadow: var(--sh);
}
.card-head { display: flex; align-items: center; justify-content: space-between; }
.card.warn { border-color: var(--warn); }
.card-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.card-actions .btn-primary { width: auto; }

.label { display: block; font-weight: 700; margin-bottom: 8px; color: var(--text); }
.hint { display: block; color: var(--muted); font-size: 13px; margin-top: 6px; }
.muted { color: var(--muted); }

.row { display: flex; gap: 10px; align-items: center; }

/* ---- Inputs ------------------------------------------------------------- */
.input {
  width: 100%;
  background: var(--card-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: 10px 13px;
  font: inherit;
  font-size: 14px;
  outline: none;
}
.input::placeholder { color: var(--faint); }
.input:focus { border-color: var(--accent); background: var(--card); }
.input.valid { border-color: var(--green); }
.input.error { border-color: var(--red); }

.feedback { display: block; font-size: 13px; margin-top: 8px; min-height: 18px; }
.feedback.valid { color: var(--green); }
.feedback.error { color: var(--red); }

/* ---- Buttons ------------------------------------------------------------ */
.btn {
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text);
  border-radius: 999px;
  padding: 8px 15px;
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: var(--sh-sm);
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent-ink); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary {
  background: var(--accent-solid); border-color: var(--accent-solid); color: var(--on-accent);
  font-family: var(--display); font-weight: 700; width: 100%; justify-content: center;
  padding: 11px 18px; font-size: 14.5px;
}
.btn-primary:hover:not(:disabled) { background: var(--accent-press); border-color: var(--accent-press); color: var(--on-accent); }
/* 21 call sites across 6 files emit `btn btn-secondary` and it has never had a
   rule of its own — it has always just been a plain .btn. Named explicitly so
   the next person editing .btn knows the secondary variant rides on it. */
.btn-secondary { /* intentionally identical to .btn — the default variant */ }
.btn-link { background: none; border-color: transparent; box-shadow: none; color: var(--accent-ink); padding: 5px 8px; }
.btn-link:hover:not(:disabled) { background: var(--accent-tint); border-color: transparent; }
.btn-danger { background: none; border-color: var(--red); color: var(--red); }
.btn-danger:hover:not(:disabled) { background: var(--red); color: #fff; border-color: var(--red); }

.card-head-actions { display: flex; gap: 4px; }

/* ---- Agents area (unchanged structure, brand skin) ---------------------- */
.add-form {
  background: var(--card-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 14px;
  margin-bottom: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.add-form .ctrl { flex: 1; }
.add-form .ctrl label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 4px; display: block; font-weight: 700; }
.add-row { display: flex; gap: 12px; }
.add-actions { display: flex; gap: 10px; align-items: center; }
.add-actions .btn-primary { width: auto; }

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

.badge.badge-manual { color: var(--accent); border-color: var(--accent); }
.remove-btn {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font-size: 16px; line-height: 1; padding: 0 2px;
}
.remove-btn:hover { color: var(--red); }

.agent {
  background: var(--card-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
}
.agent-top { display: flex; align-items: center; gap: 10px; }
.agent-top input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); }
.agent-name { font-weight: 700; }
.agent-badges { margin-left: auto; display: flex; gap: 6px; }
.badge {
  font-size: 11px; padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--border); color: var(--muted);
}

.agent-controls {
  display: none;
  gap: 12px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
  flex-wrap: wrap;
  align-items: center;
}
.agent.selected .agent-controls { display: flex; }

.cli-configs { display: flex; flex-direction: column; gap: 10px; width: 100%; }
.cli-config {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 8px 10px; background: var(--card); border: 1px solid var(--border); border-radius: 8px;
}
.cli-config-tag { font-size: 12px; font-weight: 700; color: var(--accent-ink); min-width: 50px; }

.ctrl { display: flex; flex-direction: column; gap: 4px; }
.ctrl label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 700; }
.ctrl select, .seg { font-size: 13px; }

.select {
  background: var(--card); border: 1px solid var(--border); color: var(--text);
  border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 13px;
}

.seg { display: inline-flex; border: 1px solid var(--border); border-radius: 999px; overflow: hidden; }
.seg button {
  background: var(--card); color: var(--muted); border: none; padding: 6px 13px;
  cursor: pointer; font: inherit; font-size: 13px; font-weight: 600;
}
.seg button.active { background: var(--accent-solid); color: var(--on-accent); font-weight: 700; }
.seg button:disabled { opacity: 0.35; cursor: not-allowed; }

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

.results .result { padding: 9px 11px; border-radius: 8px; background: var(--card-2); margin-top: 8px; font-size: 13px; }
.result.ok { border-left: 3px solid var(--green); }
.result.fail { border-left: 3px solid var(--red); }
.result code { display: block; color: var(--muted); margin-top: 4px; font-size: 12px; word-break: break-all; }

#preset-select { flex: 1; }
#running-list .result { margin-top: 0; margin-bottom: 8px; }
#running-list .result:last-child { margin-bottom: 0; }
#history-list .result .row { margin-top: 6px; gap: 8px; }
#history-list .result .btn-link { padding: 2px 6px; }

/* .hidden lives at the END of this file — see the note beside its rule. */

/* ---- Login gate --------------------------------------------------------- */
#app-gate { max-width: 440px; padding-top: 88px; }
#app-gate .card { display: flex; flex-direction: column; gap: 12px; }

/* ---- Home dashboard ----------------------------------------------------- */
.health-tiles { display: flex; gap: var(--space); flex-wrap: wrap; margin-top: 16px; }
.health-tile { flex: 1 1 180px; margin-top: 0; }
.status-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 8px; }
.status-dot.ok { background: var(--green); }
.status-dot.warn { background: var(--warn); }
.status-dot.bad { background: var(--red); }
.area-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--space); margin-top: 16px; }

/* =========================================================================
   GROUP CHAT
   ========================================================================= */

/* ---- Two-pane workspace: full-height panes, one scroll region each ------ */
.group-workspace {
  display: grid;
  grid-template-columns: minmax(300px, 360px) 1fr;
  gap: 18px;
  align-items: stretch;
  flex: 1;
  min-height: 0;
}
.group-workspace #group-master,
.group-workspace #group-detail {
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--sh);
  overflow: hidden;
}
/* Inside a pane the cards are already framed by the pane itself — flatten
   them so the two panes read as two clean surfaces, not nested boxes. */
.group-workspace #group-master > .card,
.group-workspace #group-detail > .card {
  background: none;
  border: none;
  box-shadow: none;
  border-radius: 0;
  margin-top: 0;
}
.group-workspace #group-master { overflow-y: auto; padding: 4px 0 10px; }
.group-workspace #group-detail { overflow: hidden; }

.group-header { flex-wrap: wrap; margin-bottom: 4px; gap: 10px; }
.group-header .group-title { flex: 1; margin: 0; font-size: 21px; font-weight: 800; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* Join-code chip beside the title. Never shrinks (flex: 0 0 auto) — the title
   is the one that ellipsizes, because a truncated name is still recognisable
   whereas half a code is worse than no code at all. */
.group-code {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
  padding: 2px 4px 2px 8px; border: 1px solid var(--border); border-radius: 999px;
}
.group-code-value {
  font-size: 12px; font-weight: 800; letter-spacing: 0.09em;
  color: var(--faint); font-variant-numeric: tabular-nums;
}
.group-code-copy {
  font: inherit; font-size: 11px; font-weight: 700;
  padding: 2px 8px; border-radius: 999px; cursor: pointer;
  color: var(--accent); background: transparent; border: 1px solid transparent;
}
.group-code-copy:hover { background: var(--card-hover, rgba(127, 127, 127, 0.12)); }
.group-code-copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Master pane header (New group / Refresh) sticks to the top of its scroll. */
.group-workspace #group-master > .row.group-header {
  position: sticky; top: 0; z-index: 3;
  background: var(--card); border-bottom: 1px solid var(--border-soft);
  margin: 0; padding: 10px 14px;
}
.group-workspace #group-master > .row.group-header .group-title { font-size: 17px; }
.group-workspace #group-master > .row.group-header .btn-primary { width: auto; padding: 8px 14px; font-size: 13.5px; }
.group-workspace #group-master > .card { padding: 10px 12px; }

.group-section { display: flex; flex-direction: column; gap: 8px; }
.group-section > .label {
  font-size: 10.5px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--faint); margin: 4px 0 2px;
}
.group-tile-list { display: flex; flex-direction: column; gap: 8px; }

.group-tile {
  display: block;
  position: relative;
  width: 100%;
  text-align: left;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 11px 13px;
  color: var(--text);
  cursor: pointer;
  font: inherit;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.group-tile:hover { border-color: var(--accent); box-shadow: var(--sh-sm); }
.group-tile .group-tile-name { flex: 1; font-weight: 700; font-size: 14.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.group-tile > .muted { font-size: 13px; margin: 5px 0 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.group-tile-meta { justify-content: space-between; margin-top: 9px; font-size: 12px; }
.group-workspace .group-tile.active {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
  background: var(--card-2);
}
.group-workspace .group-tile.active::before {
  content: ""; position: absolute; left: 0; top: 11px; bottom: 11px; width: 4px;
  border-radius: 0 4px 4px 0; background: var(--accent);
}

.status-badge {
  font-size: 10px; padding: 2px 8px; border-radius: 999px; border: 1px solid;
  text-transform: uppercase; font-weight: 800; letter-spacing: 0.07em;
}
.status-badge.active { color: var(--green); border-color: var(--green); }
.status-badge.ended { color: var(--faint); border-color: var(--border); }
.status-badge.revoked { color: var(--red); border-color: var(--red); }
.unread-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); display: inline-block; flex-shrink: 0; }

/* TS-019 (PB-COLLAB-001) / T2: sharing-state badge — reuses the .status-badge
   pill shape/weight; muted for Private, brand-accent for Shared by me,
   neutral outline for Shared with me (member view). */
.sharing-badge {
  font-size: 10px; padding: 2px 8px; border-radius: 999px; border: 1px solid;
  font-weight: 700; letter-spacing: 0.02em;
}
.sharing-badge.sharing-private { color: var(--faint); border-color: var(--border); }
.sharing-badge.sharing-shared_by_me { color: var(--accent); border-color: var(--accent); }
.sharing-badge.sharing-shared_with_me { color: var(--muted); border-color: var(--border); }

/* The status + sharing pills sit on their own row BELOW the group-tile title,
   so a long title is never covered by them. Wraps if both pills are wide. */
.group-tile-badges { margin-top: 4px; gap: 6px; flex-wrap: wrap; align-items: center; }

/* Face pile of the group's agents, coloured + model-badged like the thread. */
.group-tile-faces { display: flex; align-items: center; }
/* At pile size the colour halo turns to confetti, so faces get a flat ring and
   a card-coloured gap instead — enough to tell agents apart, quiet enough to
   sit under the group name. */
.group-tile-faces .avatar {
  width: 24px; height: 24px; border-radius: 8px; margin-left: -4px;
  border-width: 1.5px;
  box-shadow: 0 0 0 2px var(--card);
}
.group-tile-faces .avatar.avatar-logo { box-shadow: 0 0 0 2px var(--card); }
.group-tile-faces .avatar:first-child { margin-left: 0; }
.group-tile-faces-more {
  margin-left: 6px; font-size: 11px; font-weight: 700; color: var(--muted);
}
.group-tile-faces .avatar img { width: 70%; height: 70%; }
.group-tile.active .group-tile-faces .avatar,
.group-tile.active .group-tile-faces .avatar.avatar-logo { box-shadow: 0 0 0 2px var(--card-2); }

.join-code-box .input { font-family: ui-monospace, Menlo, monospace; letter-spacing: 1.5px; font-weight: 700; }
.join-code-snippet {
  display: block; margin-top: 6px; padding: 9px 11px; background: var(--card-2);
  border: 1px solid var(--border); border-radius: 8px; color: var(--accent-ink);
  font-family: ui-monospace, Menlo, monospace; font-size: 13px; word-break: break-all;
}

/* ---- Roster ------------------------------------------------------------- */
.roster-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.roster-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
.roster-row-name { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.muted-participant { opacity: 0.5; }

/* ---- Avatar: model logo in an identity-coloured ring -------------------- */
.avatar {
  width: 30px; height: 30px; border-radius: 10px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--avatar-fg); font-weight: 800; font-size: 12px;
  /* Painted entirely from the inline --agent custom property. */
  background: var(--agent-ink, var(--accent));
  border: 2px solid transparent;
  box-shadow: var(--sh-sm);
}
/* An agent whose CLI we know shows that CLI's logo on a light plate, with the
   identity colour as the ring + halo, so "which model" and "which agent" are
   both legible at a glance. */
.avatar.avatar-logo {
  background: var(--logo-plate);
  border-color: var(--agent-ink, var(--accent));
  box-shadow: 0 0 0 3px var(--agent-halo, transparent), var(--sh-sm);
}
.avatar img { width: 66%; height: 66%; object-fit: contain; display: block; }

/* --agent-ink / --agent-halo derive from the single inline --agent value.
   Light theme uses it as-authored; dark lifts it toward cream for contrast. */
.avatar, .thread-bubble, .thread-sender-name, .thread-target-chip {
  --agent-ink: var(--agent, var(--accent));
  --agent-halo: color-mix(in srgb, var(--agent, var(--accent)) 16%, transparent);
}
:root:not([data-theme="light"]) .avatar,
:root:not([data-theme="light"]) .thread-bubble,
:root:not([data-theme="light"]) .thread-sender-name,
:root:not([data-theme="light"]) .thread-target-chip {
  --agent-ink: color-mix(in srgb, var(--agent, var(--accent)) var(--agent-lift), #ffe7cf);
  --agent-halo: color-mix(in srgb, var(--agent, var(--accent)) 30%, transparent);
}

.presence-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.presence-dot.state-live { background: var(--green); }
.presence-dot.state-idle { background: var(--warn); }
.presence-dot.state-offline { background: var(--muted); }
.presence-dot.state-unknown { background: transparent; border: 1px solid var(--border); }
.presence-label { font-size: 11px; color: var(--muted); margin-left: 4px; }

/* Remote Agent Control: the status chip on an agent's roster row.
   Mirrors agent_state_chip.dart, including the choice NOT to colour "Can't
   tell" as an error — an unmatched agent is routine (every batch spawn and
   every hand-started agent lands there), and a panel that shows red for
   something normal teaches you to ignore red. */
.agent-chip {
  font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 4px;
  border: 1px solid var(--muted); color: var(--muted); white-space: nowrap;
}
.agent-chip.tone-ok { border-color: var(--green); color: var(--green); }
.agent-chip.tone-warn { border-color: var(--warn); color: var(--warn); }
.agent-chip.tone-neutral { border-color: var(--border); color: var(--muted); }
.roster-row .btn-link.danger { color: var(--red); }
.roster-row .btn-link:disabled { color: var(--muted); opacity: 0.6; cursor: not-allowed; }

/* ---- Detail pane: header, owner toolbar, thread, composer --------------- */
/* The detail pane is a single column: fixed header + toolbar, one scrolling
   message list, fixed composer. Nothing else scrolls, so the composer never
   drifts below the fold the way it did in the 760px shell. */
.group-workspace #group-detail > .row.group-header {
  flex: none; margin: 0; padding: 9px 18px;
  border-bottom: 1px solid var(--border-soft);
  background: var(--card);
}
.group-workspace #group-detail > .thread-list { flex: 1; min-height: 0; }
.group-workspace #group-detail > #thread-composer-mount { flex: none; }

/* Owner bar: two labelled clusters (session / broadcast) on one aligned rail
   instead of the previous stacked rows with a stray "Broadcasts" heading. */
.owner-bar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 7px 18px; margin: 0;
  flex: none;
}
/* The owner bar IS a `.card` (groups.js buildOwnerControls) mounted as a direct
   child of #group-detail, so the pane-flattening rule above — which strips
   background and borders off in-pane cards — outranks a bare `.owner-bar` and
   its tinted band silently disappeared. Matched here at equal specificity plus
   one class so the band wins. */
.group-workspace #group-detail > .card.owner-bar {
  background: var(--card-2);
  border-bottom: 1px solid var(--border-soft);
}
.owner-bar > .label {
  font-size: 10px; font-weight: 800; letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--faint); margin: 0 2px 0 0;
}
.owner-bar .btn { padding: 5px 13px; font-size: 12.5px; box-shadow: none; }
.owner-bar .row { gap: 6px; flex-wrap: wrap; }
.owner-bar .feedback { flex-basis: 100%; margin: 0; min-height: 0; }
.owner-bar .feedback:empty { display: none; }
/* The divider between the session cluster and the broadcast cluster. */
.owner-bar > .label + .row { margin-right: 4px; }
.owner-bar .owner-bar-sep {
  width: 1px; align-self: stretch; min-height: 22px; background: var(--border); margin: 0 4px;
}
.interval-picker { background: var(--card-2); }

/* ---- Message thread ----------------------------------------------------- */
.thread-list {
  display: flex; flex-direction: column; gap: 4px;
  overflow-y: auto; padding: 12px 18px 6px; margin: 0;
  /* Outside the two-pane workspace (narrow/fallback mount) fall back to a
     bounded height rather than growing the page forever. */
  max-height: 100%;
}
.group-workspace .thread-list { max-height: none; }
.thread-bubble-wrap { display: flex; }
.thread-bubble-wrap.align-right { justify-content: flex-end; }
.thread-bubble-wrap.align-left { justify-content: flex-start; }

.thread-bubble {
  /* Capped rather than free-flowing: at 98vw on a wide monitor an uncapped
     bubble would run to ~2000px and the eye loses the line on every wrap. The
     cap is generous enough that the pane still looks used, not empty. */
  max-width: min(1180px, 86%);
  margin: 5px 0;
  padding: 11px 14px 9px;
  border-radius: 4px 15px 15px 15px;
  background: var(--card);
  border: 1px solid var(--border);
  border-left: 3px solid var(--agent-ink, var(--accent));
  box-shadow: var(--sh-sm);
}
.thread-bubble.bubble-agent { border-bottom-left-radius: 15px; }
.thread-bubble.bubble-user {
  /* --own-bubble, not --accent-solid: light mode aliases the two so nothing
     changes there, while the black theme can demote this one large block
     without dimming any button. */
  background: var(--own-bubble);
  color: var(--on-own-bubble);
  border-color: var(--own-bubble);
  border-left: none;
  border-right: 3px solid var(--on-own-bubble);
  border-radius: 15px 4px 15px 15px;
}
.thread-bubble-header { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 5px; }
.thread-sender-name { font-family: var(--display); font-weight: 700; font-size: 14px; color: var(--agent-ink, var(--accent)); }
.thread-bubble-chips { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 4px; }
.thread-target-chip {
  font-size: 10.5px; font-weight: 700; padding: 2px 8px; border-radius: 999px;
  color: var(--agent-ink, var(--accent));
  border: 1px solid color-mix(in srgb, var(--agent-ink, var(--accent)) 34%, transparent);
  background: color-mix(in srgb, var(--agent-ink, var(--accent)) 10%, transparent);
}
.thread-target-chip.on-primary { color: var(--on-accent); border-color: var(--overlay-soft); background: rgba(255, 255, 255, 0.16); }
/* Reply: last on the read-more row at the BOTTOM of the bubble, pushed hard
   right. It used to sit in the header, which put it off-screen on exactly the
   long messages most worth replying to — you had to scroll back up to answer.
   margin-left:auto rather than position:absolute so it can never sit on top of
   the Read more toggle — it wraps below it instead. */
.thread-reply-btn {
  margin-left: auto;
  flex: none;
  /* font: inherit first — a bare <button> otherwise falls back to the system
     UI font and this would be the one control in the bubble not in the app's
     typeface (the sibling target chips are spans and inherit for free). */
  font: inherit;
  font-size: 10.5px; font-weight: 700; line-height: 1;
  padding: 3px 8px; border-radius: 999px; cursor: pointer;
  color: var(--agent-ink, var(--accent));
  border: 1px solid color-mix(in srgb, var(--agent-ink, var(--accent)) 34%, transparent);
  background: transparent;
  /* Quiet until wanted: the button is one per bubble, and at full strength a
     long thread reads as a column of buttons rather than a conversation.
     Never display:none — hidden-until-hover is unreachable by keyboard and
     invisible on touch, so it stays in the layout and in the tab order. */
  opacity: 0.35;
  transition: opacity 0.12s ease, background 0.12s ease;
}
.thread-bubble:hover .thread-reply-btn,
.thread-reply-btn:focus-visible { opacity: 1; }
.thread-reply-btn:hover { opacity: 1; background: color-mix(in srgb, var(--agent-ink, var(--accent)) 12%, transparent); }
/* Touch/pen: there is no hover to reveal it, so it is always fully legible. */
@media (hover: none) {
  .thread-reply-btn { opacity: 1; }
}
/* Composer gone (ended group) — the buttons already rendered have nowhere to
   write, so they are removed rather than left to no-op. See syncReplyAffordance. */
.thread-list.no-reply .thread-reply-btn { display: none; }
.thread-bubble-body { white-space: pre-wrap; word-break: break-word; font-size: 14px; }
/* Underlined as well as coloured, so the affordance never rests on colour
   alone. break-word on the anchor too: a long URL is one unbreakable token and
   would otherwise push the bubble past its max-width. */
.thread-link { color: var(--accent-ink); text-decoration: underline; word-break: break-all; }
.thread-link:hover { text-decoration-thickness: 2px; }
/* The viewer's own bubble is a filled tonal block, so --accent-ink is close to
   invisible on it. --on-own-bubble (NOT --on-accent) is that block's real
   foreground: on the dark theme --on-accent is near-black, which on the dark
   --own-bubble fill would be unreadable. */
.bubble-user .thread-link { color: var(--on-own-bubble); text-decoration-color: var(--on-own-bubble); }
.thread-bubble-body.collapsed { max-height: 7.5em; overflow: hidden; }
/* Read more (left) and Reply (right) share one row under the body.
   No min-height and no margin on purpose: a short message the viewer wrote has
   neither control, and `.hidden` is display:none, so the row must collapse to
   nothing rather than pad every bubble in the thread. The children's own
   padding provides the separation from the body. */
.thread-body-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.thread-read-more { background: none; border: none; color: var(--accent-ink); font-size: 12.5px; font-weight: 700; cursor: pointer; padding: 3px 0; }
/* --on-own-bubble, NOT --on-accent — the same trap the .thread-link comment
   above describes, which every rule in this block had fallen into. On the dark
   theme --on-accent is #20120a (near-black, meant for text ON the bright
   accent); over the --own-bubble fill it measured 1.36:1. Effectively
   invisible, and the light theme hid it because both tokens are #ffffff there. */
.bubble-user .thread-read-more { color: var(--on-own-bubble); text-decoration: underline; }
.thread-attachments { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.thread-bubble-footer { display: flex; align-items: center; gap: 5px; margin-top: 6px; font-size: 11px; color: var(--faint); }
.bubble-user .thread-bubble-footer { color: var(--on-own-bubble); opacity: 0.85; justify-content: flex-end; }
.thread-sent-tick { color: var(--green); }
.bubble-user .thread-sent-tick { color: var(--on-own-bubble); }
/* Who has actually picked the message up. Sits in the footer's muted register —
   it is a reassurance you glance at, not something competing with the message.
   No opacity of its own: `opacity` on the footer already applies to this whole
   subtree, so a second value would MULTIPLY (0.85 x 0.9 = 0.765) and leave the
   receipt fainter than the timestamp beside it. Weight carries the emphasis. */
.thread-delivered-to { font-weight: 700; letter-spacing: 0.1px; }
.bubble-user .thread-delivered-to { color: var(--on-own-bubble); }
.thread-pending-spinner { display: block; font-size: 11px; opacity: 0.75; margin-top: 2px; }
.thread-retry { display: block; background: none; border: none; color: var(--red); font-size: 11px; cursor: pointer; padding: 2px 0; margin-top: 2px; }
/* Same token fix. This one is not cosmetic: it is the "Failed to send — tap
   to retry" control, so at 1.98:1 the recovery affordance for a failed send
   was invisible on the dark theme. */
.bubble-user .thread-retry { color: var(--on-own-bubble); text-decoration: underline; }

.thread-system-note { display: flex; align-items: center; gap: 10px; padding: 8px 4px; }
.thread-note-rule { flex: 1; height: 1px; background: var(--border); }
.thread-note-label { font-size: 12px; font-style: italic; color: var(--faint); white-space: nowrap; }

.thread-pill {
  position: sticky; bottom: 8px; margin: 0 auto; display: block; z-index: 2;
  background: var(--accent-solid); color: var(--on-accent); border: none; border-radius: 999px;
  padding: 7px 16px; font: inherit; font-size: 12.5px; font-weight: 700; cursor: pointer;
  box-shadow: var(--sh);
}

/* ---- Composer ----------------------------------------------------------- */
.thread-composer {
  border-top: 1px solid var(--border-soft);
  padding: 10px 18px;
  margin: 0;
  background: var(--card);
}
/* The composer always rendered a char counter AND an aria-live feedback span
   under the row, both permanently reserving height even with nothing to say —
   ~40px of dead space under the input. They now collapse when empty and the
   counter only appears once you start typing. */
.thread-composer > .feedback:empty,
.thread-composer > .feedback.hidden { display: none; min-height: 0; margin: 0; }
.thread-composer > .thread-char-counter:empty { display: none; margin: 0; }
.thread-composer-row { align-items: flex-end; gap: 8px; }
.thread-textarea {
  resize: none; min-height: 44px; max-height: 140px; font: inherit; font-size: 14px;
  border-radius: var(--radius); padding: 12px 14px; flex: 1;
}
/* Composer icon buttons are one consistent square so the row never staggers. */
.thread-actions-btn, .thread-attach-btn {
  width: 38px; height: 38px; padding: 0; justify-content: center;
  border-radius: 12px; border: 1px solid var(--border); background: var(--card-2);
  color: var(--muted); font-size: 16px; font-weight: 700; box-shadow: none;
}
.thread-actions-btn:hover:not(:disabled), .thread-attach-btn:hover:not(:disabled) {
  border-color: var(--accent); color: var(--accent-ink); background: var(--accent-tint);
}
.thread-send-btn {
  flex-shrink: 0; width: auto; height: 38px; padding: 0 22px;
  border-radius: 12px; font-size: 14.5px;
}
.thread-char-counter { text-align: right; color: var(--faint); font-size: 11.5px; margin-top: 6px; }
.thread-char-counter.error { color: var(--red); }
.thread-mention-popup {
  display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius-sm); max-height: 190px; overflow-y: auto; margin-bottom: 8px; box-shadow: var(--sh);
}
.thread-mention-option {
  background: none; border: none; color: var(--text); text-align: left;
  padding: 8px 13px; cursor: pointer; font: inherit; font-size: 13px;
}
.thread-mention-option:hover, .thread-mention-option.active { background: var(--accent-tint); color: var(--accent-ink); }
.thread-actions-menu { display: flex; flex-direction: column; gap: 4px; }
.thread-ended-banner { margin: 0; border-top: 1px solid var(--border-soft); border-radius: 0; }

/* ---- Attachments -------------------------------------------------------- */
.thread-attachment-tray { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.attachment-tray-chip {
  display: flex; align-items: center; gap: 6px; max-width: 220px;
  background: var(--card-2); border: 1px solid var(--border); border-radius: 999px;
  padding: 4px 10px; font-size: 12px;
}
.attachment-tray-chip.is-failed { border-color: var(--red); }
.attachment-tray-thumb { width: 24px; height: 24px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
.attachment-tray-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 110px; }
.attachment-tray-spinner { color: var(--muted); font-style: italic; }
.attachment-tray-check { color: var(--green); font-weight: 700; }
.attachment-tray-error { color: var(--red); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100px; }
.attachment-tray-retry, .attachment-tray-remove { padding: 0 2px; font-size: 12px; flex-shrink: 0; }

.thread-composer.drag-over { outline: 2px dashed var(--accent); outline-offset: 4px; border-radius: var(--radius-sm); }

.attachment-thumb-btn { padding: 0; border: none; background: none; cursor: pointer; display: inline-block; }
.attachment-thumb {
  max-width: 200px; max-height: 200px; width: auto; height: auto;
  object-fit: cover; border-radius: 10px; border: 1px solid var(--border); display: block;
}
/* The pill must CONTAIN its contents at any filename length.
 *
 * It used to be `max-width: 220px` with nothing telling the filename to
 * truncate. A long name therefore WRAPPED — five lines for
 * "probe-v06-two-screens-reel-t33.png" — the chip grew tall, and
 * `border-radius: 999px` on a tall box is an ellipse, not a pill. Its curve cut
 * straight across the icon and the Download link, so the border no longer
 * enclosed what was inside it.
 *
 * Three changes, each load-bearing:
 *   max-width: 100%   - bound by the BUBBLE, not a magic number that a wide
 *                       bubble makes pointless and a narrow one makes wrong.
 *   white-space: nowrap on the name (below) - one line, so the chip is always
 *                       one row tall and 999px stays a pill.
 *   min-width: 0      - a flex item defaults to min-width:auto and refuses to
 *                       shrink below its text, which is why max-width was being
 *                       exceeded by 22px in the first place.
 */
.attachment-chip {
  display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--accent-ink);
  text-decoration: none; border: 1px solid var(--border); border-radius: 999px; padding: 3px 10px;
  max-width: 100%; min-width: 0; vertical-align: middle;
}
/* The only part allowed to shrink, and the only one with an upper bound.
 *
 * The 140px cap is measured, not guessed: with it the error chip (icon + name +
 * Retry + Download) is 268px and the plain chip 223px, both inside a 280px
 * bubble; at 160px the error chip reaches 288px and overflows. It is on the
 * NAME rather than the chip because `max-width` on an inline-flex CHIP does not
 * force its items to shrink — measured: the chip stayed 302px against a 280px
 * parent. Only `display:flex` clamps it, and that would stack the pills into
 * one per row instead of side by side.
 *
 * white-space:nowrap is the part that fixes the reported bug: it is what stops
 * the name wrapping, which is what made the chip tall, which is what turned
 * border-radius:999px into an ellipse cutting through the content. The full
 * name is still available on hover via title. */
.attachment-chip .attachment-name {
  min-width: 0; max-width: 140px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Retry / Download / the icon / the size must never be squeezed out — losing
 * the controls is worse than losing filename characters, which the title
 * attribute still carries in full. */
.attachment-chip > .btn-link,
.attachment-chip > .attachment-meta,
.attachment-chip > span:first-child { flex: 0 0 auto; white-space: nowrap; }
.bubble-user .attachment-chip { color: var(--on-own-bubble); border-color: var(--overlay-soft); }
.attachment-chip.attachment-broken { color: var(--red); border-color: var(--red); }

.attachment-lightbox {
  position: fixed; inset: 0; z-index: 1000; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 12px;
  background: var(--overlay); padding: 24px;
}
.attachment-lightbox img { max-width: 90vw; max-height: 80vh; object-fit: contain; border-radius: 10px; }
.attachment-lightbox-caption { color: #fff; font-size: 13px; }
.attachment-lightbox-controls { display: flex; gap: 8px; }

/* ---- Setup & Keys ------------------------------------------------------- */
.key-row { display: flex; flex-direction: column; gap: 4px; padding: 10px 0; border-bottom: 1px solid var(--border); }
.key-row:last-child { border-bottom: none; }
.key-row.child { margin-left: 24px; border-left: 2px solid var(--border); padding-left: 12px; }
.key-row-head { display: flex; align-items: center; gap: 10px; justify-content: space-between; }
.key-row-title { display: flex; align-items: center; gap: 8px; }
.key-chip {
  display: inline-block; border: 1px solid var(--border); border-radius: 999px;
  padding: 1px 9px; font-size: 11px; color: var(--muted);
}
.key-children { display: flex; flex-direction: column; }
.key-children.collapsed { display: none; }
.expand-toggle { background: none; border: none; color: var(--accent-ink); cursor: pointer; font: inherit; font-size: 13px; padding: 4px 0; text-align: left; }

/* ---- Scrollbars --------------------------------------------------------- */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; border: 3px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: var(--faint); background-clip: content-box; }
::-webkit-scrollbar-track { background: transparent; }

/* ---- Narrow / single-pane fallback -------------------------------------- */
/* Narrow: one pane at a time. The full-height single-scroll model is KEPT
   rather than collapsing to height:auto — letting the panes grow naturally
   gave the page its own scrollbar on top of the thread list's, so a drag
   could move either one and the composer sat below the fold. */
@media (max-width: 1000px) {
  .wrap-wide { width: 98vw; }
  .group-workspace { grid-template-columns: 1fr; }
  .group-workspace #group-detail { display: none; }
  .group-workspace.showing-detail #group-master { display: none; }
  .group-workspace.showing-detail #group-detail { display: flex; }
}

/* Short viewports (a laptop with the dock and a browser toolbar, a split
   screen): stop the fixed chrome from squeezing the message list to nothing.
   Once it hits the floor #app-view's own scroll takes over. */
@media (max-height: 620px) {
  /* Specificity must match the pane rule that sets min-height on this element,
     or the floor never applies. */
  .group-workspace #group-detail > .thread-list { min-height: 160px; }
  /* ...and once that floor engages, the pane's fixed children can total more
     than the pane's height. The pane clips (overflow:hidden), so without this
     the COMPOSER is cut off with no scrollbar anywhere able to reach it. */
  .group-workspace #group-detail { overflow-y: auto; }
  /* Trim chrome, never remove it — .head carries the docked nav, so hiding it
     would strand the user in the chat area with no way out. */
  .wrap-wide { padding: 4px 0; }
  .wrap-wide .head h1 { display: none; }
  .owner-bar { padding: 5px 14px; }
}

/* ---- Visibility primitive — MUST BE LAST --------------------------------
   .hidden is the app's only show/hide mechanism (4 files toggle it, including
   the auth gate). It is a single class, so any component rule of equal
   specificity that sets `display` and appears LATER in the file silently beats
   it. That is not hypothetical: .thread-pill{display:block} and
   .thread-mention-popup{display:flex} both did, so thread.js's
   `class="thread-pill hidden"` and `class="thread-mention-popup hidden"`
   rendered anyway — an empty orange lozenge and a 2px bordered strip parked
   above every composer, permanently.
   Keeping this rule last makes the primitive win by source order instead of
   requiring !important, and means new component rules cannot re-break it. */
.hidden { display: none; }

/* Markdown tables in an agent message.
   Mirrors app_markdown.dart's _ScrollableTable: the table scrolls sideways
   inside its own box so a wide one never squashes its columns or forces the
   whole thread to scroll horizontally. Font is inherited deliberately — a table
   is part of the message, not a different kind of content, which is the same
   thing the phone was corrected on. */
.md-table-scroll {
  overflow-x: auto;
  margin: 8px 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.md-table {
  border-collapse: collapse;
  width: max-content;
  min-width: 100%;
  font: inherit;
}
.md-table th,
.md-table td {
  padding: 8px 10px;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--border-soft);
  white-space: normal;
}
.md-table tr:last-child th,
.md-table tr:last-child td { border-bottom: 0; }
.md-table th {
  font-weight: 700;
  background: var(--card-2);
}
