/* Design tokens ------------------------------------------------------------
   The look follows two references the user asked for: abb.com and the ERP
   Document Hub. Both are white surfaces, hairlines, near-black text and exactly
   one accent colour used sparingly -- so the weight of this file lives in borders
   and spacing, not in fills, gradients and shadows.

   Red does double duty as the brand accent and as failure, which is ABB's own
   language. It stays unambiguous because the accent is never a large fill -- a
   mark, a button and a 2px underline -- while a failure is a filled chip or a
   coloured bar plus the word itself. Failures use the deeper red so they still
   read as more serious than a brand action. */
:root {
  color-scheme: light;

  --abb-red: #ff000f;          /* ABB brand red, kept as the reference point */
  --abb-accent: #e0000d;       /* white text on it measures 5.03:1 */
  --abb-accent-dark: #b6000a;
  --abb-accent-soft: rgba(224, 0, 13, .08);

  /* The agents' own colour. It marks who produced a bubble and which tool ran, and
     is never used for structure, so it stays quiet. */
  --abb-ai: #4a47c4;
  --abb-ai-soft: rgba(74, 71, 196, .10);
  --abb-ai-line: rgba(74, 71, 196, .34);

  --abb-bg: #f7f7f8;
  --abb-card: #ffffff;
  --abb-subtle: #f4f4f5;
  --abb-text: #1f1f1f;         /* abb.com's own near-black, not pure black */
  --abb-text-2: #6a6a70;
  --abb-divider: #e5e5e7;
  --abb-success: #00a651;
  --abb-success-ink: #067a3d;  /* the same green, dark enough for text */
  --abb-warning: #f0ab00;
  --abb-error: #cc0000;

  --shell-gutter: clamp(10px, 1.25vw, 24px);
  --radius: 12px;
  --radius-sm: 10px;
  --radius-pill: 999px;
  /* One height for the top bar on both pages, and one height for the controls in it
     (--ctl-h below that). The console and the chat link to each other, so a header
     that changed size between them would jump on every switch. */
  --topbar-h: 64px;
  --topbar-ctl-h: 36px;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .04);
  --shadow-md: 0 1px 3px rgba(0, 0, 0, .07);
  --font: "Segoe UI Variable Text", "Segoe UI", Aptos, system-ui, "Microsoft YaHei", sans-serif;
  --font-display: "Segoe UI Variable Display", "Segoe UI", Bahnschrift, system-ui, "Microsoft YaHei", sans-serif;
  --font-mono: "Cascadia Mono", Consolas, "SF Mono", monospace;
}

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

    /* Neutral greys: the purple-tinted navy that used to be here was the dark-mode
       half of the same heaviness. */
    --abb-bg: #161617;
    --abb-card: #1d1d1f;
    --abb-subtle: #232325;
    --abb-text: #f0f0f1;
    --abb-text-2: #a2a2a8;
    --abb-divider: #313134;
    --abb-ai: #a9a6ff;
    --abb-ai-soft: rgba(169, 166, 255, .14);
    --abb-ai-line: rgba(169, 166, 255, .40);
    --abb-accent-soft: rgba(224, 0, 13, .22);
    --abb-success-ink: #4fd18b;
    --shadow-sm: none;
    --shadow-md: none;
  }
}

* { box-sizing: border-box; }

/* The scrolling element is html, not body, so this is where the gutter is reserved.
   Without it the console (taller than the chat page) shows a scrollbar and the chat
   page does not: a 15px difference in layout width, which moved the centred header by
   7px -- and the cross-page view transition then animated that move, making the ABB
   logo slide on every switch. */
html {
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  min-height: 100vh;
  color: var(--abb-text);
  font-family: var(--font);
  font-size: 15px;
  /* Flat. The two radial glows that used to sit here tinted the whole page pink
     and lavender; the references are white throughout. */
  background: var(--abb-bg);
}

h1, h2, p { margin: 0; }

[hidden] { display: none !important; }

