/* ===== Main Chat Area ===== */
.main {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  position: relative;
}
#chatView {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* Positioning context for the absolutely-anchored composer
     (`.inputArea`). The composer floats over the bottom of the chat
     area instead of pushing it shorter — so the welcome screen / chat
     content keep a fixed layout and the fn-form just overlaps them
     when it expands. */
  position: relative;
}

/* Top bar */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  height: 48px;
  box-sizing: border-box;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  background: var(--bg-primary);
  z-index: 5;
  /* Container query target — badges below adapt in stages based on
     this element's inline-size, so the four chips never overlap or
     push siblings off-screen. */
  container-type: inline-size;
  container-name: topbar;
}

.topbar-left {
  display: flex;
  align-items: center;
  gap: 8px;
  /* min-width: 0 lets the flex children shrink properly so the
     ellipsis on individual badges activates instead of pushing
     siblings off-screen. */
  min-width: 0;
  flex: 1 1 auto;
  overflow: hidden;
}

/* Each badge in the topbar (status / branch / chat / exec) is a
   two-part chip: an always-visible short label + an optional details
   span that shrinks-then-disappears as the topbar narrows. Making
   the badge itself an inline-flex container lets the details span
   ellipsis-truncate independently of the short label. */
.status-badge,
.runtime-badge {
  display: inline-flex;
  align-items: center;
  /* Chips do NOT shrink: a chip is shown at its full content width or its
     label is hidden wholesale by the @container thresholds below. Letting
     it shrink (flex-shrink:1) squeezed labels down to a 0-width / 1-char
     stub, which is exactly what we're avoiding. */
  flex-shrink: 0;
  box-sizing: border-box;
  /* Uniform fixed height for EVERY topbar chip. Without this, icon-only
     chips (status / project / exec) render ~4px shorter than text chips
     (branch / chat) because the text line-box is taller than the 14px
     icon. Fixed height + border-box makes per-type padding/border height-
     neutral, so all chips are exactly the same height. */
  height: 24px;
  /* Uniform label weight across every chip — without this the branch name
     and agent model rendered at 400 while status / project sat at 500,
     which read as a font mismatch. */
  font-weight: 500;
}
.badge-short {
  font-weight: 500;
  flex-shrink: 0;
}
.badge-details {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex-shrink: 0;
  /* Generous fixed cap: a normal model name shows in full; only a
     pathologically long id ellipsizes. We never shrink it down to a
     meaningless stub — instead the whole label is hidden once the bar is
     too narrow (see the @container thresholds below). */
  max-width: 32ch;
}

.branch-name {
  display: inline-block;
  vertical-align: bottom;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex-shrink: 0;
  max-width: 24ch;
}

/* Overflow-driven label collapse. The actual fit is measured in JS
   (top-bar/index.tsx via ResizeObserver), which sets data-compact on
   .topbar-left ONLY when the chips would genuinely overflow — never
   prematurely while there's still room. Labels drop longest-first:
   level 1 = agent models, 2 = + branch name, 3 = + status/project →
   a clean row of icons. Full-or-hidden, never a truncated stub. */
.topbar-left[data-compact="1"] .agent-badge .badge-details,
.topbar-left[data-compact="2"] .agent-badge .badge-details,
.topbar-left[data-compact="3"] .agent-badge .badge-details {
  display: none;
}
.topbar-left[data-compact="2"] .branch-badge .branch-name,
.topbar-left[data-compact="3"] .branch-badge .branch-name {
  display: none;
}
.topbar-left[data-compact="3"] .status-badge .badge-short,
.topbar-left[data-compact="3"] .project-badge .badge-short,
.topbar-left[data-compact="3"] .project-badge .project-caret {
  display: none;
}

.topbar-right {
  display: flex;
  align-items: center;
  gap: 8px;
}

.menu-btn {
  display: none;
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 0;
  width: 32px;
  height: 32px;
  border-radius: 6px;
  align-items: center;
  justify-content: center;
}
.menu-btn svg { flex-shrink: 0; }

.menu-btn:hover { color: var(--text-bright); }

.status-badge {
  font-size: 12px;
  padding: 2px 8px;
  border-radius: 3px;
  background: rgba(152, 195, 121, 0.10);
  color: var(--accent-green);
  border: 1px solid rgba(152, 195, 121, 0.22);
  cursor: pointer;
}
/* `[data-state="open"]` keeps the trigger lit while its popover is open
   (radix stamps data-state on the trigger), so the chip reads as "active"
   the whole time the menu is showing — not just on hover. */
.status-badge:hover,
.status-badge[data-state="open"] {
  background: rgba(152, 195, 121, 0.18);
}
/* Initial / probing state — yellow until the worker has actually
   answered (set in HTML as the bootstrap class, removed by
   updateStatus once the WS receives its first envelope). */
.status-badge.connecting {
  background: rgba(198, 144, 38, 0.10);
  color: var(--accent-yellow);
  border-color: rgba(198, 144, 38, 0.22);
}
.status-badge.connecting:hover,
.status-badge.connecting[data-state="open"] {
  background: rgba(198, 144, 38, 0.18);
}
/* Status badge carries a source name (potentially long, e.g.
   "WeChat (xxx) · run analyze_sentiment") inside its short label.
   Allow it to ellipsis-shrink — the other badges' .badge-short
   (Chat / Exec / branch name) stays whole because they're short
   already, so the default flex-shrink:0 rule above is fine for
   them. */
