/* SwarmDeck web UI — dependency-free, two palettes, one accent.
 *
 * Every colour in this file is a custom property declared in one of the two palette blocks below and
 * read back through `var()`. That is not tidiness for its own sake: a literal `#123456` inside a rule
 * is a colour that cannot follow the theme, and the light theme is only as complete as the tokens
 * are. `test/theme.test.mjs` enforces it — it fails on any hex or `rgb()` outside a palette block.
 *
 * The palette is switched by `html[data-theme]` and never by `@media (prefers-color-scheme)`. `system`
 * is one of the three modes the sidebar button cycles, and resolving it — and re-resolving it when the
 * OS changes — is `lib/theme.js`'s job, so this stylesheet only ever sees a concrete theme. That is
 * also why light is written once instead of twice. Block order matters: light comes last, and the two
 * palette selectors have the same specificity.
 */

:root {
  /* Geometry, type and layout: identical in every theme, which is why none of it is a palette. */
  --radius:        10px;
  --radius-sm:     7px;
  --radius-lg:     14px;
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px;

  --mono: ui-monospace, "Cascadia Mono", "Consolas", "SFMono-Regular", Menlo, monospace;
  --sans: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;

  --sidebar-w: 236px;
  --sidebar-w-narrow: 60px;
  --topbar-h: 56px;
  --agent-w: 0px;

  /* An email is written for white paper and rendered in an iframe, so this one surface is the same
     in both themes on purpose. */
  --surface-email: #ffffff;
}

/* --------------------------------------------------------------- the dark palette
   The app's own theme, and what a browser that runs no script at all still gets. */

:root,
:root[data-theme="dark"] {
  color-scheme: dark;

  --bg:            #0c0f16;
  --bg-elev:       #11151f;
  --panel:         #151a26;
  --panel-2:       #1a2130;
  --panel-3:       #202839;
  --border:        #263043;
  --border-soft:   #1e2636;
  --border-strong: #33405a;

  --text:          #e7ecf6;
  --text-dim:      #aab4c8;
  --text-mute:     #7c8798;
  --text-faint:    #4d576a;
  --placeholder:   #5d6879;
  --on-accent:     #ffffff;
  --on-accent-soft: rgba(255, 255, 255, 0.35);

  --accent:        #4c8dff;
  --accent-hi:     #6ba1ff;
  --accent-lo:     #2f6ad0;
  --accent-wash:   rgba(76, 141, 255, 0.14);
  --accent-border: rgba(76, 141, 255, 0.32);

  --ok:            #37c98b;
  --warn:          #e0a92e;
  --danger:        #f0575c;
  --info:          #58b6d8;
  --ok-wash:       rgba(55, 201, 139, 0.09);
  --warn-wash:     rgba(224, 169, 46, 0.07);
  --danger-wash:   rgba(240, 87, 92, 0.08);
  --danger-wash-hi: rgba(240, 87, 92, 0.16);
  --ok-border:     rgba(55, 201, 139, 0.32);
  --warn-border:   rgba(224, 169, 46, 0.32);
  --danger-border: rgba(240, 87, 92, 0.34);
  /* The ring a status dot glows with. Ink on a backlit panel; nothing at all on e-ink. */
  --ok-halo:       rgba(55, 201, 139, 0.14);
  --warn-halo:     rgba(224, 169, 46, 0.14);
  --danger-halo:   rgba(240, 87, 92, 0.14);
  --ok-flash:      rgba(55, 201, 139, 0.55);

  --shadow:          0 10px 30px rgba(0, 0, 0, 0.45);
  --shadow-fab:      0 10px 26px rgba(0, 0, 0, 0.5), 0 0 0 3px var(--accent-wash);
  --scrim:           rgba(5, 7, 11, 0.66);
  --scrim-soft:      rgba(5, 7, 11, 0.55);
  --surface-code:    #080a0f;
  --surface-shot:    #05070b;
  --surface-viewer:  rgba(5, 7, 11, 0.97);
  --surface-faint:   rgba(255, 255, 255, 0.012);
  --surface-overlay: rgba(12, 15, 22, 0.9);
  --overlay-chip:    rgba(6, 8, 13, 0.82);
  --row-hover:       rgba(255, 255, 255, 0.017);
  --scroll-thumb:    #2a3346;
  --scroll-thumb-hi: #3a465e;
  --select-arrow: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%237c8798' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* -------------------------------------------------------------- the light palette
 * Built for an e-ink panel, which is a different instrument from a bright LCD: it reflects light
 * instead of emitting it, it has no reliable colour, and a soft grey is not a gradient there — it is a
 * dither pattern, which is why nothing here has a blurred shadow, a glow, a gradient or a translucent
 * wash.
 *
 * Four decisions, in order of how much they matter:
 *
 *   • **Paper, not white-ish panels.** `--bg` is true white and the structure is carried by 1px rules
 *     that are dark enough to survive a 16-level grey panel, rather than by filled cards. A large
 *     filled area is also the one thing that ghosts after a refresh.
 *   • **The accent is ink.** The dark theme's blue is a hue, and a greyscale panel has no hue to give,
 *     so the accent — which says "this is a control", never "this is information" — is near-black.
 *     Selected rows, primary buttons and links are all ink on paper.
 *   • **Status hues are dark and desaturated, and shape carries the rest.** `--ok`, `--warn` and
 *     `--danger` differ in luminance, but on a grey panel luminance alone cannot tell three 8px dots
 *     apart, so the dot shapes below differ too (see "status without colour").
 *   • **Contrast: 7:1 or better for every text and status colour**, with one deliberate pair of
 *     exceptions — `--text-faint` (line numbers, timestamps) and `--placeholder` — which clear 4.5:1
 *     like everything else. Measured on the rendered page by `tools/verify-ui-theme.mjs`, not by eye,
 *     because e-ink contrast is lower than the spec sheet's and its refresh leaves residue behind. */

:root[data-theme="light"] {
  color-scheme: light;

  --bg:            #ffffff;
  --bg-elev:       #f4f4f1;
  --panel:         #ffffff;
  --panel-2:       #f0f0ec;
  --panel-3:       #e3e3dd;
  --border:        #9a9a92;
  --border-soft:   #d3d3cb;
  --border-strong: #6e6e66;

  --text:          #0d0d0d;
  --text-dim:      #33332f;
  --text-mute:     #52524c;
  --text-faint:    #6f6f68;
  --placeholder:   #6b6b64;
  --on-accent:     #ffffff;
  --on-accent-soft: rgba(255, 255, 255, 0.4);

  --accent:        #14140f;
  --accent-hi:     #000000;
  --accent-lo:     #55554e;
  --accent-wash:   #e8e8e2;
  --accent-border: #9a9a92;

  --ok:            #0b5c33;
  --warn:          #6f4a00;
  --danger:        #9c1418;
  --info:          #0f5468;
  --ok-wash:       #e6efe9;
  --warn-wash:     #f4eedf;
  --danger-wash:   #f6e7e7;
  --danger-wash-hi: #eecccc;
  --ok-border:     #7fa08d;
  --warn-border:   #b3a071;
  --danger-border: #bf8f8f;
  --ok-halo:       transparent;
  --warn-halo:     transparent;
  --danger-halo:   transparent;
  --ok-flash:      rgba(11, 92, 51, 0.45);

  /* Hard-edged and 1px: a blurred shadow becomes a dithered halo here, and a solid offset line is
     both cheaper to draw and sharper. */
  --shadow:          0 1px 0 rgba(0, 0, 0, 0.2);
  --shadow-fab:      0 2px 0 rgba(0, 0, 0, 0.2), 0 0 0 3px var(--accent-wash);
  --scrim:           rgba(0, 0, 0, 0.38);
  --scrim-soft:      rgba(0, 0, 0, 0.28);
  --surface-code:    #f7f7f4;
  --surface-shot:    #e9e9e4;
  --surface-viewer:  #f2f2ef;
  --surface-faint:   #fafaf7;
  --surface-overlay: rgba(255, 255, 255, 0.92);
  --overlay-chip:    rgba(255, 255, 255, 0.9);
  --row-hover:       #f4f4f0;
  --scroll-thumb:    #bcbcb4;
  --scroll-thumb-hi: #9c9c94;
  --select-arrow: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%2352524c' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* ------------------------------------------------------- status without colour
 * Three 8px dots that differ only in hue are three identical dots on a grey panel — and `--ok` and
 * `--danger` land within 5% of each other in luminance, so brightness does not rescue them either.
 * The shape does: a filled disc is fine, a hollow ring is a warning, a square is a failure. It is the
 * same convention a monochrome printout would need, and it costs nothing on a backlit screen. */

:root[data-theme="light"] .dot[data-state="warn"],
:root[data-theme="light"] .dot[data-state="queued"],
:root[data-theme="light"] .dot[data-state="external"],
:root[data-theme="light"] .dot[data-state="blocked"] {
  background: transparent;
  border: 2px solid var(--warn);
}
:root[data-theme="light"] .dot[data-state="down"],
:root[data-theme="light"] .dot[data-state="failed"],
:root[data-theme="light"] .dot[data-state="error"],
:root[data-theme="light"] .dot[data-state="cancelled"] {
  border-radius: 2px;
}

/* -------------------------------------------------------------------- on e-ink
 * Motion is the other thing an e-ink panel cannot afford: a looping animation is a full refresh every
 * frame, over the whole screen, forever. Everything decorative is off — and the two things that are
 * not decoration stay: the spinner and the busy-button spinner are the only indications that work is
 * happening at all, and a still frame of either says nothing. */

:root[data-theme="light"] *,
:root[data-theme="light"] *::before,
:root[data-theme="light"] *::after { transition: none !important; }

:root[data-theme="light"] .dot.pulse,
:root[data-theme="light"] .agent-fab[data-busy="true"],
:root[data-theme="light"] .tl-node[data-state="running"],
:root[data-theme="light"] .agent-wrote-dot,
:root[data-theme="light"] .toast,
:root[data-theme="light"] .wrote-strip.fresh,
:root[data-theme="light"] .agent-wrote.fresh,
:root[data-theme="light"] .flash { animation: none !important; }

:root[data-theme="light"] .content { scroll-behavior: auto; }
/* A blur behind a floating toolbar is meaningless where there is nothing to blur through. */
:root[data-theme="light"] .code-tools .btn { backdrop-filter: none; }

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.5 var(--sans);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -0.01em; }
h1 { font-size: 19px; }
h2 { font-size: 16px; }
h3 { font-size: 14px; }
h4 { font-size: 13px; color: var(--text-dim); }
p  { margin: 0 0 var(--sp-3); }
a  { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
code, pre, .mono { font-family: var(--mono); font-size: 12.5px; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}
:focus:not(:focus-visible) { outline: none; }

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--accent); color: var(--on-accent); padding: 8px 14px; border-radius: 0 0 8px 0;
}
.skip-link:focus { left: 0; }