/* Header ------------------------------------------------------------------
   White, one hairline, no shadow. This used to be a red-to-violet gradient slab
   with a second gradient layered over it -- the loudest thing on the page, and it
   was chrome rather than content. Sticking to the top replaces the height it gave
   up: the header stays visible without having to shout.

   Both pages draw this same bar at the same height (--topbar-h) with controls of the
   same height (--topbar-ctl-h). They link to each other, and the header is the one
   thing that does not change when you switch, so it must not move either. */
.topbar {
  position: sticky;
  top: 0;
  z-index: 10;
  color: var(--abb-text);
  background: var(--abb-card);
  border-bottom: 1px solid var(--abb-divider);
  /* Chromium keeps this element still across the navigation instead of flashing a
     blank frame; other engines ignore the declaration. */
  view-transition-name: topbar;
}

.topbar-inner {
  width: calc(100% - (var(--shell-gutter) * 2));
  min-height: var(--topbar-h);
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 12px 0;
}

/* Far left: the official ABB wordmark, then this app's own identity behind a
   hairline -- the arrangement abb.com uses for a product that sits under the group
   brand. It is a link, because it is also the way back to the chat page from the
   console, so the two things an <a> brings -- an underline and the link colour --
   are reset here. Deliberately no hover styling: the wordmark is brand artwork and
   is not recoloured anywhere, and the pointer plus the tooltip are affordance
   enough. */
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}

/* The console has a ring for its own controls, but that rule is scoped to the admin
   body and the brand exists on both pages. */
.brand:focus-visible {
  outline: 2px solid var(--abb-accent);
  outline-offset: 4px;
  border-radius: var(--radius-sm);
}

.abb-logo {
  display: block;
  flex: 0 0 auto;
  height: 20px;
  width: auto;
}

.brand-divider {
  flex: 0 0 auto;
  width: 1px;
  height: 18px;
  background: var(--abb-divider);
}

/* The mark is a hub with three nodes -- what an orchestrator does -- instead of the
   letters "A2A", which only repeated the product name. Sized in CSS so both pages
   stay in step; the tab icon draws the same geometry on the red tile (favicon.svg).

   It used to be a filled red tile. That red now belongs to the ABB logo beside it, so
   the mark is a plain grey glyph: two marks fighting over the same corner would read
   as a mistake rather than as a brand. */
.brand-mark {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  color: var(--abb-text-2);
}

.brand-mark svg {
  display: block;
  width: 20px;
  height: 20px;
}

h1 {
  overflow: hidden;
  font: 600 16px/1.3 var(--font-display);
  letter-spacing: -.005em;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Outline pills, because these now sit on white. Readiness became a dot rather than
   a green fill: the same information, with no coloured block competing with the
   content for attention. */
.system-status {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 30px;
  padding: 0 12px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--abb-divider);
  background: var(--abb-card);
  color: var(--abb-text-2);
  font-size: 12.5px;
  font-weight: 500;
  white-space: nowrap;
}

.system-status::before {
  content: "";
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #c4c4c8;
}

.system-status.ready::before { background: var(--abb-success); }
.system-status.warn::before { background: var(--abb-warning); }

/* Login ------------------------------------------------------------------- */
.login-shell {
  width: min(1320px, calc(100vw - (var(--shell-gutter) * 2)));
  min-height: min(720px, calc(100vh - var(--topbar-h) - (var(--shell-gutter) * 2)));
  margin: var(--shell-gutter) auto;
  display: grid;
  grid-template-columns: minmax(520px, 1.08fr) minmax(460px, .74fr);
  gap: 24px;
  align-items: stretch;
}

.login-intro,
.login-card {
  border: 1px solid var(--abb-divider);
  border-radius: var(--radius);
  background: var(--abb-card);
}

.login-intro {
  padding: clamp(34px, 5vw, 62px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 18px;
}

.eyebrow {
  color: var(--abb-accent);
  font: 700 12px/1.4 var(--font-display);
  letter-spacing: .08em;
  text-transform: uppercase;
}

.login-intro h2 {
  max-width: 680px;
  font: 650 clamp(30px, 5vw, 56px)/1.05 var(--font-display);
  letter-spacing: 0;
}

.login-intro p:not(.eyebrow) {
  max-width: 720px;
  color: var(--abb-text-2);
  font-size: 16px;
  line-height: 1.75;
}

.login-card {
  align-self: stretch;
  padding: clamp(30px, 3.5vw, 46px);
  display: grid;
  align-content: center;
  gap: 12px;
}

.login-card-head {
  margin-bottom: 4px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--abb-divider);
}

