/* CodeZero application layer.
 *
 * design.css is the untouched Industry blueprint system (tokens, buttons,
 * cards, tables, the hairline frame with registration-mark corners).  This
 * file adds only what a security console needs on top of it: a severity ramp,
 * the shell chrome, and a handful of layout primitives.  Nothing here
 * overrides a design.css component -- it extends the same token vocabulary.
 */

/* ── severity ramp ───────────────────────────────────────────────────────
 * The base palette is monochrome blue, which is beautiful and completely
 * unreadable for triage: an analyst has to be able to find CRITICAL in a
 * 900-row table by colour alone.  These five are muted and ink-tinted so they
 * sit in the same visual family as the accents rather than shouting over
 * them -- same lightness band, low chroma, no neon.
 */
:root {
  --sev-critical: #8c2f39;
  --sev-critical-bg: #f6ebec;
  --sev-high: #a75c25;
  --sev-high-bg: #f8efe7;
  --sev-medium: #7d6a1f;
  --sev-medium-bg: #f6f3e6;
  --sev-low: #52789e;
  --sev-low-bg: #eef3f8;
  --sev-info: #6f7073;
  --sev-info-bg: #f1f1f2;

  --state-ok: #3f7150;
  --state-warn: #a75c25;
  --state-err: #8c2f39;

  --shell-header-h: 56px;
  --page-max: 1560px;

  /* The left rail. Two widths, because the rail collapses to icons rather
     than disappearing: a security console is used on 13" laptops next to a
     terminal, and 248px of chrome is real estate the findings table wants. */
  --rail-w: 248px;
  --rail-w-narrow: 64px;
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ── base shell ──────────────────────────────────────────────────────── */
html, body { height: 100%; }
body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font-body);
  color: var(--color-text);
  background: var(--color-bg);
  -webkit-font-smoothing: antialiased;
}
#app { min-height: 100vh; display: flex; flex-direction: column; }

.mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
.muted { color: var(--color-neutral-600); }
.dim { color: var(--color-neutral-500); }
.nowrap { white-space: nowrap; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.right { text-align: right; }
.center { text-align: center; }
.grow { flex: 1; }
.hidden { display: none !important; }

.page {
  width: 100%;
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 28px 24px 64px;
}
.page-narrow { max-width: 1240px; }
.page-wide { max-width: 1720px; }

.page-head {
  display: flex;
  align-items: flex-end;
  gap: 16px;
  margin-bottom: 22px;
  flex-wrap: wrap;
}
/* A finished scan can carry seven actions (report, three artifacts, CSV, log,
   rescan, delete). Without wrapping, the last one is clipped off the viewport
   -- and the clipped one is the destructive one. */
.page-head .actions {
  margin-left: auto; display: flex; gap: 8px; align-items: center;
  flex-wrap: wrap; justify-content: flex-end; max-width: 100%;
}
.kicker {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-accent);
  margin: 0 0 4px;
}
.crumb { font-size: 13px; color: var(--color-neutral-600); margin-bottom: 12px; }
.crumb a { color: inherit; text-decoration: none; cursor: pointer; }
.crumb a:hover { color: var(--color-accent); }

/* ── blueprint frame helper ─────────────────────────────────────────── */
/* `min-width: 0` because a panel is usually a grid or flex item, and the
   default `auto` minimum means it refuses to shrink below whatever its widest
   child asks for -- one <select> listing a long filename was enough to hold a
   whole form open past the edge of a phone. */
.bp {
  position: relative; min-width: 0;
  border: 1px solid var(--color-divider); background: var(--color-bg);
}
.bp > .corner {
  position: absolute; width: 11px; height: 11px;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.bp > .corner::before, .bp > .corner::after { content: ""; position: absolute; background: currentColor; }
.bp > .corner::before { left: 5px; top: 0; width: 1px; height: 100%; }
.bp > .corner::after { top: 5px; left: 0; width: 100%; height: 1px; }
.bp > .corner.tl { top: -6px; left: -6px; }
.bp > .corner.tr { top: -6px; right: -6px; }
.bp > .corner.bl { bottom: -6px; left: -6px; }
.bp > .corner.br { bottom: -6px; right: -6px; }
.bp-pad { padding: 20px; }

.panel-head {
  display: flex; align-items: baseline; gap: 12px;
  padding: 12px 16px; border-bottom: 1px solid var(--color-divider);
}
.panel-head h4 { margin: 0; }
/* Row-level actions inside dense tables and panel headers: same button, one
   notch down, so an "Edit" never outweighs the row it belongs to. */
.btn-sm { font-size: 12.5px; padding: 3px 10px; min-height: 26px; }
.btn-ghost.btn-sm { padding-inline: 6px; }
.section-rule {
  display: flex; align-items: baseline; gap: 16px; margin-bottom: 24px;
}

/* ── marketing header ────────────────────────────────────────────────────
 * In CSS rather than inline so the breakpoint can reflow it. As inline
 * styles the nav could not wrap: "How it works" broke across three lines and
 * the sign-in buttons were pushed off the right edge of the phone. */
.pub-head {
  display: flex; align-items: center; gap: 26px;
  padding: 16px 40px; max-width: 1360px; margin: 0 auto;
}
.pub-nav { display: flex; gap: 22px; font-size: 14px; margin-left: 16px; }
.pub-actions { margin-left: auto; display: flex; gap: 10px; align-items: center; }
/* The sentence under a form field explaining what it does. Small on a desktop
   where it sits beside the control it explains; raised at the breakpoint,
   which is only possible because it is a class and not an inline size. */
.hint { font-size: 11px; line-height: 1.5; }
.section-rule .line { flex: 1; height: 1px; background: var(--color-divider); }

/* ── metric grid (hairline-separated cells) ─────────────────────────── */
.metrics {
  display: grid; gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  background: var(--color-divider);
  border: 1px solid var(--color-divider);
}
/* Explicit counts so a five-cell strip stays five across rather than
   re-flowing to four-plus-one at an awkward width. */
.metrics.m2 { grid-template-columns: repeat(2, 1fr); }
.metrics.m3 { grid-template-columns: repeat(3, 1fr); }
.metrics.m4 { grid-template-columns: repeat(4, 1fr); }
.metrics.m5 { grid-template-columns: repeat(5, 1fr); }
.metrics.m6 { grid-template-columns: repeat(6, 1fr); }
.metrics > div { background: var(--color-bg); padding: 14px 16px; min-width: 0; }
.metric-label {
  font-size: 10px; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--color-neutral-600); margin-bottom: 6px;
}
.metric-value { font-family: var(--font-heading); font-size: 32px; line-height: 1; }
.metric-value.sm { font-size: 24px; }
.metric-note { font-size: 12px; color: var(--color-neutral-600); margin-top: 6px; }

/* ── severity chips & text ──────────────────────────────────────────── */
.sev { font-family: var(--font-heading); font-weight: var(--font-heading-weight);
       font-size: 12px; letter-spacing: 0.05em; text-transform: uppercase; }
.sev-critical { color: var(--sev-critical); }
.sev-high { color: var(--sev-high); }
.sev-medium { color: var(--sev-medium); }
.sev-low { color: var(--sev-low); }
.sev-info { color: var(--sev-info); }

.dot { display: inline-block; width: 8px; height: 8px; flex: none; }
.dot-critical { background: var(--sev-critical); }
.dot-high { background: var(--sev-high); }
.dot-medium { background: var(--sev-medium); }
.dot-low { background: var(--sev-low); }
.dot-info { background: var(--sev-info); }