/* ---------------------------------------------------------------- layout */

.app {
  /*
   * Three tracks: sidebar, page, dock. The dock track is what the user asked for (`--agent-w`, set by
   * app.js) capped so the page keeps `--page-min` — the topbar's own width, below which the shell
   * rather than the table is what breaks. Doing it here rather than in JS means the cap follows a
   * window resize and the sidebar toggle for free, and the stored width stays the user's intent, so
   * a narrow window never permanently shrinks it.
   */
  --sidebar-track: var(--sidebar-w);
  --page-min: 560px;
  --dock-max: max(0px, calc(100vw - var(--sidebar-track) - var(--page-min)));
  display: grid;
  grid-template-columns: var(--sidebar-track) minmax(0, 1fr) min(var(--agent-w, 0px), var(--dock-max));
  height: 100vh;
  transition: grid-template-columns 120ms ease;
}
.app.sidebar-collapsed { --sidebar-track: var(--sidebar-w-narrow); }

.sidebar {
  background: var(--bg-elev);
  border-right: 1px solid var(--border-soft);
  display: flex; flex-direction: column;
  min-height: 0; overflow: hidden;
}

.brand {
  display: flex; align-items: center; gap: 10px;
  height: var(--topbar-h); padding: 0 var(--sp-4);
  border-bottom: 1px solid var(--border-soft);
  flex: 0 0 auto;
}
.brand-mark { color: var(--accent); font-size: 15px; flex: 0 0 auto; }
.brand-text { display: flex; flex-direction: column; min-width: 0; }
.brand-name { font-weight: 650; font-size: 14.5px; letter-spacing: -0.01em; white-space: nowrap; }
.brand-sub { font-size: 11px; color: var(--text-mute); white-space: nowrap; }

.nav { flex: 1 1 auto; overflow-y: auto; padding: var(--sp-3) var(--sp-2); display: flex; flex-direction: column; gap: 2px; }

.nav-item {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 8px 10px;
  background: transparent; border: 0; border-radius: var(--radius-sm);
  color: var(--text-dim); font: inherit; font-size: 13.5px; text-align: left;
  cursor: pointer;
}
.nav-item:hover { background: var(--panel); color: var(--text); }
.nav-item[aria-current="page"] { background: var(--accent-wash); color: var(--text); font-weight: 600; }
.nav-item[aria-current="page"] .nav-icon { color: var(--accent); }
.nav-icon { width: 18px; text-align: center; flex: 0 0 auto; font-size: 14px; color: var(--text-mute); }
.nav-label { flex: 1 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nav-dot { flex: 0 0 auto; }

.app.sidebar-collapsed .brand-text,
.app.sidebar-collapsed .nav-label,
.app.sidebar-collapsed .nav-dot,
.app.sidebar-collapsed .foot-text,
.app.sidebar-collapsed .foot-meta { display: none; }
.app.sidebar-collapsed .nav-item { justify-content: center; padding: 9px 4px; }
.app.sidebar-collapsed .brand { justify-content: center; padding: 0; }

/* --------------------------------------------------------- the theme switcher
 * One button, three modes: system → light → dark. It sits below the scrolling nav, above the status
 * footer, and it is an ordinary `.nav-item` — so it inherits the hover, the keyboard focus ring and,
 * in the collapsed rail, the icon-only treatment without a rule of its own.
 *
 * The icon *is* the state, and it is drawn by CSS from `html[data-theme-mode]` — the attribute
 * `index.html`'s head script sets before this button exists — so the button is never briefly showing
 * the wrong mode. Inline SVG rather than ☀ and ☾ because some fonts render those as colour emoji, and
 * a colour emoji is a grey smudge on an e-ink panel.
 */
.sidebar-tools {
  flex: 0 0 auto; padding: var(--sp-2);
  border-top: 1px solid var(--border-soft);
}
.theme-glyph { display: inline-flex; align-items: center; justify-content: center; }
.theme-btn svg { display: block; }
.theme-btn .tg { display: none; }
/* No `data-theme-mode` at all (a browser that ran no script) reads as system, which is the default. */
:root:not([data-theme-mode="light"]):not([data-theme-mode="dark"]) .theme-btn .tg-system,
:root[data-theme-mode="light"] .theme-btn .tg-light,
:root[data-theme-mode="dark"] .theme-btn .tg-dark { display: block; }
.theme-btn:hover .nav-icon { color: var(--text); }

.sidebar-foot {
  flex: 0 0 auto; border-top: 1px solid var(--border-soft);
  padding: var(--sp-3) var(--sp-4); font-size: 11.5px; color: var(--text-mute);
}
.foot-row { display: flex; align-items: center; gap: 7px; }
.foot-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.foot-meta { margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.main { display: flex; flex-direction: column; min-width: 0; min-height: 0; position: relative; }

.topbar {
  flex: 0 0 auto; height: var(--topbar-h);
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 0 var(--sp-5);
  background: var(--bg-elev);
  border-bottom: 1px solid var(--border-soft);
  position: sticky; top: 0; z-index: 20;
}
.page-title { flex: 0 0 auto; }
.topbar-actions { display: flex; align-items: center; gap: var(--sp-2); flex: 1 1 auto; min-width: 0; }
.topbar-right { display: flex; align-items: center; gap: var(--sp-2); flex: 0 0 auto; }

/* Always available, at every width: the panel collapses to an icon rail rather than disappearing. */
.sidebar-toggle { flex: 0 0 auto; margin-left: -6px; }

/*
 * The agent dock's only handle. It is anchored to the page column, not the window, so opening the
 * dock (which narrows the page rather than the window) never puts it under the panel — and when the
 * panel is a drawer over the page at narrow widths, the scrim covers it and the scrim is the close.
 */
.agent-fab {
  position: absolute; right: var(--sp-5); bottom: var(--sp-5); z-index: 40;
  width: 52px; height: 52px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; border: 1px solid var(--accent-lo);
  background: var(--accent); color: var(--on-accent);
  font: inherit; font-size: 19px; line-height: 1; cursor: pointer;
  box-shadow: var(--shadow-fab);
  transition: background 110ms ease, transform 110ms ease, box-shadow 110ms ease, color 110ms ease;
}
.agent-fab:hover { background: var(--accent-hi); transform: translateY(-2px); }
.agent-fab:active { transform: translateY(0); }
.agent-fab[data-open="true"] {
  background: var(--panel-2); color: var(--accent); border-color: var(--accent-lo);
  box-shadow: var(--shadow);
}
.agent-fab[data-open="true"]:hover { background: var(--panel-3); }
.agent-fab-mark { display: block; }
/* A turn in flight is the one thing worth saying from here. */
.agent-fab[data-busy="true"] { animation: pulse 1.6s ease-in-out infinite; }


.conn {
  display: flex; align-items: center; gap: 7px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 999px; padding: 5px 12px 5px 10px;
  color: var(--text-dim); font: inherit; font-size: 12.5px; cursor: pointer;
  min-width: 132px; height: 30px;
}
.conn:hover { border-color: var(--accent-lo); color: var(--text); }
.conn-text { white-space: nowrap; }

.content {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: var(--sp-5) var(--sp-5) var(--sp-7);
  scroll-behavior: smooth;
}
.content:focus { outline: none; }
.view-loading { color: var(--text-mute); display: flex; align-items: center; gap: 10px; padding: var(--sp-5) 0; }

/* ---------------------------------------------------------------- atoms */

.dot {
  width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
  background: var(--text-mute); display: inline-block;
}
.dot[data-state="ok"], .dot[data-state="up"], .dot[data-state="running"],
.dot[data-state="succeeded"], .dot[data-state="platform"] { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-halo); }
.dot[data-state="warn"], .dot[data-state="queued"], .dot[data-state="external"],
.dot[data-state="blocked"] { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-halo); }
.dot[data-state="down"], .dot[data-state="failed"], .dot[data-state="error"],
.dot[data-state="cancelled"] { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-halo); }
.dot[data-state="stopped"], .dot[data-state="unknown"] { background: var(--text-mute); }
.dot[data-state="unknown"] { opacity: 0.5; }
.dot.pulse { animation: pulse 1.6s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }

.spinner {
  width: 13px; height: 13px; flex: 0 0 auto; display: inline-block;
  border: 2px solid var(--border); border-top-color: var(--accent);
  border-radius: 50%; animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 7px 13px; font: inherit; font-size: 13px; font-weight: 500;
  cursor: pointer; white-space: nowrap; min-height: 32px;
  transition: background 90ms ease, border-color 90ms ease;
}
.btn:hover:not(:disabled) { background: var(--panel-3); border-color: var(--border-strong); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn.primary:hover:not(:disabled) { background: var(--accent-hi); border-color: var(--accent-hi); }
.btn.danger { color: var(--danger); border-color: var(--danger-border); background: var(--danger-wash); }
.btn.danger:hover:not(:disabled) { background: var(--danger-wash-hi); border-color: var(--danger); }
.btn.ghost { background: transparent; border-color: transparent; color: var(--text-dim); }
.btn.ghost:hover:not(:disabled) { background: var(--panel-2); color: var(--text); }
.btn.sm { padding: 4px 9px; font-size: 12px; min-height: 26px; }
.btn.icon { padding: 5px 8px; min-width: 30px; }
.btn.busy { position: relative; color: transparent !important; }
.btn.busy::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 13px; height: 13px; border: 2px solid var(--on-accent-soft);
  border-top-color: var(--on-accent); border-radius: 50%; animation: spin 0.7s linear infinite;
}
/* Every busy button that is not filled with the accent sits on a light surface in the light theme,
   so it needs the accent spinner rather than the white one — including `.danger`, which used to be
   excluded and would have spun white-on-white. */
.btn.busy:not(.primary)::after { border-color: var(--border); border-top-color: var(--accent); }

.icon-btn {
  background: transparent; border: 1px solid transparent; color: var(--text-dim);
  border-radius: var(--radius-sm); width: 32px; height: 32px; cursor: pointer;
  font-size: 15px; line-height: 1; display: inline-flex; align-items: center; justify-content: center;
}
.icon-btn:hover { background: var(--panel-2); color: var(--text); }

/*
 * The "+" menu (lib/ui.js popupMenu). The panel is portalled to <body> and `position: fixed`
 * because both composers sit inside `#view`, which scrolls: an absolutely positioned panel there
 * is clipped at the box's edge, and the menu has to open upward over the transcript.
 */
.menu-wrap { display: inline-flex; position: relative; }
.menu-trigger { min-width: 34px; }
.menu-trigger span { font-size: 17px; line-height: 1; }
.menu {
  position: fixed; z-index: 80;
  min-width: 236px; max-width: min(340px, calc(100vw - 16px));
  max-height: min(70vh, 560px); overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px; padding: 6px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.menu[hidden] { display: none; }
.menu:focus { outline: none; }
.menu-section { display: flex; flex-direction: column; gap: var(--sp-2); padding: 4px 6px 6px; }
.menu-section[hidden] { display: none; }
.menu-section .field { gap: 3px; }
.menu-section .field > label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-mute); }
.menu-section select { font-size: 12.5px; padding: 5px 26px 5px 8px; min-height: 28px; background-position: right 8px center; }
.menu-note { font-size: 11.5px; color: var(--text-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu-list { display: flex; flex-direction: column; gap: 2px; }
.menu-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  background: transparent; border: 0; border-radius: var(--radius-sm);
  color: var(--text); font: inherit; font-size: 13px; text-align: left;
  padding: 8px 9px; cursor: pointer;
}
.menu-item:hover { background: var(--panel-2); }
.menu-item.danger { color: var(--danger); }
.menu-item.danger:hover { background: var(--danger-wash-hi); }
.menu-check { flex: 0 0 auto; width: 13px; color: var(--accent); font-size: 12px; }
.menu-label { flex: 1 1 auto; min-width: 0; }
.menu-hint { flex: 0 0 auto; color: var(--text-mute); font-size: 11.5px; }
.menu-sep { height: 1px; background: var(--border-soft); margin: 4px 2px; }

input[type="text"], input[type="number"], input[type="password"], input[type="search"],
input[type="email"], input[type="url"], select, textarea {
  width: 100%; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 7px 10px; font: inherit; font-size: 13px; min-height: 32px;
}
input::placeholder, textarea::placeholder { color: var(--placeholder); }
input:focus, select:focus, textarea:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-wash); }
input:disabled, select:disabled, textarea:disabled { opacity: 0.55; }
textarea { resize: vertical; min-height: 80px; line-height: 1.5; }
/*
 * A native select paints the platform's own arrow, which is the one control in this theme that looks
 * like the operating system rather than the app. `appearance: none` plus a drawn chevron makes a
 * `<select>` a peer of the text inputs beside it — and the padding is what keeps the label off it.
 */
select {
  cursor: pointer;
  appearance: none; -webkit-appearance: none;
  padding-right: 30px;
  background-image: var(--select-arrow);
  background-repeat: no-repeat;
  background-position: right 11px center;
}
select:hover { border-color: var(--border-strong); }
option { background: var(--panel); color: var(--text); }
option:checked { background: var(--accent-lo); color: var(--on-accent); }
input[type="checkbox"] { width: 15px; height: 15px; min-height: 0; accent-color: var(--accent); cursor: pointer; }
label { color: var(--text-dim); font-size: 12.5px; }

.field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.field > .field-help { font-size: 11.5px; color: var(--text-mute); }
.field.inline { flex-direction: row; align-items: center; gap: 8px; }
.field.inline label { color: var(--text); font-size: 13px; }

/*
 * A filter bar. Form fields stretch to their container (`width: 100%` above), which is right in a
 * form and wrong in a filter row: three filters became three stacked full-width boxes and pushed the
 * card head onto two lines. A filter sizes itself to its content instead, and wraps as a group.
 *
 * Top-aligned, not bottom: every field's label is one line, so aligning the labels is what aligns the
 * controls. A control with no label above it opts into that line with `.align-control`.
 */