.login-card h2 {
  font: 650 22px/1.25 var(--font-display);
}

.login-card p {
  margin-top: 6px;
  color: var(--abb-text-2);
  font-size: 13px;
  line-height: 1.6;
}

.login-card label {
  color: var(--abb-text-2);
  font-size: 12.5px;
  font-weight: 600;
}

input {
  width: 100%;
  min-height: 42px;
  padding: 10px 12px;
  border: 1px solid var(--abb-divider);
  border-radius: var(--radius-sm);
  background: var(--abb-card);
  color: var(--abb-text);
  font: 14px/1.4 var(--font);
}

input:focus {
  outline: none;
  border-color: var(--abb-accent);
  box-shadow: 0 0 0 3px var(--abb-accent-soft);
}

.login-card button {
  min-height: 44px;
}

.login-message {
  min-height: 21px;
  color: var(--abb-text-2);
}

.login-message.error { color: var(--abb-error); }

.logout-action {
  font: 500 13px/1 var(--font-display);
}

/* Layout ------------------------------------------------------------------ */
main {
  width: calc(100vw - (var(--shell-gutter) * 2));
  height: calc(100vh - var(--topbar-h) - (var(--shell-gutter) * 2));
  min-height: 560px;
  margin: var(--shell-gutter) auto;
  display: grid;
  /* The role cards need enough width for an icon, a module badge, a human-readable title
     and the technical skill id. Keeping the rail too close to the old compact status list
     makes every role wrap into fragments, so the rail gets a little more room while the
     conversation still owns the larger pane. */
  grid-template-columns: clamp(420px, 34vw, 520px) minmax(0, 1fr);
  border: 1px solid var(--abb-divider);
  border-radius: var(--radius);
  background: var(--abb-card);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.workspace {
  grid-column: 2;
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
}

/* Conversation ------------------------------------------------------------ */
.conversation {
  min-height: 0;
  max-height: none;
  overflow-y: auto;
  padding: 26px 26px 8px;
  background: var(--abb-card);
}

.message {
  position: relative;
  width: min(980px, 88%);
  margin-bottom: 16px;
  padding: 15px 18px;
  border: 1px solid var(--abb-divider);
  border-radius: var(--radius-sm);
  background: var(--abb-card);
  animation: rise .28s ease-out both;
}

/* The bubble pair is the one place a fill beats an outline: the user's own turn is a
   plain grey block, the answer is a bordered white card. The 3px gradient bar that
   hung on the left edge of every bubble is gone -- it was on all of them, so it
   decorated rather than distinguished. */
.user-message {
  margin-left: auto;
  background: var(--abb-subtle);
  border-color: transparent;
}

/* Who is talking: a quiet label, no chip behind it. The agents' colour marks their
   turns, grey marks ours. */
.message-label {
  display: inline-block;
  margin-bottom: 6px;
  color: var(--abb-ai);
  font: 600 11px/1.5 var(--font-display);
  letter-spacing: .08em;
  text-transform: uppercase;
}

.user-message .message-label { color: var(--abb-text-2); }

/* How the plan was routed */
.plan-badge {
  display: inline-block;
  margin-bottom: 8px;
  padding: 2px 9px;
  border-radius: var(--radius-pill);
  border: 1px dashed var(--abb-ai-line);
  color: var(--abb-ai);
  font: 600 11px/1.6 var(--font-display);
  cursor: help;
}

.plan-badge.fallback {
  border-color: rgba(240, 171, 0, .6);
  color: #8a5a00;
}

@media (prefers-color-scheme: dark) {
  .plan-badge.fallback { color: var(--abb-warning); }
}

.message p { line-height: 1.72; white-space: pre-wrap; overflow-wrap: anywhere; }
.message > p { color: var(--abb-text); }

.result-block {
  margin-top: 14px;
  padding: 14px 16px;
  border: 1px solid var(--abb-divider);
  border-left: 3px solid var(--abb-success);
  border-radius: var(--radius-sm);
  background: var(--abb-card);
}

/* A failure keeps the one saturated fill left in the app, plus its bar and the word
   itself: the accent colour is a brand action, never an alarm. */
.result-block.failed { border-left-color: var(--abb-error); background: rgba(204, 0, 0, .035); }

/* A run still going: the accent says "working", where failed's red says "look at this".
   The progress feed is monospaced and quiet -- it is a log, not prose. */
.result-block.running { border-left-color: var(--abb-warning); background: rgba(240, 171, 0, .04); }

/* The orchestrator's own answer, sitting above the agents' own answers. It is the headline,
   so it carries the AI accent rather than an agent's green, and reads a touch larger. */
.result-block.aggregate-block {
  border-left-color: var(--abb-ai);
  background: var(--abb-ai-soft);
}
.result-block.aggregate-block > strong { color: var(--abb-ai); }
.result-block.aggregate-block p { font-size: 15px; }

/* Only ever written when the summary is being written a second time, after a late answer. */
.aggregate-state { margin-top: 8px; font-size: 12.5px; color: var(--abb-text-2); }
.aggregate-state:empty { display: none; }
.run-lines {
  font: 12.5px/1.7 var(--font-mono);
  color: var(--abb-text-2);
  max-height: 132px;
  overflow-y: auto;
}
.run-lines div { white-space: pre-wrap; }
.run-lines div:last-child { color: var(--abb-text); }

/* A block whose agent is still working and publishes nothing to read. The bar moves, and the
   elapsed seconds in its row come from a real clock -- there is no event count here, because
   a synchronous agent reports no run state. The DSH blocks have numbers because the DSH
   gateway really publishes them. */
.pending-bar {
  height: 3px;
  margin: 10px 0 9px;
  border-radius: 2px;
  background: var(--abb-divider);
  overflow: hidden;
}
.pending-bar span {
  display: block;
  width: 35%;
  height: 100%;
  background: var(--abb-ai);
  animation: pending-slide 1.3s ease-in-out infinite;
}
@keyframes pending-slide {
  from { transform: translateX(-110%); }
  to { transform: translateX(320%); }
}
@media (prefers-reduced-motion: reduce) {
  /* Motion is the whole point of the bar, so where it is unwelcome say it in words instead. */
  .pending-bar span { animation: none; width: 100%; opacity: .45; }
}
.result-block > .rich > .waiting {
  margin: 0;
  color: var(--abb-text-2);
  font-size: 12.5px;
}
/* Each agent's answer folds shut. With several agents in play the combined answer on top is
   the reply and the rest is evidence, but the row stays visible and carries the live state:
   a run that is still going has to be readable without opening anything. */
.result-block > summary {
  display: flex;
  align-items: baseline;
  gap: 2px;
  cursor: pointer;
  list-style: none;
}
.result-block > summary::-webkit-details-marker { display: none; }
.result-block > summary::before {
  content: "▸ ";
  color: var(--abb-text-2);
  font-size: 11px;
}
.result-block[open] > summary::before { content: "▾ "; }
.block-state {
  margin-left: 8px;
  color: var(--abb-text-2);
  font: 400 11.5px/1.3 var(--font);
}
.result-block.failed > summary > .block-state { color: var(--abb-error); }
.result-block.running > summary > .block-state { color: var(--abb-warning); }

.result-block > strong,
.result-block > summary > strong {
  display: block;
  margin-bottom: 8px;
  color: var(--abb-success-ink);
  font: 600 13px/1.3 var(--font-display);
  letter-spacing: .01em;
}

.result-block.failed > strong,
.result-block.failed > summary > strong { color: var(--abb-error); }

/* In the folded rows the heading shares its line with the state, so it must not push down. */
.result-block > summary > strong { margin-bottom: 0; }

.result-block p { font-size: 14.5px; }

/* The question this agent was actually asked, when it is not the whole thing: the first
   thing to check when a block reads wrong, so it goes above the answer it produced. */
.result-block > .asked {
  margin: 8px 0 0;
  color: var(--abb-text-2);
  font-size: 12.5px;
}
.result-block[open] > .rich,
.result-block[open] > .result-meta,
.result-block[open] > .run-lines { margin-top: 8px; }

.result-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 7px;
  margin-top: 12px;
}