.status-badge .badge-short {
  flex-shrink: 0;
  min-width: 0;
  max-width: 24ch;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Connection-state dot inside .status-badge is now an
   .indicator-dot — see the indicator-dot section below. */
/* Double-class selector so this beats `.runtime-badge`'s green
   default (defined later in the file at L886). Without the extra
   `.runtime-badge` qualifier these rules and that block tie on
   specificity and the later one wins. */
.runtime-badge.branch-badge {
  font-size: 12px;
  padding: 2px 8px;
  border-radius: 3px;
  /* Orange — same family as git's branch / tag color so the
     "this is a VCS-ish branch" association reads quickly. */
  background: rgba(209, 154, 102, 0.10);
  color: var(--accent-orange);
  border: 1px solid rgba(209, 154, 102, 0.25);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.runtime-badge.branch-badge:hover,
.runtime-badge.branch-badge[data-state="open"] {
  background: rgba(209, 154, 102, 0.18);
}
.branch-badge .branch-icon {
  display: inline-flex;
  align-items: center;
  opacity: 0.85;
}
.branch-badge .branch-icon svg { display: block; }

/* Project chip — the "where am I" anchor, mirrors Claude's composer
   project pill. Deliberately NEUTRAL (not a loud accent colour) so it
   reads as the calm context label that the louder green status / orange
   branch+agent chips hang off. Double-class qualifier beats the green
   `.runtime-badge` default at L886, same as branch-badge above. */
.runtime-badge.project-badge {
  font-size: 12px;
  padding: 2px 8px;
  border-radius: 3px;
  background: color-mix(in srgb, var(--text-primary) 6%, transparent);
  color: var(--text-secondary);
  border: 1px solid var(--border);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}
.runtime-badge.project-badge:hover,
.runtime-badge.project-badge[data-state="open"] {
  background: color-mix(in srgb, var(--text-primary) 12%, transparent);
  color: var(--text-bright);
}
.project-badge .project-icon {
  display: inline-flex;
  align-items: center;
  opacity: 0.8;
  flex-shrink: 0;
}
.project-badge .project-icon svg { display: block; }
.project-badge .project-caret { opacity: 0.6; flex-shrink: 0; }
/* Let long project names ellipsize rather than push the other chips
   off-screen (.badge-short is flex-shrink:0 by default). */
.project-badge .badge-short {
  flex-shrink: 0;
  min-width: 0;
  max-width: 24ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Hover tooltips (topbar chips + message actions) — Claude-style:
   NO border, a solid badge a touch lighter than the surface ("泛白"),
   crisp text. Geometry / shadow / animation live on the Tailwind
   classes in components/ui/tooltip.tsx. */
.hover-tip {
  background: color-mix(in srgb, var(--text-primary) 14%, var(--bg-secondary));
  color: var(--text-bright);
  border: none;
}

/* The topbar branch dropdown is the React <BranchMenu /> (shadcn
   <Popover> + Tailwind) now — its `.branch-selector` / `.model-dd-*`
   rules were removed. */

.status-badge.disconnected {
  background: rgba(224, 108, 117, 0.12);
  color: var(--accent-red);
  border-color: rgba(224, 108, 117, 0.25);
}

.status-badge.paused {
  background: rgba(229, 192, 123, 0.12);
  color: var(--accent-yellow);
  border-color: rgba(229, 192, 123, 0.25);
}


/* Chat messages */
.chat-area {
  flex: 1;
  overflow-y: auto;
  padding: 0;
  display: flex;
  flex-direction: column;
}
/* Lock scrolling while the welcome panel is up. Driven by the React
   portal host: `<WelcomeScreen />` renders a child into `#welcome-mount`
   when visible, so `:has(#welcome-mount > *)` is true exactly then — one
   source of truth, no imperative `.welcome-visible` class. We key off
   the mount's stable id, NOT `.welcome`: that class is CSS-Module-hashed
   (`welcome-screen_welcome__…`), so a literal `.welcome` in this global
   sheet never matches it. */
.chat-area:has(#welcome-mount > *) { overflow: hidden; }



.chat-messages {
  flex: 1;
  /* Bottom padding gives the latest message room to scroll up to the
     top of the viewport so the assistant reply lands underneath it
     (ChatGPT / Claude behaviour). Capped at 25vh — a full
     `100vh - 220px` was so tall the chat-area could open with the
     entire viewport empty below the last message even on a fresh
     short conversation. */
  padding: 24px 0 25vh 0;
  display: flex;
  flex-direction: column;
  gap: 0;
}
/* Bottom padding clears the absolutely-anchored composer so the welcome
   examples row sits just above it. Keyed off the React portal host
   `#welcome-mount` HAVING A CHILD (i.e. welcome visible) — NOT
   `.welcome`, which is CSS-Module-hashed and so untargetable from this
   global sheet. (That dead `:has(.welcome)` is why the row used to fall
   through to the 25vh base rule and float ~50px too high once the legacy
   inline 150px stopped being stamped.) 150px matches the composer's
   occupied height so the chips land right above it — same position the
   legacy framework gave. Bump toward ~165px for a larger visible gap. */
.chat-messages:has(#welcome-mount > *) {
  padding-bottom: 150px;
}

.message {
  padding: 16px 24px;
  max-width: 100%;
  animation: msgAppear 0.3s ease forwards;
  position: relative;
}

/* Hover-reveal action bar (copy / retry / branch) — mirrors
   ChatGPT/Claude UX. Hidden by default; fades in when the user mouses
   over the bubble. Individual buttons have their own hover state for
   the background pill. */
.message-actions {
  display: flex;
  align-items: center;
  gap: 2px;
  margin-left: auto;  /* push to right of flex .message-header */
  opacity: 0;
  transition: opacity 150ms ease;
}
.message:hover .message-actions,
.message-actions:focus-within {
  opacity: 1;
}
.message-action-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  background: transparent;
  border: none;
  border-radius: 5px;
  color: var(--text-muted);
  cursor: pointer;
  padding: 0;
}
.message-action-btn:hover {
  background: rgba(0, 0, 0, 0.08);
  color: var(--text-primary);
}
@media (prefers-color-scheme: dark) {
  .message-action-btn:hover { background: rgba(255, 255, 255, 0.1); }
}
.message-action-btn[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
}

/* ContextGit — run-active gating. When an agent run is in flight,
   grey out Edit and Retry buttons so the user can't fork mid-run
   (the server rejects it anyway with 409, but the UI should make
   the rule obvious). Copy and Branch stay enabled — they don't
   fork the DAG. */
[data-run-active="true"] .message-action-btn[data-action="retry"],
[data-run-active="true"] .message-action-btn[data-action="edit"] {
  opacity: 0.35;
  pointer-events: none;
}

/* < N / M > sibling navigator. Lives INSIDE .message-actions so it
   shares the bar's hover-gated visibility and sits on the same row
   as the copy/retry/edit icons. */
.message-nav {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-left: 6px;
  font-size: 12px;
  color: var(--text-muted);
  user-select: none;
}
.message-nav-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px; height: 22px;
  background: transparent;
  border: none;
  border-radius: 5px;
  color: var(--text-muted);
  cursor: pointer;
  padding: 0;
}
.message-nav-btn:hover:not([disabled]) {
  background: rgba(0, 0, 0, 0.08);
  color: var(--text-primary);
}
@media (prefers-color-scheme: dark) {
  .message-nav-btn:hover:not([disabled]) { background: rgba(255, 255, 255, 0.1); }
}
.message-nav-btn[disabled] {
  opacity: 0.35;
  cursor: default;
}
.message-nav-btn svg { width: 16px; height: 16px; }
.message-nav-label {
  font-variant-numeric: tabular-nums;
  min-width: 34px;
  text-align: center;
}

/* Timestamp badge inside the action bar. Small and unobtrusive;
   hovering it shows the full date via the native title tooltip. Kept
   separate from the bubble so the tooltip doesn't fire across the
   whole message area. */
.message-timestamp {
  display: inline-flex;
  align-items: center;
  padding: 0 6px;
  font-size: 12px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  user-select: none;
}

/* Inline edit mode — the bubble's content swaps out for a textarea
   and Save/Cancel buttons. */
.message.is-editing .message-actions,
.message.is-editing .message-nav {
  display: none;
}
.message-edit-textarea {
  width: 100%;
  min-height: 60px;
  padding: 8px 10px;
  background: var(--bg-primary);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-primary);
  font: inherit;
  font-size: 14px;
  line-height: 1.5;
  resize: vertical;
  outline: none;
}
.message-edit-textarea:focus {
  border-color: var(--accent-blue);
}
.message-edit-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 6px;
}
.message-edit-btn {
  padding: 5px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-primary);
  color: var(--text-primary);
  cursor: pointer;
  font-size: 14px;
}
.message-edit-btn:hover:not([disabled]) {
  background: var(--bg-hover, rgba(0,0,0,0.05));
}
.message-edit-btn[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}
.message-edit-btn.message-edit-save {
  background: var(--accent-blue);
  color: #fff;
  border-color: var(--accent-blue);
}
.message-edit-btn.message-edit-save:hover:not([disabled]) {
  filter: brightness(1.08);
}
.message-action-btn svg {
  width: 16px;
  height: 16px;
}
/* Brief "Copied!" confirmation after Copy click. */
.message-action-btn.is-copied {
  color: var(--accent-green, #22c55e);
}

@keyframes msgAppear {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Three message roles render with three distinct surfaces so the
   eye can sort them at a glance:
   - user      → ``--user-msg-bg``       (slightly warmer/darker tint)
   - assistant → ``--assistant-msg-bg``  (slightly cooler tint)
   - system    → transparent (chat background bleed-through;
                  these are typically attach pointers / runtime
                  blocks / tool-call cards rendered as standalone
                  cards with their own border, NOT as message
                  bubbles, so they read as overlays). */
.message.user {
  background: var(--user-msg-bg);
}

.message.assistant {
  background: var(--assistant-msg-bg);
}

.message.system {
  background: transparent;
}

.message-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.message-avatar {
  width: 28px;
  height: 28px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 700;
  flex-shrink: 0;
}

.message-avatar.user-avatar {
  background: var(--accent-blue);
  color: #fff;
}

.message-avatar.bot-avatar {
  background: var(--accent-green);
  color: #fff;
}

.message-sender {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-bright);
}

.message-content {
  padding-left: 36px;
  padding-right: 36px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-primary);
  word-wrap: break-word;
}