.filters {
  display: flex; align-items: flex-start; flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-3); min-width: 0;
}
.filters .field { flex: 0 1 auto; min-width: 0; }
.filters .field > label {
  font-size: 11px; line-height: 16px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-mute); white-space: nowrap;
}
.filters .field > .field-help { font-size: 11.5px; }
.filters select, .filters input { width: auto; max-width: 100%; }
.filters select { min-width: 9.5rem; }
/* Never a checkbox: `min-width` would stretch the box and leave its label stranded far to its right. */
.filters input:not([type="checkbox"]):not([type="radio"]) { min-width: 12rem; }
.filters .field.grow { flex: 1 1 15rem; }
.filters .field.grow input, .filters .field.grow select { width: 100%; }
.filters .field.w-lg select, .filters .field.w-lg input:not([type="checkbox"]) { min-width: 14rem; }
/*
 * One label line (16px) plus the field gap (5px) is where every control starts — and the control row
 * is 36px tall, so anything without a label above it is centred on that row rather than on the top.
 */
.filters .field.inline,
.filters .align-control {
  margin-top: 21px; min-height: 36px;
  display: flex; align-items: center;
}
/* A note beside the row: a block so `text-overflow` still truncates it, 36px so it sits on the row. */
.filters .align-note { display: block; margin-top: 21px; min-height: 36px; min-width: 0; line-height: 36px; }
/* A checkbox's own label sits beside it, so it keeps sentence case and stays off the label line. */
.filters .field.inline > label {
  font-size: 12.5px; line-height: inherit; text-transform: none;
  letter-spacing: 0; color: var(--text-dim);
}
.filters .filters-end { margin-left: auto; }

.card {
  background: var(--panel); border: 1px solid var(--border-soft);
  border-radius: var(--radius); padding: var(--sp-4);
}
.card.pad-lg { padding: var(--sp-5); }
.card-head { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-3); }
.card-head h2, .card-head h3 { flex: 1 1 auto; min-width: 0; }
.card-sub { color: var(--text-mute); font-size: 12px; }

.grid { display: grid; gap: var(--sp-4); }
.grid.cols-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.grid.cols-3 { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
.grid.cols-4 { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.stack { display: flex; flex-direction: column; gap: var(--sp-4); }
.stack.tight { gap: var(--sp-3); }
.row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.row.end { justify-content: flex-end; }
.row.between { justify-content: space-between; }
.spacer { flex: 1 1 auto; }

.muted { color: var(--text-mute); }
.dim { color: var(--text-dim); }
.tiny { font-size: 11.5px; }
.small { font-size: 12.5px; }
.nowrap { white-space: nowrap; }
.ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.right { text-align: right; }
.mt-2 { margin-top: var(--sp-2); } .mt-3 { margin-top: var(--sp-3); } .mt-4 { margin-top: var(--sp-4); }
.mb-2 { margin-bottom: var(--sp-2); } .mb-3 { margin-bottom: var(--sp-3); }

.pill {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--panel-2); border: 1px solid var(--border);
  border-radius: 999px; padding: 2px 9px; font-size: 11.5px; color: var(--text-dim);
  white-space: nowrap; max-width: 100%;
}
.pill .mono { font-size: 11.5px; }
.pill.ok { color: var(--ok); border-color: var(--ok-border); background: var(--ok-wash); }
.pill.warn { color: var(--warn); border-color: var(--warn-border); background: var(--warn-wash); }
.pill.bad { color: var(--danger); border-color: var(--danger-border); background: var(--danger-wash); }
.pill.accent { color: var(--accent); border-color: var(--accent-border); background: var(--accent-wash); }

/* tables */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border-soft); border-radius: var(--radius); background: var(--panel);
  /* The table answers to ITS OWN width, not the viewport's: the side-by-side agent dock narrows this
     box without the window changing size, so a media query could never see it. */
  container-type: inline-size;
}
table.tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
table.tbl th {
  text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-mute); font-weight: 600; padding: 9px var(--sp-3);
  border-bottom: 1px solid var(--border-soft); background: var(--bg-elev);
  position: sticky; top: 0; z-index: 1; white-space: nowrap;
}
table.tbl td { padding: 9px var(--sp-3); border-bottom: 1px solid var(--border-soft); vertical-align: middle; }
table.tbl tr:last-child td { border-bottom: 0; }
table.tbl tbody tr:hover { background: var(--row-hover); }
table.tbl td.actions { text-align: right; white-space: nowrap; }
table.tbl td.actions .btn { margin-left: 4px; }
/* Editable tables: give the controls a floor so a select never renders as a bare caret. */
table.tbl td > select { min-width: 116px; }
table.tbl td > input[type="text"], table.tbl td > input[type="number"] { min-width: 92px; }
table.tbl td > textarea { min-width: 92px; }
table.tbl td.actions { min-width: 108px; }
.tbl.empty-note { padding: var(--sp-4); }

/* Column priorities — how a table loses width without losing its buttons (see `dataTable`).
   A rank is a claim about importance: 1 is kept longest, 6 goes first. Each threshold is the width
   the widest table in the app needs with that rank still shown, rounded up — the Spaces table wants
   483px for Name+State+Actions, 633 with the egress IP, 818 with the proxy port, 896 with the country,
   974 with who holds control and 1185 with the kernel and platform columns. A narrower table (Runs,
   Projects) therefore drops its lowest ranks a little earlier than it strictly must, which is the
   safe direction: the other direction would slide a column under the pinned one. */
@container (max-width: 1210px) { table.tbl .prio-6 { display: none; } }
@container (max-width: 1000px) { table.tbl .prio-5 { display: none; } }
@container (max-width: 920px) { table.tbl .prio-4 { display: none; } }
@container (max-width: 840px) { table.tbl .prio-3 { display: none; } }
@container (max-width: 660px) { table.tbl .prio-2 { display: none; } }

/* The pinned column (`.col-pin`, normally a row's actions) sticks to the right edge of the scroll
   box, so a button is never something you have to scroll sideways to find. It carries its own opaque
   background because the scrolled columns pass underneath it, and an inset shadow for its divider —
   a border on a sticky cell scrolls away with the table. */
table.tbl th.col-pin,
table.tbl td.col-pin { position: sticky; right: 0; }
table.tbl td.col-pin {
  z-index: 2; white-space: nowrap;
  background-color: var(--panel);
  box-shadow: inset 1px 0 0 var(--border-soft);
}
table.tbl th.col-pin { z-index: 3; box-shadow: inset 1px 0 0 var(--border-soft); }
/* One line, right-aligned: a row of buttons that wraps makes every row of the table two lines tall,
   and "the buttons on the right" is what the cell is for. Stated here rather than inline, because
   `el()`'s style objects go through `setProperty`, which silently ignores a camelCase name like
   `flexWrap` — the inline declaration several tables carry never applied. */
table.tbl td.col-pin > .row { flex-wrap: nowrap; justify-content: flex-end; }
table.tbl tbody tr:hover td.col-pin {
  background-color: var(--panel);
  background-image: linear-gradient(var(--row-hover), var(--row-hover));
}
/* A name is the one column worth keeping, so it gets a ceiling rather than a floor: without one, a
   long profile name sets the column's width and the actions are the first thing pushed out of the
   box. The text is ellipsised inside the ceiling; the full value stays in the title. */
table.tbl td.col-name { max-width: 240px; }

/* empty state */
.empty {
  border: 1px dashed var(--border); border-radius: var(--radius);
  padding: var(--sp-6) var(--sp-5); text-align: center; background: var(--surface-faint);
}
.empty .empty-icon { font-size: 26px; color: var(--text-mute); display: block; margin-bottom: var(--sp-3); }
.empty h3 { margin-bottom: 6px; }
.empty p { color: var(--text-mute); max-width: 460px; margin: 0 auto var(--sp-4); font-size: 13px; }

.notice {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  border: 1px solid var(--border); border-left: 3px solid var(--warn);
  border-radius: var(--radius-sm); background: var(--warn-wash);
  padding: var(--sp-3) var(--sp-4); font-size: 13px;
}
.notice.info { border-left-color: var(--accent); background: var(--accent-wash); }
.notice.bad { border-left-color: var(--danger); background: var(--danger-wash); }
.notice strong { display: block; margin-bottom: 2px; }

.banner-na {
  border: 1px dashed var(--border); border-radius: var(--radius);
  padding: var(--sp-5); text-align: center; color: var(--text-mute);
}

/* code */
pre.code {
  background: var(--surface-code); border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
  padding: var(--sp-3); overflow: auto; margin: 0; max-height: 460px;
  font-size: 12.5px; line-height: 1.55; tab-size: 2;
}
.code-wrap { position: relative; }
.code-wrap > .code-tools { position: absolute; top: 6px; right: 6px; display: flex; gap: 4px; opacity: 0; transition: opacity 90ms; }
.code-wrap:hover > .code-tools, .code-wrap:focus-within > .code-tools { opacity: 1; }
.code-tools .btn { background: var(--surface-overlay); backdrop-filter: blur(2px); }