/* Copy sits inside the block, under the answer it copies, so a folded block keeps its answer
   and its button together. */
.answer-actions {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

.answer-actions button {
  min-height: 30px;
  padding: 0 12px;
  font-size: 12.5px;
}

.copy-button.is-done {
  border-color: rgba(0, 166, 81, .38);
  color: var(--abb-success-ink);
}

.tool-chip,
.source-badge {
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  font: 500 11.5px/1.7 var(--font-mono);
}

.tool-chip {
  background: var(--abb-ai-soft);
  border: 1px solid var(--abb-ai-line);
  color: var(--abb-ai);
}

.source-badge {
  background: var(--abb-subtle);
  border: 1px solid var(--abb-divider);
  color: var(--abb-text-2);
}

.meta-label { color: var(--abb-text-2); font-size: 11.5px; }

/* Rich agent output (markdown rendered as DOM) ---------------------------- */
.rich { white-space: normal; }
.rich p { margin: 0 0 10px; line-height: 1.72; }
.rich p:last-child { margin-bottom: 0; }
.rich strong { font-weight: 650; }
.rich code { padding: 1px 6px; border-radius: 6px; background: var(--abb-subtle); font: 12.5px/1.6 var(--font-mono); }
.rich ul,
.rich ol { margin: 6px 0 10px; padding-left: 20px; }
.rich li { margin: 4px 0; line-height: 1.65; }
.rich-heading { font-weight: 650; color: var(--abb-text); }
.rich hr { margin: 14px 0; border: 0; border-top: 1px solid var(--abb-divider); }

/* Fenced blocks: kept verbatim so indentation and alignment survive, scrolled
   sideways rather than wrapped. */
.rich pre {
  margin: 8px 0 12px;
  padding: 10px 12px;
  overflow-x: auto;
  white-space: pre;
  background: var(--abb-bg);
  border: 1px solid var(--abb-divider);
  border-radius: var(--radius-sm);
}

.rich pre code {
  padding: 0;
  border-radius: 0;
  background: none;
  font: 12.5px/1.5 var(--font-mono);
}

.table-wrap {
  margin: 8px 0 12px;
  overflow-x: auto;
  border: 1px solid var(--abb-divider);
  border-radius: var(--radius-sm);
}

.rich table { width: 100%; border-collapse: collapse; font-size: 13.5px; }

.rich th,
.rich td {
  padding: 8px 12px;
  text-align: left;
  white-space: nowrap;
  border-bottom: 1px solid var(--abb-divider);
}

.rich thead th {
  background: var(--abb-bg);
  color: var(--abb-text-2);
  font: 600 12.5px/1.5 var(--font-display);
}

.rich tbody tr:last-child td { border-bottom: 0; }
.rich tbody tr:hover td { background: var(--abb-bg); }

/* Composer ---------------------------------------------------------------- */
.composer {
  padding: 16px 22px 20px;
  border-top: 1px solid var(--abb-divider);
  background: var(--abb-card);
}

.composer label {
  display: block;
  margin-bottom: 9px;
  color: var(--abb-text-2);
  font-size: 12.5px;
  font-weight: 500;
}

/* The label row carries the only hint about the keyboard, and it belongs next to the input
   rather than in a tooltip nobody opens. */
.composer-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.composer-hint { color: var(--abb-text-2); font-size: 12px; }

.suggestions { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 11px; }

.chip {
  padding: 6px 12px;
  border: 1px solid var(--abb-divider);
  border-radius: var(--radius-pill);
  background: var(--abb-card);
  color: var(--abb-text-2);
  font: 500 12.5px/1.4 var(--font);
  cursor: pointer;
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}

/* Hover tints instead of lifting: six suggestions that each jumped and glowed was a
   lot of motion for something that is only a shortcut. */
.chip:hover {
  background: var(--abb-subtle);
  color: var(--abb-text);
  border-color: #d6d6da;
}

/* The row used to stretch its button to the height of the textarea, so a 3-row input
   turned 发送 into a 130px red slab -- the loudest thing on the page. It is a button
   again, sitting at the end of the input where the eye expects it. */
.input-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: end;
  gap: 10px;
}