.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; letter-spacing: 0.02em; padding: 3px 9px;
  border: 1px solid var(--color-divider);
}
.chip-critical { background: var(--sev-critical-bg); color: var(--sev-critical); border-color: var(--sev-critical); }
.chip-high { background: var(--sev-high-bg); color: var(--sev-high); border-color: var(--sev-high); }
.chip-medium { background: var(--sev-medium-bg); color: var(--sev-medium); border-color: var(--sev-medium); }
.chip-low { background: var(--sev-low-bg); color: var(--sev-low); border-color: var(--sev-low); }
.chip-info { background: var(--sev-info-bg); color: var(--sev-info); border-color: var(--sev-info); }
.chip-ok { background: #ecf3ee; color: var(--state-ok); border-color: var(--state-ok); }
.chip-run { background: var(--color-accent-100); color: var(--color-accent-800); border-color: var(--color-accent-500); }

/* ══ left rail ═══════════════════════════════════════════════════════════
 * Navigation is vertical and permanent. A horizontal bar has to spend its
 * width on the page title *and* the destinations, so it either truncates the
 * nav or hides it behind a menu the moment a third of the screen goes to a
 * detail panel. Down the side, the destination list is fixed furniture: it
 * costs the same on every screen and never competes with page content.
 */
.app-shell { display: flex; flex: 1; min-height: 100vh; align-items: stretch; }

.app-side {
  width: var(--rail-w);
  flex: none;
  position: sticky; top: 0; align-self: flex-start;
  height: 100vh;
  display: flex; flex-direction: column;
  border-right: 1px solid var(--color-divider);
  background: var(--color-bg);
  z-index: 40;
  transition: width .18s var(--ease-out);
}
/* Drafting-paper grid: the same reference the registration corners come from.
   Dots rather than rules, and faint enough to read as paper stock -- a
   visible lattice competes with the nav items sitting on top of it, which is
   the opposite of what a background is for. */
.app-side::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background-image: radial-gradient(var(--color-text) 0.7px, transparent 0.7px);
  background-size: 22px 22px;
  opacity: .07;
}
.app-side > * { position: relative; }

.side-brand {
  display: flex; align-items: center; gap: 10px;
  height: 60px; padding: 0 18px; flex: none; cursor: pointer;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 19px; letter-spacing: -0.01em; white-space: nowrap;
  border-bottom: 1px solid var(--color-divider);
}
.side-scroll { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 14px 0 8px; }
.side-group + .side-group { margin-top: 18px; }
.side-label {
  font-size: 10px; letter-spacing: .11em; text-transform: uppercase;
  color: var(--color-neutral-500); padding: 0 18px; margin-bottom: 6px;
  white-space: nowrap;
}
.side-link {
  display: flex; align-items: center; gap: 11px;
  padding: 8px 18px; cursor: pointer; text-decoration: none;
  color: var(--color-neutral-700); font-size: 14px; white-space: nowrap;
  border-left: 2px solid transparent;
  transition: color .12s, background .12s;
}
.side-link svg { flex: none; opacity: .85; }
.side-link:hover { color: var(--color-text); background: color-mix(in srgb, var(--color-text) 5%, transparent); }
.side-link.active {
  color: var(--color-accent);
  border-left-color: var(--color-accent);
  background: var(--color-accent-100);
}
.side-link.active svg { opacity: 1; }
.side-link .badge {
  margin-left: auto; font-size: 10px; padding: 1px 6px;
  background: var(--color-accent); color: var(--color-bg);
}

.side-cta { padding: 14px 14px 4px; flex: none; }
.side-cta .btn { width: 100%; height: 36px; }

.side-foot {
  flex: none; border-top: 1px solid var(--color-divider);
  padding: 12px 14px; display: grid; gap: 10px;
}
.credit-meter { display: grid; gap: 5px; cursor: pointer; }
.credit-meter .cm-top {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--color-neutral-600);
}
.credit-meter .cm-n {
  font-family: var(--font-heading); font-size: 15px; letter-spacing: 0;
  text-transform: none; color: var(--color-text);
}
.credit-meter .bar { height: 4px; }
.credit-meter.empty .bar > span { background: var(--sev-critical); }

.side-account {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 7px 8px; cursor: pointer; text-align: left;
  background: transparent; border: 1px solid transparent; color: inherit;
  font: inherit;
}
.side-account:hover { border-color: var(--color-divider); }
.side-account .who { min-width: 0; flex: 1; line-height: 1.25; }
.side-account .who b { display: block; font-size: 13px; font-weight: 500; }
.side-account .who span { display: block; font-size: 11px; color: var(--color-neutral-600); }

/* Collapsed: icons only. Labels are removed from flow rather than hidden, so
   the rail is genuinely 64px and the icons centre in it.
 *
 * Wrapped in `min-width: 861px` -- below that the rail is a drawer over the
 * page, where there is no second column to protect and icon-only navigation
 * with no labels is a guessing game. Written as a floor rather than undone
 * later on purpose: `display: revert` reverts to the *browser's* default, not
 * to the rule above it, so "un-hiding" a flex row silently turns it into a
 * block and the credit meter loses its layout. */
@media (min-width: 861px) {
  .app-shell.rail-narrow .app-side { width: var(--rail-w-narrow); }
  .rail-narrow .side-label,
  .rail-narrow .side-brand span:not(.brand-mark),
  .rail-narrow .side-link span,
  .rail-narrow .side-link .badge,
  .rail-narrow .credit-meter .cm-top,
  .rail-narrow .side-account .who,
  .rail-narrow .side-cta .btn span { display: none; }
  .rail-narrow .side-brand,
  .rail-narrow .side-link,
  .rail-narrow .side-account { justify-content: center; padding-inline: 0; }
  .rail-narrow .side-cta { padding-inline: 12px; }
  .rail-narrow .side-foot .theme-toggle { flex-direction: column; }
  .rail-narrow .side-group + .side-group {
    margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--color-divider);
  }
}

.rail-toggle {
  position: absolute; right: -11px; top: 72px; z-index: 5;
  width: 22px; height: 22px; display: grid; place-items: center;
  background: var(--color-bg); border: 1px solid var(--color-divider);
  color: var(--color-neutral-600); cursor: pointer; padding: 0;
  opacity: 0; transition: opacity .15s;
}
.app-side:hover .rail-toggle, .rail-toggle:focus-visible { opacity: 1; }
.rail-toggle:hover { color: var(--color-accent); border-color: var(--color-accent); }

.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* Mobile: the rail becomes a drawer over the page, opened from a top bar. */
.rail-scrim { display: none; }
.mobile-bar { display: none; }

/* ── app header (landing/marketing pages keep the horizontal bar) ────── */
.app-header {
  display: flex; align-items: center; gap: 26px;
  padding: 11px 24px;
  border-bottom: 1px solid var(--color-divider);
  background: var(--color-bg);
  position: sticky; top: 0; z-index: 30;
}
.brand {
  display: flex; align-items: center; gap: 9px; cursor: pointer;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 19px; letter-spacing: -0.01em;
}
.brand-mark {
  display: inline-block; width: 14px; height: 14px;
  border: 1.5px solid var(--color-accent); position: relative; flex: none;
}
.brand-mark::after { content: ""; position: absolute; inset: 3px; background: var(--color-accent); }
.app-nav { display: flex; gap: 20px; align-items: center; font-size: 14px; }
.app-nav a {
  color: inherit; text-decoration: none; cursor: pointer;
  padding-bottom: 2px; border-bottom: 1px solid transparent;
}
.app-nav a:hover { color: var(--color-accent); }
.app-nav a.active { color: var(--color-accent); border-bottom-color: var(--color-accent); }
.avatar {
  width: 30px; height: 30px; flex: none;
  border: 1px solid var(--color-divider);
  display: grid; place-items: center;
  font-size: 11px; font-family: var(--font-heading); cursor: pointer;
}
.avatar:hover { border-color: var(--color-accent); color: var(--color-accent); }
.avatar { background: transparent; padding: 0; color: inherit; }