/* editor with gutter */
.editor { display: flex; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; background: var(--bg); }
.editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }
.editor-gutter {
  flex: 0 0 auto; padding: 9px 8px 9px 10px; text-align: right;
  font: 12.5px/1.55 var(--mono); color: var(--text-faint);
  background: var(--bg-elev); border-right: 1px solid var(--border-soft);
  user-select: none; overflow: hidden; white-space: pre;
}
.editor-body { flex: 1 1 auto; position: relative; min-width: 0; display: flex; }
.editor textarea {
  border: 0; border-radius: 0; background: transparent; box-shadow: none !important;
  font: 12.5px/1.55 var(--mono); padding: 9px 10px; resize: vertical;
  min-height: 120px; tab-size: 2; white-space: pre; overflow-wrap: normal; overflow-x: auto;
}
.editor textarea:focus { border: 0; box-shadow: none; }

.kv { display: grid; grid-template-columns: minmax(90px, auto) 1fr; gap: 3px var(--sp-3); font-size: 12.5px; }
.kv dt { color: var(--text-mute); }
.kv dd { margin: 0; overflow-wrap: anywhere; }

/* ---------------------------------------------------------------- toasts */

.toasts {
  /* Clear of the agent button in the corner below it, at the window edge rather than the page. */
  position: fixed; right: var(--sp-5); bottom: calc(var(--sp-5) + 64px); z-index: 90;
  display: flex; flex-direction: column; gap: var(--sp-2); max-width: 420px;
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex; gap: 10px; align-items: flex-start;
  background: var(--panel-2); border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius); padding: 11px var(--sp-3);
  box-shadow: var(--shadow); font-size: 13px;
  animation: toast-in 140ms ease-out;
}
.toast.warn { border-left-color: var(--warn); }
.toast.error { border-left-color: var(--danger); }
.toast .toast-msg { flex: 1 1 auto; overflow-wrap: anywhere; }
.toast .toast-close { flex: 0 0 auto; background: none; border: 0; color: var(--text-mute); cursor: pointer; font-size: 15px; line-height: 1; padding: 0 2px; }
.toast .toast-close:hover { color: var(--text); }
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ---------------------------------------------------------------- modal */

.modal-root { position: fixed; inset: 0; z-index: 100; display: none; }
.modal-root.open { display: block; }
.modal-backdrop { position: absolute; inset: 0; background: var(--scrim); }
.modal {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow);
  width: min(620px, calc(100vw - 40px)); max-height: calc(100vh - 80px);
  display: flex; flex-direction: column;
}
.modal.wide { width: min(1040px, calc(100vw - 40px)); }
.modal.narrow { width: min(440px, calc(100vw - 40px)); }
/* The step editor: a code editor is the reason it is this wide, and 40px of margin is the whole
   reason it is not `100vw` — the backdrop has to stay visible, or the dialog reads as a page. */
.modal.editor { width: min(1240px, calc(100vw - 40px)); }
.modal-head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-soft);
  flex: 0 0 auto;
}
.modal-head h2 { flex: 1 1 auto; }
.modal-body { padding: var(--sp-5); overflow-y: auto; flex: 1 1 auto; min-height: 0; }
.modal-foot {
  display: flex; justify-content: flex-end; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--border-soft); flex: 0 0 auto;
}

/* ---------------------------------------------------------------- view bits */

.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(178px, 1fr)); gap: var(--sp-3); }
.stat {
  background: var(--panel); border: 1px solid var(--border-soft); border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4); min-height: 78px; display: flex; flex-direction: column; gap: 3px;
}
.stat-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-mute); }
.stat-value { font-size: 19px; font-weight: 600; display: flex; align-items: center; gap: 8px; min-height: 26px; }
/*
 * Two lines, then an ellipsis. One line cut the dashboard's own subtitles mid-word ("3 profiles ·
 * 1 running · rest :…"), which hides the number the card exists to show; unbounded, a long error
 * string would make one card three rows tall.
 */
.stat-sub {
  font-size: 11.5px; color: var(--text-mute);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: break-word;
}

.thumb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); gap: var(--sp-4); }
.thumb {
  background: var(--panel); border: 1px solid var(--border-soft); border-radius: var(--radius);
  overflow: hidden; display: flex; flex-direction: column;
}
.thumb:hover { border-color: var(--border); }
.thumb-shot {
  position: relative; aspect-ratio: 16 / 10; background: var(--surface-shot);
  display: flex; align-items: center; justify-content: center; cursor: pointer; border: 0; padding: 0; width: 100%;
}
.thumb-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.thumb-shot .na { color: var(--text-mute); font-size: 12px; text-align: center; padding: var(--sp-4); }
.thumb-age {
  position: absolute; top: 7px; right: 7px; background: var(--overlay-chip);
  border: 1px solid var(--border-soft);
  border-radius: 999px; padding: 2px 8px; font-size: 11px; color: var(--text-dim);
}
.thumb-meta { padding: 9px var(--sp-3); display: flex; flex-direction: column; gap: 3px; }
/* The card's enlarge affordance: a thumbnail is 268px wide and a screenshot's text is unreadable at
   that size, so the way to read one is one click away rather than a new tab away. */
.thumb-meta .enlarge-btn { flex: 0 0 auto; }

/* ------------------------------------------------------------ image lightbox */
/*
 * Click a screenshot and it opens over the whole window (lib/lightbox.js).
 *
 * This is a full-window modal, not a panel: `inset: 0` beats the shared `.modal` centring (and the
 * `transform` reset is what actually un-centres it), because the surface itself is the backdrop. The
 * height is `100dvh`, not `100%`: on a phone `100vh` is the URL-bar-less height and the foot of the
 * viewer would sit under the browser chrome.
 *
 * The image is a flex item with `min-height: 0` inside a stage whose height comes from the viewport —
 * NOT `max-width: 100%` inside a scrolling body. That was the old live view's bug (NOTES.md, 1442px of
 * scroll for a 720x1600 frame): a percentage bound against a height the flex chain is still deriving
 * from the image resolves to `none`. Fitting here is a layout result, and scrolling appears only when
 * the user zooms in and asks for it.
 */
.modal.image {
  inset: 0; top: 0; left: 0; transform: none;
  width: 100vw; height: 100dvh; max-height: none;
  border: 0; border-radius: 0;
  background: var(--surface-viewer);
}
.modal.image.no-head > .modal-head { display: none; }
.modal.image .modal-body { padding: 0; overflow: hidden; display: flex; }
.modal-backdrop.invisible { background: none; }

.img-viewport { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; min-width: 0; }
.img-head {
  display: flex; align-items: baseline; gap: var(--sp-3);
  padding: 10px var(--sp-5); border-bottom: 1px solid var(--border-soft);
  flex: 0 0 auto; min-width: 0;
}
.img-name { font-size: 13px; font-weight: 550; min-width: 0; }
.img-head .tiny { flex: 0 0 auto; margin-left: auto; }
.img-viewport > .tiny.img-caption {
  padding: 6px var(--sp-5) 10px; flex: 0 0 auto;
  /* Opaque, because the fallback message is centred in a stage that has already been told to hide it:
     an element hidden with `display: none` cannot show through, but the stage it was in can still be
     the thing behind this line — and the caption is a label, not a window. */
  background: var(--surface-viewer);
}

.img-stage {
  position: relative; flex: 1 1 auto; min-height: 0; min-width: 0;
  /*
   * `hidden`, not `auto`: the picture is placed with a `translate` inside a box that never scrolls.
   * A scroll box here is not merely unnecessary — Chromium's drag-auto-scroll rewrites `scrollLeft`
   * between one frame and the next while the button is held, so a pan built on it silently does
   * nothing (see `layout()` in lib/lightbox.js for the measurement).
   */
  overflow: hidden;
  padding: var(--sp-3); background: var(--surface-shot);
  overscroll-behavior: contain;
}
.img-stage.pannable { cursor: grab; }
.img-stage.dragging { cursor: grabbing; }
/* Absolutely placed and sized by `layout()`: the picture's own position is the state, so nothing
   upstream (flex centring, intrinsic size, a scrollbar) gets a say in where it lands. */
.img-shot { display: block; position: absolute; top: 0; left: 0; transform-origin: 0 0; }
.img-stage.dragging .img-shot { user-select: none; }
.img-fallback {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: var(--text-mute); font-size: 13px;
}
/* `[hidden]` is `display: none` from the UA sheet, and ANY `display` in a later rule beats it — so
   without this line the message sits over every picture that loaded fine. */