.input-row button {
  min-height: 44px;
  padding: 0 22px;
  white-space: nowrap;
}

/* Two buttons at the end of the input: the quiet one clears the view, the filled one sends.
   The row stays a two-column grid, so this is one cell, not a third column. */
.composer-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}

.composer-actions button.quiet { padding: 0 16px; }

textarea {
  width: 100%;
  min-height: 84px;
  padding: 13px 15px;
  resize: vertical;
  border: 1px solid var(--abb-divider);
  border-radius: var(--radius-sm);
  background: var(--abb-card);
  color: var(--abb-text);
  font: 15px/1.55 var(--font);
  transition: border-color .15s ease, box-shadow .15s ease;
}

textarea::placeholder { color: var(--abb-text-2); }

textarea:focus {
  outline: none;
  border-color: var(--abb-accent);
  box-shadow: 0 0 0 3px var(--abb-accent-soft);
}

/* Quiet by default: an outline button, the way abb.com does it. Only the one primary
   action of a view is filled -- when they all were, a table of row actions turned
   into a wall of shouting pills. */
button {
  border: 1px solid var(--abb-divider);
  border-radius: var(--radius-sm);
  background: var(--abb-card);
  color: var(--abb-text);
  font: 500 14px/1 var(--font-display);
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}

button:hover:not(:disabled) { background: var(--abb-subtle); border-color: #d6d6da; }
button:active:not(:disabled) { background: #ebebed; }
button:disabled { cursor: wait; opacity: .55; }

button.primary {
  border-color: var(--abb-accent);
  background: var(--abb-accent);
  color: #fff;
}

button.primary:hover:not(:disabled) {
  background: var(--abb-accent-dark);
  border-color: var(--abb-accent-dark);
}

button.danger { color: var(--abb-error); }
button.danger:hover:not(:disabled) {
  background: rgba(204, 0, 0, .06);
  border-color: rgba(204, 0, 0, .32);
}

/* Activity sidebar -------------------------------------------------------- */
.activity {
  grid-column: 1;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  border-right: 1px solid var(--abb-divider);
  background: var(--abb-bg);
}

.activity-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 20px 20px 16px;
  border-bottom: 1px solid var(--abb-divider);
}

.activity h2 {
  flex: 0 0 auto;
  font: 600 16px/1.2 var(--font-display);
}

.activity-heading .system-status {
  min-width: 0;
  max-width: 100%;
  justify-content: center;
}

.agent-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* Two per row: ten rows one under the other needed ~730px and a scroll, five rows of
     two need ~450px and fit. `minmax(0, 1fr)` rather than `1fr` so a long skill id
     cannot set the column's minimum width and push the grid wider than the panel. */
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-content: start;
  gap: 10px;
  padding: 16px 16px 6px;
}