.message-content pre {
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px;
  margin: 8px 0;
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-primary);
  white-space: pre-wrap;
  word-break: break-word;
}

.message-content code {
  font-family: var(--font-mono);
  font-size: 12px;
  background: var(--bg-secondary);
  padding: 1px 5px;
  border-radius: 3px;
}
.message-content pre code { background: none; padding: 0; }
.message-content p { margin: 4px 0; }
.message-content ul, .message-content ol { margin: 4px 0; padding-left: 20px; }
.message-content li { margin: 2px 0; }
.message-content h1, .message-content h2, .message-content h3 { margin: 12px 0 6px; color: var(--text-bright); }
.message-content h1 { font-size: 18px; } .message-content h2 { font-size: 16px; } .message-content h3 { font-size: 14px; }
.message-content a { color: var(--accent-blue); text-decoration: none; }
.message-content a:hover { text-decoration: underline; }
.message-content blockquote { border-left: 3px solid var(--border); margin: 8px 0; padding: 4px 12px; color: var(--text-secondary); }
.message-content table { border-collapse: collapse; margin: 8px 0; font-size: 14px; }
.message-content th, .message-content td { border: 1px solid var(--border); padding: 4px 8px; }
.message-content th { background: var(--bg-secondary); }

/* ===== Runtime Block (function execution card) ===== */
.runtime-block {
  /* Left/right margin matches where .message-content starts —
     .message has 24px horizontal padding plus .message-content adds
     another 36px (the 28px avatar + 8px header gap), so chat text
     begins 60px from the chat-area edge. Mirror that here so the
     runtime block aligns with the conversation text column, not
     with the message's avatar column. */
  margin: 20px 60px;
  border: 1px solid var(--border);
  border-left: 3px solid #a08a6c;
  border-radius: var(--radius);
  background: var(--bg-tertiary);
  animation: msgAppear 0.3s ease forwards;
  overflow: hidden;
}
.runtime-block + .message, .message + .runtime-block { margin-top: 20px; }
/* When a RuntimeBlock is nested INSIDE an assistant bubble
 * (LLM-initiated agentic call), drop the outer margins so it sits
 * tight against the surrounding text and reads as part of the same
 * card, not a separate floating element. */