.img-fallback[hidden] { display: none; }
/* Non-interactive: the thing behind it is the picture, and a drag that starts on the message must
   still pan (or select) rather than being swallowed by a caption. */
.img-fallback { pointer-events: none; }
.img-controls {
  display: flex; align-items: center; gap: 6px; flex: 0 0 auto;
  padding: 8px var(--sp-5); border-top: 1px solid var(--border-soft);
}
.img-controls .tiny { font-family: var(--mono); }
.img-controls .btn.sm { min-width: 34px; }
/* Which of Fit / 1:1 the viewer is in. `.btn.ghost` has no state of its own, and "am I looking at
   the picture or at pixels?" is exactly what this row exists to answer. */
.img-controls .btn.on { border-color: var(--accent); color: var(--accent-hi); background: var(--accent-wash); }

/* live view *//*
 * The frame is the whole point of this modal, so it is shown WHOLE: bounded by the window on both
 * axes, centred, and never upscaled. It used to be `width: 100%`, which only works for a
 * landscape page. A Space is a browser profile, and an Android one is a phone: `hk-android-1` is
 * 360x800, its frame arrives 720x1600, and 100% of a 1040px modal made that 977x2171 — the view
 * could only be read by scrolling 1400px, with the Screenshot button 1300px below the fold.
 *
 * `--live-chrome` is everything in this modal that is NOT the frame (head 65 + foot 61 + body
 * padding 48 + bar 46 + the stack's gaps 25 + the modal's own 80px margin, rounded up), measured
 * in the real modal. Bounding the frame by `100dvh - chrome` is what leaves the modal nothing to
 * scroll: `max-height: 100%` cannot do it, because a percentage against a height that the flex
 * chain is still deriving from this very image resolves to `none` and the frame overflows.
 * `--live-shot` is a saved screenshot's thumbnail, which takes its room out of the frame rather
 * than out of the window.
 */
.modal.live { --live-chrome: 370px; --live-shot: 150px; }
.live-stage {
  position: relative; background: var(--surface-shot); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
/* An empty stage still has to look like a viewport: `waiting for the first frame…` needs a box. */
.live-stage:not(:has(img[src])) { min-height: 240px; }
/* …and once there is one, the stage hugs the frame it holds, so a phone-shaped Space is not a
   990px letterbox. */
.live-stage:has(img[src]) { width: fit-content; max-width: 100%; margin-left: auto; margin-right: auto; }
.live-stage img {
  display: block; width: auto; height: auto; max-width: 100%;
  max-height: max(60px, calc(100dvh - var(--live-chrome, 370px)));
}
/* The frame is also the way to the full-window view (lib/lightbox.js); the bar's Enlarge button is
   the same action said out loud, for anyone who does not think to click a picture. */
.live-stage:has(img[src]) { cursor: zoom-in; }
.modal.live.has-shot .live-stage img {
  max-height: max(60px, calc(100dvh - var(--live-chrome, 370px) - var(--live-shot, 150px)));
}
/* A saved screenshot is a thumbnail — the full-resolution PNG is one click away. It is centred
   like the frame above it, and it is small: at `width: 100%` a 1080x2400 capture was another
   2000px of scroll in the same modal. */
.modal.live .thumb-sm { width: fit-content; margin-left: auto; margin-right: auto; }
.modal.live .thumb-sm img { width: auto; height: auto; max-width: 200px; max-height: 120px; }
.live-bar {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-3); background: var(--panel-2);
  border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
}

/* runs */
.timeline { display: flex; flex-direction: column; gap: 0; }
.tl-item { display: flex; gap: var(--sp-3); }
.tl-rail { flex: 0 0 auto; width: 18px; display: flex; flex-direction: column; align-items: center; padding-top: 5px; }
.tl-node { width: 10px; height: 10px; border-radius: 50%; background: var(--panel-3); border: 2px solid var(--border); }
.tl-node[data-state="running"] { background: var(--accent); border-color: var(--accent); animation: pulse 1.4s infinite; }
.tl-node[data-state="succeeded"] { background: var(--ok); border-color: var(--ok); }
.tl-node[data-state="failed"] { background: var(--danger); border-color: var(--danger); }
.tl-node[data-state="cancelled"], .tl-node[data-state="skipped"] { background: var(--text-mute); border-color: var(--text-mute); }
.tl-line { flex: 1 1 auto; width: 2px; background: var(--border-soft); min-height: 8px; }
.tl-body { flex: 1 1 auto; min-width: 0; padding-bottom: var(--sp-3); }
.tl-head { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; cursor: pointer; }
.tl-detail { margin-top: var(--sp-2); }

.log-pane {
  background: var(--surface-code); border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
  font: 12.5px/1.6 var(--mono); padding: var(--sp-3); overflow: auto;
  height: 380px; resize: vertical; min-height: 140px;
}
.log-line { display: flex; gap: 10px; white-space: pre-wrap; overflow-wrap: anywhere; }
.log-line .lt { flex: 0 0 auto; color: var(--text-faint); }
.log-line .lm { flex: 1 1 auto; }
.log-line[data-level="warn"] .lm { color: var(--warn); }
.log-line[data-level="error"] .lm { color: var(--danger); }
.log-line[data-level="success"] .lm, .log-line[data-level="ok"] .lm { color: var(--ok); }
.log-line[data-level="debug"] .lm { color: var(--text-mute); }

/* egress */
.metric-big { font-size: 22px; font-weight: 650; font-family: var(--mono); letter-spacing: -0.02em; }
.rotate-progress { display: flex; flex-direction: column; gap: 6px; }
.bar { height: 6px; background: var(--panel-3); border-radius: 999px; overflow: hidden; }
.bar > i { display: block; height: 100%; background: var(--accent); border-radius: 999px; transition: width 300ms ease; }
.hist { display: flex; flex-direction: column; gap: 4px; font-size: 12px; }
.hist-row { display: flex; gap: var(--sp-3); align-items: center; padding: 4px 0; border-bottom: 1px solid var(--border-soft); }
.hist-row:last-child { border-bottom: 0; }

/* inboxes */
.split { display: grid; grid-template-columns: minmax(230px, 300px) minmax(0, 1fr); gap: var(--sp-4); align-items: start; }
.split.wide-left { grid-template-columns: minmax(260px, 360px) minmax(0, 1fr); }
.list { display: flex; flex-direction: column; border: 1px solid var(--border-soft); border-radius: var(--radius); background: var(--panel); overflow: hidden; max-height: 70vh; overflow-y: auto; }
.list-item {
  display: flex; flex-direction: column; gap: 2px; text-align: left;
  padding: 10px var(--sp-3); background: transparent; border: 0;
  border-bottom: 1px solid var(--border-soft); color: var(--text);
  font: inherit; cursor: pointer; width: 100%;
}
.list-item:last-child { border-bottom: 0; }
.list-item:hover { background: var(--panel-2); }
.list-item[aria-selected="true"] { background: var(--accent-wash); box-shadow: inset 2px 0 0 var(--accent); }
.list-item .li-title { font-size: 13px; font-weight: 550; }
.list-item .li-sub { font-size: 11.5px; color: var(--text-mute); }

.email-frame { width: 100%; height: 460px; border: 1px solid var(--border-soft); border-radius: var(--radius-sm); background: var(--surface-email); }
.email-body-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; }

/* chat */
.chat-shell { display: flex; flex-direction: column; height: calc(100vh - var(--topbar-h) - 96px); min-height: 380px; }
.chat-log { flex: 1 1 auto; overflow-y: auto; display: flex; flex-direction: column; gap: var(--sp-4); padding: var(--sp-4) var(--sp-2); }
.msg { display: flex; flex-direction: column; gap: 6px; max-width: 92%; }
.msg.user { align-self: flex-end; align-items: flex-end; }
.msg.assistant { align-self: flex-start; }
.msg-role { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-mute); }
.msg-bubble {
  background: var(--panel); border: 1px solid var(--border-soft);
  border-radius: var(--radius); padding: var(--sp-3) var(--sp-4);
  font-size: 13.5px; overflow-wrap: anywhere; max-width: 100%;
}
.msg.user .msg-bubble { background: var(--accent-wash); border-color: var(--accent-border); }
.msg.error .msg-bubble { border-color: var(--danger-border); background: var(--danger-wash); }
.chat-compose { flex: 0 0 auto; display: flex; flex-direction: column; gap: var(--sp-2); border-top: 1px solid var(--border-soft); padding-top: var(--sp-3); }
.chat-compose textarea { min-height: 62px; max-height: 220px; }