.agent-row {
  position: relative;
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr);
  gap: 12px;
  /* The marker is centred on the row rather than pinned to the title line. Aligning it to
     the first of two text lines puts it in the top half of the card with a gap of empty
     card underneath, which reads as a misalignment: a row that wraps to two status lines
     (an unreachable backend says more than a healthy one) then leaves the marker even
     further from the middle. One rule for both markers keeps an avatar from sitting at a
     different height from the dot in the row below it. */
  align-items: center;
  min-height: 104px;
  padding: 12px 30px 12px 13px;
  border: 1px solid var(--abb-divider);
  border-radius: var(--radius-sm);
  background: var(--abb-card);
  transition: border-color .2s ease, background .2s ease;
}

.agent-copy { min-width: 0; }

.agent-role-icon {
  --role-accent: #5f6368;
  --role-bg: color-mix(in srgb, var(--role-accent) 10%, var(--abb-card));
  position: relative;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border: 1px solid color-mix(in srgb, var(--role-accent) 36%, var(--abb-divider));
  border-radius: 50%;
  background: var(--role-bg);
  color: var(--role-accent);
}

.agent-role-icon[data-tone="sales"] { --role-accent: #087f69; }
.agent-role-icon[data-tone="material"] { --role-accent: #8a5a00; }
.agent-role-icon[data-tone="production"] { --role-accent: #0b6f92; }
.agent-role-icon[data-tone="security"] { --role-accent: #5a55a6; }
.agent-role-icon[data-tone="controlling"] { --role-accent: #7a4f9f; }
.agent-role-icon[data-tone="finance"] { --role-accent: #2f7d32; }
.agent-role-icon[data-tone="abap"] { --role-accent: #3b5f7c; }
.agent-role-icon[data-tone="code"] { --role-accent: #a13d6b; }
.agent-role-icon[data-tone="itf"] { --role-accent: #6b21a8; }
.agent-role-icon[data-tone="report"] { --role-accent: #b25c00; }
.agent-role-icon[data-tone="knowledge"] { --role-accent: #4d6b35; }
.agent-role-icon[data-tone="remote"] { --role-accent: #516173; }

.agent-role-icon svg {
  width: 23px;
  height: 23px;
  fill: currentColor;
}

.agent-role-badge {
  position: absolute;
  right: -7px;
  bottom: -6px;
  min-width: 24px;
  height: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 5px;
  border: 1px solid var(--abb-card);
  border-radius: var(--radius-pill);
  background: var(--role-accent);
  color: #fff;
  font: 700 9px/1 var(--font-display);
  letter-spacing: 0;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--role-accent) 22%, transparent);
}

.agent-state {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #c4c4c8;
  box-shadow: 0 0 0 2px var(--abb-card);
  transition: background .2s ease;
}

.agent-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #c4c4c8;
  transition: background .2s ease;
}

/* A skill that stands for a person's capability leads with an avatar instead of the plain
   dot, so the sidebar says "a consultant covers this" before any task runs. The state
   colour moves from the dot's fill to the avatar's ring and glyph -- nothing is lost, and
   the pulse still reads as work in progress. */
.agent-area,
.agent-meta {
  display: block;
  color: var(--abb-text-2);
  font-size: 11.5px;
  line-height: 1.35;
}

.agent-row small { display: grid; gap: 3px; margin-top: 5px; color: var(--abb-text-2); line-height: 1.45;
  /* Balancing keeps "可调用 · DSH" and "网关已响应" from splitting in a visually awkward
     place when a card is narrow. */
  text-wrap: balance; }

.agent-status {
  display: block;
  font-size: 12px;
  line-height: 1.38;
}

/* A custom role name can still be longer than the card, so keep title wrapping defensive. */
.agent-row strong { display: block; font-size: 13.5px; line-height: 1.25; overflow-wrap: anywhere; }

/* State lives in the dot and, when it matters, the border. Three tinted card
   backgrounds stacked in the sidebar read as decoration rather than status. */
.agent-row.running { border-color: rgba(240, 171, 0, .5); }
.agent-row.running .agent-dot,
.agent-row.running .agent-state { background: var(--abb-warning); animation: pulse 1.4s ease-in-out infinite; }

.agent-row.success .agent-dot,
.agent-row.success .agent-state { background: var(--abb-success); }

/* Amber without the pulse: an unreachable backend is a state to report, not work in
   progress, and a pulsing dot would say "wait, something is happening". */
.agent-row.warn { border-color: rgba(240, 171, 0, .5); }
.agent-row.warn .agent-dot,
.agent-row.warn .agent-state { background: var(--abb-warning); }

.agent-row.failed { border-color: rgba(204, 0, 0, .42); background: rgba(204, 0, 0, .03); }
.agent-row.failed .agent-dot,
.agent-row.failed .agent-state { background: var(--abb-error); }

/* A task waiting for an upstream task. Grey and still: it is queued, which is not
   the same as running, and a pulsing dot would claim work is happening. */
.agent-row.queued { border-color: var(--abb-divider); }
.agent-row.queued .agent-dot,
.agent-row.queued .agent-state { background: var(--abb-divider); }

.activity-note {
  margin: auto 16px 18px;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  background: var(--abb-subtle);
  color: var(--abb-text-2);
  font-size: 12px;
  line-height: 1.6;
}

/* Motion ------------------------------------------------------------------ */
@keyframes rise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(240, 171, 0, .45); }
  50% { box-shadow: 0 0 0 6px rgba(240, 171, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/* Responsive -------------------------------------------------------------- */
@media (max-width: 880px) {
  .topbar-inner { flex-wrap: wrap; }
  .login-shell {
    grid-template-columns: 1fr;
    min-height: 0;
  }
  .login-card { align-self: stretch; }
  main {
    width: calc(100vw - (var(--shell-gutter) * 2));
    height: auto;
    min-height: 0;
    grid-template-columns: 1fr;
  }
  .workspace { grid-column: 1; }
  .activity { grid-column: 1; grid-row: 1; border-right: 0; border-bottom: 1px solid var(--abb-divider); }
  .activity-note { margin-top: 12px; }
  .conversation { min-height: 340px; max-height: none; padding: 20px 18px 4px; }
  .input-row { grid-template-columns: 1fr; }
  .input-row button { min-height: 48px; }
  .message { width: 100%; }
}

/* Below this a two-column card cannot hold a skill id even wrapped, so the list falls back
   to one column rather than showing two columns of broken words. 560px is where a card
   gets down to the 166px the longest id needs before it has to break mid-token. */
@media (max-width: 560px) {
  .agent-list { grid-template-columns: 1fr; }
}

/* Hero header actions (added 2026-09-20). The cluster holds the readiness pill
   and the entry that leads to the console. */
.topbar-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* An outline button now that the header is white. The white fill it had existed to
   survive the gradient; on white it became the brightest thing in the header.

   The pill and the round icon variant below it are the same control at the same size
   -- one is "管理后台", the other "返回对话", and they are what you click to go
   between the pages. */
.hero-action {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: var(--topbar-ctl-h);
  padding: 0 15px;
  border: 1px solid var(--abb-divider);
  border-radius: var(--radius-pill);
  background: var(--abb-card);
  color: var(--abb-text);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .01em;
  text-decoration: none;
  white-space: nowrap;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}

.hero-action:hover { background: var(--abb-subtle); border-color: #d6d6da; }
.hero-action:active { background: #ebebed; }

.hero-action:focus-visible {
  outline: 2px solid var(--abb-accent);
  outline-offset: 2px;
}

.hero-action-icon {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
}

/* Icon-only round variant, used by the chat header's console entry: a ghost button,
   so it sits quietly next to the readiness pill instead of becoming a second pill
   competing with it. The label lives in aria-label / title. */
.hero-action.is-icon {
  width: var(--topbar-ctl-h);
  height: var(--topbar-ctl-h);
  min-height: var(--topbar-ctl-h);
  padding: 0;
  gap: 0;
  justify-content: center;
  border-color: transparent;
  background: transparent;
  border-radius: 50%;
  color: var(--abb-text-2);
}

.hero-action.is-icon .hero-action-icon {
  width: 17px;
  height: 17px;
}

.hero-action.is-icon:hover,
.hero-action.is-icon:active {
  background: var(--abb-subtle);
  border-color: transparent;
  color: var(--abb-text);
}

.hero-action.is-icon:focus-visible {
  outline: 2px solid var(--abb-accent);
  outline-offset: 2px;
}

@media (max-width: 880px) {
  .topbar-actions { width: 100%; justify-content: space-between; }
}

/* Navigation between the chat and the console -------------------------------------
   They are separate documents, so a switch is a real navigation. Chromium can keep
   the two pages' headers still and cross-fade the rest, which is what makes going
   back and forth feel like one app instead of two; engines without support simply
   navigate. The header is named above (view-transition-name: topbar) so the browser
   has something to hold on to. */
@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .18s;
}