.assistant-runtime-children .runtime-block {
  margin: 12px 0 0 0;
}
.runtime-block-header {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 10px 36px 10px 12px;
  background: var(--bg-secondary);
  border-bottom: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--text-bright);
  cursor: pointer;
  user-select: none;
  flex-wrap: nowrap;
  overflow: hidden;
  min-width: 0;
}
.runtime-block-header:hover { background: var(--bg-hover); }
.runtime-block.collapsed .runtime-block-header { border-bottom: none; }
.runtime-block.collapsed .runtime-block-body,
.runtime-block.collapsed .runtime-block-footer { display: none; }
.runtime-icon {
  color: var(--text-secondary);
  font-size: 14px;
  flex-shrink: 0;
  width: 18px;
  text-align: center;
  transition: transform var(--transition);
  display: inline-block;
  transform: rotate(90deg);
}
.runtime-block.collapsed .runtime-icon { transform: rotate(0deg); }
.runtime-block-header .runtime-result-preview { display: none; }
.runtime-block.collapsed .runtime-block-header .runtime-result-preview {
  /* Collapsed-state preview tail: "-> success · 1 steps · 37s".
   * inline-flex + flex-shrink:0 so it sits on the same row as the
   * function signature; the .runtime-func ellipsis-truncates as
   * needed to make room. */
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  white-space: nowrap;
  color: var(--text-muted);
  font-size: 14px;
  margin-left: 8px;
  font-weight: normal;
  max-width: 40%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.runtime-func {
  color: var(--text-bright);
  font-weight: 600;
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.runtime-params {
  color: var(--text-secondary);
  font-size: 14px;
  white-space: nowrap;
}
.runtime-block-body {
  padding: 0;
}
.runtime-block-content {
  margin: 10px 36px;
}
.runtime-block-content > * {
  margin-left: 0;
  padding-left: 0;
}
.runtime-block-body .message-content {
  padding-left: 0;
  padding-right: 0;
}
.runtime-block-content .runtime-result {
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-primary);
  word-wrap: break-word;
}
.runtime-return-label {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--accent-purple);
  font-weight: 600;
}
.runtime-block-content .inline-tree {
  margin: 14px 0 14px;
}
.runtime-output {
  padding: 4px 32px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-primary);
  word-wrap: break-word;
}
.runtime-output ul, .runtime-output ol { margin: 4px 0; padding-left: 20px; }
.runtime-output li { margin: 2px 0; }
.runtime-block-content .typing-indicator {
  padding-left: 32px;
  margin-top: 6px;
}
.runtime-block-footer {
  padding: 10px 36px;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.runtime-footer-left, .runtime-footer-center, .runtime-footer-right {
  display: flex;
  align-items: center;
  flex: 1;
}
.runtime-footer-left { justify-content: flex-start; }
.runtime-footer-center { justify-content: center; }
.runtime-footer-right { justify-content: flex-end; }
.usage-footer-label {
  font-size: 12px;
  color: var(--text-muted);
  font-family: var(--font-mono);
  white-space: nowrap;
}
.runtime-block-footer .rerun-btn,
.runtime-block-footer .attempt-nav {
  margin: 0;
  padding-top: 0;
  padding-bottom: 0;
}
.runtime-block .rerun-btn {
  margin-top: 6px;
}
.runtime-block.interrupted { border-left-color: var(--text-muted); }
.runtime-block.error { border-left-color: var(--accent-red); }

/* Pending runtime block (typing indicator) */
.runtime-block-pending .typing-indicator {
  padding-left: 0;
}


.spinner {
  width: 16px;
  height: 16px;
  border: 2px solid var(--border);
  border-top-color: var(--accent-blue);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}


/* Execution log */
.exec-log {
  margin: 4px 24px 4px 48px;
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: 12px;
  overflow: hidden;
  animation: msgAppear 0.2s ease forwards;
}
.exec-log-header {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px; cursor: pointer;
  color: var(--text-secondary); user-select: none;
  border-bottom: 1px solid var(--border);
}
.exec-log-header:hover { background: var(--bg-hover); }
.exec-log-header .spinner-sm { width: 12px; height: 12px; border: 1.5px solid var(--border); border-top-color: var(--accent-blue); border-radius: 50%; animation: spin 0.8s linear infinite; flex-shrink: 0; }
.exec-log-title { flex: 1; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.exec-log-chevron { font-size: 12px; transition: transform 0.2s; }
.exec-log.collapsed .exec-log-body { display: none; }
.exec-log.collapsed .exec-log-chevron { transform: rotate(-90deg); }
.exec-log-body {
  max-height: 400px; overflow-y: auto; padding: 6px 0;
}
.exec-log-entry {
  display: flex; align-items: flex-start; gap: 6px;
  padding: 3px 12px; line-height: 1.5;
  animation: logSlide 0.15s ease forwards;
}
@keyframes logSlide {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}
.exec-log-indent { flex-shrink: 0; }
.exec-log-icon { flex-shrink: 0; width: 14px; text-align: center; font-size: 12px; }
.exec-log-icon.running { color: var(--accent-blue); }
.exec-log-icon.done { color: var(--accent-green); }
.exec-log-icon.error { color: var(--accent-red); }
.exec-log-name { color: var(--text-bright); cursor: pointer; }
.exec-log-name:hover { text-decoration: underline; }
.exec-log-params { color: var(--text-muted); }
.exec-log-time { color: var(--text-muted); margin-left: auto; flex-shrink: 0; font-size: 12px; }
.exec-log-output { color: var(--text-secondary); padding-left: 20px; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 500px; }

.system-message {
  text-align: center;
  color: var(--text-muted);
  font-size: 12px;
  padding: 6px 0;
  opacity: 0.7;
}

/* Typing indicator (three bouncing dots) */
.typing-indicator {
  display: flex;
  align-items: center;
  gap: 4px;
  height: 24px;
}
/* Align with the text column in assistant bubbles (avatar takes 36px). */
.message.assistant > .typing-indicator {
  padding-left: 36px;
}
/* Standalone "AI is thinking" indicator — full assistant-bubble
   shape (avatar + Agentic header + body with breathing pulse)
   while we're waiting on the real reply. Inherits the base
   ``.message`` and ``.message.assistant`` styles for padding /
   background so it sits inline with neighbouring bubbles. */
.pending-body {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  /* Match where chat text starts inside a normal assistant bubble:
     avatar (28px) + flex gap (8px) = 36px past the bubble's left
     padding. No further indent here — the parent ``.chat-stream-body``
     already sits inside the bubble's natural padding. */
  padding-left: 0;
}
/* .pending-pulse retired — runtime / chat surfaces now use the
   generic .indicator-dot system. See section below. */
.pending-label {
  color: var(--text-muted);
  font-size: 13px;
  letter-spacing: 0.1px;
  font-style: italic;
  opacity: 0.85;
}

/* ===== indicator-dot — unified status dot system =====
 * One CSS class for every "small coloured circle" indicator in
 * the chat UI (Function call header dot, Running… pulse,
 * provider connection status, attach-card status). The outer
 * box is always 12.8x12.8 (matches the ``●`` glyph advance
 * width at 14px font) so dots line up by layout — no per-call-
 * site margin tweaks. ::before paints the visible disc and
 * carries any breathing animation, keeping the layout slot
 * stable. Modifier classes pick size, colour, animation.
 *
 * See docs/design/indicator-dots.md for the full plan.            */
.indicator-dot {
  display: inline-block;
  position: relative;
  width: 12.8px;
  height: 12.8px;
  vertical-align: middle;
  color: var(--accent-blue, #6cb4ff);
  flex-shrink: 0;
}
.indicator-dot::before {
  content: "";
  position: absolute;
  inset: 1.4px;             /* default md = 10x10 disc inside 12.8x12.8 box */
  border-radius: 50%;
  /* Disc colour resolution order:
       1. --dot-color set by a modifier (.--ok / .--warn / ...)
       2. currentColor — lets the caller drive the dot by setting
          ``color`` on a parent (matches how the old
          .attach-card-status-dot inherited its parent pill's hue).
     Outer ``.indicator-dot`` sets a sane ``color`` default so an
     unscoped <span class="indicator-dot" /> still shows up. */
  background: var(--dot-color, currentColor);
}
.indicator-dot.sm {
  width: 10px;
  height: 10px;
}
.indicator-dot.sm::before {
  inset: 2px;                /* sm = 6x6 disc inside 10x10 box */
}
.indicator-dot.--ok      { --dot-color: var(--accent-green); }
.indicator-dot.--warn    { --dot-color: var(--accent-yellow); }
.indicator-dot.--err     { --dot-color: var(--accent-red); }
.indicator-dot.--neutral { --dot-color: var(--accent-blue, #6cb4ff); }
.indicator-dot.pulse-opacity::before {
  animation: indicatorPulseOpacity 1.5s ease-in-out infinite;
}
.indicator-dot.pulse-scale::before {
  animation: indicatorPulseScale 1.4s ease-in-out infinite;
}
@keyframes indicatorPulseOpacity {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.4; }
}
@keyframes indicatorPulseScale {
  0%, 100% { transform: scale(0.85); opacity: 0.9; }
  50%      { transform: scale(1.15); }
}
.typing-indicator .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent-blue, #6cb4ff);
  opacity: 0.85;
  animation: bounce 1.4s ease-in-out infinite;
}
.typing-indicator .dot:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator .dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
  0%, 80%, 100% { transform: translateY(0); }
  40% { transform: translateY(-6px); }
}

/* Runtime info badge */
.runtime-badge {
  font-size: 12px;
  padding: 2px 8px;
  border-radius: 3px;
  background: rgba(152,195,121,0.12);
  color: var(--accent-green);
  border: 1px solid rgba(152,195,121,0.25);
  white-space: nowrap;
}
.runtime-badge.session {
  background: rgba(86,182,194,0.12);
  color: #56b6c2;
  font-family: monospace;
  font-size: 12px;
}
.runtime-badge.model {
  background: rgba(198,120,221,0.12);
  color: var(--accent-purple);
  border-color: rgba(198,120,221,0.25);
  cursor: pointer;
}
.runtime-badge.model:hover {
  background: rgba(198,120,221,0.2);
}
.runtime-badge.agent-badge {
  cursor: pointer;
  font-size: 12px;
  /* Blue chip for the model selectors (chat + exec). Distinct from
     the status badge's green ("Local / connected") and the branch
     badge's orange, so the topbar reads as three different roles
     at a glance even when all chips collapse to a single letter.
     The `.runtime-badge` qualifier keeps this from being tied with
     the plain `.runtime-badge` default that lives later in the
     file — without it that green default wins on cascade order. */
  background: color-mix(in srgb, var(--accent-orange) 10%, transparent);
  color: var(--accent-orange);
  border-color: color-mix(in srgb, var(--accent-orange) 25%, transparent);
}
.runtime-badge.agent-badge:hover,
.runtime-badge.agent-badge[data-state="open"] {
  background: color-mix(in srgb, var(--accent-orange) 18%, transparent);
  border-color: color-mix(in srgb, var(--accent-orange) 35%, transparent);
}
.agent-badge.locked {
  cursor: default;
}
.agent-badge.locked:hover {
  background: inherit;
  border-color: inherit;
}
/* The topbar agent / channel / branch dropdowns are React popovers
   (shadcn <Popover> + Tailwind) now — the `.agent-selector` and
   `.model-dropdown` chrome rules were removed. */

/* Context tree card (inline in chat) */
.context-card {
  margin: 12px 0;
  margin-left: 36px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-secondary);
  overflow: hidden;
}
#liveExecTree { margin: 12px 24px 16px; }

.context-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  background: var(--bg-tertiary);
  cursor: pointer;
  user-select: none;
}