.account-menu {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 40;
  min-width: 210px; display: grid;
  background: var(--color-bg); border: 1px solid var(--color-divider);
  box-shadow: 0 10px 30px rgb(0 0 0 / 12%);
}
/* In the rail the trigger sits at the bottom of the viewport, so the menu
   opens upward and hugs the left edge instead of running off-screen. */
.account-menu.up {
  top: auto; bottom: calc(100% + 8px); right: auto; left: 0;
  min-width: 216px;
}
.account-menu-head {
  padding: 10px 14px; border-bottom: 1px solid var(--color-divider);
  font-size: 13px; line-height: 1.4;
}
.account-menu-item {
  text-align: left; padding: 9px 14px; font: inherit; font-size: 13px;
  background: transparent; border: 0; border-top: 1px solid var(--color-divider);
  cursor: pointer; color: var(--color-text);
}
.account-menu-item:first-of-type { border-top: 0; }
.account-menu-item:hover { background: var(--color-accent-100); color: var(--color-accent-800); }
.account-menu-item.active { color: var(--color-accent); }
.account-menu-item.danger:hover { background: #fbecee; color: var(--sev-critical); }
.live-pip { width: 6px; height: 6px; background: var(--color-accent); animation: blink 1.4s infinite; }

/* ── progress bars ──────────────────────────────────────────────────── */
.bar { height: 8px; background: var(--color-neutral-200); overflow: hidden; }
.bar > span { display: block; height: 100%; background: var(--color-accent); transition: width .4s ease; }
.bar.thin { height: 5px; }
.bar.tall { height: 10px; }

/* ── stage strip (live monitor) ─────────────────────────────────────── */
.stages { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px;
          background: var(--color-divider); border: 1px solid var(--color-divider); }
.stage { background: var(--color-bg); padding: 12px 15px; }
.stage-n { font-size: 10px; letter-spacing: 0.09em; text-transform: uppercase; opacity: .7; margin-bottom: 5px; }
.stage-name { font-family: var(--font-heading); font-size: 17px; display: flex; align-items: center; gap: 8px; }
.stage.done { background: var(--color-accent-100); color: var(--color-accent-900); }
.stage.active { background: var(--color-accent-900); color: var(--color-bg); }
.stage.active .stage-name::after {
  content: ""; width: 6px; height: 6px; background: var(--color-accent-300);
  animation: blink 1.2s infinite;
}
.stage.skipped { background: var(--color-neutral-100); color: var(--color-neutral-500); }

/* ── console (engine log) ───────────────────────────────────────────── */
.console {
  background: var(--color-accent-900);
  padding: 12px 16px;
  /* Tall enough to hold a meaningful slice of the run, but bounded so the
     page itself never becomes the scroll container for engine output. */
  height: clamp(380px, calc(100vh - 400px), 760px);
  overflow: auto;
  font-family: ui-monospace, monospace; font-size: 12px; line-height: 1.75;
  color: var(--color-accent-200);
}
.console .row { display: flex; gap: 12px; }
.console .t { color: var(--color-accent-600); flex: none; }
.console .tag { flex: none; width: 68px; text-transform: uppercase; font-size: 10px;
                letter-spacing: 0.06em; padding-top: 2px; background: transparent;
                border: 0; padding-inline: 0; }
.console .msg { white-space: pre-wrap; word-break: break-word; min-width: 0; }

/* A phase boundary is the outline of the run. Drawn as a rule, the log reads
   as a sequence of stages instead of an undifferentiated scroll. */
.console .row.banner { align-items: center; margin: 10px 0 4px; }
.console .row.banner .msg {
  flex: 1; color: #fff; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase; font-size: 11px;
  display: flex; align-items: center; gap: 12px; white-space: nowrap;
}
.console .row.banner .msg::after {
  content: ""; flex: 1; height: 1px;
  background: color-mix(in srgb, #fff 22%, transparent);
}
.console .row.banner + .row { margin-top: 2px; }

/* Operator-only lines reach administrators over the socket but stay hidden
   until they ask for them. Members are never sent these at all -- the filter
   that matters is server-side; this switch only changes what is displayed. */
.console .row.op { display: none; }
.console.detail .row.op { display: flex; }
.console.detail .row.op .msg, .console.detail .row.op .tag { color: var(--color-accent-600); }
.lvl-info { color: var(--color-accent-300); }
.lvl-debug { color: var(--color-accent-600); }
.lvl-phase { color: #fff; font-weight: 600; }
.lvl-success { color: #8fd6a5; }
.lvl-warning { color: #e3b872; }
.lvl-error { color: #f0918f; }
.lvl-finding { color: #9fd0f5; }
.lvl-system { color: var(--color-accent-500); }
.caret::after { content: "▊"; animation: blink 1s infinite; }

/* A command line is a terminal, so it stays dark in both themes. Steps 900
   and 200 trade places on the dark ramp, which rendered this block as dark
   text on a light panel -- the exact inverse of what it depicts. */
.cmd-preview {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11px; line-height: 1.65;
  background: #12171d; color: #b9d3ea;
  padding: 11px 12px; margin: 0;
  white-space: pre-wrap; word-break: break-all;
  max-height: 170px; overflow: auto;
  border: 1px solid transparent;
}
:root[data-theme="dark"] .cmd-preview { background: #101317; border-color: var(--color-divider); }

/* ── code blocks ──────────────────────────────────────────────────────
 * Source code is dark in either theme, and pinned rather than expressed in
 * ramp steps: 900 and 200 trade places on the dark ramp, which rendered the
 * vulnerable-code block as dark text on a pale panel -- and washed the
 * highlighted line out to pink in the process.
 */
.code {
  background: #12171d;
  font-family: ui-monospace, monospace; font-size: 12px; line-height: 1.75;
  padding: 10px 0; overflow-x: auto;
  border: 1px solid transparent;
}
:root[data-theme="dark"] .code { background: #101317; border-color: var(--color-divider); }
.code .ln { display: flex; gap: 12px; padding: 0 12px; }
.code .ln .n { color: #5f7f9c; flex: none; width: 34px; text-align: right; user-select: none; }
.code .ln .c { color: #b9d3ea; white-space: pre; }
/* Pinned for the same reason as the block itself: --sev-critical lifts on the
   dark ramp, and a lifted red at 32% over a light panel is pink. */
.code .ln.hit { background: rgb(140 47 57 / 45%); }
.code .ln.hit .c, .code .ln.hit .n { color: #fff; }
.code .ln.add { background: color-mix(in srgb, #3f7150 30%, transparent); }
.code .ln.del { background: color-mix(in srgb, #8c2f39 28%, transparent); }

/* ── data-flow list ─────────────────────────────────────────────────── */
.flow-step {
  display: flex; gap: 12px; padding: 9px 0;
  border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent);
}
.flow-kind {
  font-family: var(--font-heading); font-size: 11px; letter-spacing: 0.06em;
  text-transform: uppercase; flex: none; width: 62px; padding-top: 2px;
}
.flow-source { color: var(--sev-high); }
.flow-sink { color: var(--sev-critical); }
.flow-flow { color: var(--color-accent); }

/* ── dropzone ───────────────────────────────────────────────────────── */
.dropzone {
  border: 1px dashed var(--color-accent-500);
  background: var(--color-accent-100);
  padding: 34px 20px; text-align: center; cursor: pointer;
  transition: background .15s ease, border-color .15s ease;
}
.dropzone:hover, .dropzone.over { background: var(--color-accent-200); border-color: var(--color-accent-700); }
.dropzone.busy { cursor: progress; opacity: .8; }
.dropzone h4 { margin: 0 0 6px; font-size: 21px; }

/* ── list rows ──────────────────────────────────────────────────────── */
.row-item {
  display: flex; align-items: center; gap: 12px; padding: 11px 0;
  border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent);
  font-size: 13px;
}
/* The row's fixed columns. In CSS rather than inline so the breakpoint below
   can release them -- as inline styles their minimums summed past a 360px
   screen and pushed the timestamp off the right edge. */
.ri-seq { min-width: 44px; }
.ri-name { font-weight: 500; min-width: 150px; }
.ri-when { min-width: 74px; text-align: right; }

.row-item.clickable { cursor: pointer; }
.row-item.clickable:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
.table tbody tr.sel { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
.table td, .table th { vertical-align: middle; }
.table.dense td { padding: 6px var(--space-2); }

/* ── tabs ───────────────────────────────────────────────────────────── */
.tabs { display: flex; border-bottom: 1px solid var(--color-divider); }
.tabs > span {
  flex: 1; text-align: center; cursor: pointer; padding: 10px 8px;
  font-family: var(--font-heading); font-size: 13px;
  letter-spacing: 0.04em; text-transform: uppercase;
  border-bottom: 2px solid transparent; color: var(--color-neutral-600);
}
.tabs > span:hover { color: var(--color-text); }
.tabs > span.on { border-bottom-color: var(--color-accent); color: var(--color-text); }

/* ── empty / loading states ─────────────────────────────────────────── */
.empty {
  padding: 48px 24px; text-align: center; color: var(--color-neutral-600);
}
.empty h4 { margin-bottom: 6px; color: var(--color-text); }
.empty p { font-size: 14px; max-width: 48ch; margin: 0 auto 16px; }
.skeleton {
  position: relative; overflow: hidden;
  background: var(--color-neutral-200); height: 12px;
}
.skeleton::after {
  content: ""; position: absolute; inset: 0; width: 25%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.7), transparent);
  animation: sweep 1.2s infinite;
}

/* ── toast ──────────────────────────────────────────────────────────── */
#toasts {
  position: fixed; right: 20px; bottom: 20px; z-index: 200;
  display: flex; flex-direction: column; gap: 8px; max-width: 420px;
}
.toast {
  background: var(--color-neutral-900); color: var(--color-neutral-100);
  padding: 11px 15px; font-size: 13px; line-height: 1.45;
  box-shadow: var(--shadow-md); border-left: 3px solid var(--color-accent);
}
.toast.err { border-left-color: var(--sev-critical); }
.toast.ok { border-left-color: var(--state-ok); }

/* ── dialog ─────────────────────────────────────────────────────────── */
.backdrop {
  position: fixed; inset: 0; z-index: 150;
  display: grid; place-items: center; padding: 24px;
  background: color-mix(in srgb, var(--color-neutral-900) 55%, transparent);
}
.modal {
  width: min(520px, 100%); background: var(--color-bg);
  border: 1px solid var(--color-divider); box-shadow: var(--shadow-lg);
  padding: 24px; display: flex; flex-direction: column; gap: 14px;
  max-height: 88vh; overflow: auto;
}
.modal h3 { margin: 0; }
/* The engine log needs the width; everything else reads better narrow. */
.modal:has(> .log-modal) { width: min(1040px, 100%); }
.modal:has(> .log-modal) .console { height: min(62vh, 540px); }

/* ══ finding detail ══════════════════════════════════════════════════════
 * A vulnerability is read, not skimmed: an explanation, the offending code,
 * the route the data took to reach it, and a request that proves it. Squeezed
 * into a column beside the list, all four were cropped -- code wrapped at
 * forty characters, the data flow lost its shape, and a raw HTTP request was
 * unreadable. Centred and full-width, each one gets the room it needs.
 */
.modal:has(> .finding-modal) {
  width: min(1180px, 96vw);
  padding: 0;
  overflow: hidden;
  max-height: 92vh;
}
.finding-modal { display: flex; flex-direction: column; max-height: 92vh; min-height: 0; }

.fm-head {
  flex: none; padding: 18px 22px 16px;
  border-bottom: 1px solid var(--color-divider);
  display: flex; align-items: flex-start; gap: 16px;
}
.fm-head h3 { margin: 4px 0 5px; font-size: 23px; line-height: 1.2; }
.fm-meta { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; font-size: 12px; }
.fm-loc {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12.5px; color: var(--color-neutral-600); word-break: break-all;
}
.fm-nav { flex: none; display: flex; align-items: center; gap: 6px; }
.fm-nav .pos { font-size: 12px; color: var(--color-neutral-600); white-space: nowrap; }

/* Body: narrative left, facts right. The facts column does not scroll with
   the prose -- severity and status are the two things a reader keeps
   glancing back at. */
/* The two columns scroll independently: severity, classification and status
   are what a reader keeps glancing back at, and they used to slide off the
   top the moment the explanation got long. */
.fm-body {
  flex: 1; min-height: 0; overflow: hidden;
  display: grid; grid-template-columns: minmax(0, 1fr) 290px; gap: 0;
}
.fm-main { padding: 20px 22px 26px; min-width: 0; min-height: 0; overflow: auto; }
.fm-side {
  padding: 20px 20px 26px; border-left: 1px solid var(--color-divider);
  background: color-mix(in srgb, var(--color-text) 3%, transparent);
  display: grid; gap: 18px; align-content: start;
  min-height: 0; overflow: auto;
}

.fm-section + .fm-section { margin-top: 26px; }
.fm-section > h5 {
  margin: 0 0 4px; font-size: 15px;
  display: flex; align-items: center; gap: 9px;
}
.fm-section > h5 svg { color: var(--color-accent); flex: none; }
.fm-section > .lede {
  margin: 0 0 12px; font-size: 12.5px; color: var(--color-neutral-600); line-height: 1.5;
}
.fm-prose {
  font-size: 14px; line-height: 1.7; color: var(--color-neutral-800);
  white-space: pre-wrap; margin: 0;
}
/* Paragraphs arrive as separate nodes now that code samples are lifted out of
   the text, so the blank line between them has to be put back. */
.fm-prose + .fm-prose { margin-top: 13px; }
.fm-block + .fm-prose { margin-top: 15px; }
.fm-empty { font-size: 13px; color: var(--color-neutral-600); font-style: italic; }

/* Facts: label above value, so a long CWE title does not squeeze the label. */
.fm-facts { display: grid; gap: 11px; }
.fm-fact > span {
  display: block; font-size: 10px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--color-neutral-600); margin-bottom: 2px;
}
.fm-fact > b { font-weight: 500; font-size: 13.5px; line-height: 1.35; display: block; }

/* Source → sink, as a route rather than a list. The rail and the dots are
   what make it read as one continuous path; a flat list of rows does not. */
/* The rail sits under the centre of the markers: padding 26 + node offset -23
   puts a 9px marker at x=3..12, centre 7.5, so the rail's 1px column starts
   at 7. Off by even a pixel and it reads as two unrelated decorations. */
.flow { position: relative; padding-left: 26px; }
.flow::before {
  content: ""; position: absolute; left: 7px; top: 8px; bottom: 8px; width: 1px;
  /* A divider-weight hairline disappears at 1px against the page; the rail is
     what makes three markers read as one route. */
  background: var(--color-neutral-400);
}
.flow-node { position: relative; padding: 0 0 16px; }
.flow-node:last-child { padding-bottom: 0; }
.flow-node::before {
  content: ""; position: absolute; left: -23px; top: 4px;
  width: 9px; height: 9px; background: var(--color-bg);
  border: 1.5px solid var(--color-neutral-400);
}
.flow-node.is-source::before { border-color: var(--sev-high); background: var(--sev-high); }
.flow-node.is-sink::before { border-color: var(--sev-critical); background: var(--sev-critical); }
.flow-kind2 {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--color-neutral-600);
}
.flow-node.is-source .flow-kind2 { color: var(--sev-high); }
.flow-node.is-sink .flow-kind2 { color: var(--sev-critical); }
.flow-code {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12.5px; line-height: 1.6; margin-top: 3px;
  word-break: break-word; white-space: pre-wrap;
}
.flow-where { font-size: 11.5px; color: var(--color-neutral-600); margin-top: 2px; }

/* Code and evidence blocks share one look. */
.fm-block { margin-top: 12px; }
.fm-block-head {
  display: flex; align-items: center; gap: 10px; margin-bottom: 6px;
  font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--color-neutral-600);
}
.fm-pre {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px; line-height: 1.7;
  background: #12171d; color: #b9d3ea;
  padding: 12px 14px; margin: 0;
  white-space: pre-wrap; word-break: break-word;
  max-height: 340px; overflow: auto;
  border: 1px solid transparent;
}
:root[data-theme="dark"] .fm-pre { background: #101317; border-color: var(--color-divider); }

.fm-foot {
  flex: none; padding: 13px 22px;
  border-top: 1px solid var(--color-divider); background: var(--color-bg);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}

@media (max-width: 900px) {
  .fm-body { grid-template-columns: minmax(0, 1fr); }
  .fm-side { border-left: 0; border-top: 1px solid var(--color-divider); }
  .modal:has(> .finding-modal) { width: 100%; }
}

/* ── forms ──────────────────────────────────────────────────────────── */
/* A read-only field has to look read-only; otherwise people type into the
   e-mail box on the settings screen and wonder why nothing saves. */
.input:disabled, .input[readonly] {
  background: var(--color-neutral-100);
  color: var(--color-neutral-600);
  cursor: not-allowed;
}
.field + .field { margin-top: 14px; }
/* Inside a grid the gap already separates them, and the sibling margin pushes
   the second field half a row down so the two inputs no longer line up. */
.grid-2 > .field + .field, .grid-3 > .field + .field { margin-top: 0; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.check {
  display: flex; align-items: center; gap: 10px; cursor: pointer; padding: 6px 0;
  font-size: 14px;
}
.check .box {
  width: 16px; height: 16px; flex: none; border: 1.5px solid var(--color-divider);
  display: grid; place-items: center; font-size: 11px; line-height: 1;
  color: var(--color-bg);
}
.check.on .box { background: var(--color-accent); border-color: var(--color-accent); }
.check:hover .box { border-color: var(--color-accent); }
.check.disabled { opacity: .5; cursor: not-allowed; }

.radio-card {
  background: var(--color-bg); padding: 13px 16px; cursor: pointer;
  display: flex; align-items: flex-start; gap: 12px;
}
.radio-card:hover { background: color-mix(in srgb, var(--color-accent) 6%, transparent); }
.radio-card.on { background: var(--color-accent-100); }
.radio-card .ring {
  width: 15px; height: 15px; flex: none; border-radius: 50%;
  border: 1.5px solid var(--color-divider); margin-top: 3px;
}
.radio-card.on .ring {
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 3.5px var(--color-accent);
}

/* ══ explanation primitives ══════════════════════════════════════════════
 * The scan screen is configured by people who have never read a static
 * analyser's manual. Every control therefore carries two things: what it
 * does, and what choosing it costs. These are the shapes that carry them.
 */

/* A numbered step header — turns a wall of panels into a sequence. */
.step-head { display: flex; align-items: flex-start; gap: 13px; margin-bottom: 14px; }
.step-n {
  flex: none; width: 26px; height: 26px; display: grid; place-items: center;
  border: 1px solid var(--color-accent); color: var(--color-accent);
  font-family: var(--font-heading); font-size: 13px; line-height: 1;
}
.step-head h4 { margin: 0 0 2px; }
.step-head p { margin: 0; font-size: 13px; color: var(--color-neutral-600); line-height: 1.5; }

/* Consequence line: what happens if you pick this. Not decoration -- it is
   the difference between a setting and an informed choice. */
.consequence {
  display: flex; gap: 8px; align-items: flex-start;
  font-size: 12px; line-height: 1.5; color: var(--color-neutral-700);
  margin-top: 8px;
}
.consequence svg { flex: none; margin-top: 2px; opacity: .75; }
.consequence.warn { color: var(--sev-high); }

/* Callout: a fact about how the deployment behaves, not an action. */
.callout {
  display: flex; gap: 11px; align-items: flex-start;
  padding: 12px 14px; font-size: 12.5px; line-height: 1.55;
  border: 1px solid var(--color-accent-300); background: var(--color-accent-100);
}
.callout svg { flex: none; margin-top: 2px; color: var(--color-accent); }
.callout.neutral { border-color: var(--color-divider); background: color-mix(in srgb, var(--color-text) 4%, transparent); }
.callout.neutral svg { color: var(--color-neutral-600); }
.callout b { font-weight: 500; }

/* Preset cards: the whole configuration in one click. */
.presets { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.preset {
  position: relative; cursor: pointer; padding: 16px;
  border: 1px solid var(--color-divider); background: var(--color-bg);
  display: grid; gap: 7px; align-content: start;
  width: 100%; color: var(--color-text); text-align: left;
  font: inherit; appearance: none;
  transition: border-color .14s, background .14s, transform .14s var(--ease-out);
}
.preset:hover { border-color: var(--color-accent-500); transform: translateY(-2px); }
.preset:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.preset.on { border-color: var(--color-accent); background: var(--color-accent-100); }
.preset.on::after {
  content: "✓"; position: absolute; top: 10px; right: 12px;
  color: var(--color-accent); font-size: 14px; line-height: 1;
}
.preset .p-name {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 17px; display: flex; align-items: center; gap: 8px;
}
.preset .p-for { font-size: 12.5px; color: var(--color-neutral-700); line-height: 1.5; }
/* Pushed to the bottom so the three fact tables line up across cards whose
   descriptions run to different lengths. */
.preset .p-facts {
  display: grid; gap: 3px; font-size: 11.5px; color: var(--color-neutral-600);
  padding-top: 8px; margin-top: auto; border-top: 1px solid var(--color-divider);
}
.preset .p-facts div { display: flex; justify-content: space-between; gap: 10px; }
.preset .p-facts b { font-weight: 500; color: var(--color-text); }

/* Immutable scan-runtime manifest. Dense, aligned facts read like an
   operations console while remaining native text at every zoom level. */
.runtime-manifest {
  display: grid; grid-template-columns: .75fr 1.65fr 1fr 1fr;
  margin: 17px 0 14px; border: 1px solid var(--color-divider);
  background: color-mix(in srgb, var(--color-neutral-900) 4%, var(--color-bg));
}
.runtime-cell {
  min-width: 0; padding: 10px 12px; display: grid; gap: 4px;
  border-right: 1px solid var(--color-divider);
}
.runtime-cell:last-child { border-right: 0; }
.runtime-key {
  color: var(--color-neutral-600); font-size: 9.5px; line-height: 1;
  letter-spacing: .1em; text-transform: uppercase;
}
.runtime-cell b, .runtime-cell code {
  font-size: 11.5px; font-weight: 500; line-height: 1.35;
}
.runtime-cell code {
  color: var(--color-accent-700); overflow-wrap: anywhere;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
.runtime-ready b { display: flex; align-items: center; gap: 7px; }
.runtime-led {
  width: 6px; height: 6px; flex: none; background: var(--color-neutral-500);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-neutral-500) 15%, transparent);
}
.runtime-ready .runtime-led {
  background: var(--sev-low);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--sev-low) 15%, transparent);
}
.runtime-unavailable b { color: var(--sev-high); }

/* Codex-native effort matrix: six levels stay visible even when a plan locks
   the upper rows. The real radio inputs preserve keyboard and screen-reader
   behaviour; the surrounding labels provide the larger target. */
.effort-fieldset {
  min-width: 0; margin: 18px 0 0; padding: 15px;
  border: 1px solid var(--color-divider);
}
.effort-fieldset legend {
  padding: 0 7px; color: var(--color-accent); font-family: var(--font-heading);
  font-size: 13px; font-weight: var(--font-heading-weight);
  letter-spacing: .04em; text-transform: uppercase;
}
.effort-fieldset > p {
  margin: 0 0 11px; color: var(--color-neutral-600);
  font-size: 12px; line-height: 1.45;
}
.effort-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px;
}
.effort-option {
  position: relative; min-width: 0; min-height: 52px; padding: 9px 10px;
  display: flex; align-items: center; gap: 9px; cursor: pointer;
  border: 1px solid var(--color-divider); background: var(--color-bg);
  transition: border-color .12s, background .12s;
}
.effort-option:hover { border-color: var(--color-accent-500); }
.effort-option.on { border-color: var(--color-accent); background: var(--color-accent-100); }
.effort-option:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.effort-option input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.effort-marker {
  width: 14px; height: 14px; flex: none; border: 1.5px solid var(--color-divider);
  border-radius: 50%;
}
.effort-option.on .effort-marker {
  border-color: var(--color-accent); box-shadow: inset 0 0 0 3.5px var(--color-accent);
}
.effort-copy { min-width: 0; display: grid; gap: 2px; }
.effort-copy b { font-size: 12.5px; font-weight: 500; }
.effort-copy small {
  color: var(--color-neutral-600); font-size: 10px; line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.effort-lock {
  margin-left: auto; color: var(--color-neutral-600); font-size: 8.5px;
  line-height: 1.15; letter-spacing: .07em; text-align: right;
  text-transform: uppercase;
}
.effort-option.locked { cursor: not-allowed; opacity: .48; background: var(--color-neutral-100); }
.effort-option.locked:hover { border-color: var(--color-divider); }

/* An option row that explains itself: title, one line of plain English,
   and what it costs. Used for phases and for depth. */
.opt {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 13px 15px; background: var(--color-bg); cursor: pointer;
  transition: background .12s;
}
.opt:hover { background: color-mix(in srgb, var(--color-accent) 6%, transparent); }
.opt.on { background: var(--color-accent-100); }
.opt.locked { opacity: .5; cursor: not-allowed; }
.opt-body { min-width: 0; flex: 1; }
.opt-title { font-size: 14px; font-weight: 500; display: flex; align-items: center; gap: 8px; }
.opt-what { font-size: 12.5px; color: var(--color-neutral-700); line-height: 1.5; margin-top: 2px; }
.opt-cost {
  flex: none; text-align: right; font-size: 11px; color: var(--color-neutral-600);
  display: grid; gap: 2px; padding-top: 2px;
}
.opt-list { display: grid; gap: 1px; background: var(--color-divider); border: 1px solid var(--color-divider); }
/* The tick and the radio ring are shared with .check / .radio-card, but those
   centre their marker on a single line of text; here the label wraps to three,
   so the marker aligns to the first line instead. */
.opt > .box {
  width: 16px; height: 16px; flex: none; border: 1.5px solid var(--color-divider);
  display: grid; place-items: center; font-size: 11px; line-height: 1;
  color: var(--color-bg); background: var(--color-bg);
}
.opt > .box.on { background: var(--color-accent); border-color: var(--color-accent); }
.opt:hover > .box { border-color: var(--color-accent); }
.opt > .ring {
  width: 15px; height: 15px; flex: none; border-radius: 50%;
  border: 1.5px solid var(--color-divider);
}
.opt.on > .ring { border-color: var(--color-accent); box-shadow: inset 0 0 0 3.5px var(--color-accent); }

/* Cost dots — an ordinal, never a fabricated multiplier. */
.pips { display: inline-flex; gap: 3px; align-items: center; }
.pips i {
  width: 6px; height: 6px; display: block;
  border: 1px solid var(--color-neutral-400); background: transparent;
}
.pips i.on { background: var(--color-accent); border-color: var(--color-accent); }

/* Disclosure for the fine-tuning nobody should have to open. */
.disclosure > summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: 9px;
  padding: 14px 18px; font-family: var(--font-heading);
  font-weight: var(--font-heading-weight); font-size: 15px;
}
.disclosure > summary::-webkit-details-marker { display: none; }
.disclosure > summary .chev { transition: transform .18s var(--ease-out); flex: none; }
.disclosure[open] > summary .chev { transform: rotate(90deg); }
.disclosure > summary:hover { color: var(--color-accent); }
.disclosure > summary .sum-note {
  margin-left: auto; font-family: var(--font-body); font-weight: 400;
  font-size: 12px; color: var(--color-neutral-600);
}
.disclosure-body { padding: 0 18px 18px; border-top: 1px solid var(--color-divider); padding-top: 16px; }

/* Sub-group inside advanced settings. */
.adv-group + .adv-group { margin-top: 22px; padding-top: 20px; border-top: 1px solid var(--color-divider); }
.adv-group > h6 { color: var(--color-accent); margin: 0 0 4px; }
.adv-group > .lede { font-size: 12.5px; color: var(--color-neutral-600); margin: 0 0 14px; line-height: 1.5; }

/* Sticky summary rail on the scan screen. */
.sum-row { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; }
.sum-row > span:first-child { color: var(--color-neutral-600); }
.sum-row > span:last-child { text-align: right; min-width: 0; }

/* Page-mount reveal: one orchestrated entrance, not per-element flourish. */
@media (prefers-reduced-motion: no-preference) {
  .page > * { animation: rise .34s var(--ease-out) both; }
  .page > *:nth-child(2) { animation-delay: .04s; }
  .page > *:nth-child(3) { animation-delay: .08s; }
  .page > *:nth-child(n+4) { animation-delay: .12s; }
}
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ── report iframe ──────────────────────────────────────────────────── */
.report-frame {
  width: 100%; height: calc(100vh - 210px); min-height: 520px;
  border: 1px solid var(--color-divider); background: #fff;
}

/* ── landing ────────────────────────────────────────────────────────── */
.landing-hero {
  display: grid; grid-template-columns: 1.05fr 1fr; gap: 56px;
  align-items: center; padding: 68px 40px 60px;
  max-width: 1360px; margin: 0 auto;
}
.landing-hero h1 {
  font-size: 62px; line-height: 1.03; letter-spacing: -0.02em;
  margin: 0 0 20px; max-width: 16ch;
}
.landing-section { padding: 8px 40px 72px; max-width: 1360px; margin: 0 auto; }
.terminal {
  font-family: ui-monospace, monospace; font-size: 12px; line-height: 1.85;
  background: var(--color-accent-900); color: var(--color-accent-200);
  padding: 14px; overflow: hidden;
}
/* A terminal is dark in any theme. Steps 900 and 200 trade places on the dark
   ramp, which would otherwise render this block as dark text on a light
   panel -- the exact inverse of a terminal. */
:root[data-theme="dark"] .terminal {
  background: #101317;
  color: var(--color-accent-700);
  border: 1px solid var(--color-divider);
}
.split-auth { display: grid; grid-template-columns: 1fr 1fr; min-height: 100vh; }
/* The brand side is the deep panel of the pair, in either theme. Expressed in
   ramp steps it inverted: 900 and 200 trade places on the dark ramp, so a
   dark-mode visitor got a bright slab beside a near-black sign-in form. */
.auth-aside {
  background: #16202b; color: #b9d3ea;
  padding: 44px; display: flex; flex-direction: column; justify-content: space-between;
}
.auth-aside h1, .auth-aside h2, .auth-aside .brand { color: #f0f6fb; }
.auth-aside .brand-mark { border-color: #7fb0d8; }
.auth-aside .brand-mark::after { background: #7fb0d8; }
.auth-aside .aside-facts { color: #7f9db8; }
:root[data-theme="dark"] .auth-aside {
  background: #101317; border-right: 1px solid var(--color-divider);
}

/* ── responsive ─────────────────────────────────────────────────────── */
@media (max-width: 1200px) {
  .split-2, .split-detail { grid-template-columns: 1fr !important; }
  .metrics.m5, .metrics.m6 { grid-template-columns: repeat(3, 1fr); }
}
/* Between 861 and 1320 the rail is the cheapest thing to shrink: the tables
   it sits next to are the reason anyone opened the page. Same floor as above,
   and for the same reason. */
@media (min-width: 861px) and (max-width: 1320px) {
  .app-shell:not(.rail-wide) .app-side { width: var(--rail-w-narrow); }
  .app-shell:not(.rail-wide) .side-label,
  .app-shell:not(.rail-wide) .side-brand span:not(.brand-mark),
  .app-shell:not(.rail-wide) .side-link span,
  .app-shell:not(.rail-wide) .side-link .badge,
  .app-shell:not(.rail-wide) .credit-meter .cm-top,
  .app-shell:not(.rail-wide) .side-account .who,
  .app-shell:not(.rail-wide) .side-cta .btn span { display: none; }
  .app-shell:not(.rail-wide) .side-brand,
  .app-shell:not(.rail-wide) .side-link,
  .app-shell:not(.rail-wide) .side-account { justify-content: center; padding-inline: 0; }
  .app-shell:not(.rail-wide) .side-cta { padding-inline: 12px; }
  .app-shell:not(.rail-wide) .side-foot .theme-toggle { flex-direction: column; }
}

/* ── side-by-side layouts that have to stack on a phone ──────────────────
 *
 * The template comes from `--cols`, set inline at each site, rather than
 * `grid-template-columns` itself. That indirection is the whole point: an
 * inline `grid-template-columns` wins over every stylesheet rule including a
 * media query, so the layouts written that way stayed two-up on a 375px
 * screen and pushed the page 100px wider than the phone. A custom property
 * carries the desktop intent without claiming the property the breakpoint
 * needs to change.
 *
 * `minmax(0, ...)` on the fallback matters too: a grid track's default
 * minimum is `auto`, which refuses to shrink below its content, so one long
 * unbroken string in a cell widens the whole page.
 */
.gcols {
  display: grid;
  grid-template-columns: var(--cols, 1fr);
  gap: var(--gap, 24px);
  align-items: start;
  min-width: 0;
}
.gcols > * { min-width: 0; }

@media (max-width: 860px) {
  .gcols { grid-template-columns: minmax(0, 1fr); gap: var(--gap-sm, 20px); }
  /* Counts and other small tiles stay side by side -- stacking eight numbers
     into a column is worse than two rows of them. */
  .gcols.gcols-keep { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* The pipeline row is a number, a name and a paragraph. Fully stacked it
     leaves "01" alone on a line looking like a heading; this keeps the number
     with its name and gives the prose the full width underneath. */
  .gcols.pipeline-row {
    grid-template-columns: 34px minmax(0, 1fr);
    row-gap: 6px; padding: 18px 16px;
  }
  .gcols.pipeline-row > :nth-child(3) { grid-column: 1 / -1; }

  /* A row of scan facts: a number, a project, a status, a count and a time.
     Their minimums add up to more than a 360px screen, so the last one --
     always the timestamp -- was pushed past the right edge and took the whole
     document with it. Let the line wrap and stop reserving width. */
  .row-item { flex-wrap: wrap; row-gap: 4px; }
  .ri-seq, .ri-name, .ri-when { min-width: 0; }
  .ri-when { margin-left: auto; }

  /* Brand and the two buttons on the first line, the section links on their
     own line underneath, scrolling sideways rather than wrapping into a
     ragged block. */
  .pub-head { flex-wrap: wrap; gap: 10px; padding: 12px 16px; }
  .pub-nav {
    order: 3; width: 100%; margin-left: 0; gap: 20px;
    overflow-x: auto; white-space: nowrap; padding-bottom: 2px;
  }
  .pub-nav a { padding: 4px 0; }
  .pub-actions { margin-left: auto; }

  /* The finding title shared a line with the prev/next controls and got half
     the width, so a normal-length title broke into five lines with the other
     half of the dialog empty. The controls take their own row. */
  .fm-head { flex-wrap: wrap; padding: 14px 16px; gap: 10px; }
  .fm-head > :first-child { order: 2; width: 100%; }
  .fm-nav { order: 1; width: 100%; justify-content: flex-end; }
  .fm-head h3 { font-size: 20px; }

  /* Three preset cards side by side leave ~100px each. Stacked, every one
     gets the full width for the sentence that explains when to choose it --
     which is the only reason the card exists. */
  .presets { grid-template-columns: minmax(0, 1fr); }
  .runtime-manifest { grid-template-columns: 1fr 1fr; }
  .runtime-cell:nth-child(2) { border-right: 0; }
  .runtime-cell:nth-child(-n+2) { border-bottom: 1px solid var(--color-divider); }
  .effort-grid { grid-template-columns: 1fr 1fr; }

  /* ── touch and legibility ───────────────────────────────────────────
   *
   * A pointer is a pixel; a fingertip is about 9mm. Controls that read as
   * comfortably small with a mouse are a miss-and-try-again on a phone, so
   * the ones below the usable floor get their height back here rather than
   * everywhere -- a 26px button is right on a dense desktop table.
   */
  .btn-sm { min-height: 36px; padding-inline: 12px; }
  .theme-toggle button { min-height: 34px; min-width: 40px; }
  .crumb { font-size: 14px; }
  .crumb a { display: inline-block; padding: 4px 0; }

  /* Anything under about 12px stops being readable at arm's length outdoors.
     Uppercase labels keep their smaller size -- they are short and spaced --
     but everything that is a sentence comes up to 12.5px. */
  .metric-label { font-size: 11px; }
  .muted, .dim, .tag, .metric-note { font-size: 12.5px; }
  .hint { font-size: 12.5px; }

  /* iOS zooms the whole page in when a focused field is under 16px, and it
     does not zoom back out. This is the fix for that, not a design choice. */
  .input, select.input, textarea.input { font-size: 16px; }

  /* A phone is held one-handed: the primary action should be reachable and
     unmistakable rather than sharing a row with three others. */
  .page-head .actions { width: 100%; justify-content: flex-start; }

  /* A findings table has seven columns and cannot usefully become one; it
     scrolls sideways inside its own panel instead of widening the page. */
  .table-wrap, .panel > .table, .bp > .table { display: block; overflow-x: auto; }
  .table { min-width: 0; }

  .landing-hero, .split-auth { grid-template-columns: 1fr; }
  .landing-hero { padding: 40px 20px; }
  .landing-hero h1 { font-size: 42px; }
  .landing-section { padding: 8px 20px 48px; }
  .grid-3, .grid-2 { grid-template-columns: 1fr; }
  .metrics.m4, .metrics.m5, .metrics.m6 { grid-template-columns: repeat(2, 1fr); }
  .stages { grid-template-columns: repeat(2, 1fr); }
  .page { padding: 20px 14px 48px; }
  .app-header { gap: 14px; padding: 10px 14px; flex-wrap: wrap; }
  .app-nav { order: 3; width: 100%; overflow-x: auto; }
  .auth-aside { display: none; }

  /* The rail becomes a drawer. It keeps its full width here -- on a phone
     there is no second column to protect, and icon-only navigation with no
     labels is a guessing game. */
  .app-shell { flex-direction: column; }
  .mobile-bar {
    display: flex; align-items: center; gap: 12px;
    position: sticky; top: 0; z-index: 45;
    padding: 9px 12px; background: var(--color-bg);
    border-bottom: 1px solid var(--color-divider);
  }
  .mobile-bar .brand { font-size: 17px; }
  .app-shell .app-side {
    position: fixed; inset: 0 auto 0 0; height: 100dvh;
    width: var(--rail-w) !important;
    transform: translateX(-100%);
    transition: transform .2s var(--ease-out);
    box-shadow: var(--shadow-lg);
  }
  .app-shell.rail-open .app-side { transform: none; }
  .app-shell.rail-open .rail-scrim {
    display: block; position: fixed; inset: 0; z-index: 39;
    background: color-mix(in srgb, var(--color-neutral-900) 45%, transparent);
  }
  .rail-toggle { display: none; }
}

/* Wide tables scroll inside their own frame; the page never scrolls sideways. */
.table-scroll { overflow-x: auto; }

/* ── dark theme ──────────────────────────────────────────────────────────
 * `data-theme` is stamped on <html> by theme.js before first paint, so this
 * is a straight token swap rather than a media query: an explicit choice has
 * to be able to beat the OS in both directions, and a media query cannot do
 * that on its own.
 *
 * The two ramps are not simply inverted. Step 100 is used throughout as a
 * *tint background* (selected rows, the current phase, chip fills) and step
 * 900 as a deep ground; on a dark canvas those roles trade places, so both
 * ramps are rewritten end to end rather than reversed.
 */
:root[data-theme="dark"] {
  --color-bg: #16181a;
  --color-surface: #1c1f22;
  --color-text: #e6e7e9;
  --color-accent: #7aa4cd;
  --color-accent-2: #8fa9c4;
  --color-divider: color-mix(in srgb, #e6e7e9 18%, transparent);

  --color-neutral-100: #202326;
  --color-neutral-200: #2a2e32;
  --color-neutral-300: #3a3f44;
  --color-neutral-400: #4e545a;
  --color-neutral-500: #767c83;
  --color-neutral-600: #9aa0a7;
  --color-neutral-700: #b8bdc3;
  --color-neutral-800: #d2d6da;
  --color-neutral-900: #eceef0;

  /* 100 is the faint tint, 900 the strong ink — same roles as light, moved
     onto a dark ground. */
  --color-accent-100: #1b242e;
  --color-accent-200: #22303d;
  --color-accent-300: #2c4054;
  --color-accent-400: #3c5771;
  --color-accent-500: #4f708f;
  --color-accent-600: #6b90b3;
  --color-accent-700: #8fb2d2;
  --color-accent-800: #b3cde4;
  --color-accent-900: #d7e6f3;

  --color-accent-2-100: #1c2329;
  --color-accent-2-200: #232d35;
  --color-accent-2-300: #2e3b45;
  --color-accent-2-400: #3f4f5c;
  --color-accent-2-500: #536676;
  --color-accent-2-600: #6f8494;
  --color-accent-2-700: #92a5b3;
  --color-accent-2-800: #b6c5d0;
  --color-accent-2-900: #d9e2e9;

  /* On a dark ground a drop shadow reads as nothing. Depth comes from a
     lit hairline plus ambient darkness underneath. */
  --shadow-sm: 0 1px 2px rgb(0 0 0 / 45%);
  --shadow-md: 0 4px 14px rgb(0 0 0 / 50%);
  --shadow-lg: 0 16px 40px rgb(0 0 0 / 60%);

  /* Severity keeps its hue so a red row stays red, but lifts in lightness
     and drops in chroma: full-saturation red on near-black vibrates. */
  --sev-critical: #e4818a;
  --sev-critical-bg: #2e1c1f;
  --sev-high: #e0a271;
  --sev-high-bg: #2e2419;
  --sev-medium: #d6c274;
  --sev-medium-bg: #2b2819;
  --sev-low: #8fb6dd;
  --sev-low-bg: #1b242e;
  --sev-info: #a4a6ab;
  --sev-info-bg: #232528;

  --state-ok: #7fc496;
  --state-warn: #e0a271;
  --state-err: #e4818a;
}

/* The engine console is a terminal: it stays dark in both themes, so it must
   not follow the accent ramp when that ramp flips. */
.console { background: #12171d; }
:root[data-theme="dark"] .console {
  background: #101317;
  border: 1px solid var(--color-divider);
}

/* Most surfaces follow the tokens and need nothing here. The toast is
   deliberately left alone too: it is an inverted chip (neutral-900 on
   neutral-100), so swapping the ramp keeps it inverted rather than making it
   vanish into the page.

   These three are literal colours in the light sheet, which is why they need
   restating. */
:root[data-theme="dark"] .account-menu-item.danger:hover {
  background: var(--sev-critical-bg);
}
/* Opaque, not a translucent mix: a see-through chip picks up whatever row
   tint is behind it and changes colour on hover. */
:root[data-theme="dark"] .chip-ok { background: #1c2b22; }
/* The loading sweep is a white highlight; at full strength on a dark bar it
   reads as a strobe. */
:root[data-theme="dark"] .skeleton::after {
  background: linear-gradient(90deg, transparent, rgb(255 255 255 / 12%), transparent);
}
/* The report iframe renders the engine's own light-themed HTML; give it a
   light backdrop so it is not framed by a black gutter. */
:root[data-theme="dark"] .report-frame { background: #f2f2f3; }

/* ── theme toggle ────────────────────────────────────────────────────── */
.theme-toggle {
  display: inline-flex; align-items: center; gap: 2px; flex: none;
  border: 1px solid var(--color-divider); padding: 2px;
}
.theme-toggle button {
  display: grid; place-items: center;
  width: 26px; height: 24px; padding: 0;
  background: transparent; border: 0; cursor: pointer;
  color: var(--color-neutral-600);
}
.theme-toggle button:hover { color: var(--color-accent); }
.theme-toggle button.on {
  background: var(--color-accent-100);
  color: var(--color-accent);
}
/* In the rail the toggle spans the full width; collapsed, three 26px buttons
   plus padding do not fit 64px, so it stacks. */
.side-foot .theme-toggle { display: flex; }
.side-foot .theme-toggle button { flex: 1; }
/* The avatar is a span inside the account button now, not the button itself. */
.side-account .avatar { width: 28px; height: 28px; }

/* A skipped stage is still worth showing -- it explains a gap in the run --
   but it did not happen, so it does not get the weight of one that did. */
.console .row.banner.faint .msg { color: var(--color-accent-600); font-weight: 400; }
.console .row.banner.faint .msg::after { background: color-mix(in srgb, #fff 12%, transparent); }