/*
 * One send row, both surfaces: the "+" menu on the left, Send and Stop on the right.
 *
 * "Send" and "Stop" are the same number of characters but not the same number of pixels (57 vs 55 at
 * this font), so "the same size" cannot come from padding that happens to be equal — it needs a
 * width floor. `stretch` then makes the row one height: the trigger is a shorter button than a
 * labelled one at its natural size, and a row of near-misses reads as a mistake.
 */
.compose-actions { align-items: stretch; }
.compose-actions > .btn { min-width: 64px; }
.compose-actions .menu-wrap { display: flex; }
.compose-actions .menu-trigger { height: 100%; }

.tool-card { border: 1px solid var(--border-soft); border-radius: var(--radius-sm); background: var(--panel-2); overflow: hidden; }
.tool-head {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  background: transparent; border: 0; color: var(--text); font: inherit; font-size: 12.5px;
  padding: 7px var(--sp-3); cursor: pointer; text-align: left;
}
.tool-head:hover { background: var(--panel-3); }
.tool-head .caret { color: var(--text-mute); font-size: 10px; width: 10px; flex: 0 0 auto; }
.tool-name { font-family: var(--mono); font-size: 12px; color: var(--accent); }
.tool-body { padding: 0 var(--sp-3) var(--sp-3); border-top: 1px solid var(--border-soft); }
.tool-body pre.code { max-height: 260px; margin-top: var(--sp-3); }

/* agent panel — the docked, always-on agent beside the current screen */
.agent-panel {
  display: none;
  flex-direction: column;
  min-width: 0; min-height: 0;
  background: var(--bg-elev);
  border-left: 1px solid var(--border-soft);
  position: relative;
  overflow: hidden;
}
.app.agent-open .agent-panel { display: flex; }

.agent-resize {
  position: absolute; left: -2px; top: 0; bottom: 0; width: 5px; z-index: 2;
  background: transparent; cursor: col-resize;
}
.agent-resize:hover, .agent-resize:focus-visible { background: var(--accent-wash); box-shadow: inset -2px 0 0 var(--accent); }
body.agent-resizing { cursor: col-resize; }
body.agent-resizing * { cursor: col-resize !important; user-select: none !important; }
body.agent-resizing .app { transition: none; }

.agent-shell { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; min-height: 0; }

.agent-head {
  flex: 0 0 auto; display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border-soft);
}
.agent-title { display: flex; align-items: center; gap: 6px; font-size: 13.5px; font-weight: 650; flex: 0 0 auto; }
.agent-title .agent-mark { color: var(--accent); font-size: 13px; }

.agent-ctx {
  flex: 0 0 auto; display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border-soft); min-width: 0;
}
.agent-ctx-label { max-width: 100%; }
.agent-ctx-label .agent-mark { color: var(--accent); font-size: 10px; }
.agent-ctx-label[data-updated="true"] { color: var(--ok); border-color: var(--ok-border); background: var(--ok-wash); }
.agent-ctx-label[data-updated="true"] .agent-mark { color: var(--ok); }
.agent-ctx-chips { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; min-width: 0; }
.agent-ctx-chips[hidden] { display: none; }
.agent-ctx-chips .pill { font-size: 11px; padding: 1px 8px; }

.agent-banner { flex: 0 0 auto; padding: var(--sp-2) var(--sp-3) 0; }
.agent-banner[hidden] { display: none; }
.agent-banner .notice { padding: var(--sp-2) var(--sp-3); font-size: 12.5px; }

.agent-log {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--sp-3); padding: var(--sp-3);
}
.agent-log .msg { max-width: 100%; }
.agent-log .msg-role { font-size: 10.5px; }
.agent-log .msg-bubble { padding: var(--sp-2) var(--sp-3); font-size: 13px; border-radius: var(--radius-sm); }
.agent-log .empty { padding: var(--sp-4) var(--sp-3); }
.agent-log .empty p { margin-bottom: var(--sp-3); font-size: 12.5px; }
.agent-log .tool-body pre.code { max-height: 200px; }

.agent-suggest { display: flex; flex-direction: column; align-items: stretch; gap: 6px; }
.agent-suggest .btn { justify-content: flex-start; text-align: left; white-space: normal; height: auto; min-height: 30px; line-height: 1.35; }

.agent-compose {
  flex: 0 0 auto; display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-3); border-top: 1px solid var(--border-soft);
}
.agent-compose textarea { min-height: 54px; max-height: 180px; font-size: 13px; }

/* the drawer backdrop — never shown above 1080px, where the panel is docked */
.agent-scrim {
  display: none; position: fixed; inset: var(--topbar-h) 0 0 0; z-index: 55;
  width: auto; padding: 0; border: 0; border-radius: 0;
  background: var(--scrim-soft); cursor: default;
}
.agent-scrim[hidden] { display: none !important; }

/* ------------------------------------------------- editor state + agent writes */

/*
 * A Save button has to be readable at a glance: "nothing to save" and "you have changes" must not
 * look the same. Saved is muted and disabled; dirty is the accent, with a dot.
 */
.btn.saved,
.btn:disabled.saved { background: var(--panel-2); border-color: var(--border); color: var(--text-mute); opacity: 1; }
.btn.dirty { box-shadow: 0 0 0 3px var(--accent-wash); }
.btn.dirty::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor; display: inline-block; margin-right: 2px;
}
.btn.hidden { display: none; }

/* "the agent wrote to this" — one line, above the list it changed, gone after a few seconds */
.wrote-strip {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  border: 1px solid var(--ok-border); background: var(--ok-wash);
  border-radius: var(--radius-sm); padding: 7px var(--sp-3);
  font-size: 12.5px; color: var(--text); margin-bottom: var(--sp-3);
}
.wrote-strip[hidden] { display: none; }
.wrote-strip .wrote-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); flex: 0 0 auto; }
.wrote-strip.fresh { animation: wrote-in 260ms ease-out; }
@keyframes wrote-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }

/*
 * A step the agent changed. `just-changed` is the flat "this one differs" marker and stays until the
 * user saves or reloads; `flash` is the one-shot pulse that makes it noticeable in the first place.
 */
.script-card.just-changed { border-color: var(--ok-border); }
.script-card.just-changed .script-head.changed-head { background: var(--ok-wash); }
.script-card.flash { animation: step-flash 1.5s ease-out; }
@keyframes step-flash {
  0%   { box-shadow: 0 0 0 0 var(--ok-flash); background: var(--ok-wash); }
  60%  { box-shadow: 0 0 0 6px transparent; background: var(--ok-wash); }
  100% { box-shadow: 0 0 0 0 transparent; background: transparent; }
}
/* An input row the agent touched, and the banner itself, use the same pulse. */
.flash { animation: step-flash 1.5s ease-out; }
tr.just-changed > td { background: var(--ok-wash); }
.notice.flash { animation: step-flash 1.5s ease-out; }

.agent-wrote {
  flex: 0 0 auto; display: flex; align-items: center; gap: 7px;
  margin: 0 var(--sp-3) var(--sp-2); padding: 6px 10px;
  border: 1px solid var(--ok-border); background: var(--ok-wash);
  border-radius: var(--radius-sm); font-size: 12px; color: var(--text);
}
.agent-wrote[hidden] { display: none; }
.agent-wrote-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); flex: 0 0 auto; animation: pulse 1.4s ease-in-out 3; }
.agent-wrote-text { flex: 1 1 auto; overflow-wrap: anywhere; font-family: var(--mono); font-size: 11.5px; }
.agent-wrote-when { flex: 0 0 auto; color: var(--text-mute); font-size: 11px; }
.agent-wrote.fresh { animation: wrote-in 260ms ease-out; }