.context-card-header:hover { background: var(--bg-hover); }

.context-card-title {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  color: var(--text-bright);
  display: flex;
  align-items: center;
  gap: 6px;
}

.context-card-toggle {
  color: var(--text-secondary);
  font-size: 12px;
  transition: transform var(--transition);
}

.context-card-toggle.expanded { transform: rotate(90deg); }

.context-card-body {
  padding: 8px;
  display: none;
}

.context-card-body.expanded { display: block; }

/* Inline execution tree (inside function result messages) */
.inline-tree {
  margin: 8px 0 4px;
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  background: var(--bg-secondary);
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: 12px;
}
.inline-tree-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 10px;
  /* `--bg-hover` (lighter) instead of `--bg-tertiary` — the runtime
     block body is already `--bg-tertiary`, so a same-colour header
     vanished into it. */
  background: var(--bg-hover);
  cursor: pointer;
  user-select: none;
  color: var(--text-secondary);
  /* Match normal chat-text size (14px) — Function call / Thinking
     read as first-class blocks, not footnotes. The Copy / toggle
     action elements inside .inline-tree-actions keep their own
     smaller sizes. */
  font-size: 14px;
}
.inline-tree-header:hover { background: var(--bg-selected); }
.inline-tree-toggle {
  color: var(--text-secondary);
  font-size: 12px;
  transition: transform var(--transition);
  display: inline-block;
}
.inline-tree-toggle { transform: rotate(90deg); }
.inline-tree-toggle.collapsed { transform: rotate(0deg); }
.inline-tree-actions {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.inline-tree-copy {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-size: 12px;
  font-family: var(--font-mono);
  padding: 2px 8px;
  border-radius: 3px;
  cursor: pointer;
  line-height: 1.3;
  transition: all var(--transition);
}
.inline-tree-copy:hover {
  background: var(--bg-hover);
  color: var(--text-primary);
  border-color: var(--text-muted);
}
.inline-tree-copy.copied {
  color: var(--accent-green, #98c379);
  border-color: var(--accent-green, #98c379);
}
.inline-tree-body {
  /* padding-left matches .inline-tree-header (10px) so dots and
     labels in the body line up with the header's left column —
     mismatched paddings (was 8px) used to leave .pending-pulse
     2px further left than the header ``●``. */
  padding: 6px 10px;
  display: block;
  max-height: 300px;
  overflow-y: auto;
}
.inline-tree-body.collapsed { display: none; }

/* Stream terminal */
.stream-terminal-wrap {
  margin: 4px 0 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-secondary);
  overflow: hidden;
}
.stream-terminal-header {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  background: var(--bg-tertiary);
  cursor: pointer;
  font-size: 12px;
  color: var(--text-secondary);
  user-select: none;
}
.stream-terminal-header:hover { color: var(--text-primary); }
.stream-terminal-toggle {
  font-size: 12px;
  transition: transform var(--transition);
  display: inline-block;
  transform: rotate(90deg);
}
.stream-terminal-wrap.collapsed .stream-terminal-toggle { transform: rotate(0deg); }
.stream-terminal-wrap.collapsed .stream-terminal { display: none; }
.stream-terminal {
  padding: 8px 10px;
  background: #1a1a1a;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.5;
  max-height: 200px;
  overflow-y: auto;
  color: #a0a0a0;
  white-space: pre-wrap;
  word-break: break-word;
}
.stream-terminal .stream-text { color: #e0e0e0; }
.stream-terminal .stream-tool { color: #56b6c2; font-weight: 600; }
.stream-terminal .stream-status { color: #e5c07b; font-style: italic; }
.stream-terminal .stream-time { color: var(--text-muted); font-size: 12px; }

/* Error card */
.error-content {
  background: rgba(224,108,117,0.08);
  border: 1px solid rgba(224,108,117,0.2);
  border-radius: var(--radius);
  padding: 12px;
  margin: 8px 0;
  margin-left: 36px;
  color: var(--accent-red);
  font-size: 14px;
  font-family: var(--font-mono);
  white-space: pre-wrap;
  word-break: break-word;
}
.message.assistant > .error-content + .rerun-btn {
  margin-left: 36px;
  margin-top: 4px;
}

/* Soft terminal-state badges below the chat body — interrupted and
   cancelled are not full-on errors but still mark the turn as not
   reaching a clean "done", so they hint at it with a small inline
   strip rather than recolouring the whole bubble. */
.bubble-badge {
  margin: 8px 0 0 36px;
  padding: 6px 10px;
  border-radius: var(--radius);
  font-size: 12px;
  font-family: var(--font-sans);
  display: inline-block;
}
.bubble-badge-interrupted {
  background: rgba(224,170,80,0.10);
  border: 1px solid rgba(224,170,80,0.25);
  color: #c98a30;
}
.bubble-badge-cancelled {
  background: rgba(160,160,160,0.10);
  border: 1px solid rgba(160,160,160,0.25);
  color: var(--text-muted);
}

/* Welcome screen styles moved to
   `components/chat/welcome-screen.module.css`. The legacy
   `newSession()` no longer injects `.welcome*` DOM — it leaves the
   React portal target alone — so these global rules are dead. */

/* Input area / input wrapper / chat input / send-btn / stop-btn /
   thinking selector / plus button + menu — all migrated to
   `components/chat/composer/composer.module.css` (+ companion
   `fn-form.module.css`). The legacy DOM that used these global
   class names is stripped by `page-shell.tsx::stripLegacyChatChrome`
   before render, so none of these rules match anymore. */

/* Slash-command suggestion menu (.slash-clip / .slash-menu /
   .slash-menu-item) moved to `components/chat/composer/composer.module.css`
   under the React composer. The body.slash-menu-open coupling that
   used to fade the welcome examples row is now handled by the
   `welcome-screen.module.css` `:global(body.slash-menu-open) .examples`
   rule. */

/* `.plus-menu-item / -left / -right / -icon / -label / -sep`,
   `.active-tool-chips / .tool-chip / -icon / -close` (with the
   `[data-tooltip]` hover popup), and `.thinking-menu / .thinking-
   option / .thinking-opt-{label,desc,check}` all moved to the
   React composer (`components/chat/composer/composer.module.css`
   + the `<ThinkingEffortPill />` Tailwind utilities in
   `composer/index.tsx`). The legacy DOM under `.input-area` is
   stripped on mount by `page-shell.tsx::stripLegacyChatChrome`,
   so none of those rules ever match — block removed. */

/* `.input-wrapper.fn-form-mode` + the whole `.fn-form-*` family
   (header / title / name / desc / close / body / field / label{,
   -name, -type, -required, -optional, -desc} / input / textarea /
   footer{,-left,-right} / run-btn / cancel-btn / toggle / toggle-
   btn / options / option-chip / option-custom / no-params) moved
   to `components/chat/composer/fn-form.module.css` under the React
   composer. The legacy `.input-wrapper` parent is also stripped
   from the DOM by `page-shell.tsx::stripLegacyChatChrome` on mount,
   so even the combined selectors no longer match — block removed. */

/* ===== Streaming chat blocks (thinking, tool calls, text) ===== */
.chat-stream-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-left: 36px;
  margin-right: 36px;
}
/* .message-content adds its own 36px padding-left for the avatar column —
   cancel it here since .chat-stream-body is already offset the same amount. */
.chat-stream-body .chat-text { margin-left: 0; padding-left: 0; padding-right: 0; }
.chat-thinking {
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-secondary);
  overflow: hidden;
  font-size: 12px;
}
/* Make the Thinking header visually match the @agentic_function
 * runtime-block header (mono + bold + bright) so the assistant's
 * pre-tool reasoning and tool call card read as one consistent
 * family, not "small muted sans" next to "big bold mono". */
.chat-thinking > .chat-fold-btn {
  font-family: var(--font-mono);
  color: var(--text-bright);
  font-weight: 600;
}
/* Tool calls render as a single bordered "card" containing dense
   rows separated by hairlines, instead of each call being its own
   rounded box with an 8px gap. Cuts vertical space ~60% for the
   long tool-storm assistant turns. */
.chat-tools {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-secondary);
  overflow: hidden;
  font-size: 12px;
}
.chat-tool {
  border: none;
  border-radius: 0;
  background: transparent;
  overflow: visible;
  font-size: 12px;
}
.chat-tool + .chat-tool {
  border-top: 1px solid var(--border);
}
.chat-tool.is-error { background: rgba(201, 75, 75, 0.06); }
/* The outer .chat-tools card is now itself collapsible. Its
   .chat-fold-content carries the row list; kill the generic content
   padding so rows still sit flush, and add a divider between the
   header button and the rows. */
.chat-tools > .chat-fold-content {
  padding: 0;
  background: transparent;
  color: inherit;
}
/* Reuse the entire `.inline-tree` / `.inline-tree-header` look from
   execution trees so chat-tools and execution trees read as one
   family. The chat-tools card markup adds `.inline-tree` directly
   so most of the visual style comes from those classes — these
   overrides only handle the count badge + hide/show the body. */
.chat-tools .chat-tools-count {
  background: var(--bg-secondary);
  color: var(--text-muted);
  border-radius: 10px;
  padding: 0 7px;
  margin-left: 4px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 16px;
  display: inline-block;
  vertical-align: middle;
}
.chat-tools[data-collapsed="1"] > .chat-tools-body { display: none; }
.chat-tools[data-collapsed="0"] > .chat-tools-body { display: block; }
.chat-tools[data-collapsed="0"] > .inline-tree-header { border-bottom: 1px solid var(--border); }
/* The .inline-tree-body rules (padding) are for execution-tree
   content; for our flat tool-row list keep zero outer padding so
   rows sit flush to the card edges (their own row padding handles
   breathing). */
.chat-tools > .inline-tree-body { padding: 0; }
.chat-fold-btn {
  display: flex;
  align-items: center;
  width: 100%;
  gap: 8px;
  /* Match .inline-tree-header (6px 10px) so the Thinking header,
   * chat-tools card header, and Execution-DAG inline-tree headers
   * all hit the same ~28px row height. Was 3px 10px which made the
   * Thinking row look ~4px shorter than its neighbours. */
  padding: 6px 10px;
  background: transparent;
  border: none;
  color: var(--text-secondary);
  cursor: pointer;
  font-size: 12px;
  font-family: inherit;
  text-align: left;
  transition: background-color var(--transition), color var(--transition);
}
.chat-fold-btn:hover { background: var(--bg-hover); color: var(--text-bright); }
.chat-fold-caret {
  display: inline-block;
  font-size: 12px;
  width: 10px;
  color: var(--text-muted);
  transition: transform 150ms ease;
}
[data-collapsed="0"] > .chat-fold-btn .chat-fold-caret { transform: rotate(90deg); }
.chat-fold-label {
  flex: 1;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chat-fold-elapsed {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 12px;
  flex-shrink: 0;
}
.chat-tool-name {
  font-family: var(--font-mono);
  color: var(--text-primary);
  font-weight: 500;
}
.chat-tool-args {
  color: var(--text-muted);
  font-family: var(--font-mono);
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.chat-tool.is-error .chat-tool-status { color: var(--accent-red, #c94b4b); }
.chat-fold-content {
  display: none;
  padding: 8px 12px 10px;
  border-top: 1px solid var(--border);
  color: var(--text-primary);
  background: var(--bg-primary);
}
[data-collapsed="0"] > .chat-fold-content { display: block; }
.chat-thinking .chat-fold-content {
  white-space: pre-wrap;
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-secondary);
}
.chat-tool-section + .chat-tool-section { margin-top: 8px; }
.chat-tool-section-label {
  color: var(--text-muted);
  font-size: 12px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  margin-bottom: 4px;
}
.chat-tool-pre {
  margin: 0;
  padding: 6px 8px;
  background: var(--bg-tertiary);
  border-radius: 6px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-primary);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 320px;
  overflow: auto;
}


/* Per-conversation token-usage pill in the composer's bottom-right slot.
   Rendered by <ContextBadge> (web/components/chat/context-badge.tsx) as a
   global class. The rule lived in the old 05-chat.css and was dropped in
   58e7709 along with the rest of the legacy composer CSS; restore it here
   so the badge stops rendering as unstyled text. */
.context-stats-label {
  font-size: 12px;
  color: var(--text-bright);
  font-family: var(--font-mono);
  height: 30px;
  line-height: 28px;
  padding: 0 12px;
  border-radius: 999px;
  background: var(--bg-tertiary);
  border: 1px solid var(--border);
  box-sizing: border-box;
  white-space: nowrap;
  cursor: default;
  display: inline-flex;
  align-items: center;
}

/* Composer bottom-row container. Declared globally (not in the CSS
   module) because CSS Modules in this project scope `container-name`
   into a hashed identifier, which makes the `@container` rule below
   never match. The class is applied alongside `styles.inputBottomRow`
   in composer/index.tsx. Container queries (not viewport `@media`)
   are required here — the composer also renders inside narrow side
   panels where viewport width says nothing useful. */
.composer-bottom-row {
  container-type: inline-size;
  container-name: composer-row;
}

/* Effort pill — collapsed it's a 32px round chip like the neighbouring
   tool chips: icon pinned left, hovering widens the pill to 48px and
   slides the caret out the right edge (the chips do the same with
   their ×). Click expands the shell to 260px for the slider.
   7 + 18 + 7 = 32 collapsed; 7 + 18 + 4 + 12 + 7 = 48 hovered. */
.effort-pill-host {
  flex: 0 0 auto;
  width: var(--composer-button-size);
}

.effort-pill-shell {
  width: var(--composer-button-size);
}

.effort-pill-shell[data-expanded="true"] {
  width: 260px;
}

.effort-pill-shell:not([data-expanded="true"]):hover {
  width: 48px;
}

.effort-pill-shell:not([data-expanded="true"]):hover .effort-pill-caret {
  width: 12px;
  margin-left: 4px;
  opacity: 1;
}

.effort-pill-expanded {
  width: 260px;
}

.effort-pill-compact-icon {
  display: inline-flex;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

.effort-pill-caret {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  width: 0;
  margin-left: 0;
  opacity: 0;
  overflow: hidden;
  transition: width 180ms cubic-bezier(0.16, 1, 0.3, 1),
              margin-left 180ms cubic-bezier(0.16, 1, 0.3, 1),
              opacity 180ms ease;
}

/* The animated ChevronRightIcon draws at 24-viewBox / stroke 2; scaled
   to 12px the line thins to 1px — 3 brings it back to ~1.5px rendered.
   flex-shrink on the svg + its wrapper div: without it the flex caret
   span squeezes the icon to 0 width during the slide-out. */
.effort-pill-caret svg {
  stroke-width: 3;
  flex-shrink: 0;
}

.effort-pill-caret > div {
  flex-shrink: 0;
}

/* Slider-thumb biceps: coloured outline, interior filled with the
   expanded pill's own background — the body of the glyph masks the
   track behind it without turning into a coloured silhouette, and no
   backing disc is needed. */
.effort-pill-thumb-icon svg path {
  fill: var(--bg-hover);
}

/* Narrow composer row → drop the token-usage pill entirely instead of
   letting it clip past the right edge (the previous behavior showed
   half a "5.1k in" pill cut off mid-character). */
@container composer-row (max-width: 380px) {
  .context-stats-label {
    display: none;
  }
}

@container composer-row (max-width: 520px) {
  .composer-bottom-row:has([data-effort-expanded="true"]) .context-stats-label {
    display: none;
  }
}

/* Narrow composer row → the expanded slider can't afford its full
   260px; clamp it to the remaining composer row width. */
@container composer-row (max-width: 520px) {
  .effort-pill-shell[data-expanded="true"] {
    width: min(260px, calc(100cqw - 136px));
  }

  .effort-pill-expanded {
    width: min(260px, calc(100cqw - 136px));
  }
}


/* Per-turn modified-files chips at the bottom of an assistant bubble.
   Populated lazily via the `list_turn_files` WS action; absent when
   the turn touched no files (chat-only replies). */
.turn-files-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 8px;
}
.turn-files-chip {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.4;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--bg-tertiary);
  border: 1px solid var(--border);
  color: var(--text-secondary);
  white-space: nowrap;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
}


/* AttachCard — a pointer row written when this turn referenced a
   peer agent (via spawn / attach operations). Rendered standalone
   (no avatar, no "Agentic" header) because it's not a turn — it's
   a marker. `.attach-row` mirrors `.runtime-block`'s 60px horizontal
   margin so the card lines up with the chat text column instead of
   the avatar column. */
.attach-row {
  /* Vertical gap matches the user / assistant bubble spacing (16px)
     so the card flows with the chat rhythm instead of feeling
     tighter than the surrounding turns. */
  margin: 16px 60px;
}
.attach-card {
  margin-top: 0;
  padding: 10px 12px;
  background: var(--bg-tertiary);
  border: 1px solid var(--border);
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.attach-card-header {
  display: flex;
  align-items: center;
  gap: 10px;
}
.attach-card-icon {
  width: 18px; height: 18px;
  color: var(--text-secondary);
  flex: 0 0 18px;
}
.attach-card-icon svg { width: 100%; height: 100%; }
.attach-card-meta {
  flex: 1 1 auto;
  min-width: 0;
}
.attach-card-label {
  font-weight: 500;
  font-size: 13px;
  color: var(--text-bright);
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
/* Live status pill — shows running / errored / cancelled etc.
   Hidden when status is "completed" or absent. */
.attach-card-status-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 1px 7px;
  border-radius: 999px;
  font-family: var(--font-sans);
}
.attach-card-status-pending,
.attach-card-status-queued {
  background: rgba(160, 160, 160, 0.2);
  color: var(--text-secondary);
}
.attach-card-status-running {
  background: rgba(108, 180, 255, 0.18);
  color: var(--accent-blue, #6cb4ff);
}
.attach-card-status-errored {
  background: rgba(229, 83, 75, 0.18);
  color: var(--accent-red, #e5534b);
}
.attach-card-status-cancelled {
  background: rgba(160, 160, 160, 0.18);
  color: var(--text-muted);
}
/* Cancel button — only shown while the attach's task is still
   running (or pending / queued). Subtle outlined pill that doesn't
   compete with the running-status pill in the header. */
.attach-card-cancel {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 24px;
  padding: 0 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
}
.attach-card-cancel:hover {
  border-color: var(--accent-red, #e5534b);
  color: var(--accent-red, #e5534b);
}
.attach-card-sub {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.attach-card-head {
  margin-left: 6px;
  color: var(--text-secondary);
}
.attach-card-open {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 26px;
  padding: 0 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-secondary);
  color: var(--text-bright);
  font-size: 12px;
  cursor: pointer;
}
.attach-card-open:hover { background: var(--bg-primary); }
.attach-card-open svg { width: 12px; height: 12px; }
/* Drop-in replacement when the chat is already on the branch this
   card points at — same slot as the Switch button, neutral colour
   so it reads as a passive tag, not an action. */
.attach-card-here {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  height: 26px;
  padding: 0 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-muted);
  font-size: 12px;
  user-select: none;
}
.attach-card-source {
  color: var(--text-bright);
  font-weight: 600;
}
.attach-card-preview-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  border-top: 1px solid var(--border);
  padding-top: 8px;
  margin-bottom: 4px;
}
.attach-card-preview {
  font-size: 13px;
  color: var(--text-secondary);
  padding-left: 10px;
  border-left: 2px solid var(--border);
  max-height: 140px;
  overflow: auto;
}
/* Footer surfacing the staged-attach status — only present on
   user-initiated attaches (manual=true). Spawn-attached cards stay
   plain because the spawn turn already happened, nothing is staged. */
.attach-card-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-top: 8px;
  margin-top: 4px;
  border-top: 1px dashed var(--border);
}
.attach-card-status {
  flex: 1 1 auto;
  font-size: 12px;
  color: var(--text-muted);
  font-style: italic;
  min-width: 0;
}
/* Auto-followup notice — synthetic ``[系统消息]…`` user msg the
   runner writes after the sub-task finishes (display=runtime, hidden
   from the chat panel). Surfacing it inline on the attach card so
   the card represents the full sub-task lifecycle. */
.attach-card-followup {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px dashed var(--border);
}
.attach-card-followup-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  margin-bottom: 4px;
}
.attach-card-followup-body {
  font-size: 12px;
  color: var(--text-muted);
  font-style: italic;
  white-space: pre-wrap;
  line-height: 1.45;
  max-height: 96px;
  overflow: auto;
}

/* Brief flash applied by history-graph._scrollChatTo when a DAG
 * dblclick maps to an existing chat row (function-internal node →
 * its owner's runtime block). Pure visual cue, no layout effect. */
@keyframes dagFlash {
  0%   { background-color: transparent; }
  20%  { background-color: rgba(255, 196, 92, 0.28); }
  100% { background-color: transparent; }
}
.dag-flash {
  animation: dagFlash 1.3s ease-out 1;
  border-radius: 6px;
}

/* ===== Unified function-call card ===== */
/* Both agentic ``RuntimeBlock`` and regular tool calls render as a
 * single ``.inline-tree`` (header + body). When stacked inside an
 * assistant bubble give each its own breathing room without the
 * legacy ``.runtime-block`` margins. */
.assistant-runtime-children > .inline-tree,
.assistant-runtime-children > div > .inline-tree,
.chat-stream-body > .inline-tree {
  margin: 10px 0 0;
}
/* Top-level RuntimeBlock — a message whose display === "runtime"
 * (e.g. a fn-form spawn). Without a host bubble providing 36px chat
 * column indent, the inline-tree would span full chat-area width.
 * Wrap it in this margin-aware host so it aligns with chat text
 * (avatar 28px + gap 8px + parent .message 24px padding = 60px). */
.runtime-card-host {
  margin: 20px 60px;
}
.runtime-card-host > div > .inline-tree {
  margin: 0;
}
/* Truncate the inline-tree-header label so long ``fnName(params)``
 * signatures don't shove the Retry / Copy JSON / collapse buttons
 * off-row or out of view. The label span shrinks; the actions span
 * is flex-shrink:0. */
.inline-tree-header > span:first-child {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  display: inline-block;
}
.inline-tree-actions {
  flex-shrink: 0;
}
/* Slot for Retry / attempt-nav inside the inline-tree header. */
.inline-tree-actions .attempt-nav {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.inline-tree-actions .attempt-nav-btn {
  -webkit-appearance: none; appearance: none;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-size: 11px;
  font-family: var(--font-mono);
  padding: 1px 6px;
  border-radius: 3px;
  cursor: pointer;
  line-height: 1.3;
}
.inline-tree-actions .attempt-nav-btn:hover:not(:disabled) {
  border-color: var(--accent-blue);
  color: var(--accent-blue);
}
.inline-tree-actions .attempt-nav-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.inline-tree-actions .attempt-nav-label {
  font-size: 11px;
  color: var(--text-muted);
  font-family: var(--font-mono);
  padding: 0 2px;
}
/* Buttons inside .inline-tree-actions — match each other so Retry
 * and Copy don't read as two different control families. Same
 * background / border / radius / font, identical to .inline-tree-copy
 * so the row reads as a uniform action strip. */
.inline-tree-actions .rerun-btn,
.inline-tree-actions .inline-tree-copy {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-size: 12px;
  font-family: var(--font-mono);
  padding: 2px 8px;
  height: auto;
  line-height: 1.3;
  border-radius: 3px;
  cursor: pointer;
  transition: all var(--transition);
}
.inline-tree-actions .rerun-btn:hover,
.inline-tree-actions .inline-tree-copy:hover {
  background: var(--bg-hover);
  color: var(--text-primary);
  border-color: var(--text-muted);
}
/* Usage stat strip rendered below the inline-tree for agentic
 * function calls — small muted footer, no border / background. */
.runtime-usage-footer {
  margin: 4px 0 0;
  padding: 0;
  font-size: 11px;
  color: var(--text-muted);
  font-family: var(--font-mono);
  text-align: right;
}
.inline-tree.is-error {
  border-left: 3px solid var(--accent-red);
}

/* Inline-tree header script glyph — 𝓕 / 𝓣 prefix on Function call
 * / Thinking cards. Slightly larger than the 14px label + cyan so
 * it reads as a decorative type-set prefix, distinct from regular
 * label text. */
.inline-tree-script {
  color: var(--accent-cyan);
  font-size: 18px;
  line-height: 1;
  display: inline-block;
}

/* Thinking body — paragraph of text the model produced while
 * reasoning. Slightly muted vs the chat-text content. */
.inline-tree-body .thinking-text {
  white-space: pre-wrap;
  word-wrap: break-word;
  color: var(--text-secondary);
  font-size: 13px;
  line-height: 1.55;
  /* No own max-height/overflow: the shared .inline-tree-body already scrolls
     (max-height 300px, overflow-y auto). A second scroll container here gave
     the Thinking block two nested scrollbars. Let the body be the single
     scroll surface, like the runtime / tools cards. */
  padding: 2px 4px;
}

/* User-message attachment chips (Claude-Code-style file pills) — the
   composer encodes dropped files into the message text; UserBubble
   pulls them out and renders them here instead of as raw
   "[attached: …]" prose. */
.user-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 8px;
}
.user-attach-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 320px;
  padding: 4px 9px 4px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: color-mix(in srgb, var(--text-primary) 5%, transparent);
  font-size: 12.5px;
  line-height: 1.3;
}
.user-attach-glyph {
  color: var(--text-muted);
  flex-shrink: 0;
}
.user-attach-name {
  color: var(--text-secondary);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.user-attach-meta {
  color: var(--text-muted);
  flex-shrink: 0;
}