/* markdown */
.md { font-size: 13.5px; line-height: 1.62; overflow-wrap: anywhere; }
.md > *:first-child { margin-top: 0; }
.md > *:last-child { margin-bottom: 0; }
.md h1 { font-size: 20px; margin: var(--sp-5) 0 var(--sp-3); }
.md h2 { font-size: 16.5px; margin: var(--sp-5) 0 var(--sp-2); padding-bottom: 5px; border-bottom: 1px solid var(--border-soft); }
.md h3 { font-size: 14.5px; margin: var(--sp-4) 0 var(--sp-2); }
.md h4 { font-size: 13px; margin: var(--sp-3) 0 var(--sp-2); }
.md p { margin: 0 0 var(--sp-3); }
.md ul, .md ol { margin: 0 0 var(--sp-3); padding-left: 22px; }
.md li { margin-bottom: 3px; }
.md li > ul, .md li > ol { margin-top: 3px; margin-bottom: 3px; }
.md blockquote { margin: 0 0 var(--sp-3); padding: 2px var(--sp-4); border-left: 3px solid var(--border); color: var(--text-dim); }
.md hr { border: 0; border-top: 1px solid var(--border-soft); margin: var(--sp-4) 0; }
.md code { background: var(--surface-code); border: 1px solid var(--border-soft); border-radius: 4px; padding: 1px 5px; font-size: 12px; }
.md pre code { background: none; border: 0; padding: 0; }
.md table { border-collapse: collapse; width: 100%; margin: 0 0 var(--sp-4); font-size: 12.5px; display: block; overflow-x: auto; }
.md th, .md td { border: 1px solid var(--border-soft); padding: 6px 10px; text-align: left; vertical-align: top; }
.md th { background: var(--bg-elev); color: var(--text-dim); font-weight: 600; }
.md a { text-decoration: underline; }

/* misc */
.select-row { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 999px;
  padding: 3px 10px; font-size: 11.5px; color: var(--text-dim);
}
/* A chip that links somewhere (the run history's batch chip). Inline-block so its vertical padding
   is part of the line box — as a plain inline it paints over the line above it. */
a.chip { display: inline-block; }
.script-card { border: 1px solid var(--border-soft); border-radius: var(--radius); background: var(--panel); overflow: hidden; }
/* A step is ONE row: its fields live in a dialog, so the head is the whole card and there is no
   border under it. Before, the head was a lid — the fields it hid were the only way to edit a step,
   and they were laid out in the width left beside the list (a step's own code got ~200px of it). */
.script-head { display: flex; align-items: center; gap: var(--sp-2); padding: 7px var(--sp-3); background: var(--bg-elev); flex-wrap: wrap; }
.script-head .step-index { flex: 0 0 auto; min-width: 1.1em; text-align: right; font-variant-numeric: tabular-nums; }
.script-head .step-actions { flex: 0 0 auto; gap: 2px; flex-wrap: nowrap; }
.step-title {
  display: flex; align-items: center; gap: var(--sp-2); min-width: 0; max-width: 60ch;
  background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--text); font: inherit; font-size: 15px; font-weight: 600; letter-spacing: -0.01em;
  padding: 4px 10px; min-height: 30px; cursor: pointer; text-align: left;
}
.step-title:hover { background: var(--panel-2); border-color: var(--border-soft); }
.step-title .step-name { min-width: 0; }
/* Off is a state of the row, not a badge on it: the switch below shows it and so does this. */
.script-card.off { border-style: dashed; }
.script-card.off .step-title { color: var(--text-mute); font-weight: 500; }
.btn.switch { padding: 3px 6px; min-width: 38px; }
.switch-track {
  position: relative; display: inline-block; width: 26px; height: 15px;
  border-radius: 999px; background: var(--panel-3); border: 1px solid var(--border);
  transition: background 120ms ease, border-color 120ms ease;
}
.switch-knob {
  position: absolute; top: 50%; left: 2px; width: 9px; height: 9px; border-radius: 50%;
  background: var(--text-mute); transform: translate(0, -50%);
  transition: transform 120ms ease, background 120ms ease;
}
.btn.switch.on { color: var(--accent); }
.btn.switch.on .switch-track { background: var(--accent-wash); border-color: var(--accent); }
.btn.switch.on .switch-knob { background: var(--accent); transform: translate(11px, -50%); }
.type-badge {
  font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--accent); background: var(--accent-wash); border: 1px solid var(--accent-border);
  border-radius: 5px; padding: 1px 6px;
}
.drag-handle { color: var(--text-mute); cursor: default; }
/* Step reordering: the project editor owns the behaviour, these are only its affordances. */
.script-head .drag-handle {
  cursor: grab; user-select: none; color: var(--text-mute);
  padding: 0 3px; font-size: 13px; line-height: 1; letter-spacing: -1px;
}
.script-head .drag-handle:hover { color: var(--text); }
.script-head .drag-handle:active { cursor: grabbing; }
.script-card.dragging { opacity: 0.6; }
.script-card.dragging, .script-card.drop-before, .script-card.drop-after { position: relative; }
.script-card.drop-before::before, .script-card.drop-after::after {
  content: ""; position: absolute; left: 0; right: 0; height: 2px; z-index: 2;
  background: var(--accent); border-radius: 2px;
  box-shadow: 0 0 0 3px var(--accent-wash);
  pointer-events: none;
}
.script-card.drop-before::before { top: -6px; }
.script-card.drop-after::after { bottom: -6px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--sp-3); }
/* A field whose content is a block — a code editor, a JSON blob, a body of text — takes the row
   rather than a column. `auto-fit` would give it 200px, which is unusable for something you type
   paragraphs into; this is the half of "make the script field bigger" that is not just the dialog. */
.form-grid > .field-wide { grid-column: 1 / -1; }
/* Fields you read first get more than one column: a step's Name three (it is the one field a long
   name still has to fit in), its Type two. Below ~480px the dialog is one column wide, where any
   span but 1 would overflow the grid. */
.form-grid > .field-span-3 { grid-column: span 3; }
.form-grid > .field-span-2 { grid-column: span 2; }
@media (max-width: 620px) { .form-grid > .field-span-3 { grid-column: span 2; } }
@media (max-width: 480px) { .form-grid > .field-span-2, .form-grid > .field-span-3 { grid-column: auto; } }
.sub-card { border: 1px solid var(--border-soft); border-radius: var(--radius-sm); background: var(--panel-2); padding: var(--sp-3); }
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border-soft); margin-bottom: var(--sp-4); }
.tab {
  background: transparent; border: 0; border-bottom: 2px solid transparent;
  color: var(--text-mute); font: inherit; font-size: 13px; padding: 8px 14px; cursor: pointer;
}
.tab:hover { color: var(--text); }
.tab[aria-selected="true"] { color: var(--text); border-bottom-color: var(--accent); font-weight: 600; }
.thumb-sm { border: 1px solid var(--border-soft); border-radius: var(--radius-sm); overflow: hidden; cursor: zoom-in; background: var(--surface-shot); }
.thumb-sm img { display: block; width: 100%; height: auto; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 6px; border: 3px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hi); background-clip: content-box; }

/* ---------------------------------------------------------------- responsive */

@media (max-width: 1080px) {
  /* The agent column is gone here (the panel floats), so every grid variant is two columns. */
  .app, .app.sidebar-collapsed, .app.agent-open { grid-template-columns: var(--sidebar-w-narrow) minmax(0, 1fr); }
  .brand-text, .nav-label, .nav-dot, .foot-text, .foot-meta { display: none; }
  .nav-item { justify-content: center; padding: 9px 4px; }
  .brand { justify-content: center; padding: 0; }
  .app.sidebar-expanded { grid-template-columns: var(--sidebar-w) minmax(0, 1fr); }
  .app.sidebar-expanded .brand-text, .app.sidebar-expanded .nav-label,
  .app.sidebar-expanded .nav-dot, .app.sidebar-expanded .foot-text,
  .app.sidebar-expanded .foot-meta { display: revert; }
  .app.sidebar-expanded .nav-item { justify-content: flex-start; padding: 8px 10px; }
  .app.sidebar-expanded .brand { justify-content: flex-start; padding: 0 var(--sp-4); }

  /* Below this width the dock would squeeze the page, so it becomes a drawer over it. */
  .agent-panel {
    position: fixed; right: 0; top: var(--topbar-h); bottom: 0;
    width: min(420px, 92vw); z-index: 60;
    border-left-color: var(--border);
    box-shadow: var(--shadow);
  }
  .agent-resize { display: none; }
  .app.agent-open .agent-scrim:not([hidden]) { display: block; }
}

@media (max-width: 900px) {
  .content { padding: var(--sp-4) var(--sp-4) var(--sp-6); }
  .topbar { padding: 0 var(--sp-4); }
  .split, .split.wide-left { grid-template-columns: minmax(0, 1fr); }
  .thumb-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
  .toasts { right: var(--sp-3); left: var(--sp-3); bottom: calc(var(--sp-3) + 64px); max-width: none; }
  .agent-fab { right: var(--sp-4); bottom: var(--sp-4); }
  .chat-shell { height: auto; min-height: 0; }
  .chat-log { max-height: 52vh; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}
