/* =====================================================================
   Faultline design system.

   Structure: tokens -> base -> primitives -> components -> screens.
   Everything below reads from the token layer; no component hardcodes a
   colour, size or space value. That rule is the whole point -- the
   previous stylesheet had ~18 unsystematic font sizes and two hardcoded
   rgba() brand tints that silently ignored the theme switch, which is
   exactly the class of drift a token layer exists to prevent.

   Both themes are fully specified and contrast-verified (every pair
   below was computed, not eyeballed). Dark is :root because that's what
   the app has always defaulted to and app.js stamps data-theme before
   first paint; light is an explicit, equally-considered stamp.
   ===================================================================== */

@import url("./vendor/fonts/fonts.css");

/* ---------------------------------------------------------------------
   TOKENS -- dark (default)
   --------------------------------------------------------------------- */
:root {
  /* Surface ramp. The old palette kept bg/panel/card inside one narrow
     luminance band, so nothing separated from anything; these are real
     steps. */
  --surface-0: #080d13;   /* app ground */
  --surface-1: #0f1720;   /* panels */
  --surface-2: #16202b;   /* raised cards */
  --surface-3: #1e2a38;   /* hover / active */

  /* Two border weights, deliberately. Subtle is decorative (a card
     outline where the surface step already does the separating);
     --border is for anything interactive or state-bearing and clears
     WCAG 1.4.11's 3:1 against its own surface. */
  --border-subtle: #243140;
  --border: #35485c;
  --border-strong: #4a6076;

  --text: #e9eff4;        /* 15.1:1 on surface-1 */
  --text-2: #a9bac7;      /*  8.1:1 */
  --text-3: #7b8d9c;      /*  4.7:1 */

  /* Brand ramp. Teal is retained -- it's established equity and it isn't
     broken -- but it is now ONLY brand: chrome, links, active state. */
  --brand-300: #6fdccb;
  --brand-400: #3fc7b6;
  --brand-500: #2a9d8f;
  --brand-600: #1f7c71;
  --brand-on: #06201c;    /* text that sits on a brand fill */

  /* Semantics, split away from brand. Previously --good WAS --accent, so
     "passed" and "ordinary chrome" were the same colour and a passing
     verdict carried no unique signal. */
  --success: #35c48d;
  --success-soft: color-mix(in srgb, var(--success) 14%, transparent);
  --warning: #e9a23b;
  --warning-soft: color-mix(in srgb, var(--warning) 14%, transparent);
  --danger: #f2704f;
  --danger-soft: color-mix(in srgb, var(--danger) 14%, transparent);
  --info: #5aa9f0;
  --info-soft: color-mix(in srgb, var(--info) 14%, transparent);
  --info-on: #06131f;     /* text that sits on a solid --info fill */
  --brand-soft: color-mix(in srgb, var(--brand-500) 16%, transparent);

  /* Priority scale (ticket cards) -- semantic, not brand. p2 added for
     NOC (critical/high/normal/low maps onto p1-p4); Labs never used p2
     itself but the scale always had a gap at it. */
  --priority-p1: #f2704f;
  --priority-p2: #f0913f;
  --priority-p3: #e9a23b;
  --priority-p4: #5aa9f0;

  --term-bg: #05090d;
  --term-fg: #e9eff4;
  --term-cursor: #3fc7b6;
  --term-selection: #3fc7b655;
  /* Brand teal, not a generic terminal green -- the prompt/hostname
     should read as THIS app's own accent (matches --term-cursor above),
     not xterm's default olive or a "hacker movie" pure green. */
  --term-prompt: var(--brand-400);

  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.32);
  --shadow-md: 0 4px 12px rgb(0 0 0 / 0.38);
  --shadow-lg: 0 16px 40px rgb(0 0 0 / 0.46);

  color-scheme: dark;
}

/* ---------------------------------------------------------------------
   TOKENS -- light. Every value verified: ink 17.4:1, text-2 6.9:1,
   text-3 4.9:1, brand-text 7.1:1, button label 6.0:1, success 5.0:1,
   danger 5.9:1, warning 5.5:1, interactive border 3.3:1.
   --------------------------------------------------------------------- */
:root[data-theme="light"] {
  --surface-0: #e9edef;
  --surface-1: #ffffff;
  --surface-2: #f4f7f8;
  --surface-3: #e4eaed;

  --border-subtle: #dfe5e8;
  --border: #7e8f9b;
  --border-strong: #5f7280;

  --text: #131b21;
  --text-2: #4e5c66;
  --text-3: #64737e;

  --brand-300: #16856f;
  --brand-400: #116355;   /* brand as TEXT on light needs to be this dark */
  --brand-500: #12705f;   /* brand as a FILL */
  --brand-600: #0d5a4c;
  --brand-on: #ffffff;

  --success: #0b7a54;
  --warning: #8a5b00;
  --danger: #a83a1e;
  --info: #1a5fa8;
  --info-on: #ffffff;    /* --info is a dark navy fill here, not the light dark-mode blue */

  --priority-p1: #a83a1e;
  --priority-p2: #a05114;
  --priority-p3: #8a5b00;
  --priority-p4: #1a5fa8;

  --term-bg: #ffffff;
  --term-fg: #131b21;
  --term-cursor: #12705f;
  --term-selection: #12705f33;
  --term-prompt: var(--brand-400); /* brand teal, not a generic terminal green -- see dark theme's own comment */

  --shadow-sm: 0 1px 2px rgb(16 24 32 / 0.06);
  --shadow-md: 0 4px 12px rgb(16 24 32 / 0.10);
  --shadow-lg: 0 16px 40px rgb(16 24 32 / 0.18);

  color-scheme: light;
}

/* ---------------------------------------------------------------------
   TOKENS -- type, space, radius (theme-independent)
   --------------------------------------------------------------------- */
:root {
  --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace;

  /* 8-step scale. Minimum body 14px, minimum label 12px -- the old
     stylesheet had 10.5px labels. */
  --fs-micro: 11px;
  --fs-label: 12px;
  --fs-sm: 13px;
  --fs-body: 14px;
  --fs-lg: 16px;
  --fs-h3: 20px;
  --fs-h2: 26px;
  --fs-h1: 34px;
  --fs-display: 48px;

  --lh-tight: 1.2;
  --lh-snug: 1.35;
  --lh-body: 1.55;

  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 64px;

  --r-sm: 4px;
  --r-md: 8px;
  --r-lg: 12px;
  --r-pill: 999px;

  --dur: 140ms;
  --ease: cubic-bezier(0.2, 0, 0.2, 1);

  --appbar-h: 56px;
  --page-max: 1760px;
}

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

/* ---------------------------------------------------------------------
   BASE
   --------------------------------------------------------------------- */
* { box-sizing: border-box; }

/* min-height, not height. This is a sticky footer (header, flex:1 main,
   footer, in a column) -- `height: 100%` pins body to EXACTLY the
   viewport, and .layout's `flex: 1 1 0%` below then gives it no min-height
   of its own. On any page whose content is taller than one screen (the
   landing page, easily), the content overflows that fixed-height box with
   the default overflow:visible, and the footer -- a later flex sibling --
   still gets positioned right after the box's *stated* height, not the
   content's real height. The footer ends up overlapped by the very
   content spilling past it instead of pushed below it. `min-height: 100%`
   keeps the "footer sits at the bottom on a short page" behavior while
   letting body grow taller than the viewport for everything else. */
html, body { min-height: 100%; }

body {
  margin: 0;
  background: var(--surface-0);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* The lab workspace is the one view that must fit the viewport exactly
   rather than grow with its content. min-height:100vh above leaves every
   descendant's height *indefinite*, and an indefinite height is why
   .ws-grid's `minmax(0, 1fr)` row could not bound the rail: 1fr against
   an indefinite height resolves to max-content, so the rail's height
   (driven by the topology SVG, which scales with width) still pushed the
   row -- and the terminal that stretches to match it -- taller, without
   limit, as you zoomed out. A definite height here makes that 1fr real,
   so the rail scrolls internally and the terminal stays viewport-sized.
   Scoped to the workspace via a body class set in showView(); every
   other view keeps growing and page-scrolling as before. */
body.view-workspace, body.view-noc { height: 100vh; overflow: hidden; }
/* .layout is only a flex column in workspace mode (.layout-flush), so
   without this the dashboard section's flex:1 has nothing to grow
   against and the row stops short, leaving dead space under it. */
body.view-noc #layout { display: flex; flex-direction: column; }
/* Reverted: the module reading page had been set to edge-to-edge white
   (var(--surface-1)) instead of the app's usual pale ground, but once the
   lesson text itself went from grey to black the white page read as too
   stark -- back to the standard body background (var(--surface-0), same
   as every other view) with no override here at all. The Labs side panel
   still keeps its own card and its stronger --shadow-md (see .side-panel
   below); that shadow was tuned for the white page but a plain card still
   reads fine raised against the grey ground, so it's left as-is. */

h1, h2, h3, h4 { margin: 0; line-height: var(--lh-tight); font-weight: 650; letter-spacing: -0.015em; text-wrap: balance; }
h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); }
h4 { font-size: var(--fs-lg); }
p { margin: 0; }

:focus-visible { outline: 2px solid var(--brand-400); outline-offset: 2px; border-radius: var(--r-sm); }

/* Seamless scrolling everywhere: the bars are hidden, the scrolling
   itself is untouched -- wheel, trackpad, keys and dragging all still
   work, there is just no gutter drawn. */
* { scrollbar-width: none; -ms-overflow-style: none; }
*::-webkit-scrollbar { width: 0; height: 0; }

.hidden { display: none !important; }
/* Off-screen but still focusable and still a real form control -- used
   for the Blueprint import file input, which has to stay operable (a
   button click forwards to it) while the styled button is what shows.
   display:none would make it unclickable, which is the whole problem. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}
.mono { font-family: var(--font-mono); }
.spacer { flex: 1; }

/* A label style used everywhere a field/section needs naming. Monospace
   is doing real work in this product -- it's the typographic register of
   the thing being taught. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}

.tabular { font-variant-numeric: tabular-nums; font-family: var(--font-mono); }
.text-muted { color: var(--text-3); }
.text-success { color: var(--success); }
.text-info { color: var(--info); }

/* ---------------------------------------------------------------------
   PRIMITIVES -- buttons
   Five weights, not two: the old two-variant system meant "End session"
   (destroys the lab) looked identical to "Reveal solution" (harmless).
   --------------------------------------------------------------------- */
.btn {
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 550;
  line-height: 1;
  padding: 9px 14px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  white-space: nowrap;
  text-decoration: none;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.btn:hover { background: var(--surface-3); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
.btn:disabled:hover { background: transparent; }

.btn-primary { background: var(--brand-500); color: var(--brand-on); border-color: var(--brand-500); font-weight: 650; }
.btn-primary:hover { background: var(--brand-600); border-color: var(--brand-600); }

.btn-secondary { background: var(--surface-2); border-color: var(--border); }
.btn-secondary:hover { background: var(--surface-3); }

.btn-ghost { border-color: transparent; color: var(--text-2); }
/* The theme toggle is a 15px glyph with no label, and at --text-2 on a
   borderless button it was easy to miss entirely -- full-contrast ink
   and a faint outline make it read as a control. */
#theme-toggle { color: var(--text); border-color: var(--border-subtle); }
.btn-ghost:hover { color: var(--text); background: var(--surface-2); }

.btn-danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent); }
.btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); }

.btn-sm { font-size: var(--fs-label); padding: 6px 10px; }
.btn-icon { padding: 8px; min-width: 34px; }

/* ---------------------------------------------------------------------
   PRIMITIVES -- inputs, chips, badges
   --------------------------------------------------------------------- */
.input {
  font-family: inherit;
  font-size: var(--fs-sm);
  padding: 9px 12px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text);
  width: 100%;
}
.input::placeholder { color: var(--text-3); }

.search { position: relative; display: flex; align-items: center; }
.search .input { padding-left: 32px; }
.search-icon { position: absolute; left: 10px; color: var(--text-3); pointer-events: none; display: flex; }

.chip {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: 0.03em;
  padding: 5px 10px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
  white-space: nowrap;
  transition: all var(--dur) var(--ease);
}
.chip:hover { color: var(--text); border-color: var(--border-strong); }
.chip[aria-pressed="true"] { background: var(--brand-soft); color: var(--brand-400); border-color: var(--brand-500); }

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-subtle);
  background: var(--surface-2);
  color: var(--text-2);
}
/* color-mix, not a hardcoded rgba -- the old .badge baked the dark
   theme's teal into a literal rgba() that ignored the theme entirely,
   which is why light mode's badge was the weakest text pair in the app. */
.badge-brand { background: var(--brand-soft); color: var(--brand-400); border-color: color-mix(in srgb, var(--brand-500) 40%, transparent); }
.badge-success { background: var(--success-soft); color: var(--success); border-color: color-mix(in srgb, var(--success) 40%, transparent); }
.badge-warning { background: var(--warning-soft); color: var(--warning); border-color: color-mix(in srgb, var(--warning) 40%, transparent); }
.badge-danger { background: var(--danger-soft); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, transparent); }
.badge-info { background: var(--info-soft); color: var(--info); border-color: color-mix(in srgb, var(--info) 40%, transparent); }

.card {
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-md);
  padding: var(--sp-4);
}

.panel {
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-lg);
  padding: var(--sp-5);
}

/* ---------------------------------------------------------------------
   APP BAR
   Decorative tagline removed (it belongs in the landing hero, not in
   navigation), auth demoted from the loudest button in the product to a
   quiet affordance, and the reclaimed space carries progress.
   --------------------------------------------------------------------- */
.appbar {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
  height: var(--appbar-h);
  padding: 0 var(--sp-5);
  border-bottom: 1px solid var(--border-subtle);
  background: var(--surface-1);
  position: sticky;
  top: 0;
  z-index: 40;
  flex: none;
}
.brand {
  font-weight: 750;
  letter-spacing: 0.04em;
  font-size: var(--fs-lg);
  color: var(--text);
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.brand-mark { color: var(--brand-400); display: flex; }

.nav { display: flex; align-items: center; gap: var(--sp-1); }
.nav-link {
  font-size: var(--fs-sm);
  font-weight: 550;
  color: var(--text-2);
  text-decoration: none;
  padding: 6px 10px;
  border-radius: var(--r-sm);
  border-bottom: 2px solid transparent;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.nav-link:hover { color: var(--text); background: var(--surface-2); }
.nav-link.active { color: var(--brand-400); border-bottom-color: var(--brand-400); }

.progress-readout {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  color: var(--text-3);
  display: flex;
  gap: var(--sp-3);
  white-space: nowrap;
}
.progress-readout b { color: var(--text); font-weight: 600; }

/* ---------------------------------------------------------------------
   LAYOUT
   --------------------------------------------------------------------- */
.layout { flex: 1; width: 100%; max-width: var(--page-max); margin: 0 auto; padding: var(--sp-5); min-height: 0; }
.layout.layout-flush { padding: var(--sp-4) var(--sp-5); display: flex; flex-direction: column; }

.page-head { display: flex; align-items: flex-end; gap: var(--sp-4); flex-wrap: wrap; margin-bottom: var(--sp-5); }
.page-head h2 { margin: 0; }
.page-sub { color: var(--text-2); font-size: var(--fs-sm); margin-top: var(--sp-1); }

/* ---------------------------------------------------------------------
   LANDING
   `.layout-landing` (toggled by showView()) strips the shared `.layout`
   container's padding/max-width so the landing's own `land-*` sections
   (see the LANDING block near the end of this file) can run their
   backgrounds edge-to-edge. `.hero-term`/`.hero-cli` below stay in use
   by NOC Story Mode's stage-3 terminal chrome, and `.land-hero-cli`
   reuses the `.cli-*` classes runHeroAnimation() emits.
   --------------------------------------------------------------------- */
.layout.layout-landing { padding: 0; max-width: none; }

/* Atlas needs the same edge-to-edge treatment as landing, for a
   different reason: it's not decorative full-bleed backgrounds, it's the
   certification chart's own `.scroll-shell` (min-width:2100px on
   `.chart`) needing every pixel of real viewport width it can get before
   falling back to its own horizontal scrollbar. The shared `.layout`'s
   1760px max-width plus its side padding was shaving real width off that
   budget on every screen, including ones under 1760px wide where it had
   none to spare -- on the atlas's original standalone page (full
   viewport, no page-max) that padding never existed, so this restores
   the same amount of usable width, not a new one. */
.layout.layout-atlas { padding: 0; max-width: none; }

/* Courses / CCNA / module pages: a reading measure, not the app's full
   1760px dashboard width. A module page is a lesson column (capped at
   700px for comfortable prose) beside a 340px labs panel -- against the
   full container those two sat against the left edge with several hundred
   pixels of dead space to their right. 1200px is the width the design
   mockup uses, and it makes the pair read as one centred page. */
.layout.layout-course { max-width: 1200px; padding: var(--sp-6) var(--sp-5) var(--sp-7); }

.landing { display: block; }
.landing code { font-family: var(--font-mono); font-size: 0.92em; background: var(--surface-3); padding: 1px 5px; border-radius: var(--r-sm); }

.live-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--success);
  box-shadow: 0 0 8px 1px color-mix(in srgb, var(--success) 70%, transparent);
  animation: pulse 2.4s ease-in-out infinite;
  flex: none;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }

/* The product's whole claim is "it's real" -- so the landing shows a real
   failing diagnostic session rather than describing one. */
.hero-term {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--term-bg);
  box-shadow: var(--shadow-lg);
  text-align: left;
}
.hero-cli {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  line-height: 1.7;
  padding: var(--sp-4);
  margin: 0;
  color: var(--term-fg);
  white-space: pre-wrap;
  min-height: 300px;
}
.hero-cli .cli-note { color: var(--text-3); }
.hero-cli .cli-bad { color: var(--danger); }
.hero-cli .cli-good { color: var(--success); }
.cursor-block { background: var(--brand-400); color: var(--term-bg); animation: blink 1.1s step-end infinite; }
@keyframes blink { 50% { opacity: 0; } }

/* ---------------------------------------------------------------------
   DASHBOARD
   Skill map (grouped by protocol, click to filter open tickets) +
   activity strip replace the old track-ring cards -- see app.js's
   renderDashboard()/renderDashboardMap()/renderDashboardTickets().
   .ring-track/.ring-fill/.ring-label stay: ringSvg() (still used by the
   workspace's verdict panel) emits those exact class names regardless of
   which container it's injected into.
   --------------------------------------------------------------------- */
.ring-track { fill: none; stroke: var(--border-subtle); stroke-width: 6; }
.ring-fill { fill: none; stroke-width: 6; stroke-linecap: round; transition: stroke-dasharray var(--dur) var(--ease); }
.ring-label {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.track-detail { color: var(--text-2); font-size: var(--fs-sm); }

/* ---- Top strip ---- */
#dashboard { display: block; }
.dash-top { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--sp-4); flex-wrap: wrap; margin-bottom: var(--sp-5); }
.dash-top h2 { margin-top: var(--sp-2); font-size: var(--fs-h1); }
.dash-sub { color: var(--text-2); font-size: var(--fs-lg); margin-top: var(--sp-2); max-width: 54ch; }
.dash-when { text-align: right; flex-shrink: 0; }
.dash-greet { display: inline-flex; align-items: center; gap: 7px; color: var(--text-3); font-size: var(--fs-label); white-space: nowrap; }
@media (max-width: 760px) { .dash-when { text-align: left; } }

/* ---- Row grid --------------------------------------------------------
   Three stacked rows, each a two-column grid on desktop and one column
   under the breakpoint. No JS-measured heights anywhere here (the old
   viewport-lock/trim-to-fit machinery is gone) -- panels size to their
   own content and the page scrolls like any other view. */
.dash-row { display: grid; gap: var(--sp-4); margin-bottom: var(--sp-4); align-items: stretch; }
.dash-row:last-child { margin-bottom: 0; }
.dash-row-hero { grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); }
.dash-row.cols-map { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
.dash-row.cols-even { grid-template-columns: 1fr 1fr; }
@media (max-width: 980px) {
  .dash-row-hero, .dash-row.cols-map, .dash-row.cols-even { grid-template-columns: 1fr; }
}

.dash-panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-1); }
.dash-panel-title { margin-top: 2px; }
.dash-panel-sub { color: var(--text-2); font-size: var(--fs-sm); margin: var(--sp-1) 0 var(--sp-4); }
.dash-panel-link {
  flex-shrink: 0; background: none; border: none; padding: 2px 0; margin-top: 4px;
  font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 600; color: var(--brand-400);
  cursor: pointer; white-space: nowrap;
}
.dash-panel-link:hover { text-decoration: underline; }
.dash-panel-foot {
  margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--border-subtle);
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  font-size: var(--fs-label); color: var(--text-3); flex-wrap: wrap;
}

/* Segmented control (skill map / work queue / activity track filters) --
   independent bordered pills rather than a shared tray, each carrying its
   own status dot via ::before (currentColor, so it inherits whichever
   state color the pill is in -- no extra markup per button). */
.dash-seg { display: flex; gap: 6px; flex-wrap: wrap; flex-shrink: 0; }
.dash-seg button {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-mono); font-size: var(--fs-micro); font-weight: 600; letter-spacing: 0.03em;
  padding: 5px 10px; border-radius: var(--r-pill); border: 1px solid var(--border-subtle);
  background: var(--surface-1); color: var(--text-3);
  cursor: pointer; white-space: nowrap;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.dash-seg button::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.dash-seg button:hover:not(:disabled) { color: var(--text); border-color: var(--border-strong); }
.dash-seg button[aria-pressed="true"] {
  color: var(--brand-400); border-color: color-mix(in srgb, var(--brand-500) 40%, transparent);
  background: var(--brand-soft); font-weight: 700;
}
.dash-seg button:disabled { opacity: 0.4; cursor: not-allowed; }

/* ---- Certification journey -- decorative concentric rings in the
   corner, purely a ::after (no image asset, no extra markup); the
   card's real children get z-index:1 so the rings stay behind them. ---- */
.dash-journey {
  position: relative; overflow: hidden; display: flex; flex-direction: column;
  background: linear-gradient(120deg, var(--surface-1) 0%, var(--surface-1) 55%, color-mix(in srgb, var(--brand-500) 10%, var(--surface-1)) 100%);
}
.dash-journey::after {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  right: -100px; top: -130px; width: 340px; height: 340px; border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--brand-500) 28%, transparent);
  box-shadow:
    0 0 0 22px color-mix(in srgb, var(--brand-500) 7%, transparent),
    0 0 0 44px color-mix(in srgb, var(--brand-500) 4%, transparent);
}
.dash-journey > * { position: relative; z-index: 1; }
.dash-journey h3 { margin-top: var(--sp-2); font-size: var(--fs-h2); }
.dash-journey-sub { color: var(--text-2); font-size: var(--fs-sm); margin-top: var(--sp-2); max-width: 58ch; }
.dash-journey-body {
  display: flex; align-items: center; gap: var(--sp-6); flex-wrap: wrap;
  margin-top: var(--sp-5); padding-top: var(--sp-5); border-top: 1px solid var(--border-subtle);
}
.dash-journey-stats { display: flex; gap: var(--sp-6); flex-wrap: wrap; flex: 1; min-width: 200px; }
.dash-journey-stat { display: flex; flex-direction: column; gap: 2px; }
.dash-journey-stat b { font-family: var(--font-mono); font-size: var(--fs-h3); font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.dash-journey-stat span { font-size: var(--fs-label); color: var(--text-3); }
.dash-journey-stat.open b { color: var(--danger); }
.dash-ring { width: 104px; height: 104px; position: relative; flex: none; }
/* Rotated so the fill starts at 12 o'clock, like a conventional progress
   ring -- scoped here only; the shared .ring-fill/.ring-track the
   verdict panel also uses keep their own (unrotated) convention. */
.dash-ring svg { width: 100%; height: 100%; display: block; transform: rotate(-90deg); transform-origin: center; }
.dash-ring .ring-label { flex-direction: column; gap: 2px; }
.dash-ring .ring-label b { font-family: var(--font-mono); font-size: var(--fs-h3); font-weight: 700; color: var(--text); }
.dash-ring .ring-label span { font-family: var(--font-mono); font-size: 9px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; }
.dash-journey-actions { display: flex; gap: var(--sp-3); margin-top: var(--sp-5); flex-wrap: wrap; }

/* ---- Recommended next -- the one deliberately theme-invariant surface
   on the dashboard: a dark "spotlight" card that looks the same in light
   and dark mode by design, not a themed component reading brand tokens.
   Its colors are declared once, here, rather than duplicated under
   either theme block -- same reasoning as --font-sans, --sp-N, --r-N
   living outside the theme blocks, just scoped to this one component
   instead of the whole app. ---- */
:root {
  --mission-bg-1: #0d2624;
  --mission-bg-2: #123a35;
  --mission-glow: #4fc3b0;
  --mission-ink: #f3faf8;
  --mission-ink-2: #a9c9c2;
  --mission-ink-3: #7fa39a;
  --mission-amber: #ffb55a;
  --mission-cta-bg: #bfe9dc;
  --mission-cta-ink: #0c3830;
}
.dash-recommend {
  position: relative; overflow: hidden; display: flex; flex-direction: column;
  background: linear-gradient(130deg, var(--mission-bg-1), var(--mission-bg-2));
  color: var(--mission-ink); border-color: transparent; box-shadow: var(--shadow-lg);
}
.dash-recommend::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.55;
  background: radial-gradient(circle at 100% 0%, var(--mission-glow), transparent 55%);
}
.dash-recommend > * { position: relative; z-index: 1; }
.dash-recommend-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.dash-recommend .eyebrow { color: color-mix(in srgb, var(--mission-glow) 78%, var(--mission-ink)); }
.dash-recommend-priority {
  display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0;
  font-family: var(--font-mono); font-size: var(--fs-micro); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 4px 9px; border-radius: var(--r-sm);
  color: var(--mission-amber); background: color-mix(in srgb, var(--mission-amber) 14%, transparent);
  border: 1px solid color-mix(in srgb, #ffffff 22%, transparent);
}
.dash-recommend-priority::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--mission-amber); flex: none; }
.dash-recommend-title { margin-top: var(--sp-4); font-size: var(--fs-h2); color: var(--mission-ink); max-width: 26ch; }
.dash-recommend-sub { margin-top: var(--sp-2); font-size: var(--fs-sm); color: var(--mission-ink-2); max-width: 46ch; }
.dash-recommend-tags { display: flex; gap: var(--sp-2) var(--sp-4); flex-wrap: wrap; margin-top: var(--sp-4); }
.dash-recommend-tag { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--mission-ink-3); }
.dash-recommend-tag::before { content: ""; width: 4px; height: 4px; border-radius: 50%; background: currentColor; flex: none; }
.dash-recommend-actions { margin-top: auto; padding-top: var(--sp-5); display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.dash-recommend-actions #dash-recommend-cta { background: var(--mission-cta-bg); color: var(--mission-cta-ink); border-color: transparent; font-weight: 650; }
.dash-recommend-actions #dash-recommend-cta:hover { background: color-mix(in srgb, var(--mission-cta-bg) 85%, #ffffff); }
.dash-recommend-secondary {
  background: color-mix(in srgb, var(--mission-ink) 8%, transparent); color: var(--mission-ink);
  border-color: color-mix(in srgb, var(--mission-ink) 22%, transparent); font-weight: 600;
}
.dash-recommend-secondary:hover { background: color-mix(in srgb, var(--mission-ink) 14%, transparent); }
.dash-recommend-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: var(--sp-2); height: 100%; min-height: 160px; color: var(--mission-ink); }
.dash-recommend-empty b { font-size: var(--fs-h3); }

/* The recommended challenge's REAL topology (renderTopologyDiagram(),
   the same renderer the lab workspace uses) as faint background texture
   -- never a competing content element, so it sits at z-index 0 behind
   the card's real z-index:1 text regardless of how tall the title gets,
   and stays legible with plenty of opacity to spare either way. Colors
   overridden to the mission-ink set since the diagram's own default
   styling reads off the page's normal (themed) tokens, which this
   deliberately-fixed-dark card doesn't use. */
.dash-recommend-topo { position: absolute; top: 8px; right: 8px; width: 48%; height: 62%; z-index: 0; opacity: 0.4; pointer-events: none; }
.dash-recommend-topo svg { width: 100%; height: 100%; display: block; }
.dash-recommend-topo .topo-link { stroke: var(--mission-ink-3); opacity: 0.6; }
.dash-recommend-topo .topo-node { fill: rgb(255 255 255 / 0.06); stroke: rgb(255 255 255 / 0.22); }
.dash-recommend-topo .topo-icon, .dash-recommend-topo .topo-icon * { stroke: var(--mission-glow); fill: var(--mission-glow); }
.dash-recommend-topo .topo-label { fill: var(--mission-ink-2); }
.dash-recommend-topo .topo-sublabel { fill: var(--mission-ink-3); }
.dash-recommend-topo .topo-iface-label { fill: var(--mission-ink-2); }
.dash-recommend-topo .topo-iface-ip { fill: var(--mission-ink-3); }
.dash-recommend-topo .topo-iface-pill { fill: rgb(255 255 255 / 0.06); stroke: rgb(255 255 255 / 0.22); }

/* ---- Skill map -- the grid-dot background is drawn inside the SVG
   itself (renderDashboardMap()'s own <pattern>), so nothing extra is
   needed here for that. .learned marks an edge whose both endpoints are
   fully mastered; .dash-link-flow is an animated dashed overlay drawn
   only along edges touching the currently-selected node. ---- */
.dash-map-canvas { background: var(--surface-2); border: 1px solid var(--border-subtle); border-radius: var(--r-md); padding: var(--sp-2); cursor: default; height: 300px; }
.dash-map-canvas svg { width: 100%; height: 100%; display: block; }
.dash-link { stroke: var(--border-strong); stroke-width: 1.5; opacity: 0.7; }
.dash-link.learned { stroke: var(--brand-500); stroke-width: 2; opacity: 0.9; }
.dash-link-flow { stroke: var(--brand-400); stroke-width: 2.5; fill: none; stroke-dasharray: 3 8; pointer-events: none; animation: dash-flow 1.4s linear infinite; }
@keyframes dash-flow { to { stroke-dashoffset: -22; } }
.dash-node { cursor: pointer; transition: opacity var(--dur) var(--ease); }
.dash-node.dimmed { opacity: 0.32; }
.dash-node-ring { fill: none; stroke: var(--brand-500); stroke-width: 0; transition: stroke-width var(--dur) var(--ease); }
.dash-node.selected .dash-node-ring { stroke-width: 3; animation: dash-halo 2s ease-in-out infinite; }
@keyframes dash-halo { 50% { opacity: 0.55; } }
.dash-node-circle { stroke-width: 2; }
.dash-node-circle.status-success { fill: var(--success-soft); stroke: var(--success); }
.dash-node-circle.status-warning { fill: var(--warning-soft); stroke: var(--warning); }
.dash-node-circle.status-danger { fill: var(--danger-soft); stroke: var(--danger); }
/* Redundant with the ring color on purpose -- a solid-fill corner dot
   reads at a glance even where the ring/soft-fill distinction (mastered
   vs partial) is subtle. */
.dash-node-dot { stroke: var(--surface-2); stroke-width: 2; }
.dash-node-dot.status-success { fill: var(--success); }
.dash-node-dot.status-warning { fill: var(--warning); }
.dash-node-dot.status-danger { fill: var(--danger); }
.dash-node-label { fill: var(--text); font-family: var(--font-mono); font-size: 11px; font-weight: 600; text-anchor: middle; pointer-events: none; }
.dash-node-sub { fill: var(--text-3); font-family: var(--font-mono); font-size: 9px; text-anchor: middle; pointer-events: none; }
.dash-map-legend { display: flex; gap: var(--sp-4); margin-top: var(--sp-3); font-size: var(--fs-label); color: var(--text-3); flex-wrap: wrap; }
.dash-map-legend .dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; margin-right: 5px; }
.dash-map-legend .dot.success { background: var(--success); }
.dash-map-legend .dot.warning { background: var(--warning); }
.dash-map-legend .dot.danger { background: var(--danger); }
.dash-map-legend-sep { margin-left: auto; }
@media (max-width: 640px) { .dash-map-legend-sep { margin-left: 0; } }
/* The map panel's own footer reads as a focus indicator (a tinted bar),
   not the plain bordered-footer look every other panel foot uses. */
.dash-map-panel .dash-panel-foot {
  border-top: none; margin-top: var(--sp-3); padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm); background: var(--surface-2); color: var(--text-2);
}
.dash-map-panel .dash-panel-foot span:first-child { display: inline-flex; align-items: center; gap: 7px; }
.dash-map-panel .dash-panel-foot span:first-child::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--brand-400); flex: none; }
.dash-map-panel .dash-panel-foot span:last-child { color: var(--brand-400); font-family: var(--font-mono); font-weight: 600; }

/* ---- Open incidents (work queue) ---- */
.tix-filter-chip {
  display: none; align-items: center; gap: 6px;
  font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 600; color: var(--brand-400);
  background: var(--brand-soft); border: 1px solid color-mix(in srgb, var(--brand-500) 35%, transparent);
  border-radius: var(--r-pill); padding: 3px 10px; cursor: pointer;
}
.tix-filter-chip.show { display: inline-flex; }
#dash-tickets-seg { margin-bottom: var(--sp-3); }
.dash-ticket {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-2); margin: 0 calc(-1 * var(--sp-2));
  border-bottom: 1px solid var(--border-subtle); border-radius: var(--r-sm); cursor: pointer;
  transition: background var(--dur) var(--ease);
}
.dash-ticket:hover { background: var(--surface-2); }
.dash-ticket:last-child { border-bottom: none; }
.dash-ticket-severity {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 24px; border: 1px solid; border-radius: var(--r-sm);
  font-family: var(--font-mono); font-size: var(--fs-micro); font-weight: 700;
}
.dash-ticket-severity.p1 { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent); background: var(--danger-soft); }
.dash-ticket-severity.p3 { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 45%, transparent); background: var(--warning-soft); }
.dash-ticket-severity.p4 { color: var(--info); border-color: color-mix(in srgb, var(--info) 45%, transparent); background: var(--info-soft); }
.dash-ticket-body { flex: 1; min-width: 0; }
.dash-ticket-title { font-size: var(--fs-sm); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-ticket-meta { font-size: var(--fs-label); color: var(--text-3); margin-top: 2px; }
.dash-ticket-arrow { flex: none; color: var(--text-3); transition: transform var(--dur) var(--ease), color var(--dur) var(--ease); }
.dash-ticket:hover .dash-ticket-arrow { color: var(--brand-400); transform: translateX(2px); }
.tix-empty { padding: var(--sp-5) var(--sp-2); color: var(--text-3); font-size: var(--fs-sm); text-align: center; }
.tix-empty b { color: var(--success); font-size: var(--fs-h3); margin-right: var(--sp-2); }

/* ---- Practice rhythm -- gradient-filled bars with a soft drop shadow,
   scaling in from the baseline on every render (7/30-day toggle
   included) rather than just appearing. ---- */
.dash-activity { display: flex; align-items: flex-end; gap: var(--sp-2); height: 64px; }
.dash-activity-bar { flex: 1; background: var(--surface-3); border-radius: var(--r-sm) var(--r-sm) 0 0; height: 100%; position: relative; overflow: hidden; }
.dash-activity-bar i {
  position: absolute; bottom: 0; left: 0; right: 0; border-radius: var(--r-sm) var(--r-sm) 0 0;
  background: linear-gradient(to top, var(--brand-600), var(--brand-400));
  box-shadow: 0 3px 8px color-mix(in srgb, var(--brand-500) 22%, transparent);
  transform-origin: bottom; animation: dash-bar-in 0.6s ease both;
}
.dash-activity-bar:nth-child(2) i { animation-delay: 0.03s; }
.dash-activity-bar:nth-child(3) i { animation-delay: 0.06s; }
.dash-activity-bar:nth-child(4) i { animation-delay: 0.09s; }
.dash-activity-bar:nth-child(5) i { animation-delay: 0.12s; }
.dash-activity-bar:nth-child(6) i { animation-delay: 0.15s; }
.dash-activity-bar:nth-child(7) i { animation-delay: 0.18s; }
@keyframes dash-bar-in { from { transform: scaleY(0); } to { transform: scaleY(1); } }
.dash-activity-days { display: flex; gap: var(--sp-2); margin-top: var(--sp-1); }
.dash-activity-days span { flex: 1; text-align: center; font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--text-3); }
/* 30-day view packs four times the bars into the same width -- a tight
   gap keeps them legible as a shape instead of one solid smear. */
.dash-activity.dense { gap: 2px; }

/* 7-day view: a calendar-week strip of cells instead of bars -- "which
   days did I show up" reads better as presence/absence than as a height
   comparison for just seven values. */
.dash-activity.cells { display: flex; align-items: stretch; height: auto; gap: var(--sp-2); }
.dash-day { flex: 1; display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); min-width: 0; }
.dash-day-cell {
  width: 100%; aspect-ratio: 1; border-radius: var(--r-md); display: grid; place-items: center;
  background: var(--surface-2); border: 1px solid var(--border-subtle); color: var(--text-3);
}
.dash-day-cell.done { background: var(--success-soft); border-color: color-mix(in srgb, var(--success) 35%, transparent); color: var(--success); }
.dash-day-cell.today { background: var(--brand-500); border-color: var(--brand-500); color: var(--brand-on); box-shadow: var(--shadow-sm); }
.dash-day-label { font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--text-3); }

/* ---- Recently resolved -- a two-column grid of resolved rows (a check
   badge, the title, a relative time -- no per-row score or "verified"
   text; verification is an aggregate stat in the footer, not repeated
   on every row), capped in JS to a handful with a "View history" link
   for the rest. nth-last-child(-n+2) drops the divider off whichever
   row (one or two cells) ends up last, regardless of whether the total
   count is even or odd. ---- */
#dash-attempts { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--sp-4); }
.attempt-row {
  display: flex; align-items: center; gap: var(--sp-3); min-width: 0;
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--border-subtle);
  font-size: var(--fs-sm);
}
.attempt-row:nth-last-child(-n+2) { border-bottom: none; }
.attempt-check { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border-radius: 50%; color: var(--success); background: var(--success-soft); }
.attempt-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.attempt-badge {
  flex: none; padding: 2px 7px; border-radius: var(--r-sm);
  font-family: var(--font-mono); font-size: 10px; font-weight: 700; border: 1px solid;
}
.attempt-badge.p1 { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent); background: var(--danger-soft); }
.attempt-badge.p3 { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 45%, transparent); background: var(--warning-soft); }
.attempt-badge.p4 { color: var(--info); border-color: color-mix(in srgb, var(--info) 45%, transparent); background: var(--info-soft); }
.attempt-when { color: var(--text-3); font-family: var(--font-mono); font-size: var(--fs-label); white-space: nowrap; }
.dash-recent-panel .dash-panel-foot span:first-child { display: inline-flex; align-items: center; gap: 6px; }
.dash-recent-panel .dash-panel-foot span:first-child::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--success); flex: none; }
@media (max-width: 560px) { #dash-attempts { grid-template-columns: 1fr; } .attempt-row { border-bottom: 1px solid var(--border-subtle) !important; } .attempt-row:last-child { border-bottom: none !important; } }

/* Below the two-column breakpoint the workspace stacks into separate grid
   rows, where "pin the topology to the floor of the rail" has no meaning
   and the viewport height-lock actively harms: the rows get crushed to
   nothing and, with body overflow hidden, the content is unreachable
   rather than merely off-screen. So the stacked layout reverts to
   growing naturally and scrolling the page, as it did before layout A. */
@media (max-width: 900px) {
  body.view-workspace { height: auto; overflow: visible; }
  .ws-grid { grid-template-rows: none; }
  .topo-card[data-open="true"] { height: auto; }
  .topo-card[data-open="true"] .topo-body { display: block; }
  .topo-card[data-open="true"] .topology-diagram { overflow: auto; }
  .topo-card[data-open="true"] .topology-diagram svg { height: auto; }
  .results { max-height: none; }
}

/* ---------------------------------------------------------------------
   CATALOGUE
   --------------------------------------------------------------------- */
.toolbar { display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap; margin-bottom: var(--sp-5); }
.toolbar .search { flex: 1 1 260px; max-width: 380px; }
.filter-group { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.filter-count { color: var(--text-3); font-size: var(--fs-sm); font-family: var(--font-mono); white-space: nowrap; }

.challenge-groups { display: flex; flex-direction: column; gap: var(--sp-6); }
.group-head { margin-bottom: var(--sp-3); }
.challenge-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--sp-3); }

.challenge-card {
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-left: 3px solid var(--border-subtle);
  border-radius: var(--r-md);
  padding: var(--sp-4);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.challenge-card:hover { background: var(--surface-3); border-color: var(--border); border-left-color: var(--brand-500); transform: translateY(-1px); }
.challenge-card.is-solved { border-left-color: var(--success); }
.challenge-card.is-attempted { border-left-color: var(--warning); }
.challenge-card-head { display: flex; align-items: flex-start; gap: var(--sp-3); }
.challenge-card h3 { font-size: var(--fs-lg); flex: 1; min-width: 0; }
.challenge-symptom { color: var(--text-2); font-size: var(--fs-sm); line-height: var(--lh-snug); }
.challenge-meta {
  display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border-subtle);
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  color: var(--text-3);
}
/* Difficulty and protocol are badges (colour is the point for difficulty
   -- see DIFFICULTY_BADGE in app.js; protocol tags stay the neutral grey
   .badge default on purpose, so difficulty is the only thing competing
   for colour attention). Capsule-shaped here specifically, not a global
   .badge change -- elsewhere in the app (ticket status, verdict panel)
   the smaller corner radius is already the right call. Node count and
   duration stay plain text, secondary facts that don't need a badge at
   all. */
.challenge-meta .badge { border-radius: var(--r-pill); }
.challenge-meta .meta-fact { color: var(--text-3); }
.challenge-meta .reset-btn { margin-left: auto; }

.deploying { display: flex; align-items: center; justify-content: center; gap: var(--sp-3); padding: var(--sp-5) 0; color: var(--text-2); font-size: var(--fs-sm); }

.empty-state { text-align: center; padding: var(--sp-8) var(--sp-5); color: var(--text-2); }
.empty-state h3 { color: var(--text); margin-bottom: var(--sp-2); }
.empty-state p { font-size: var(--fs-sm); max-width: 46ch; margin: 0 auto var(--sp-4); }

/* ---------------------------------------------------------------------
   COURSES
   Two pages: the vendor/certification hub (static markup) and a course
   page (CCNA today, rendered by renderCourseCCNA() from the same
   state.challenges + challengeStatus() the Labs picker uses -- see that
   function's own header comment). Reuses .eyebrow/.badge/.btn/.panel
   rather than inventing a parallel set of primitives.
   --------------------------------------------------------------------- */
.course-vendors { display: flex; flex-direction: column; gap: var(--sp-7); }
.vendor-group-head { margin-bottom: var(--sp-3); }
.cert-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: var(--sp-3); }

.cert-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-left: 3px solid var(--border-subtle);
  border-radius: var(--r-md);
  padding: var(--sp-4);
  text-decoration: none;
  color: inherit;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.cert-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); }
.cert-card-head h3 { margin: 0; font-size: var(--fs-lg); }
.cert-card-desc { color: var(--text-2); font-size: var(--fs-sm); line-height: var(--lh-snug); margin: 0; }
.cert-badge-ico { flex: none; }

.cert-card.is-available { cursor: pointer; border-left-color: var(--success); }
.cert-card.is-available:hover { background: var(--surface-3); border-color: var(--border); border-left-color: var(--success); transform: translateY(-1px); }
.cert-card-cta { margin-top: auto; display: flex; align-items: center; gap: var(--sp-1); color: var(--brand-400); font-weight: 650; font-size: var(--fs-sm); }

/* Visibly greyed out, per spec -- muted text and a flat card, but still a
   genuine card with real copy, not a broken-looking placeholder. */
.cert-card.is-soon { opacity: 0.6; cursor: default; }
.cert-card.is-soon h3 { color: var(--text-2); }

.course-crumb { display: flex; align-items: center; gap: var(--sp-2); color: var(--text-3); font-size: var(--fs-sm); margin-bottom: var(--sp-4); }
.course-crumb a { color: var(--text-3); text-decoration: none; }
.course-crumb a:hover { color: var(--brand-400); }

.course-hero { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--sp-6); margin-bottom: var(--sp-6); flex-wrap: wrap; }
.course-hero-copy { max-width: 620px; }
.course-hero-copy h1 { margin: var(--sp-1) 0 var(--sp-2); }
.course-hero-desc { color: var(--text-2); font-size: var(--fs-lg); line-height: var(--lh-body); margin: 0; }

.course-progress-card { min-width: 240px; align-self: flex-start; background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: var(--r-lg); box-shadow: var(--shadow-sm); padding: var(--sp-4) var(--sp-5); }
.course-progress-head { display: flex; justify-content: space-between; color: var(--text-2); font-size: var(--fs-label); }
.course-progress-card strong { display: block; margin: var(--sp-1) 0 var(--sp-3); font-size: var(--fs-h3); letter-spacing: -0.02em; }
.course-progress-bar { height: 7px; overflow: hidden; background: var(--surface-3); border-radius: var(--r-pill); }
.course-progress-bar span { display: block; height: 100%; background: var(--brand-500); border-radius: inherit; transition: width 0.4s ease; }
.course-progress-cta { margin-top: var(--sp-4); width: 100%; }
.course-progress-done { margin: var(--sp-4) 0 0; color: var(--text-2); font-size: var(--fs-sm); }

.course-modules { display: flex; flex-direction: column; gap: var(--sp-3); }
/* The module list is a directory now, not an inline lesson list -- see
   renderCourseCCNA()'s own comment in app.js. A whole card is the click
   target; a locked one drops the pointer/hover affordances entirely
   rather than just dimming, since it genuinely does nothing on click. */
.course-module-card { background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: var(--r-lg); padding: var(--sp-5); cursor: pointer; transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease); }
.course-module-card:hover { border-color: var(--border); background: var(--surface-2); }
.course-module-card.is-locked { opacity: 0.6; cursor: not-allowed; }
.course-module-card.is-locked:hover { border-color: var(--border-subtle); background: var(--surface-1); }
.course-module-card.is-complete .course-module-card-count { color: var(--success); }
.course-module-card-top { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.course-module-card-top .eyebrow svg { vertical-align: -1px; margin-left: 2px; }
.course-module-card-count { color: var(--text-3); font-size: var(--fs-sm); font-family: var(--font-mono); white-space: nowrap; }
.course-module-card h2 { margin: var(--sp-1) 0 var(--sp-2); font-size: var(--fs-h3); }
.course-module-card p { margin: 0 0 var(--sp-4); color: var(--text-2); font-size: var(--fs-sm); line-height: var(--lh-snug); max-width: 64ch; }
.course-module-card-bar { max-width: 320px; }

/* A single module's own page -- see renderCourseModulePage(). Title +
   description only (no progress card here -- the side panel's own foot
   already carries module progress, see .side-panel-foot below). */
.module-hero { margin-bottom: var(--sp-6); padding-bottom: var(--sp-5); border-bottom: 1px solid var(--border-subtle); }
.module-hero h1 { margin: var(--sp-1) 0 var(--sp-2); }
.module-hero-desc { max-width: 680px; margin: 0; color: var(--text); font-size: var(--fs-lg); line-height: var(--lh-body); }

.course-lessons { display: flex; flex-direction: column; gap: var(--sp-2); }
.course-lesson {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.course-lesson:hover { background: var(--surface-3); border-color: var(--border); }
.course-lesson-status { display: grid; place-items: center; flex: none; width: 26px; height: 26px; border-radius: 50%; background: var(--surface-3); color: var(--text-3); }
.course-lesson-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.course-lesson-title { font-weight: 600; color: var(--text); }
.course-lesson-meta { color: var(--text-3); font-size: var(--fs-label); font-family: var(--font-mono); }
.course-lesson-narrative { margin: 2px 0 0; color: var(--text-2); font-size: var(--fs-sm); line-height: var(--lh-snug); }
.course-lesson-action { flex: none; color: var(--text-3); font-size: var(--fs-sm); font-weight: 600; }

.course-lesson.is-solved .course-lesson-status { background: var(--success-soft); color: var(--success); }
.course-lesson.is-solved .course-lesson-action { color: var(--success); }
.course-lesson.is-locked { cursor: not-allowed; opacity: 0.55; }
.course-lesson.is-locked:hover { background: var(--surface-2); border-color: var(--border-subtle); }

/* ---------------------------------------------------------------------
   MODULE PAGE -- main content (a real in-depth lesson, see
   renderLessonSectionHtml()/CCNA_CURRICULUM's learn.sections in app.js)
   plus a sticky side panel listing the module's own labs as a connected
   path. Replaces the old single-column stacked layout entirely.
   --------------------------------------------------------------------- */
.module-grid { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: var(--sp-6); align-items: start; }
@media (max-width: 900px) { .module-grid { grid-template-columns: 1fr; } }

/* Reading width is capped independently of the grid column so long-form
   prose stays comfortable even on a wide viewport -- everything inside
   .module-main (diagrams and tables included) shares this one width. */
.module-main { max-width: 700px; }
/* A hairline rule between sections instead of a card per section (or a
   card around the whole lesson) -- the intentional choice over boxing
   content that was tried and rejected: see the redesign notes in
   docs/course-module-readability-mockup-v2.html for the discarded
   card/sheet variants. The intro block (no heading, straight under the
   hero) is never a "section + section" match, so it never gets a rule
   above it. */
/* --border-strong, not --border-subtle -- against the module page's
   grey background the subtle hairline nearly disappeared (barely
   distinguishable from the page's own background-vs-border contrast);
   the stronger token plus a hair more weight keeps it a clearly visible
   divider between topics without turning it into a boxed card edge. */
.module-main section + section { margin-top: var(--sp-6); padding-top: var(--sp-6); border-top: 1px solid var(--border-strong); }
.module-main h3 { margin: 0 0 var(--sp-3); font-size: var(--fs-h3); }
/* --text, not the app's usual --text-2 for body copy: this IS the body
   copy on a module page (there's no separate heading carrying the real
   weight the way a card title elsewhere does), so it reads as full-weight
   ink rather than secondary/muted text. Same reasoning applies to every
   other lesson-content color below (.rung-desc, .callout p, .term-list
   dd, .ref-table .desc, .reasoning-list li, .concept-list li). */
.module-main p { margin: 0; color: var(--text); font-size: var(--fs-body); line-height: var(--lh-body); }
.module-main p + p { margin-top: var(--sp-3); }
.module-main .lesson-lede { font-size: var(--fs-lg); }
.module-main code { font-family: var(--font-mono); font-size: 0.92em; background: var(--surface-2); border: 1px solid var(--border-subtle); border-radius: var(--r-sm); padding: 1px 5px; }
.module-main .concept-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.module-main .concept-list li { position: relative; padding-left: 22px; color: var(--text); font-size: var(--fs-sm); line-height: var(--lh-snug); }
.module-main .concept-list li::before { content: ""; position: absolute; left: 4px; top: 7px; width: 6px; height: 6px; border-radius: 50%; background: var(--brand-400); }

/* A connected ladder -- the "levels" block kind (currently just the
   three IOS command levels): a marker+rail per rung, reusing the same
   visual language the course side panel's own Labs list (.lab-path)
   already uses, rather than a table or a card per level. */
.ladder { position: relative; list-style: none; margin: var(--sp-4) 0; padding: 0; }
.ladder::before { content: ""; position: absolute; left: 19px; top: 8px; bottom: 8px; width: 2px; background: var(--border-subtle); }
.ladder li { position: relative; display: flex; gap: var(--sp-4); padding: 0 0 var(--sp-4) 0; }
.ladder li:last-child { padding-bottom: 0; }
.rung { position: relative; flex: none; width: 40px; height: 40px; border-radius: 50%; background: var(--surface-1); border: 2px solid var(--border); display: flex; align-items: center; justify-content: center; z-index: 1; }
.rung.c-teal { border-color: var(--brand-400); color: var(--brand-400); }
.rung.c-blue { border-color: var(--info); color: var(--info); }
.rung.c-amber { border-color: var(--warning); color: var(--warning); }
.rung-body { padding-top: 2px; }
.rung-name { font-weight: 650; font-size: var(--fs-sm); color: var(--text); }
.rung-prompt { display: inline-block; margin: 6px 0; font-family: var(--font-mono); font-size: var(--fs-sm); font-weight: 650; color: var(--text); }
.rung-desc { font-size: var(--fs-sm); color: var(--text); line-height: var(--lh-snug); }

/* Anatomy-of-an-address diagram: real octets, decimal over binary --
   see the "octets" block kind in renderLessonBlockHtml(). */
.octet-grid { display: flex; align-items: center; gap: var(--sp-2); margin: var(--sp-4) 0; flex-wrap: wrap; }
.octet { flex: 1 1 90px; background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: var(--r-md); padding: var(--sp-3) var(--sp-2); text-align: center; }
.octet-dec { font-family: var(--font-mono); font-size: var(--fs-h3); font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.octet-bin { font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--brand-400); margin-top: var(--sp-2); letter-spacing: 0.03em; }
.octet-dot { flex: none; font-family: var(--font-mono); font-size: var(--fs-h3); font-weight: 700; color: var(--text-3); padding-bottom: 18px; }

/* Network-vs-host bit-split bar, proportioned by real bit counts via
   flex-grow ratios (network:host), not fixed percentages -- see the
   "bitbar" block kind. */
.bit-bar-legend { display: flex; gap: var(--sp-5); font-size: var(--fs-label); color: var(--text-3); margin: var(--sp-4) 0 var(--sp-2); }
.bit-bar-legend span { display: inline-flex; align-items: center; gap: 6px; }
.bit-bar-legend .sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px; }
.bit-bar-legend .sw.network { background: var(--brand-400); }
.bit-bar-legend .sw.host { background: var(--border-strong); }
.bit-bar { display: flex; gap: 3px; height: 44px; border-radius: var(--r-sm); overflow: hidden; }
.bit-bar-seg { display: flex; align-items: center; justify-content: center; font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 700; }
.bit-bar-seg.network { background: var(--brand-soft); color: var(--brand-400); border: 1px solid color-mix(in srgb, var(--brand-500) 45%, transparent); }
.bit-bar-seg.host { background: var(--surface-3); color: var(--text-2); border: 1px solid var(--border); }
.bit-bar-octets { display: flex; margin-top: var(--sp-1); }
.bit-bar-octets span { flex: 1; text-align: center; font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--text-3); }

/* Reasoning-through-it numbered list -- the "reasoning" block kind, used
   where the lesson works through why a decision gets made. */
.reasoning-list { list-style: none; counter-reset: reasoning; margin: var(--sp-4) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.reasoning-list li { counter-increment: reasoning; position: relative; padding-left: 34px; color: var(--text); font-size: var(--fs-sm); line-height: var(--lh-body); }
.reasoning-list li::before {
  content: counter(reasoning); position: absolute; left: 0; top: 0;
  width: 24px; height: 24px; border-radius: 50%; background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text-2); font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 700;
  display: flex; align-items: center; justify-content: center;
}

/* A labelled callout -- the "callout" block kind, used for the
   physical-address analogy and the "on a real device" CLI sample. An
   accent rule, not a box: no fill, no surrounding border -- the module
   page's one deliberate box is the Labs side panel, nothing in the
   lesson body itself. */
.callout { margin: var(--sp-4) 0; padding: 0 0 0 var(--sp-4); border-left: 3px solid var(--brand-500); }
.callout-label { display: block; font-family: var(--font-mono); font-size: var(--fs-micro); font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: var(--brand-400); margin-bottom: var(--sp-2); }
.callout p { color: var(--text); font-size: var(--fs-sm); margin: 0; }
.callout p + p { margin-top: var(--sp-2); }
.callout .lesson-terminal { margin: var(--sp-3) 0 0; }

.term-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.term-list dt { font-weight: 650; color: var(--text); font-size: var(--fs-sm); }
.term-list dd { margin: 2px 0 0; color: var(--text); font-size: var(--fs-sm); line-height: var(--lh-snug); }

/* 16:9 via aspect-ratio, not the old padding-top-hack box -- every
   browser this app targets supports it. Radius/background match the
   app's other embedded-content surfaces (.ref-table-wrap, .lesson-terminal)
   so a loading/blocked iframe doesn't leave a jarring blank rectangle. */
.lesson-video { position: relative; width: 100%; aspect-ratio: 16 / 9; margin: var(--sp-4) 0; border-radius: var(--r-md); overflow: hidden; background: var(--surface-2); }
.lesson-video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* Negative margin-top pulls this right under the video instead of
   reading as its own paragraph -- a copyright disclaimer, not lesson
   prose, so it's deliberately quieter (--text-3, smaller) than either. */
.lesson-video-attribution { margin: calc(var(--sp-4) * -1) 0 var(--sp-4); color: var(--text-3); font-size: var(--fs-micro); }

.ref-table-wrap { overflow-x: auto; }
.ref-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.ref-table th { text-align: left; color: var(--text-3); font-family: var(--font-mono); font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; padding-bottom: var(--sp-2); border-bottom: 1px solid var(--border-subtle); }
.ref-table td { padding: var(--sp-2) var(--sp-3) var(--sp-2) 0; border-bottom: 1px solid var(--border-subtle); vertical-align: top; }
.ref-table tr:last-child td { border-bottom: none; }
.ref-table code { font-family: var(--font-mono); font-size: var(--fs-label); background: var(--surface-2); border: 1px solid var(--border-subtle); border-radius: var(--r-sm); padding: 2px 6px; white-space: nowrap; }
.ref-table .desc { color: var(--text); }

.module-cta-row { margin-top: var(--sp-6); padding-top: var(--sp-5); border-top: 1px solid var(--border-subtle); display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.module-cta-row .course-progress-done { margin-top: 0; }
.module-cta { display: inline-flex; align-items: center; gap: var(--sp-2); }
.module-cta-note { color: var(--text-3); font-size: var(--fs-sm); }

/* ---- side panel: this module's labs, as a connected path -- guided,
   then from-memory, then troubleshoot, each gated on the last (see
   stageAvailability() in app.js). "locked" and "placeholder" look alike
   (dimmed, inert) but mean different things: locked is real content not
   reached yet, placeholder is a topic with no real lab behind it yet. ---- */
/* --shadow-md, not the app's usual --shadow-sm: on the module page the
   surrounding page is now the same white as this panel's own background
   (body.view-course-module above), so the border alone used to do more
   of the "this is a raised card" work than it will now -- a slightly
   stronger shadow keeps the Labs panel reading as the one deliberately
   boxed thing on the page instead of flattening into the page around it. */
.side-panel { position: sticky; top: calc(var(--appbar-h) + var(--sp-4)); background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: var(--r-lg); padding: var(--sp-5); box-shadow: var(--shadow-md); }
.side-panel-head { margin-bottom: var(--sp-1); }
.side-panel-head h3 { margin: var(--sp-1) 0 var(--sp-1); font-size: var(--fs-h3); }
.side-panel-sub { margin: 0 0 var(--sp-5); color: var(--text-3); font-size: var(--fs-sm); }

.lab-path { position: relative; list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.lab-item { position: relative; display: block; text-decoration: none; color: inherit; padding: var(--sp-3) 0 var(--sp-3) 46px; cursor: pointer; border-radius: var(--r-sm); }
/* The rail, drawn per-item rather than once for the whole list: a single
   .lab-path::before spanning a hardcoded inset from the list's own
   top/bottom assumed every lab's card was the same height, so it either
   fell short of or overshot the real first/last marker the moment
   descriptions wrapped to a different number of lines (confirmed: it
   overran the last marker by ~90px on a real 3-lab module). Each item
   instead draws exactly the segment it owns -- top:0 to bottom:0 through
   its own marker's vertical center (--sp-3 + half the marker's own
   height) -- so consecutive items' segments always meet exactly at the
   shared edge between them, regardless of how tall any one card is. The
   first item has nothing above its own circle to connect to, and the
   last has nothing below, so each trims its own end back to its marker's
   center instead of running the full item height. */
/* Two segments, not one -- a single top:0/bottom:0 line ran the full
   item height *behind* the marker circle, so it was still technically
   visible passing through the circle rather than stopping at its edge.
   ::before covers item-top down to the marker's own top edge, ::after
   covers the marker's bottom edge down to item-bottom; the 36px-wide,
   sp-3-offset gap between them is exactly the marker circle, so the
   rail now visibly terminates at each circle instead of running through it. */
.lab-item::before,
.lab-item::after { content: ""; position: absolute; left: 17px; width: 2px; background: var(--border-subtle); z-index: 0; }
.lab-item::before { top: 0; height: var(--sp-3); }
.lab-item::after { top: calc(var(--sp-3) + 36px); bottom: 0; }
/* :first-child/:last-child on .lab-item itself would be meaningless --
   renderLabPathItemHtml() wraps each one in its own <li>, so every
   .lab-item is trivially both the first AND last child of ITS OWN
   parent regardless of where it falls in the list (confirmed the hard
   way: that combination matched every item at once and hid every line).
   The real siblings are the <li>s under .lab-path, so that's what these
   select on instead. */
.lab-path > li:first-child .lab-item::before { display: none; }
.lab-path > li:last-child .lab-item::after { display: none; }
.lab-item:hover { background: var(--surface-2); }
.lab-item:hover .lab-item-title { color: var(--brand-400); }
.lab-marker {
  position: absolute; left: 0; top: var(--sp-3); width: 36px; height: 36px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); border: 2px solid var(--border-strong); color: var(--text-2);
  font-family: var(--font-mono); font-weight: 700; font-size: var(--fs-sm); z-index: 1;
}
.lab-item.is-current .lab-marker { border-color: var(--brand-500); background: var(--brand-soft); color: var(--brand-400); box-shadow: 0 0 0 4px color-mix(in srgb, var(--brand-500) 16%, transparent); }
.lab-item.is-done .lab-marker { border-color: var(--success); background: var(--success-soft); color: var(--success); }
.lab-item.is-locked .lab-marker { color: var(--text-3); }
.lab-item.is-locked { cursor: default; opacity: 0.6; }
.lab-item.is-locked:hover { background: transparent; }
.lab-item.is-locked .lab-item-title { color: var(--text-2); }
.lab-type { display: block; font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-3); margin-bottom: 2px; }
/* fs-lg + full --text, not the small muted title this used to be -- a
   lab's title is the thing a learner is actually scanning this list for,
   it shouldn't read quieter than its own eyebrow above it. */
.lab-item-title { display: block; font-weight: 650; font-size: var(--fs-lg); color: var(--text); }
.lab-item-desc {
  margin: var(--sp-1) 0 0; color: var(--text); font-size: var(--fs-sm); line-height: var(--lh-snug);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* A real pill, not just colored text -- same shape/weight language as
   the app's own .badge, so "Continue"/"Review"/"Coming soon" reads as a
   status chip at a glance instead of blending into the description
   above it. */
.lab-item-status {
  display: inline-flex; align-items: center; gap: 4px; margin-top: var(--sp-2);
  font-family: var(--font-mono); font-size: var(--fs-micro); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 5px 10px; border-radius: var(--r-pill); border: 1px solid var(--border-subtle); background: var(--surface-2); color: var(--text-3);
}
.lab-item.is-done .lab-item-status { color: var(--success); background: var(--success-soft); border-color: color-mix(in srgb, var(--success) 40%, transparent); }
.lab-item.is-current .lab-item-status { color: var(--brand-400); background: var(--brand-soft); border-color: color-mix(in srgb, var(--brand-500) 40%, transparent); }

.side-panel-bonus { margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--border-subtle); }
.side-panel-bonus .eyebrow { display: block; margin-bottom: var(--sp-2); }

.side-panel-foot { margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--border-subtle); }
.side-panel-foot-bar { height: 6px; background: var(--surface-3); border-radius: var(--r-pill); overflow: hidden; margin-bottom: var(--sp-2); }
.side-panel-foot-bar span { display: block; height: 100%; background: var(--brand-500); border-radius: inherit; transition: width 0.4s ease; }
.side-panel-foot-label { display: flex; justify-content: space-between; font-size: var(--fs-label); color: var(--text-3); }

@media (max-width: 700px) {
  .course-hero { flex-direction: column; }
  .course-progress-card { width: 100%; }
}

/* .lesson-terminal/.lt-prompt/.lt-cmd stay -- reused by the workspace's
   Guided Walkthrough (renderGuidedSteps()) and by a module lesson page's
   "on a real device" callout (renderLessonBlockHtml()'s "callout" kind).
   The pre-lab briefing page that used to own this whole region
   (lesson-intro -- topology/briefing/walkthrough bands, plus the "from
   memory" notice) is gone: a module's own page (see MODULE PAGE below) is
   the lesson now, and clicking a lab goes straight into the live
   workspace. Explanation lives there; a walkthrough step is just the
   instruction and its commands. */
.lesson-terminal {
  margin: 0 0 var(--sp-3);
  padding: var(--sp-4);
  background: var(--term-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--term-fg);
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  line-height: 1.75;
  overflow-x: auto;
  white-space: pre;
}
.lt-prompt { color: var(--brand-400); font-weight: 650; }
.lt-cmd { color: var(--term-fg); }

/* ---------------------------------------------------------------------
   WORKSPACE
   38/62 split: the terminal is the product's hero surface and gets the
   space to prove it. Results render in their own region beside the
   terminal rather than appended below the brief in a scroll column --
   which is what made scrollPanelIntoView() necessary before.
   --------------------------------------------------------------------- */
.workspace { display: flex; flex-direction: column; flex: 1; min-height: 0; }

.ws-header { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-4); }
.ws-crumb { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.ws-crumb-link { color: var(--text-3); font-size: var(--fs-sm); text-decoration: none; }
.ws-crumb-link:hover { color: var(--text); }
.ws-title { font-weight: 650; font-size: var(--fs-lg); }
.ws-chips { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* Session state, promoted out of the old six-fact grey run-on line. The
   idle reaper can destroy a lab mid-thought; that deserves a live
   countdown, not sixth position in a metadata string. */
.session-pill {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 600;
  font-variant-numeric: tabular-nums;
  padding: 5px 10px; border-radius: var(--r-pill);
  border: 1px solid color-mix(in srgb, var(--success) 40%, transparent);
  background: var(--success-soft);
  color: var(--success);
  white-space: nowrap;
}
.session-pill.warn { border-color: color-mix(in srgb, var(--warning) 50%, transparent); background: var(--warning-soft); color: var(--warning); }
.session-pill.warn .live-dot { background: var(--warning); box-shadow: 0 0 8px 1px color-mix(in srgb, var(--warning) 70%, transparent); }

/* grid-template-rows: minmax(0, 1fr) is load-bearing. Without it the
   single implicit row is `auto`, i.e. sized to its tallest item's
   content -- which is the rail, whose height is driven by the topology
   SVG (width:100%, height:auto, so its height scales with available
   width). Zooming out widens the layout in CSS px, the SVG gets taller,
   the row grows, the terminal column stretches to match, and
   fitTerminal() then sizes the terminal to that runaway height -- the
   terminal visibly growing as you zoom out, with the page overflowing
   past 100vh. minmax(0, 1fr) pins the row to the grid's own box so the
   rail scrolls internally (it is already overflow-y:auto) instead of
   pushing the row, the workspace, and the terminal taller. */
/* Equal columns: the panel is now the only home for the topology, and a
   diagram given the full height wants the width to match. */
/* Objectives (+ their own inline grading) get real room -- up to 46% of
   the workspace, comfortably under a 50% ceiling -- while the terminal/
   topology column keeps the rest. See applyStoryLabLayout()'s plain-mode
   branch for what actually lives in each column now. */
.ws-grid { display: grid; grid-template-columns: minmax(340px, 46%) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); gap: var(--sp-4); flex: 1; min-height: 0; }
/* Focus mode collapses to a SINGLE track. The obvious `0 1fr` + a hidden
   rail is wrong: with the rail removed from flow the terminal becomes the
   first grid item and inherits the 0-width track, so "focus the terminal"
   made the terminal disappear instead. One track, one item. */
.ws-grid.focus-terminal { grid-template-columns: 1fr; }
/* .ws-left, not .rail: the rail was renamed in the one-panel rework and
   this selector was left pointing at a class that no longer exists. The
   left column therefore stayed visible in focus mode, landed in an
   implicit (auto-height) second row, and collapsed the terminal column
   to nothing -- F appeared to blank the terminal. */
.ws-grid.focus-terminal > .ws-left { display: none; }

/* Layout K: the left column is one panel with tabs, so whatever you are
   looking at gets the column's full height rather than a share of it --
   which is what stopped a short ticket from leaving a hole under it. */
.ws-panel {
  display: flex; flex-direction: column; min-height: 0; min-width: 0;
  background: var(--surface-1); border: 1px solid var(--border-subtle);
  border-radius: var(--r-lg); overflow: hidden;
}
.ws-tabs {
  display: flex; gap: var(--sp-1); flex: 0 0 auto;
  padding: var(--sp-2) var(--sp-2) 0;
  border-bottom: 1px solid var(--border-subtle); background: var(--surface-2);
}
.ws-tab {
  border: none; background: transparent; cursor: pointer;
  font-family: inherit; font-size: var(--fs-sm); font-weight: 600; color: var(--text-3);
  padding: var(--sp-2) var(--sp-4); border-radius: var(--r-sm) var(--r-sm) 0 0;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  display: flex; align-items: center; gap: var(--sp-2);
}
.ws-tab:hover { color: var(--text); background: var(--surface-1); }
.ws-tab.active { color: var(--brand-400); border-bottom-color: var(--brand-400); background: var(--surface-1); }
/* A tab that has something new to say, before you have looked at it. */
.ws-tab.attention:not(.active) { color: var(--success); }
/* Grading before anything has been submitted: present from the start (so
   the workspace never grows a tab mid-session) but visibly inert. */
.ws-tab:disabled { cursor: default; opacity: 0.4; }
.ws-tab:disabled:hover { color: var(--text-3); background: transparent; }

.ws-pane { display: none; flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: var(--sp-4); }
.ws-pane.active { display: block; }

/* Scroll shadows. With the scrollbars hidden there was nothing to say a
   panel had more below the fold -- which is how a clipped config diff
   read as missing rather than scrollable. These are the classic
   background-attachment trick: the two `local` layers scroll with the
   content and cover the two fixed shadows, so a shadow only shows on an
   edge that actually has more content past it. No JS, no scroll
   listener. */
.ws-pane, .ws-left > .ticket {
  background-image:
    linear-gradient(var(--surface-1) 30%, transparent),
    linear-gradient(transparent, var(--surface-1) 70%),
    radial-gradient(farthest-side at 50% 0, rgb(0 0 0 / 0.22), transparent),
    radial-gradient(farthest-side at 50% 100%, rgb(0 0 0 / 0.22), transparent);
  background-position: 0 0, 0 100%, 0 0, 0 100%;
  background-size: 100% 26px, 100% 26px, 100% 9px, 100% 9px;
  background-repeat: no-repeat;
  background-attachment: local, local, scroll, scroll;
}

/* Left column is now just a frame around #ws-panel (objectives + their
   own inline grading, moved in by applyStoryLabLayout()) -- full height,
   one thing, no incident/description card and no separate diagram
   sharing the column any more (topology moved to the right, under the
   terminal -- see .term-col > .topo-card below). */
.ws-left {
  display: flex; flex-direction: column; min-height: 0; min-width: 0;
  background: var(--surface-1); border: 1px solid var(--border-subtle);
  border-radius: var(--r-lg); overflow: hidden;
}
.ws-left > .ws-panel {
  flex: 1 1 auto; min-height: 0; margin-top: 0;
  border: none; border-radius: 0; background: transparent;
}
.topo-head-static { padding: var(--sp-3) var(--sp-4) 0; flex: 0 0 auto; }

/* Right column: terminal on top, topology underneath. The terminal takes
   whatever height the topology card's own JS-computed height (see
   applyTopoCardHeight() -- capped at 60% of this column) doesn't use,
   rather than either box assuming a fixed share. */
.term-col > .terminal-pane { flex: 1 1 0; min-height: 120px; }
.term-col > .topo-card {
  flex: 0 0 260px; /* applyTopoCardHeight() overrides this with a real px value */
  min-height: 200px;
  margin-top: var(--sp-4);
}
.ws-panel .ws-pane { padding: var(--sp-3) var(--sp-4); }
/* Grading is the exception: a verdict plus a config diff needs room, so
   the box takes a share of the column and scrolls inside it. Story mode
   only -- see #story-col-steps #ws-panel's own override, and
   applyStoryLabLayout()'s plain-mode branch, which never adds this
   class at all (a plain session's tasks pane already gets the whole
   column, verdict/diff/solution included). */
.ws-panel.grading-open { flex: 0 0 50%; min-height: 0; }
/* gap: 0 + a margin on .results only when it actually has something in
   it. The column gap used to apply even before grading, when #results is
   present but all its panels are .hidden and it measures 0px tall -- so
   the terminal stopped a gap's width short of the pinned topology's
   bottom edge instead of level with it. */
.term-col { display: flex; flex-direction: column; gap: 0; min-width: 0; min-height: 0; }

/* --- ticket --- */
.ticket { background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: var(--r-md); overflow: hidden; }
.ticket-head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  cursor: pointer; width: 100%; border: none; background: transparent; color: inherit;
  font-family: inherit; text-align: left;
}
.ticket-head:hover { background: var(--surface-2); }
.ticket-num { font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 600; color: var(--text-3); }
.ticket-chevron { color: var(--text-3); transition: transform var(--dur) var(--ease); flex: none; }
.ticket[data-open="false"] .ticket-chevron { transform: rotate(-90deg); }
.ticket[data-open="false"] .ticket-body { display: none; }
.ticket-body { padding: 0 var(--sp-4) var(--sp-4); }
/* One row of four now that the ticket has the page's width -- as a 2x2
   grid it cost twice the height for no benefit. */
.ticket-fields { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-3) var(--sp-4); padding: var(--sp-3) 0; border-top: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle); margin-bottom: var(--sp-3); }
@media (max-width: 900px) { .ticket-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.ticket-field { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ticket-value { font-size: var(--fs-sm); }
.ticket-value.p1 { color: var(--priority-p1); font-weight: 650; }
.ticket-value.p2 { color: var(--priority-p2); font-weight: 650; }
.ticket-value.p3 { color: var(--priority-p3); font-weight: 650; }
.ticket-value.p4 { color: var(--priority-p4); font-weight: 650; }
.narrative { font-size: var(--fs-sm); color: var(--text-2); line-height: var(--lh-body); }
.narrative p + p { margin-top: var(--sp-3); }

/* --- tasks: the persistent spine, now carrying probe evidence --- */
.rail-section-head { display: flex; align-items: baseline; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.task-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.task {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-left: 3px solid var(--border-subtle);
  border-radius: var(--r-md);
  padding: var(--sp-3);
}
.task.pass { border-left-color: var(--success); }
.task.fail { border-left-color: var(--danger); }
.task-marker {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 700;
  font-variant-numeric: tabular-nums;
  background: var(--surface-1); border: 1px solid var(--border); color: var(--text-3);
}
.task.pass .task-marker { color: var(--success); border-color: var(--success); }
.task.fail .task-marker { color: var(--danger); border-color: var(--danger); }
.task-body { min-width: 0; flex: 1; }
.task-text { font-size: var(--fs-sm); line-height: var(--lh-snug); }
/* The measurement the probe actually took. This is where the product's
   "deterministic, never an LLM" claim becomes visible instead of stated. */
.task-evidence {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  color: var(--text-3);
  margin-top: var(--sp-2);
  line-height: var(--lh-snug);
  word-break: break-word;
}
.task.pass .task-evidence { color: var(--success); }
.task.fail .task-evidence { color: var(--danger); }

/* --- guided steps (story-mode Tasks pane): a walkthrough of the same
   objective, one CLI step at a time with progressive Reveal. Reuses
   .task's shape entirely -- only the third state (revealed-but-not-yet-
   passed) and the reveal affordance are new. --- */
.task.in-progress { border-left-color: var(--info); }
.task.in-progress .task-marker { color: var(--info); border-color: var(--info); }
.guided-step-node {
  display: block; font-family: var(--font-mono); font-size: var(--fs-micro);
  color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em;
  margin-top: 2px; margin-bottom: var(--sp-2);
}
.guided-step-reveal-btn { margin-top: var(--sp-2); }
.guided-step-reveal { margin-top: var(--sp-2); margin-bottom: 0; }
.guided-step-hide-btn { margin-top: var(--sp-2); }

/* ---------------------------------------------------------------------
   GUIDED WALKTHROUGH -- the Tasks pane's real content whenever
   step-by-step content exists for the session (a CCNA course lab's
   CCNA_WALKTHROUGHS entry), in place of the plain Objectives list. Never
   shown for a NOC story ticket -- see #ticket (index.html) above instead.
   See renderGuidedSteps() / guidedWalkthroughItems() in app.js. One step is
   one action: an instruction and its commands, nothing else -- commands
   stay hidden behind Reveal Commands, and revealed commands reuse
   .lesson-terminal rather than a second code-block style. Explanation
   deliberately does not live here; the module's own lesson page is where
   a topic is taught. No header above the list either -- the lab's title
   is already the breadcrumb's ws-title, and "done" is exactly the linked
   objective's real graded state (the marker below), not a separate
   manual toggle or a step-count readout to keep in sync with it. ------ */
.walk-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.walk-step {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  background: var(--surface-2); border: 1px solid var(--border-subtle); border-left: 3px solid var(--border-subtle);
  border-radius: var(--r-md); padding: var(--sp-4);
  transition: border-color var(--dur) var(--ease);
}
.walk-step.is-done { border-left-color: var(--success); }
.walk-step.is-revealed:not(.is-done) { border-left-color: var(--info); }
.walk-step-marker {
  flex: none; width: 26px; height: 26px; border-radius: 50%; margin-top: 1px;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 700;
  background: var(--surface-1); border: 1px solid var(--border); color: var(--text-3);
}
.walk-step.is-done .walk-step-marker { color: var(--success); border-color: var(--success); background: var(--success-soft); }
.walk-step.is-revealed:not(.is-done) .walk-step-marker { color: var(--info); border-color: var(--info); }
.walk-step-body { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: var(--sp-1); }
.walk-step-text-col { display: flex; flex-direction: column; }
.walk-step-device {
  align-self: flex-start; /* .walk-step-text-col is a flex column (align-items: stretch by
                              default) -- without this the chip stretched to the full row width */
  display: inline-block; font-family: var(--font-mono); font-size: var(--fs-micro); font-weight: 700; letter-spacing: 0.03em;
  color: var(--brand-400); background: var(--brand-soft); border: 1px solid color-mix(in srgb, var(--brand-500) 40%, transparent);
  border-radius: var(--r-sm); padding: 2px 7px; margin-bottom: var(--sp-1);
}
.walk-step-title { display: block; font-size: var(--fs-lg); font-weight: 650; line-height: var(--lh-snug); }

/* align-self: like .walk-step-device, this sits in a stretching flex
   column (.walk-step-body) -- without it the row (and the button inside)
   stretched to the full card width instead of hugging its own content.
   Scoped to the collapsed (button-only) state with :has() -- once
   revealed, the row holds a <pre> of command text that should still use
   the full card width, not shrink-wrap to its longest line. */
.walk-reveal-row { margin-top: var(--sp-3); }
.walk-reveal-row:has(> .walk-reveal-btn) { align-self: flex-start; }
.walk-reveal-btn {
  display: inline-flex; align-items: center; gap: var(--sp-2); width: auto; text-align: left;
  font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 650; letter-spacing: 0.02em;
  color: var(--brand-400); background: var(--surface-1); border: 1px dashed var(--border-strong);
  border-radius: var(--r-sm); padding: 8px 12px; cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.walk-reveal-btn:hover { background: var(--brand-soft); border-color: var(--brand-500); border-style: solid; }
.walk-reveal-btn .chev { flex: none; color: var(--text-3); }
.walk-reveal-btn .lock-ico { flex: none; opacity: 0.7; }
.walk-reveal-body .lesson-terminal { margin: 0 0 var(--sp-2); }
.walk-hide-btn { margin-top: 0; }

/* Lab 1 (guided stage) only -- see courseContent.js's own header comment
   on `why`, and renderGuidedSteps()'s autoReveal branch in app.js. Same
   language as the lesson page's own .callout (label + prose), but a
   filled success-tinted box rather than a left-rule-only one, so it reads
   as a distinct "here's the payoff" beat under the command rather than
   just more lesson prose. */
.walk-step-why {
  display: grid; grid-template-columns: auto 1fr; gap: var(--sp-2); align-items: baseline;
  margin-top: var(--sp-3); padding: var(--sp-3);
  background: var(--success-soft); border-left: 3px solid var(--success); border-radius: var(--r-sm);
}
.walk-step-why b { color: var(--success); font-size: var(--fs-micro); font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
.walk-step-why span { color: var(--text-2); font-size: var(--fs-sm); line-height: var(--lh-snug); }

/* Closing note + a second Submit at the end of the step list. */
.walk-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap;
  margin-top: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--border-subtle);
}
.walk-foot-note { margin: 0; color: var(--text-3); font-size: var(--fs-sm); max-width: 46ch; }

/* --- topology, now a rail card rather than a tab that hides the brief --- */
.topo-card { background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: var(--r-md); overflow: hidden; }
/* Fills its tab, so the diagram gets the panel's whole height. */
.topo-card[data-open="true"] { display: flex; flex-direction: column; min-height: 0; height: 100%; }
.topo-head { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); width: 100%; border: none; background: transparent; color: inherit; cursor: pointer; font-family: inherit; text-align: left; }
.topo-head:hover { background: var(--surface-2); }
.topo-card[data-open="false"] .topo-body { display: none; }
.topo-body { padding: 0 var(--sp-3) var(--sp-3); }
/* Dotted backdrop as a plain CSS background on the container, not an SVG
   pattern inside it -- it needs to cover the diagram's whole box even
   where the fitted viewBox letterboxes (a container whose aspect ratio
   doesn't match the diagram's) or a pan/zoom has moved the SVG content
   away from it. Matches the topo-content shadow filter's 24px rhythm. */
.topology-diagram {
  width: 100%; overflow: auto;
  background-image: radial-gradient(var(--border-subtle) 1px, transparent 1.5px);
  background-size: 24px 24px;
}
.topology-diagram svg { width: 100%; height: auto; display: block; }
/* Pan cursor: only the interactive workspace diagram binds drag-to-pan
   (bindTopologyPan() in app.js) -- read-only previews (dashboard,
   NOC reading pane, lesson pages) never get this class. */
.topo-pannable { cursor: grab; touch-action: none; }
.topo-pannable.is-panning { cursor: grabbing; }
/* Inside the pinned card the diagram fits its box instead of deriving
   its height from its width -- the viewBox + default preserveAspectRatio
   letterbox it. This is also what stops the diagram's height from
   tracking the layout width, which is what made the whole workspace grow
   when zooming out. */
.topo-card[data-open="true"] .topo-body { flex: 1; min-height: 0; display: flex; }
.topo-card[data-open="true"] .topology-diagram { flex: 1; min-height: 0; overflow: hidden; }
.topo-card[data-open="true"] .topology-diagram svg { height: 100%; }
/* These are SVG user units, so they get multiplied by the letterbox
   scale (~0.65) before they reach the screen -- 11 used to land at about
   7 real px, which is why the labels read as unlabelled smudges. */
.topo-link { stroke: var(--text-3); stroke-width: 2.5; opacity: 0.55; }
.topo-node { fill: var(--surface-2); stroke: var(--border); stroke-width: 1.5; cursor: pointer; }
.topo-icon { pointer-events: none; }
.topo-node:hover { stroke: var(--brand-400); }
.topo-node.active { stroke: var(--brand-400); stroke-width: 2.5; }
.topo-label { fill: var(--text); font-size: 16px; font-weight: 700; text-anchor: middle; pointer-events: none; }
/* Side placements (see nodeLabelPlacement() in app.js) -- the label sits
   to the left or right of the node instead of above/below, so it needs
   its anchor flipped to grow away from the box rather than through it. */
.topo-label-left { text-anchor: end; }
.topo-label-right { text-anchor: start; }
.topo-sublabel { fill: var(--text-3); font-size: 12px; text-anchor: middle; pointer-events: none; }
/* Real interface name at each end of a link (e.g. "Gi0/0"), EVE-NG-style --
   smaller than topo-sublabel since a busy topology carries one of these
   per link end (far more of them on screen at once than the one sublabel
   per node), so the backing pill (below) carries more of the legibility
   burden here -- it's what actually keeps this readable against link
   lines/grid dots/other labels crossing behind it, not just size alone. */
.topo-iface-label { fill: var(--text); font-size: 10px; text-anchor: middle; pointer-events: none; }
/* The real IP on that interface, stacked under its alias -- smaller and
   muted so the alias (what a learner actually types) still reads first,
   the same visual hierarchy --text-2/--text-3 already give secondary
   facts everywhere else in the app. */
.topo-iface-ip { fill: var(--text-3); font-size: 8px; font-family: var(--font-mono); text-anchor: middle; pointer-events: none; }
/* pointer-events: all (not none, unlike every other diagram overlay) --
   this is the actual hover target; the label text on top of it stays
   none since the pill beneath already covers its full footprint. */
.topo-iface-pill { fill: var(--surface-2); stroke: var(--border); stroke-width: 1; pointer-events: all; }
/* Two labels crowded on a short link (an accepted trade-off -- see
   ifaceLabelClearance's own docstring) become fully legible on hover:
   a stronger, opaque pill and heavier text, plus the matching JS that
   moves this label's whole group to the end of its parent so it
   actually paints on top of whatever it was crowded against (SVG has
   no z-index -- paint order is DOM order). */
.topo-iface-labelgroup:hover .topo-iface-pill {
  fill: var(--surface-3);
  stroke: var(--brand-400);
  stroke-width: 1.5;
}
.topo-iface-labelgroup:hover .topo-iface-label { font-weight: 600; }

/* --- terminal: the hero --- */
.terminal-pane {
  background: var(--term-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
.device-tabs { display: flex; gap: 2px; background: var(--surface-2); padding: var(--sp-1); overflow-x: auto; align-items: center; flex: none; }
.device-tab {
  padding: 6px 12px;
  font-size: var(--fs-label);
  font-family: var(--font-mono);
  font-weight: 600;
  border-radius: var(--r-sm) var(--r-sm) 0 0;
  cursor: pointer;
  color: var(--text-3);
  border: 1px solid transparent;
  background: transparent;
  white-space: nowrap;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.device-tab:hover { color: var(--text); }
.device-tab:disabled { cursor: not-allowed; opacity: 0.35; }
.device-tab:disabled:hover { color: var(--text-3); }
.device-tab.active { background: var(--term-bg); color: var(--brand-400); border-color: var(--border); border-bottom-color: var(--term-bg); }
/* Split: the other pane's device is open too, but the tabs act on the
   focused one -- so it reads as "on screen", not "selected". */
.device-tab.in-pane { background: var(--term-bg); color: var(--text-2); border-color: var(--border); border-bottom-color: var(--term-bg); }
/* The container is now just the split frame -- padding and scrolling
   belong to each pane, so a split gives two independently-sized boxes. */
.terminal-container { display: flex; flex: 1; min-height: 0; overflow: hidden; }

/* overflow: auto, deliberately, even though xterm scrolls its own
   viewport. hidden was tidier in theory, but it turned any sizing error
   into content that is gone rather than content you can still reach --
   which is exactly how the prompt disappeared off the bottom while
   typing. The scrollbars are hidden globally, so this costs nothing
   visually and means the worst case is a scroll instead of a loss. */
.term-slot { padding: var(--sp-2); overflow: auto; flex: 1 1 0; min-width: 0; min-height: 0; }

/* Unsplit is the default: the second pane and the handle keep their
   terminals alive in the DOM but take no space. */
.terminal-container:not(.split) > .term-slot[data-pane="1"],
.terminal-container:not(.split) > .term-split-handle { display: none; }

/* --split-ratio is written by the drag handle; the left pane is sized
   from it and the right one takes the remainder, so rounding never
   leaves a sliver of background between them. */
.terminal-container.split > .term-slot[data-pane="0"] { flex: 0 0 calc(var(--split-ratio, 50%) - 3px); }
.terminal-container.split > .term-slot[data-pane="1"] { flex: 1 1 0; }

/* Which pane the device tabs act on has to be visible, or clicking R2
   appears to do nothing when the wrong pane is focused. Inset, so it
   reads as "this box is live" without shifting the terminal by a pixel. */
.terminal-container.split > .term-slot { border-radius: var(--r-sm); box-shadow: inset 0 0 0 1px transparent; }
.terminal-container.split > .term-slot.focused { box-shadow: inset 0 0 0 1px var(--brand-500); }

.term-split-handle {
  flex: 0 0 6px; cursor: col-resize; background: var(--border);
  border-radius: 3px; align-self: stretch;
  transition: background var(--dur) var(--ease);
}
.term-split-handle:hover, .term-split-handle:focus-visible, .term-split-handle.dragging { background: var(--brand-500); }
/* Stacked, the row heights are auto, so terminal-pane's flex:1 has
   nothing to grow into and collapses to the device tabs (~55px). Give it
   a real height, and stop the diagram -- which is free to use its full
   aspect ratio here -- from running to ~860px. This block has to sit
   after the base rules above to win at equal specificity. */
@media (max-width: 900px) {
  .terminal-pane { min-height: 55vh; }
  .topo-card[data-open="true"] .topology-diagram svg { max-height: 46vh; }
}
.term-instance { display: none; height: 100%; }
.term-instance.active { display: block; }
/* xterm positions its offscreen input textarea right on top of the
   emulated cursor cell (so IME candidate windows and mobile keyboards
   land in the right spot), and briefly gives it a real width/height
   while doing so. Its own stylesheet already zeroes opacity, but that
   doesn't suppress the native OS caret in every browser -- Windows in
   particular still blinks a real text-input bar cursor there for one
   cycle right as a line is submitted, on top of xterm's own drawn
   cursor. caret-color has no such gap. */
.xterm .xterm-helper-textarea { caret-color: transparent; }

/* --- results surface --- */
/* Capped as a share of the terminal column rather than of the viewport
   (46vh before): the column now has a definite, viewport-bounded height,
   so a percentage keeps the split honest -- results take at most half,
   the terminal always keeps the rest, and the column's bottom edge stays
   level with the pinned topology's. */
/* Fills the grading pane rather than capping itself. The old
   max-height:52% dates from when this sat under the terminal and took a
   share of that column; inside the pane it resolved against the pane
   instead, so the config diff was sliced off mid-line with empty space
   underneath -- and with scrollbars hidden it just looked missing. The
   pane owns the scrolling now. */
.results { display: none; flex-direction: column; gap: var(--sp-3); flex: 1 1 auto; min-height: 0; }
.results:has(> *:not(.hidden)) { display: flex; }
/* Without this the blocks inside shrink to fit the cap above instead of
   overflowing it, and since .result-block is overflow:hidden the config
   diff just gets clipped -- .results then reports nothing to scroll, so
   the hidden lines are unreachable rather than merely off-screen.
   Pinning each block to its natural height is what actually hands the
   overflow to .results, which does scroll. */
.results > * { flex: 0 0 auto; }

.verdict {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface-1);
}
.verdict.pass { border-color: color-mix(in srgb, var(--success) 45%, var(--border)); }
.verdict.fail { border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); }
.verdict-head { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-4); flex-wrap: wrap; }
.verdict.pass .verdict-head { background: var(--success-soft); }
.verdict.fail .verdict-head { background: var(--danger-soft); }
.verdict-ring { flex: none; width: 52px; height: 52px; position: relative; }
.verdict-ring .ring-label { font-size: var(--fs-sm); }
.verdict-copy { min-width: 0; flex: 1 1 220px; }
.verdict-headline { font-size: var(--fs-lg); font-weight: 700; line-height: var(--lh-tight); }
.verdict.pass .verdict-headline { color: var(--success); }
.verdict.fail .verdict-headline { color: var(--danger); }
/* How the score was arrived at -- live probe, elapsed time, no answer
   key. The strongest technical claim in the product, finally on screen. */
.verdict-method { font-family: var(--font-mono); font-size: var(--fs-label); color: var(--text-3); margin-top: var(--sp-1); }
.verdict-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; }

.result-block { background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: var(--r-md); overflow: hidden; }
.result-block-head { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); width: 100%; border: none; background: transparent; color: inherit; cursor: pointer; font-family: inherit; text-align: left; }
.result-block-head:hover { background: var(--surface-2); }
.result-block[data-open="false"] .result-block-body { display: none; }
.result-block-body { padding: 0 var(--sp-4) var(--sp-4); }

.code-block {
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-sm);
  padding: var(--sp-3);
  margin: 0 0 var(--sp-2);
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  line-height: var(--lh-snug);
  white-space: pre-wrap;
  overflow-x: auto;
}
.diff-add { color: var(--success); }
.diff-remove { color: var(--danger); }
.diff-node { font-family: var(--font-mono); font-weight: 650; color: var(--brand-400); font-size: var(--fs-label); margin-bottom: var(--sp-1); }
.diff-empty { font-size: var(--fs-sm); color: var(--text-3); padding: var(--sp-2) 0; }

.prelim { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); margin-bottom: var(--sp-3); font-size: var(--fs-sm); color: var(--text-2); }

.grading-status {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  font-size: var(--fs-sm);
  color: var(--text-2);
}
.grading-status.error { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent); background: var(--danger-soft); }

/* ---------------------------------------------------------------------
   SPINNERS / OVERLAYS / MODALS
   --------------------------------------------------------------------- */
.spinner, .spinner-sm {
  border-radius: 50%;
  border: 2px solid var(--border-subtle);
  border-top-color: var(--brand-400);
  animation: spin 0.8s linear infinite;
  flex: none;
}
.spinner { width: 34px; height: 34px; border-width: 3px; }
.spinner-sm { width: 14px; height: 14px; }
@keyframes spin { to { transform: rotate(360deg); } }

.overlay {
  position: fixed; inset: 0; z-index: 100;
  background: rgb(0 0 0 / 0.55);
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp-5);
  backdrop-filter: blur(2px);
}
.modal {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--sp-5);
  max-width: 460px;
  width: 100%;
  box-shadow: var(--shadow-lg);
}
.modal h3 { margin-bottom: var(--sp-3); }
.modal p { font-size: var(--fs-sm); color: var(--text-2); margin-bottom: var(--sp-5); line-height: var(--lh-body); }
.modal-actions { display: flex; justify-content: flex-end; gap: var(--sp-3); flex-wrap: wrap; }
.loading-box {
  background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--r-lg);
  padding: var(--sp-6); box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-4);
  max-width: 400px; text-align: center;
}
.loading-box p { font-size: var(--fs-sm); color: var(--text-2); }

/* Overflow menu -- where destructive actions live now. */
.menu-wrap { position: relative; }
.menu {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 50;
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: var(--r-md); box-shadow: var(--shadow-md);
  min-width: 210px; padding: var(--sp-1);
  display: flex; flex-direction: column;
}
.menu-item {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 9px 12px; border: none; background: transparent;
  color: var(--text); font-family: inherit; font-size: var(--fs-sm);
  text-align: left; cursor: pointer; border-radius: var(--r-sm); width: 100%;
}
.menu-item:hover { background: var(--surface-2); }
.menu-item.danger { color: var(--danger); }
.menu-item.danger:hover { background: var(--danger-soft); }
.menu-sep { height: 1px; background: var(--border-subtle); margin: var(--sp-1) 0; }
.menu-hint { margin-left: auto; font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--text-3); }

/* Toasts -- background events that shouldn't steal focus. */
.toasts { position: fixed; right: var(--sp-5); bottom: var(--sp-5); z-index: 90; display: flex; flex-direction: column; gap: var(--sp-2); max-width: 380px; }
.toast {
  background: var(--surface-1); border: 1px solid var(--border);
  border-left: 3px solid var(--brand-500);
  border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4);
  box-shadow: var(--shadow-md); font-size: var(--fs-sm);
  animation: toast-in var(--dur) var(--ease);
}
.toast.warn { border-left-color: var(--warning); }
.toast.danger { border-left-color: var(--danger); }
.toast.success { border-left-color: var(--success); }
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } }

/* Keyboard shortcut sheet */
.shortcuts { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-2) var(--sp-4); font-size: var(--fs-sm); }
.shortcuts dt { color: var(--text-2); }
.shortcuts dd { margin: 0; }
kbd {
  font-family: var(--font-mono); font-size: var(--fs-micro);
  background: var(--surface-2); border: 1px solid var(--border);
  border-bottom-width: 2px; border-radius: var(--r-sm);
  padding: 2px 6px; color: var(--text);
}

/* ---------------------------------------------------------------------
   FOOTER
   --------------------------------------------------------------------- */
.site-footer { border-top: 1px solid var(--border-subtle); background: var(--surface-1); flex: none; }
.footer-main { max-width: var(--page-max); margin: 0 auto; padding: var(--sp-6) var(--sp-5) var(--sp-5); display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--sp-6); }
.footer-brand { max-width: 380px; }
.footer-brand p { color: var(--text-2); font-size: var(--fs-sm); margin-top: var(--sp-2); }
.footer-powered-by { display: flex; align-items: center; gap: var(--sp-3); color: var(--text-3); font-size: var(--fs-micro); margin-top: var(--sp-3); }
.footer-powered-by b { color: var(--text-2); font-weight: 600; }
.footer-powered-by-line { flex: 1; height: 1px; background: var(--border-subtle); }
.footer-links { display: flex; gap: var(--sp-7); flex-wrap: wrap; }
.footer-col { display: flex; flex-direction: column; gap: var(--sp-2); }
.footer-col a { color: var(--text-2); text-decoration: none; font-size: var(--fs-sm); }
.footer-col a:hover { color: var(--brand-400); }
.info-page { max-width: var(--page-max); margin: 0 auto; padding: var(--sp-7) var(--sp-5); text-align: center; }
.info-page .page-head { justify-content: center; }
.info-page .page-head > div { width: 100%; }
.info-page .page-sub { margin-left: auto; margin-right: auto; }
.info-page-content { max-width: 760px; margin: 0 auto; padding: var(--sp-2) 0; background: transparent; border: 0; box-shadow: none; }
.info-page-content h3 { margin: var(--sp-5) 0 var(--sp-2); }
.info-page-content h3:first-child { margin-top: 0; }
.info-page-content p, .info-page-content li { color: var(--text-2); line-height: 1.7; }
.info-page-content ul { max-width: 680px; margin: 0 auto var(--sp-4); padding-left: 1.25rem; text-align: left; }
.info-page-content li { margin-bottom: var(--sp-2); }
.info-page-content a { color: var(--brand-400); }

/* Roadmap: a status-banded timeline -- Live / In Progress / Next / Future,
   each its own horizontal row of connected nodes -- rather than the prose
   column the rest of .info-page-content is built for. Widened and
   left-aligned like the ul override just above, for the same reason (a
   list of discrete items reads worse centered than plain paragraphs do). */
.roadmap-content { max-width: 1040px; text-align: left; }

.rm-legend {
  display: flex; flex-wrap: wrap; gap: var(--sp-5);
  background: var(--surface-2); border: 1px solid var(--border-subtle); border-radius: var(--r-md);
  padding: var(--sp-4); margin-bottom: var(--sp-6);
}
.rm-legend-item { display: flex; align-items: center; gap: var(--sp-2); }
.rm-legend-item b { display: block; font-size: var(--fs-sm); color: var(--text); }
.rm-legend-item small { display: block; color: var(--text-3); font-size: var(--fs-micro); }
.rm-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; }
.rm-dot.live { background: var(--success); }
.rm-dot.progress { background: var(--info); }
.rm-dot.next { background: var(--warning); }
.rm-dot.future { background: var(--text-3); }

.rm-row { margin-bottom: var(--sp-3); }
/* nowrap, not wrap: the connecting line below is one straight element
   the full width of the row, which only stays accurate as long as every
   item is genuinely on one line. Letting items wrap (a plain flex-wrap)
   would strand a wrapped item under a line that ends before it -- exactly
   what happened during review with 4 Live items at a middling viewport
   width. Items shrink instead, with a horizontal scroll fallback on the
   very narrowest viewports (a scrollable timeline strip, rather than a
   broken line) -- overflow-x auto, not a media-query breakpoint, since
   there's no single width where "4 fit" flips to "4 don't" that's true
   for every row (Live has 4 items, Progress has 2). */
.rm-items { list-style: none; margin: 0; padding: 0 0 var(--sp-2); display: flex; flex-wrap: nowrap; gap: var(--sp-5) var(--sp-4); position: relative; overflow-x: auto; }
.rm-item {
  flex: 1 1 0; min-width: 150px; max-width: 260px; position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--sp-2);
}
.rm-pill {
  font-family: var(--font-mono); font-size: var(--fs-micro); font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  padding: 3px 9px; border-radius: 999px; border: 1px solid transparent;
}
.rm-marker {
  width: 40px; height: 40px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-1); border: 2px solid var(--border); color: var(--text-2);
}
.rm-item h3 { margin: 0; font-size: var(--fs-lg); }
.rm-item p { margin: 0; color: var(--text-2); font-size: var(--fs-sm); line-height: 1.6; }
/* The connecting line: one element behind the whole row (not a per-item
   pseudo-element) so it stays a single straight line regardless of how
   many items are in a row -- markers sit on top of it, opaque, so it
   reads as passing "through" each one. Safe to always draw full-width
   now that .rm-items is nowrap (see its own comment) -- there's no wrap
   case left for it to misalign against. */
.rm-items::before { content: ""; position: absolute; left: 4%; right: 4%; top: 47px; height: 2px; z-index: 0; background: var(--border-subtle); }
.rm-row-live .rm-items::before { background: var(--success); }
.rm-row-progress .rm-items::before { background: var(--info); }
.rm-row-next .rm-items::before { background: var(--warning); }
.rm-row-future .rm-items::before { background: var(--text-3); }

.rm-row-live .rm-pill { color: var(--success); background: var(--success-soft); border-color: color-mix(in srgb, var(--success) 40%, transparent); }
.rm-row-progress .rm-pill { color: var(--info); background: var(--info-soft); border-color: color-mix(in srgb, var(--info) 40%, transparent); }
.rm-row-next .rm-pill { color: var(--warning); background: var(--warning-soft); border-color: color-mix(in srgb, var(--warning) 40%, transparent); }
.rm-row-future .rm-pill { color: var(--text-3); background: var(--surface-2); border-color: var(--border-subtle); }
/* Marker fill is always the OPAQUE surface color, never a status's own
   translucent "-soft" tint -- .rm-items::before's connecting line sits
   right behind every marker (same z-index stacking .lab-marker already
   relies on elsewhere), and a translucent fill let that line bleed
   straight through the icon instead of being hidden behind it, on every
   row except Future (the only one that was already using a solid
   surface color here). Border/icon color still carries the status tint. */
.rm-row-live .rm-marker { color: var(--success); border-color: var(--success); background: var(--surface-2); }
.rm-row-progress .rm-marker { color: var(--info); border-color: var(--info); background: var(--surface-2); }
.rm-row-next .rm-marker { color: var(--warning); border-color: var(--warning); background: var(--surface-2); }
.rm-row-future .rm-marker { color: var(--text-3); border-color: var(--border); background: var(--surface-2); }

/* A short dashed drop between status bands -- a simplified stand-in for
   the reference's full serpentine connector: this app has no charting
   library and no committed dates to lay out along a literal timeline
   axis, so a straight per-row line plus a plain connector between bands
   carries the same "one continuous path across time" idea without
   inventing precision (exact curve geometry, specific quarters) this
   roadmap deliberately doesn't have. */
.rm-connector { width: 2px; height: var(--sp-5); margin: 0 auto var(--sp-3); background: repeating-linear-gradient(to bottom, var(--border-strong) 0 4px, transparent 4px 8px); }
.rm-connector-live-progress { background-image: repeating-linear-gradient(to bottom, color-mix(in srgb, var(--success) 50%, var(--info)) 0 4px, transparent 4px 8px); }
.rm-connector-progress-next { background-image: repeating-linear-gradient(to bottom, color-mix(in srgb, var(--info) 50%, var(--warning)) 0 4px, transparent 4px 8px); }
.rm-connector-next-future { background-image: repeating-linear-gradient(to bottom, color-mix(in srgb, var(--warning) 50%, var(--text-3)) 0 4px, transparent 4px 8px); }

.rm-cta {
  display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
  margin-top: var(--sp-6); padding: var(--sp-4) var(--sp-5); text-align: left;
  background: var(--brand-soft); border: 1px solid color-mix(in srgb, var(--brand-500) 35%, transparent); border-radius: var(--r-md);
  color: var(--brand-400);
}
.rm-cta > div { flex: 1 1 260px; }
.rm-cta strong { display: block; color: var(--text); font-size: var(--fs-body); }
.rm-cta p { margin: 2px 0 0; color: var(--text-2); font-size: var(--fs-sm); }
/* .info-page-content a's own link-color rule (.info-page-content a {
   color: var(--brand-400) }) outranks plain .btn-primary on specificity
   (element+class beats class alone), which left this button's text
   using the link teal instead of .btn-primary's own high-contrast
   on-brand color -- same shade as the button's own background, nearly
   unreadable. Re-asserted here at higher specificity than either. */
.rm-cta .btn { flex: none; }
.rm-cta .btn-primary { color: var(--brand-on); }

/* FAQ: <details>/<summary> accordion rather than a flat wall of h3/p --
   native disclosure, no JS needed. Left-aligned like .roadmap-content,
   for the same reason a list reads worse centered than short paragraphs
   do. The category h3s keep .info-page-content's own centered heading
   style; only the accordion rows themselves are overridden. */
.faq-content { max-width: 760px; text-align: left; }
.faq-content h3 { text-align: center; }
.faq-group { display: flex; flex-direction: column; gap: var(--sp-2); margin-bottom: var(--sp-5); }
.faq-item {
  background: var(--surface-2); border: 1px solid var(--border-subtle); border-radius: var(--r-md);
  overflow: hidden; transition: border-color var(--dur) var(--ease);
}
.faq-item[open] { border-color: var(--brand-500); }
.faq-item summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4); cursor: pointer; list-style: none;
  font-weight: 650; font-size: var(--fs-sm); color: var(--text);
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: ""; flex: none; width: 9px; height: 9px; margin-top: -3px;
  border-right: 1.6px solid var(--text-3); border-bottom: 1.6px solid var(--text-3);
  transform: rotate(45deg); transition: transform var(--dur) var(--ease);
}
.faq-item[open] summary::after { transform: rotate(-135deg); margin-top: 3px; }
.faq-item summary:hover { background: var(--surface-3); }
.faq-item > div { padding: 0 var(--sp-4) var(--sp-4); }
.faq-item > div p { margin: 0; color: var(--text-2); line-height: 1.7; }
.faq-item code { font-family: var(--font-mono); font-size: 0.9em; background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: 4px; padding: 1px 5px; }

/* ---------------------------------------------------------------------
   NOC (StoryMode) -- a real three-pane mail client
   Rebuilt from the "The NOC Loop" design artifact: icon rail, folders,
   message list, reading pane. Viewport-locked like the workspace/
   dashboard (body.view-noc, set in showView()) -- a mail client doesn't
   grow a page footer under its reading pane, the list and reading pane
   scroll internally instead.
   --------------------------------------------------------------------- */
.noc { flex: 1; min-height: 0; display: flex; flex-direction: column; }

.mail-app {
  flex: 1; min-height: 0; display: grid;
  grid-template-columns: 56px 220px minmax(280px, 340px) 1fr;
  background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: var(--r-lg);
  overflow: hidden;
}

/* Vendor + tier picker shown before a new player's first ticket -- see
   renderNocOnboarding() in app.js. Sized and bordered like .mail-app
   itself (same surface, radius, border) so swapping between the two
   reads as one screen, not a modal dropped on top of it. */
.noc-onboarding {
  flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center;
  background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: var(--r-lg);
  padding: var(--sp-6);
}
.noc-onboard { width: 100%; max-width: 560px; }
.noc-onboard-head { text-align: center; margin-bottom: var(--sp-6); position: relative; }
.noc-onboard-head h2 { margin: var(--sp-3) 0 var(--sp-2); }
.noc-onboard-head p { color: var(--text-2); font-size: var(--fs-sm); max-width: 44ch; margin: 0 auto; }
.noc-onboard-back {
  position: absolute; top: 0; left: 0; display: inline-flex; align-items: center; gap: var(--sp-1);
  border: none; background: transparent; color: var(--text-3); font-family: inherit; font-size: var(--fs-sm);
  cursor: pointer; padding: var(--sp-1) var(--sp-2);
}
.noc-onboard-back:hover { color: var(--text); }
.noc-onboard-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-3); }
.noc-onboard-grid-tiers { grid-template-columns: 1fr; }
.noc-onboard-tile {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-1);
  padding: var(--sp-4); border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface-2); color: var(--text); font-family: inherit; text-align: left; cursor: pointer;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.noc-onboard-tile:hover:not(:disabled) { border-color: var(--brand-500); background: var(--brand-soft); }
.noc-onboard-tile:focus-visible { outline: 2px solid var(--brand-400); outline-offset: 2px; }
.noc-onboard-tile-label { font-weight: 650; font-size: var(--fs-lg); }
.noc-onboard-tile-sub { color: var(--text-3); font-size: var(--fs-label); font-family: var(--font-mono); }
.noc-onboard-tile.locked {
  cursor: not-allowed; opacity: 0.5; background: var(--surface-2); border-style: dashed;
}
.noc-onboard-tile.locked .badge { margin-top: var(--sp-1); }
@media (max-width: 560px) {
  .noc-onboard-grid { grid-template-columns: 1fr; }
}

.mail-rail {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) 0; background: var(--surface-2); border-right: 1px solid var(--border-subtle);
}
.mail-rail-mark {
  width: 30px; height: 30px; border-radius: var(--r-sm); background: var(--brand-500); color: var(--brand-on);
  display: flex; align-items: center; justify-content: center; margin-bottom: var(--sp-3); flex: none;
}
.mail-rail-btn {
  width: 34px; height: 34px; border-radius: var(--r-sm); border: none; background: transparent; color: var(--text-3);
  display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.mail-rail-btn:hover { background: var(--surface-3); color: var(--text-2); }
.mail-rail-btn.active { background: var(--brand-soft); color: var(--brand-400); }
.mail-rail-spacer { flex: 1; }
.mail-rail-avatar {
  width: 28px; height: 28px; border-radius: 50%; background: var(--surface-3); border: 1px solid var(--border);
  color: var(--text-2); font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex: none;
}

.mail-folders {
  border-right: 1px solid var(--border-subtle); padding: var(--sp-4); display: flex; flex-direction: column;
  gap: var(--sp-4); overflow-y: auto; background: var(--surface-1);
}
.mail-account-name { font-weight: 650; font-size: 13.5px; }
.mail-account-sub { font-size: 11.5px; color: var(--text-3); margin-top: 2px; }
.mail-search {
  display: flex; align-items: center; gap: var(--sp-2); color: var(--text-3);
  background: var(--surface-2); border: 1px solid var(--border-subtle); border-radius: var(--r-sm); padding: 7px 10px;
}
.mail-search input {
  flex: 1; min-width: 0; border: none; background: transparent; color: var(--text); font-family: inherit;
  font-size: 12.5px; outline: none;
}
.mail-search input::placeholder { color: var(--text-3); }
.mail-folder-list { display: flex; flex-direction: column; gap: 2px; }
.mail-folder {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%; text-align: left;
  border: none; background: transparent; color: var(--text-2); font-family: inherit; font-size: 13px;
  padding: 7px var(--sp-2); border-radius: var(--r-sm); cursor: pointer; transition: background var(--dur) var(--ease);
}
.mail-folder:hover { background: var(--surface-2); }
.mail-folder svg { flex: none; opacity: 0.75; }
.mail-folder span { flex: 1; }
.mail-folder b { font-family: var(--font-mono); font-size: 11px; font-weight: 600; color: var(--text-3); }
.mail-folder.active { background: var(--brand-soft); color: var(--brand-400); }
.mail-folder.active b { color: var(--brand-400); }
.mail-shift { margin-top: auto; padding-top: var(--sp-4); border-top: 1px solid var(--border-subtle); }
.mail-shift-bar { height: 5px; border-radius: var(--r-pill); background: var(--surface-3); margin: var(--sp-2) 0; overflow: hidden; }
.mail-shift-bar i { display: block; height: 100%; background: var(--brand-500); border-radius: var(--r-pill); transition: width var(--dur) var(--ease); }
.mail-shift-text { font-size: 11.5px; color: var(--text-3); }
.mail-shift-reset {
  display: block; margin-top: var(--sp-3); background: none; border: none; padding: 0;
  font-family: inherit; font-size: 11.5px; color: var(--text-3); cursor: pointer; text-align: left;
}
.mail-shift-reset:hover { color: var(--danger); }
.noc-toast {
  display: flex; align-items: center; gap: var(--sp-2); font-size: 12px; color: var(--brand-400);
  background: var(--brand-soft); border: 1px solid color-mix(in srgb, var(--brand-500) 35%, transparent);
  border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-3);
}

.mail-list { border-right: 1px solid var(--border-subtle); overflow-y: auto; background: var(--surface-0); }
.mail-list-head {
  display: flex; justify-content: space-between; align-items: baseline; font-weight: 650; font-size: 13.5px;
  padding: var(--sp-4) var(--sp-4) var(--sp-2); position: sticky; top: 0; background: var(--surface-0);
}
.mail-list-head span { font-family: var(--font-mono); font-size: 11px; font-weight: 600; color: var(--brand-400); }

.mail-row { display: flex; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border-subtle); cursor: pointer; position: relative; transition: background var(--dur) var(--ease); }
.mail-row:hover { background: var(--surface-2); }
.mail-row:focus-visible { outline: 2px solid var(--brand-400); outline-offset: -2px; }
/* Tickets themselves (active/in-progress/unread) no longer carry a
   colored background block or left-edge stripe -- read state is still
   legible from the bold subject + dot (see .mail-row.unread below), and
   status is still legible from its badge (see nocRowStateAndBadge() in
   app.js), so the row itself stays visually uniform rather than
   highlighted. A reply -- a genuinely new event landing in the inbox --
   keeps its own highlight (.mail-row.new below); that's a customer
   reply, not a ticket, and this request was about tickets specifically. */
.mail-row.active .mail-row-from { color: var(--brand-400); }
/* Selection is a soft, rounded, inset panel behind the row rather than a
   hard ring drawn around it -- the ring read as a box bolted on top of
   the list, and fought every other edge in the column (the row divider,
   the panel border) for the same 2px.
   Drawn as a pseudo-element rather than on .mail-row itself so it can be
   inset from the row's own box: insetting the row would need a margin,
   and a margin that only exists while selected shifts the row's content
   sideways every time you click it. The children are lifted above it
   (z-index below) because a positioned pseudo-element would otherwise
   paint over the text. */
.mail-row.selected::before {
  content: "";
  position: absolute;
  inset: 3px var(--sp-2);
  border-radius: var(--r-md);
  background: var(--brand-soft);
  z-index: 0;
  pointer-events: none;
}
.mail-row.selected > * { position: relative; z-index: 1; }
/* The row's own hover fill would sit UNDER the selection panel and never
   be seen, so a selected row deepens the panel itself instead. */
.mail-row.selected:hover { background: transparent; }
.mail-row.selected:hover::before { background: color-mix(in srgb, var(--brand-500) 24%, transparent); }
.mail-row.locked { opacity: 0.55; cursor: default; }
.mail-row.locked:hover { background: transparent; }
.mail-row.locked .mail-row-subject { color: var(--text-3); }
.mail-row.read:not(.active) .mail-row-from,
.mail-row.read:not(.active) .mail-row-subject { color: var(--text-2); font-weight: 500; }
.mail-row.resolved { opacity: 0.7; }
/* A customer reply carries no highlight of its own either -- same
   reasoning as the tickets above it. That it is unread is still legible
   from the bold subject and the dot, and that it is a reply from its NEW
   badge; .new is kept as a hook for that badge and for the inbox's own
   ordering, it just no longer paints the row. */

.mail-avatar {
  flex: none; width: 32px; height: 32px; border-radius: 50%; background: var(--surface-3); border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center; font-size: 11.5px; font-weight: 700; color: var(--text-2);
}
.mail-row.active .mail-avatar { background: var(--brand-500); border-color: var(--brand-500); color: var(--brand-on); }
.mail-avatar-lock { color: var(--text-3); }
.mail-avatar-system { background: var(--warning-soft); border-color: color-mix(in srgb, var(--warning) 40%, transparent); color: var(--warning); }
.mail-avatar-done { background: var(--success-soft); border-color: color-mix(in srgb, var(--success) 40%, transparent); color: var(--success); }

.mail-row-body { min-width: 0; flex: 1; }
.mail-row-top { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
.mail-row-from { font-size: 13px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mail-row-time { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--text-3); white-space: nowrap; }
.mail-row-subject { font-size: 12.5px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mail-row.unread .mail-row-subject { color: var(--text); font-weight: 600; }
/* The dot, not font-weight alone, is what actually reads as "unread" at
   a glance -- 600 vs 500 and one shade of text color apart was nearly
   invisible on a short subject line. Sits before the sender name, the
   same placement most real mail clients use. */
.mail-row.unread .mail-row-top::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--brand-400);
  flex: none;
}
.mail-row-preview { font-size: 11.5px; color: var(--text-3); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.mail-empty { padding: var(--sp-6) var(--sp-4); text-align: center; color: var(--text-3); font-size: 13px; }

.mail-reading { display: flex; flex-direction: column; overflow-y: auto; background: var(--surface-1); }
.mail-reading-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-3); color: var(--text-3); }
.mail-reading-empty svg { opacity: 0.5; }
.mail-reading-empty p { font-size: 13px; }

.reading-header { padding: var(--sp-5) var(--sp-5) var(--sp-4); border-bottom: 1px solid var(--border-subtle); }
.reading-header-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; }
.reading-subject { font-family: var(--font-body); font-weight: 700; font-size: 21px; margin: 0; }
.reading-ticket-id { font-family: var(--font-mono); font-size: 12px; color: var(--text-3); white-space: nowrap; }
.reading-meta { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-4); flex-wrap: wrap; }
.reading-avatar {
  flex: none; width: 38px; height: 38px; border-radius: 50%; background: var(--brand-500); color: var(--brand-on);
  display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700;
}
/* The NOC's own orientation message, not a customer -- amber like
   .mail-avatar-system, so it reads as "the system" the moment it opens. */
.reading-avatar-system { background: var(--warning-soft); color: var(--warning); }
.reading-meta-text { flex: 1; min-width: 200px; font-size: 13px; }
.reading-meta-muted { color: var(--text-3); font-size: 12px; }

.reading-toolbar { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-5); border-bottom: 1px solid var(--border-subtle); flex-wrap: wrap; }
.tb-btn {
  display: flex; align-items: center; gap: 6px; font-family: inherit; font-size: 12.5px; font-weight: 600; color: var(--text-2);
  background: transparent; border: 1px solid var(--border-subtle); border-radius: var(--r-sm); padding: 6px 12px; cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.tb-btn:hover { background: var(--surface-2); color: var(--text); }
.tb-spacer { flex: 1; }

/* Plain text-only reading (the welcome email, a customer reply): a
   generous line length, not the page's full width -- real mail clients
   don't run plain prose edge-to-edge either. Ticket emails opt into
   .reading-body-wide below, where a two-column layout actually has
   something to fill the width with. */
.reading-body { padding: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-4); max-width: 900px; }
.reading-body p { color: var(--text-2); font-size: 14.5px; line-height: 1.65; }
.reading-body-dim { opacity: 0.9; }
/* Ticket emails carry a topology attachment, so there's real content to
   balance against the scenario text -- a wider cap plus the two-column
   .reading-content grid below is what actually uses the reading pane's
   width instead of a narrow column with a huge empty gutter beside it. */
.reading-body-wide { max-width: 1180px; }

.ticket-facts {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-4);
  background: var(--surface-2); border: 1px solid var(--border-subtle); border-radius: var(--r-md); padding: var(--sp-4);
}
.ticket-facts b { display: block; font-size: 13.5px; margin: 3px 0 2px; }
.ticket-facts small { font-size: 11px; color: var(--text-3); }

/* Scenario text beside the topology attachment, not stacked above it --
   this is what turns the leftover width into something the email
   actually uses, rather than just widening a single narrow column. Flex
   + wrap, not a fixed grid track: the reading pane's real width varies
   with the folder/list columns beside it (narrower with them open, much
   wider in focus views), and wrapping the attachment to its own row
   below the text once the row can't fit both is what keeps the text
   column from being squeezed to a sliver instead of just "wider". */
.reading-content { display: flex; flex-wrap: wrap; gap: var(--sp-5); align-items: flex-start; }
.reading-text { flex: 1 1 320px; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-4); }
.reading-attach { flex: 0 0 340px; }
.reading-attach-chip {
  display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 12px; color: var(--text-2);
  background: var(--surface-2); border: 1px solid var(--border-subtle); border-radius: var(--r-sm); padding: 6px 10px; margin-bottom: var(--sp-3);
}
.reading-attach-chip span { color: var(--text-3); margin-left: 4px; }
.mini-topo { background: var(--surface-2); border: 1px solid var(--border-subtle); border-radius: var(--r-md); padding: var(--sp-3); }
/* A fixed box with the SVG filling it via height:100% (not the usual
   width:100%/height:auto), so the viewBox's own preserveAspectRatio
   letterboxes the WHOLE topology into view -- no dimension can exceed
   the box, so there is nothing left to scroll. This is a understand-
   before-you-open-the-lab preview, not a second interactive canvas: no
   pointer cursor, no hover highlight, no click handler (see
   renderTopologyDiagram's `interactive` flag). */
.mini-topo .topology-diagram { height: 230px; overflow: hidden; }
.mini-topo .topology-diagram svg { width: 100%; height: 100%; }
.mini-topo .topo-node, .mini-topo .topo-node-group { cursor: default; }
.mini-topo .topo-node:hover { stroke: var(--border); }

.reading-quote { margin-top: var(--sp-2); font-size: 13px; }
.reading-quote summary { color: var(--text-3); cursor: pointer; font-family: var(--font-mono); font-size: 11.5px; }
.reading-quote summary:hover { color: var(--text-2); }
.reading-quote p { margin-top: var(--sp-2); color: var(--text-3); padding-left: var(--sp-3); border-left: 2px solid var(--border); }

.accept-banner {
  display: flex; align-items: center; gap: var(--sp-3); margin: var(--sp-4) var(--sp-5) 0; padding: var(--sp-3) var(--sp-4);
  background: var(--info-soft); border: 1px solid color-mix(in srgb, var(--info) 35%, transparent); border-radius: var(--r-md); color: var(--info);
}
.accept-banner-text { display: flex; flex-direction: column; gap: 2px; flex: 1; }
.accept-banner-text b { color: var(--text); font-size: 13px; }
.accept-banner-text span { font-size: 12px; color: var(--text-2); }
.status-pill { flex: none; }

/* .btn-open-lab is a color modifier only -- it always pairs with the
   base .btn (+ .btn-sm) classes in the markup, same as every other
   button in the app, so it inherits the shared size/radius/font instead
   of falling back to unstyled native button chrome.

   color is --info-on, not a hardcoded dark value: --info itself flips
   between a light blue (dark theme) and a dark navy (light theme) --
   same reason --brand-on exists next to --brand-500 -- so a fixed near-
   black text color read fine in dark mode and unreadable (dark text on
   dark navy) in light mode. */
.btn-open-lab { background: var(--info); color: var(--info-on); border-color: var(--info); font-weight: 650; }
.btn-open-lab:hover { background: color-mix(in srgb, var(--info) 88%, black); border-color: color-mix(in srgb, var(--info) 88%, black); }

/* Priority badges: same P1-P4 severity scale the in-lab ticket card uses
   (.ticket-value.p1 etc), as a filled badge instead of plain text -- a
   NOC inbox is scanned, not read, so state needs to read as shape+colour,
   not just a coloured word. */
.badge.noc-priority.p1 { background: color-mix(in srgb, var(--priority-p1) 16%, transparent); color: var(--priority-p1); border-color: color-mix(in srgb, var(--priority-p1) 42%, transparent); }
.badge.noc-priority.p2 { background: color-mix(in srgb, var(--priority-p2) 16%, transparent); color: var(--priority-p2); border-color: color-mix(in srgb, var(--priority-p2) 42%, transparent); }
.badge.noc-priority.p3 { background: color-mix(in srgb, var(--priority-p3) 16%, transparent); color: var(--priority-p3); border-color: color-mix(in srgb, var(--priority-p3) 42%, transparent); }
.badge.noc-priority.p4 { background: color-mix(in srgb, var(--priority-p4) 16%, transparent); color: var(--priority-p4); border-color: color-mix(in srgb, var(--priority-p4) 42%, transparent); }

/* ---------------------------------------------------------------------
   STORY-MODE LAB LAYOUT
   Ribbon + three balanced columns, replacing the plain .ws-grid for a
   NOC-ticket session -- see app.js's applyStoryLabLayout(). The columns
   below HOST the same live topo-card/ws-panel/terminal-pane elements the
   plain layout uses (moved here, not rebuilt), so this is pure CSS: no
   split-terminal, device-tab, or grading logic is duplicated.
   --------------------------------------------------------------------- */
.lab-ribbon {
  display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-4); margin-bottom: var(--sp-3);
  background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: var(--r-md); font-size: 12.5px; flex: none;
}
.lab-ribbon-back { color: var(--text-3); display: flex; text-decoration: none; }
.lab-ribbon-back:hover { color: var(--brand-400); }
.lab-ribbon-id { font-family: var(--font-mono); font-weight: 700; color: var(--brand-400); }
.lab-ribbon-sep { color: var(--border-strong); }
.lab-ribbon-title { font-weight: 600; }
.lab-ribbon-from { color: var(--text-3); }
.lab-ribbon-link { display: flex; align-items: center; gap: 4px; color: var(--text-3); font-size: 12px; white-space: nowrap; text-decoration: none; }
.lab-ribbon-link:hover { color: var(--brand-400); }

/* Two columns, not three: topology lives inside ws-panel's Ticket tab,
   alongside #ticket, instead of a tab of its own -- so the tabbed
   workspace gets more width to itself, matched against the terminal. Can
   grow up to 40% of the page: a real ticket description plus a topology
   diagram both want more room than a plain objectives checklist did. */
.lab-columns { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(380px, 40%) 1fr; gap: var(--sp-4); }
/* Bare flex/overflow containers, deliberately with no card styling of
   their own -- topo-card/ws-panel/terminal-pane each already draw a
   complete card (background, border, radius) for the plain layout, and
   reusing those live elements unstyled here (rather than wrapping them
   in a second card) is what keeps this a pure reparent instead of a
   second visual implementation. See applyStoryLabLayout(). */
.lab-col { min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
/* The plain layout's .ws-panel { margin-top: var(--sp-4) } assumed a
   flex sibling above it (the terminal); there's no such sibling here.
   flex:1 makes the panel fill the whole column instead of sizing to
   whichever tab happens to be active. */
#story-col-steps #ws-panel { margin-top: 0; flex: 1; min-height: 0; }
/* Topology, stacked below #ticket in the same Ticket tab: plain
   document flow, not a flex column that made #ticket and topo-card
   fight over a fixed cross-size -- that fight is what forced the
   ticket's own body into a cramped 220px inner scrollbar (below) no
   matter how much room the incident text actually needed, just so
   topo-card had "the rest" to flex-grow into. #ws-pane-tasks already
   scrolls as a whole (.ws-pane's own base rule) once #ticket + topo-card
   together are taller than the tab -- one scrollbar for genuinely too
   much content, not a second one imposed on the ticket regardless.
   topo-card's real height is set inline by applyTicketTopoHeight()
   (app.js) as an explicit height (not a flex-basis -- nothing here is a
   flex item anymore), scaled to how many devices the topology actually
   has; the value below is only the pre-JS fallback. */
#ws-pane-tasks .topo-card { height: 220px; margin-top: var(--sp-4); }

/* ---------------------------------------------------------------------
   STORY-MODE RESOLUTION
   A dedicated full-panel state (design artifact Screen 5) -- closure
   framed as an incident record, not an arcade "you win" screen.
   --------------------------------------------------------------------- */
.resolve-panel { flex: 1; min-height: 0; overflow-y: auto; background: var(--surface-1); border: 1px solid var(--border-subtle); border-radius: var(--r-lg); padding: var(--sp-6); }
.resolve-head { display: flex; align-items: flex-start; gap: var(--sp-4); margin-bottom: var(--sp-5); flex-wrap: wrap; }
.resolve-check { flex: none; width: 52px; height: 52px; border-radius: 50%; background: var(--success-soft); color: var(--success); display: flex; align-items: center; justify-content: center; }
.resolve-head h3 { font-size: 24px; }
.resolve-head p { color: var(--text-2); font-size: 14px; margin-top: 6px; }
.resolve-score { margin-left: auto; font-size: 13px; padding: 6px 12px; }
.resolve-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-5); margin-bottom: var(--sp-5); max-width: 900px; }
.resolve-block { background: var(--surface-2); border: 1px solid var(--border-subtle); border-radius: var(--r-md); padding: var(--sp-4); }
.resolve-steps { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.resolve-steps li { display: flex; align-items: center; gap: var(--sp-2); font-size: 13px; color: var(--text-2); }
.resolve-steps svg { color: var(--success); flex: none; }
.resolve-skills { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-3) 0 var(--sp-4); }
.skill-chip { font-family: var(--font-mono); font-size: 11px; font-weight: 600; color: var(--brand-400); background: var(--brand-soft); border: 1px solid color-mix(in srgb, var(--brand-500) 35%, transparent); border-radius: var(--r-pill); padding: 4px 10px; }
.resolve-stat { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-top: 1px solid var(--border-subtle); font-size: 13px; }
.resolve-stat span { color: var(--text-3); }
.resolve-stat b { font-weight: 650; }
.resolve-cta { max-width: 320px; justify-content: center; padding: 12px; font-size: 14px; }
/* Two CTAs once a tier-completion promotion is on offer (see .tier-credits
   below) -- the row lets both sit side by side instead of .resolve-cta's
   own single-button max-width stacking them full-width. */
.resolve-cta-row { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.resolve-cta-row .resolve-cta { max-width: none; flex: 1 1 220px; }

/* The end-of-tier credits scene -- see nocTierCompletionCopy() in app.js.
   Set apart from the ordinary resolve-block cards above it (brand-tinted,
   its own icon) so it reads as a bigger moment than an individual
   ticket's own resolution, without becoming a separate full-screen
   interstitial the player has to click through -- it's the payoff
   sitting right where they already are. */
.tier-credits {
  margin-bottom: var(--sp-5); padding: var(--sp-5); max-width: 900px; text-align: center;
  background: var(--brand-soft); border: 1px solid color-mix(in srgb, var(--brand-500) 35%, transparent); border-radius: var(--r-md);
}
.tier-credits-icon {
  width: 44px; height: 44px; margin: 0 auto var(--sp-3); border-radius: 50%;
  background: var(--surface-1); color: var(--brand-400); display: flex; align-items: center; justify-content: center;
}
.tier-credits h4 { margin: 0 0 var(--sp-2); font-size: var(--fs-h3); }
.tier-credits p { margin: 0; color: var(--text-2); font-size: var(--fs-lg); line-height: 1.6; max-width: 640px; margin: 0 auto; }

/* ---------------------------------------------------------------------
   RESPONSIVE
   This is a desktop tool by nature -- a 100-column CLI is not a phone
   experience, and pretending otherwise would be worse than being clear
   about it. Below 1040px the workspace stacks; below 720px the terminal
   is still usable but the app says plainly that a wider screen is better.
   --------------------------------------------------------------------- */
@media (max-width: 1040px) {
  .ws-grid { grid-template-columns: 1fr; }
  .rail { max-height: none; overflow: visible; }
  .results { max-height: none; }
}
@media (max-width: 720px) {
  .appbar { gap: var(--sp-3); padding: 0 var(--sp-3); }
  .nav, .progress-readout { display: none; }
  .layout { padding: var(--sp-4) var(--sp-3); }
  .challenge-grid { grid-template-columns: 1fr; }
  .ticket-fields { grid-template-columns: 1fr; }
  .toasts { left: var(--sp-3); right: var(--sp-3); max-width: none; }
}

/* =========================================================================
   LANDING
   A 1:1 port of the landing-redesign-v6 design
   (docs/DesignMockups/landing-redesign-v6.html): its layout, spacing, type
   scale and section rhythm, rebuilt on the app's real token layer instead
   of the mock's private palette. The mock's palette was derived from this
   app's light theme in the first place (--page/#e9edef == --surface-0,
   --paper-2/#f4f7f8 == --surface-2, --teal/#12705f == --brand-500, and so
   on), so light mode renders as the mock did while dark mode keeps
   working -- which a hardcoded light palette would have broken, since the
   appbar's theme toggle is still on this page.

   Two token roles are deliberately not literal ports: brand-as-TEXT reads
   --brand-400 and brand-as-FILL reads --brand-500 (the app's own split),
   where the mock used one --teal for both. In light mode they are a
   couple of percent apart; in dark mode it is the difference between
   readable and invisible.

   Everything is prefixed land- so the landing's private styling is
   greppable as one group and can never collide with an app view. Where a
   real component already exists (topo-card/topo-node/topo-link,
   mail-row, reading-header, device-tabs, hero-term/hero-cli,
   accept-banner, badge, live-dot) the markup uses it directly rather than
   restyling a lookalike.
   ========================================================================= */

.landing {
  display: block;
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.55;
  color: var(--text);
}
/* The faint 60px graph-paper grid the design sits on, fading out down the
   page. Scoped to .landing (not body) so it exists only while the landing
   is the visible view -- .hidden removes the element and the pseudo with
   it, leaving the app views' own backgrounds untouched. */
.landing::before {
  position: fixed;
  inset: 0;
  z-index: -1;
  content: "";
  pointer-events: none;
  opacity: 0.4;
  background-image:
    linear-gradient(var(--surface-1) 1px, transparent 1px),
    linear-gradient(90deg, var(--surface-1) 1px, transparent 1px);
  background-size: 60px 60px;
  mask-image: linear-gradient(to bottom, black, transparent 72%);
}
/* The design's own display type: tighter tracking and leading than the
   app's UI headings, which are tuned for dense views rather than a page
   read at arm's length. */
.landing h1, .landing h2, .landing h3, .landing h4 {
  margin: 0;
  letter-spacing: -0.045em;
  line-height: 1.08;
  text-wrap: normal;
}
.landing h1 { font-size: clamp(45px, 6.2vw, 78px); }
.landing h2 { font-size: clamp(32px, 4.5vw, 56px); }
.landing h3 { font-size: 21px; letter-spacing: -0.03em; }
/* brand-500 (the FILL token) rather than brand-400 (the TEXT token) here:
   at display size the heavier, slightly warmer green is what the design
   uses, and it still clears contrast on the dark theme's ground. The
   small mono text below stays on brand-400, where the extra darkness in
   light mode actually matters. */
.landing h1 em, .landing h2 em { color: var(--brand-500); font-style: normal; }
/* No `.landing p { margin: 0 }` here: the base sheet already zeroes p
   margins globally, and a class-plus-type selector would out-specify the
   single-class rules below that set their own margins (the story caption
   in particular), silently collapsing them. */

.land-wrap { width: min(1240px, calc(100% - 48px)); margin: 0 auto; }
.land-section { position: relative; padding: 104px 0; border-top: 1px solid var(--border-subtle); }
.land-section.tinted { background: var(--surface-2); }

.land-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--brand-400);
  font: 600 10px/1.3 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.land-eyebrow::before { width: 18px; height: 1px; content: ""; background: var(--brand-500); }
/* The CTA eyebrow leads with a live dot instead of the rule. */
.land-cta .land-eyebrow::before { content: none; }

.land-section-head { display: flex; align-items: end; justify-content: space-between; gap: 32px; margin-bottom: 43px; }
.land-section-head h2 { max-width: 650px; margin-top: 15px; }
.land-section-head > p { max-width: 410px; color: var(--text-2); font-size: 16px; line-height: 1.6; }
.land-section-head.center { display: block; max-width: 710px; margin-right: auto; margin-left: auto; text-align: center; }
.land-section-head.center .land-eyebrow { justify-content: center; }
.land-section-head.center h2 { margin-right: auto; margin-left: auto; }
.land-section-head.center > p { max-width: 610px; margin: 18px auto 0; }

/* Scroll reveal -- per element, the way the design does it, rather than
   the app's section-plus-staggered-children pattern. */
.land-reveal { opacity: 0; transform: translateY(15px); transition: opacity 0.65s ease, transform 0.65s ease; }
.land-reveal.is-visible { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .land-reveal { opacity: 1; transform: none; }
}

/* The landing's own button, kept off the app's .btn so the two can't
   drift into each other: taller, heavier, and squared a touch more than
   the in-app control. */
.land-btn {
  display: inline-flex;
  min-height: 38px;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 14px;
  border: 1px solid var(--border);
  border-radius: 5px;
  color: var(--text);
  background: transparent;
  text-decoration: none;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  transition: transform 0.17s ease, background 0.17s ease, border-color 0.17s ease;
}
.land-btn:hover { transform: translateY(-1px); background: var(--surface-3); border-color: var(--border); }
.land-btn.primary { border-color: var(--brand-500); color: var(--brand-on); background: var(--brand-500); }
.land-btn.primary:hover { border-color: var(--brand-600); background: var(--brand-600); }
.land-btn.large { min-height: 46px; padding: 0 18px; font-size: 14px; }
.land-btn.small { min-height: 32px; padding: 0 10px; font: 600 10px var(--font-mono); }
.land-btn[disabled] { cursor: default; opacity: 0.72; transform: none; }

/* ---- Hero ------------------------------------------------------------- */
.land-hero { position: relative; overflow: hidden; padding: 88px 0 70px; background: var(--surface-2); }
/* Soft concentric rings behind the terminal, bleeding off the right edge
   of the viewport -- the hero's own overflow: hidden clips them at the
   true page edge rather than at the content column. */
.land-hero::after {
  position: absolute;
  top: 36px;
  right: -330px;
  width: 640px;
  height: 640px;
  content: "";
  border: 1px solid color-mix(in srgb, var(--brand-500) 22%, transparent);
  border-radius: 50%;
  box-shadow:
    0 0 0 28px color-mix(in srgb, var(--brand-500) 8%, transparent),
    0 0 0 58px color-mix(in srgb, var(--brand-500) 4%, transparent);
  pointer-events: none;
}
.land-hero-grid { position: relative; z-index: 1; display: grid; grid-template-columns: minmax(0, 0.94fr) minmax(440px, 1.06fr); gap: 64px; align-items: center; }
.land-hero-copy h1 { max-width: 680px; margin-top: 17px; }
.land-hero-copy > p { max-width: 620px; margin: 22px 0 27px; color: var(--text-2); font-size: 19px; line-height: 1.58; }
.land-actions { display: flex; flex-wrap: wrap; gap: 10px; }

.land-hero-console { position: relative; z-index: 1; overflow: hidden; border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--term-bg); box-shadow: var(--shadow-lg); }
.land-console-bar { display: flex; min-height: 43px; align-items: center; gap: 9px; padding: 0 15px; border-bottom: 1px solid var(--border-subtle); color: var(--text-3); background: var(--surface-2); font: 10px var(--font-mono); }
.land-traffic { display: inline-flex; gap: 5px; margin-right: 5px; }
.land-traffic i { width: 7px; height: 7px; border-radius: 50%; background: var(--border); }
.land-traffic i:nth-child(1) { background: var(--danger); }
.land-traffic i:nth-child(2) { background: var(--warning); }
.land-traffic i:nth-child(3) { background: var(--success); }
.land-console-label { color: var(--brand-400); }
.land-console-bar > span:last-child { margin-left: auto; }
/* The hero terminal is the shipped .hero-cli content (runHeroAnimation's
   real BGP session) in the design's frame -- so the classes the script
   emits (cli-note/cli-bad/cli-good, cursor-block) are the ones styled.
   A FIXED height (not min-height): the scripted session runs longer than
   304px of text fits, and a min-height lets the box keep growing as later
   lines are typed, pushing the whole page down mid-animation -- exactly
   the "page moves while it's still filling the terminal" bug this was.
   overflow-y auto + runHeroAnimation() pinning scrollTop to scrollHeight
   on every tick (see app.js) keeps the newest line in view instead, the
   way a real terminal scrolls rather than growing. Scrollbar hidden since
   this is an animated illustration, not something a visitor scrolls by
   hand. */
.land-hero-cli {
  height: 304px; overflow-y: auto; margin: 0; padding: 22px;
  color: var(--term-fg); background: var(--term-bg);
  font: 12px/1.75 var(--font-mono); white-space: pre-wrap;
  scrollbar-width: none;
}
.land-hero-cli::-webkit-scrollbar { display: none; }
.land-hero-cli .cli-note { color: var(--text-3); }
.land-hero-cli .cli-bad { color: var(--danger); }
.land-hero-cli .cli-good { color: var(--success); }
.land-console-foot { display: flex; justify-content: space-between; gap: 12px; padding: 12px 16px; border-top: 1px solid var(--border-subtle); color: var(--text-3); background: var(--surface-2); font: 10px var(--font-mono); }
.land-console-foot strong { color: var(--success); }

.land-hero-proof { position: relative; z-index: 1; display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 38px; }
.land-proof-item { display: flex; align-items: flex-start; gap: 12px; padding: 15px; border: 1px solid var(--border-subtle); border-radius: var(--r-md); background: var(--surface-1); box-shadow: var(--shadow-sm); }
.land-proof-icon {
  display: flex; flex: none; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--brand-soft); color: var(--brand-500);
}
.land-proof-copy-wrap { min-width: 0; }
.land-proof-title { font-size: 14px; font-weight: 700; line-height: 1.3; }
.land-proof-value { color: var(--brand-500); font: 700 19px/1 var(--font-mono); }
.land-proof-copy { margin-top: 5px; color: var(--text-3); font-size: 11px; line-height: 1.4; }

/* ---- 01. Real Networks. Real Packet Delivery. -------------------------
   The network is one instrument panel rather than a row of cards, so the
   moving path has a single stage to travel across. */
.land-real-layout { display: grid; grid-template-columns: 0.74fr 1.26fr; gap: 36px; align-items: center; }
.land-real-copy h3 { max-width: 430px; margin: 15px 0 14px; font-size: 30px; }
.land-real-copy > p { max-width: 410px; color: var(--text-2); font-size: 15px; line-height: 1.65; }
.land-real-points { display: grid; gap: 14px; margin-top: 27px; }
.land-real-point { display: grid; grid-template-columns: 31px 1fr; gap: 10px; color: var(--text-2); font-size: 13px; }
.land-real-point b { display: block; margin-bottom: 2px; color: var(--text); }
.land-point-index {
  display: grid; width: 26px; height: 26px; place-items: center;
  border: 1px solid color-mix(in srgb, var(--brand-500) 45%, transparent); border-radius: 50%;
  color: var(--brand-400); background: var(--brand-soft); font: 10px var(--font-mono);
}
.land-real-claim { margin-top: 23px; color: var(--brand-400); font: 600 13px var(--font-mono); }

.land-network-tool { overflow: hidden; border: 1px solid var(--border); border-radius: var(--r-lg); color: var(--text); background: var(--surface-1); box-shadow: var(--shadow-lg); }
.land-tool-head { display: flex; min-height: 49px; align-items: center; justify-content: space-between; gap: 12px; padding: 0 17px; border-bottom: 1px solid var(--border); background: var(--surface-2); }
.land-tool-name { display: flex; align-items: center; gap: 9px; font: 600 11px var(--font-mono); }
.land-tool-actions { display: flex; align-items: center; gap: 10px; }
.land-tool-state { color: var(--success); font: 10px var(--font-mono); }
.land-network-tool.checking .land-tool-state { color: var(--warning); }
.land-network-canvas { padding: 18px 18px 7px; }
.land-network-canvas svg { display: block; width: 100%; height: auto; }
/* fill: none is load-bearing: an SVG path with no fill defaults to solid
   black, which turns these open zig-zag routes into filled blobs. */
.land-link { fill: none; stroke: var(--border); stroke-width: 2.5; opacity: 1; }
.land-link-active { stroke: var(--brand-400); stroke-width: 3.2; stroke-dasharray: 7 10; animation: land-route 1.25s linear infinite; }
.land-link-backup { stroke: var(--text-3); stroke-dasharray: 4 8; }
@keyframes land-route { to { stroke-dashoffset: -34; } }
.land-node-router { stroke: var(--brand-400); stroke-width: 2; }
.land-node-switch { stroke: var(--info); stroke-width: 2; }
.land-node-host { stroke: var(--border); stroke-width: 2; }
/* topo-label/topo-sublabel are sized for the workspace's much larger
   diagram; scaled here to fit a 60px node. */
.land-label { font-family: var(--font-mono); font-size: 12px; font-weight: 700; }
.land-sublabel { font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.08em; }
.land-packet { fill: var(--brand-500); stroke: var(--surface-1); stroke-width: 2; filter: drop-shadow(0 0 5px color-mix(in srgb, var(--brand-500) 60%, transparent)); }
.land-packet-reply { fill: var(--info); }

.land-trace { display: grid; grid-template-columns: 1fr 1fr; border-top: 1px solid var(--border); }
.land-trace-list { padding: 15px 17px; border-right: 1px solid var(--border); }
.land-trace-title { margin-bottom: 8px; color: var(--text-3); font: 9px var(--font-mono); letter-spacing: 0.12em; text-transform: uppercase; }
.land-trace-row { display: flex; align-items: center; gap: 9px; padding: 5px 0; color: var(--text-2); font: 10px var(--font-mono); }
.land-trace-row i { width: 6px; height: 6px; border-radius: 50%; background: var(--success); }
.land-trace-row b { margin-left: auto; color: var(--success); font-weight: 600; }
.land-readout { display: grid; grid-template-columns: repeat(3, 1fr); }
.land-readout-item { padding: 15px 14px; border-right: 1px solid var(--border); }
.land-readout-item:last-child { border-right: 0; }
.land-readout-label { display: block; color: var(--text-3); font: 9px var(--font-mono); letter-spacing: 0.1em; text-transform: uppercase; }
.land-readout-value { display: block; margin-top: 4px; color: var(--text); font: 700 13px var(--font-mono); }
.land-readout-value.good { color: var(--success); }
.land-network-tool.checking .land-readout-value.good { color: var(--warning); }

/* ---- 02. NOC Story Mode -----------------------------------------------
   One NOC desktop with a timeline beside it: the window's content swaps
   as the selected stage changes, instead of four separate cards asking
   the visitor to assemble the story themselves. */
.land-story-layout { display: grid; grid-template-columns: 0.46fr 1.54fr; gap: 42px; align-items: center; }
.land-story-intro h2 { max-width: 370px; margin: 15px 0 16px; }
.land-story-intro > p { max-width: 340px; color: var(--text-2); font-size: 15px; line-height: 1.65; }
.land-story-steps { position: relative; display: grid; gap: 8px; margin-top: 30px; }
.land-story-steps::before { position: absolute; top: 25px; bottom: 25px; left: 18px; width: 1px; content: ""; background: var(--border); }
.land-story-step {
  position: relative; z-index: 1; display: grid; grid-template-columns: 37px 1fr; gap: 11px;
  width: 100%; padding: 8px; border: 1px solid transparent; border-radius: var(--r-md);
  color: var(--text-2); background: transparent; text-align: left; cursor: pointer; font: inherit;
}
.land-story-step:hover { background: var(--surface-1); }
.land-story-step.active { border-color: color-mix(in srgb, var(--brand-500) 40%, transparent); color: var(--text); background: var(--surface-1); box-shadow: var(--shadow-sm); }
.land-step-number {
  display: grid; width: 35px; height: 35px; place-items: center;
  border: 1px solid var(--border); border-radius: 50%;
  color: var(--text-3); background: var(--surface-0); font: 11px var(--font-mono);
}
.land-story-step.active .land-step-number { border-color: var(--brand-500); color: var(--brand-on); background: var(--brand-500); }
.land-story-step strong { display: block; margin-top: 2px; font-size: 13px; }
.land-story-step small { display: block; margin-top: 2px; color: var(--text-3); font: 10px var(--font-mono); }
.land-story-caption { margin-top: 17px; color: var(--text-3); font: 10px var(--font-mono); }

.land-noc-window { overflow: hidden; border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--surface-1); box-shadow: var(--shadow-lg); }
.land-noc-window.changing { animation: land-window 0.4s ease; }
@keyframes land-window { from { opacity: 0.55; transform: translateY(7px); } to { opacity: 1; transform: none; } }
.land-noc-top { display: flex; min-height: 49px; align-items: center; gap: 11px; padding: 0 17px; border-bottom: 1px solid var(--border-subtle); background: var(--surface-2); }
.land-noc-top strong { font: 700 11px var(--font-mono); letter-spacing: 0.08em; }
.land-noc-top span:last-child { margin-left: auto; color: var(--text-3); font: 10px var(--font-mono); }
.land-noc-content { min-height: 362px; }
/* Stage 3's lab header strip -- the terminal below it is the real
   hero-term/device-tabs/hero-cli chrome. */
.land-lab-intro { padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-subtle); background: var(--surface-2); }
.land-lab-intro h3 { margin-top: 6px; font-size: 18px; }

/* ---- 03. Powerful, flexible grading ------------------------------------
   Two configuration lanes converging into one live outcome check. */
.land-grade-board { overflow: hidden; border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--surface-1); box-shadow: var(--shadow-lg); }
.land-grade-topbar { display: flex; min-height: 49px; align-items: center; justify-content: space-between; gap: 14px; padding: 0 17px; border-bottom: 1px solid var(--border-subtle); background: var(--surface-2); }
.land-grade-topbar strong { font: 700 11px var(--font-mono); letter-spacing: 0.08em; }
.land-grade-topbar span { color: var(--text-3); font: 10px var(--font-mono); }
.land-lanes { display: grid; grid-template-columns: minmax(0, 1fr) 150px minmax(0, 1fr); align-items: center; }
.land-lane { min-width: 0; padding: 23px; }
.land-lane + .land-lane { border-left: 1px solid var(--border-subtle); }
.land-lane-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.land-engineer { display: flex; align-items: center; gap: 9px; }
.land-engineer .land-avatar { display: grid; width: 31px; height: 31px; margin-top: 0; place-items: center; border-radius: 50%; color: var(--brand-on); background: var(--brand-500); font: 700 10px var(--font-mono); }
.land-engineer strong { display: block; font-size: 13px; }
.land-engineer span { display: block; margin-top: 2px; color: var(--text-3); font: 10px var(--font-mono); }
.land-method { padding: 5px 7px; border: 1px solid color-mix(in srgb, var(--brand-500) 35%, transparent); border-radius: var(--r-pill); color: var(--brand-400); background: var(--brand-soft); font: 10px var(--font-mono); white-space: nowrap; }
.land-code { min-height: 152px; margin: 0; padding: 15px; border: 1px solid var(--border-subtle); border-radius: var(--r-md); color: var(--text); background: color-mix(in srgb, var(--surface-2) 35%, var(--surface-1)); font: 11px/1.75 var(--font-mono); white-space: pre-wrap; }
.land-code .prompt { color: var(--brand-400); }
.land-code .comment { color: var(--text-3); }

/* The centre node is the single "Grade both" control -- one button that
   drives the whole check, rather than a static label plus a submit button
   per lane. */
.land-merge { position: relative; display: grid; min-height: 245px; place-items: center; background: var(--surface-2); }
.land-merge-node {
  position: relative; z-index: 1; display: grid; width: 92px; height: 92px; place-items: center;
  padding: 0; border: 2px solid var(--brand-500); border-radius: 50%;
  color: var(--brand-400); background: var(--surface-1); box-shadow: 0 0 0 9px var(--brand-soft);
  text-align: center; font: 700 10.5px/1.35 var(--font-mono); letter-spacing: 0.04em; cursor: pointer;
  transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.land-merge-node:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 0 0 9px var(--brand-soft), 0 10px 22px color-mix(in srgb, var(--brand-500) 25%, transparent); }
.land-merge-node:disabled { cursor: default; }
.land-merge-ring { display: none; position: absolute; inset: -2px; border-radius: 50%; border: 2px solid transparent; border-top-color: var(--brand-500); border-right-color: var(--brand-500); }
.land-merge-node.checking { border-color: var(--border); color: var(--text-3); background: var(--surface-1); box-shadow: 0 0 0 9px var(--surface-3); }
.land-merge-node.checking .land-merge-ring { display: block; animation: land-spin 0.85s linear infinite; }
.land-merge-node.done { border-color: var(--success); color: #fff; background: var(--success); box-shadow: 0 0 0 9px var(--success-soft); }
.land-merge-ok { display: block; font-size: 17px; font-weight: 900; line-height: 1; }
@keyframes land-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .land-merge-node.checking .land-merge-ring { animation: none; } }

.land-outcome { display: flex; min-height: 71px; align-items: center; justify-content: space-between; gap: 15px; padding: 16px 22px; border-top: 1px solid color-mix(in srgb, var(--brand-500) 35%, transparent); background: var(--brand-soft); }
.land-outcome-main { display: flex; align-items: center; gap: 12px; }
.land-outcome strong { display: block; font-size: 14px; }
.land-outcome span { display: block; margin-top: 2px; color: var(--text-2); font-size: 11px; }
.land-evidence { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
.land-evidence span { padding: 5px 7px; border: 1px solid color-mix(in srgb, var(--success) 35%, transparent); border-radius: var(--r-pill); color: var(--success); background: var(--surface-1); font: 10px var(--font-mono); }

.land-dual-heading { display: flex; align-items: end; justify-content: space-between; gap: 22px; margin: 68px 0 25px; }
.land-dual-heading h3 { margin-top: 13px; font-size: 29px; }
.land-dual-heading > p { max-width: 420px; color: var(--text-2); font-size: 14px; }
.land-dual-terminal { overflow: hidden; border: 1px solid var(--border); border-radius: var(--r-lg); color: var(--text); background: var(--term-bg); box-shadow: var(--shadow-lg); }
.land-dual-top { display: flex; min-height: 47px; align-items: center; justify-content: space-between; gap: 12px; padding: 0 17px; border-bottom: 1px solid var(--border); background: var(--surface-2); font: 10px var(--font-mono); }
.land-dual-title { display: flex; align-items: center; gap: 9px; }
.land-dual-title strong { color: var(--text); }
.land-dual-top > span { color: var(--text-3); }
.land-dual-grid { display: grid; grid-template-columns: 1fr 1fr; }
.land-dual-pane { min-width: 0; font: 10px/1.75 var(--font-mono); }
.land-dual-pane + .land-dual-pane { border-left: 1px solid var(--border); }
.land-dual-pane pre { min-height: 174px; margin: 0; padding: 15px 17px; white-space: pre-wrap; color: var(--term-fg); }
.land-dual-pane .prompt { color: var(--brand-400); }
.land-dual-pane .good { color: var(--success); }
.land-dual-pane .muted { color: var(--text-3); }
.land-dual-foot { display: flex; justify-content: space-between; gap: 12px; padding: 11px 17px; border-top: 1px solid var(--border); color: var(--text-3); font: 10px var(--font-mono); }
.land-dual-foot strong { color: var(--success); }

/* ---- Final CTA --------------------------------------------------------- */
.land-cta { padding: 106px 0 112px; border-top: 1px solid var(--border-subtle); background: var(--surface-2); text-align: center; }
.land-cta .land-eyebrow { justify-content: center; }
.land-cta h2 { max-width: 720px; margin: 15px auto; }
.land-cta p { max-width: 560px; margin: 0 auto 26px; color: var(--text-2); font-size: 17px; }

/* ---- Responsive -------------------------------------------------------- */
@media (max-width: 1000px) {
  .land-hero-grid, .land-real-layout { grid-template-columns: 1fr; }
  .land-hero-grid { gap: 38px; }
  .land-real-copy { max-width: 700px; }
  .land-lanes { grid-template-columns: 1fr; }
  .land-lane + .land-lane { border-top: 1px solid var(--border-subtle); border-left: 0; }
  .land-merge { min-height: 110px; }
  .land-merge-node { width: 70px; height: 70px; }
  .land-story-layout { grid-template-columns: 1fr; }
  .land-story-intro { max-width: 700px; }
  .land-story-steps { grid-template-columns: repeat(4, 1fr); gap: 6px; }
  .land-story-steps::before { top: 18px; right: 18px; bottom: auto; left: 18px; width: auto; height: 1px; }
  .land-story-step { display: block; padding: 8px; text-align: center; }
  .land-step-number { margin: 0 auto 7px; }
  .land-story-step strong { font-size: 11px; }
  .land-story-step small { font-size: 9px; }
}
@media (max-width: 720px) {
  .land-wrap { width: min(100% - 32px, 1240px); }
  .land-hero { padding: 58px 0 52px; }
  .land-hero::after { right: -570px; opacity: 0.7; }
  .land-hero-copy > p { font-size: 17px; }
  .land-actions { align-items: stretch; flex-direction: column; }
  .land-actions .land-btn { width: 100%; }
  .land-hero-proof { grid-template-columns: 1fr; margin-top: 28px; }
  .land-hero-cli { min-height: 270px; padding: 17px 15px; font-size: 10px; }
  .land-section { padding: 72px 0; }
  .land-section-head { align-items: start; flex-direction: column; gap: 16px; }
  .land-section-head > p { max-width: 600px; }
  .land-trace { grid-template-columns: 1fr; }
  .land-trace-list { border-right: 0; border-bottom: 1px solid var(--border); }
  .land-readout { grid-template-columns: 1fr 1fr; }
  .land-readout-item:first-child { grid-column: 1 / -1; border-bottom: 1px solid var(--border); }
  .land-readout-item:nth-child(3) { border-right: 0; }
  .land-story-steps { grid-template-columns: 1fr 1fr; }
  .land-story-steps::before { display: none; }
  .land-story-step { display: grid; grid-template-columns: 31px 1fr; gap: 8px; text-align: left; }
  .land-step-number { width: 30px; height: 30px; margin: 0; }
  .land-story-step strong { margin-top: 0; font-size: 11px; }
  .land-dual-heading { align-items: start; flex-direction: column; margin-top: 58px; }
  .land-dual-grid { grid-template-columns: 1fr; }
  .land-dual-pane + .land-dual-pane { border-top: 1px solid var(--border); border-left: 0; }
  .land-outcome { align-items: start; flex-direction: column; }
  .land-evidence { justify-content: flex-start; }
  .land-cta { padding: 72px 0 80px; }
}
@media (max-width: 430px) {
  .landing h1 { font-size: 45px; }
  .land-tool-head, .land-grade-topbar, .land-dual-top { align-items: start; flex-direction: column; padding: 12px 14px; }
  .land-tool-actions { width: 100%; justify-content: space-between; }
  .land-network-canvas { padding-right: 9px; padding-left: 9px; }
  .land-readout { grid-template-columns: 1fr; }
  .land-readout-item:first-child { grid-column: auto; }
  .land-readout-item { border-right: 0 !important; border-bottom: 1px solid var(--border); }
  .land-readout-item:last-child { border-bottom: 0; }
  .land-lane { padding: 17px; }
  .land-lane-head { align-items: start; flex-direction: column; }
  .land-dual-pane { overflow-x: auto; }
  .land-dual-pane pre { min-width: 275px; }
}

/* =======================================================================
   ATLAS (Certification Atlas view)
   Was its own standalone <style> block on a separate static page
   (certification-atlas.html) -- moved here verbatim (minus the two
   page-level resets `*{box-sizing:border-box}`/`html,body{margin:0}`,
   which this shared stylesheet already provides above) now that the
   atlas is a real view inside the SPA, shown/hidden by the router
   exactly like #dashboard/#noc/#picker.
   ======================================================================= */

/* Fully coherent with the shared design system now, not a second
   hand-tuned palette that happens to look similar -- every one of these
   is a straight alias of a real token from this file's own :root (see
   the TOKENS comment near the top of this file for what each one means
   and why it's contrast-verified). That also means this block no longer
   needs its own duplicate light/dark definitions: --a-bg etc. resolve
   correctly per-theme for free, because var(--surface-0) etc. already
   do. The four difficulty tiers alias the real semantic colors
   (success/info/warning/danger) instead of a second hand-picked
   red/amber/blue/green, so "beginner -> expert" reads as the exact same
   color language as a passing/failing verdict elsewhere in the app, not
   a coincidentally-similar one. Per-vendor brand colors (Cisco cyan, AWS
   orange, ...) further down are left untouched -- those are real
   identity marks, not theme.
   Prefixed --a- (not reusing this file's own token names like --border)
   specifically so nothing here can shadow-and-invalidate the real
   tokens it's built from -- a "--border: var(--border)" rule on the
   same selector is a self-reference, not an inherit, and would make the
   property invalid.
   Still scoped to .atlas-scope (not :root) so none of this -- including
   the atlas's own body/header/footer element styling below -- leaks
   onto Faultline's shared header.appbar/footer.site-footer that wrap
   every view, this one included. */
.atlas-scope{
  --a-bg: var(--surface-0);
  --a-panel: var(--surface-1);
  --a-panel2: var(--surface-2);
  --a-border: var(--border);
  --a-border-soft: var(--border-subtle);
  --a-text: var(--text);
  --a-text-dim: var(--text-2);
  --a-text-faint: var(--text-3);
  --a-accent: var(--brand-400);
  --a-accent-warm: var(--warning);
  --a-grid: var(--surface-2);
  --a-tier-e: var(--danger);
  --a-tier-a: var(--warning);
  --a-tier-i: var(--info);
  --a-tier-b: var(--success);
  --a-axis-op: 0.18;
  --a-body-bg:
    radial-gradient(ellipse 1200px 600px at 15% -10%, color-mix(in srgb, var(--brand-500) 6%, transparent), transparent 60%),
    radial-gradient(ellipse 900px 500px at 90% 10%, color-mix(in srgb, var(--warning) 6%, transparent), transparent 60%),
    var(--surface-0);
}
/* Only the axis wash's opacity needs an explicit per-theme value (it's a
   bare number, not a color, so there's no token to alias) -- everything
   else above already resolves correctly per-theme via the tokens it's
   built from. */
:root[data-theme="light"] .atlas-scope{ --a-axis-op: 0.30; }
.atlas-scope{
  background:var(--a-body-bg);
  color:var(--a-text);
  font-family:var(--font-sans);
  -webkit-font-smoothing:antialiased;
  transition:background .2s ease, color .2s ease;
}
/* .mono/.eyebrow/.chip below are scoped under .atlas-scope, unlike the
   rest of this section's selectors -- these three specific class names
   collide with real components this file already defines elsewhere (the
   shared footer's own span.eyebrow among them), and an unscoped rule
   declared after those wins the cascade on equal specificity and would
   silently reskin them. Everything else in this section uses class
   names unique to the atlas (.cert, .col-head, .suggest-chip, ...) with
   no such collision, so it doesn't need the same prefix. */
.atlas-scope .mono{font-family:var(--font-mono);}

/* ---------- Header (the atlas's own internal role-gate/appView header --
   scoped so it never touches Faultline's shared header.appbar that
   wraps every view) ---------- */
.atlas-scope header{
  padding:var(--sp-7) var(--sp-5) var(--sp-4);
  border-bottom:1px solid var(--a-border-soft);
  position:relative;
  display:flex;
  justify-content:space-between;
  gap:24px;
}
.header-main{flex:1; min-width:0;}
.atlas-scope .eyebrow{
  font-family:var(--font-mono);
  font-size:var(--fs-micro);
  letter-spacing:0.18em;
  text-transform:uppercase;
  color:var(--a-accent);
  display:flex;
  align-items:center;
  gap:var(--sp-2);
  margin-bottom:var(--sp-3);
}
.atlas-scope .eyebrow::before{
  content:"";
  width:7px;height:7px;border-radius:50%;
  background:var(--a-accent);
  box-shadow:0 0 8px 1px var(--a-accent);
  animation:pulse 2.2s infinite ease-in-out;
}
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:.35;}}
.atlas-scope h1{
  font-family:var(--font-sans);
  font-weight:650;
  font-size:clamp(24px,3.4vw,var(--fs-h1));
  margin:0 0 var(--sp-3);
  letter-spacing:-0.02em;
  line-height:1.08;
  color:var(--a-text);
}
.atlas-scope h1 em{font-style:italic; color:var(--a-accent);}
.sub{
  color:var(--a-text-dim);
  font-size:var(--fs-body);
  max-width:760px;
  line-height:var(--lh-body);
  margin:0 0 var(--sp-1);
}
.hints{
  display:flex;
  gap:var(--sp-5);
  flex-wrap:wrap;
  margin-top:var(--sp-4);
  font-size:var(--fs-sm);
  color:var(--a-text-faint);
  font-family:var(--font-mono);
}
.hints .k{
  display:inline-block;
  border:1px solid var(--a-border);
  border-radius:var(--r-sm);
  padding:1px 6px;
  margin-right:5px;
  color:var(--a-accent);
  font-size:var(--fs-label);
}
/* The atlas's own theme-toggle button/CSS is gone -- Faultline's shared
   header has the only theme toggle now, and its button uses this exact
   same class name, so keeping these rules around would have styled the
   real toggle by accident. */

.header-actions{display:flex; align-items:flex-start; gap:var(--sp-3); flex:none;}
/* One "back to role selection" button now, not two redundant ones --
   it gets its actual look from the shared .btn .btn-secondary .btn-sm
   classes in the markup, same appearance as "Submit for grading"/
   "Sign in"/every other button in the app. This rule is purely
   layout: where it sits, not what it looks like. */
.role-switch-btn{ flex:none; align-self:flex-start; }

/* ---------- Role gate ---------- */
.role-gate{
  min-height:100vh;
  display:flex; align-items:center; justify-content:center;
  padding:var(--sp-8) var(--sp-5);
}
.role-gate-inner{max-width:920px; width:100%;}
.role-gate-inner h1{text-align:center; margin-bottom:var(--sp-3);}
.role-gate-sub{
  color:var(--a-text-dim); font-size:var(--fs-lg); line-height:var(--lh-body); text-align:center;
  max-width:600px; margin:0 auto var(--sp-7);
}
.role-grid{
  display:grid; grid-template-columns:repeat(auto-fit, minmax(240px,1fr)); gap:var(--sp-4);
}
.role-card{
  background:var(--a-panel); border:1.5px solid var(--a-border); border-radius:var(--r-lg);
  padding:var(--sp-5) var(--sp-5); cursor:pointer; text-align:left;
  transition:transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
  display:flex; flex-direction:column; gap:var(--sp-3);
}
.role-card:hover{
  transform:translateY(-4px);
  border-color:var(--a-accent);
  box-shadow:var(--shadow-lg);
}
.role-card .rc-icon{font-size:28px; line-height:1;}
.role-card .rc-title{font-family:var(--font-sans); font-weight:650; font-size:var(--fs-h3); color:var(--a-text);}
.role-card .rc-tagline{font-size:var(--fs-sm); color:var(--a-text-dim); line-height:var(--lh-body);}
.role-card .rc-count{font-family:var(--font-mono); font-size:var(--fs-micro); letter-spacing:.08em; text-transform:uppercase; color:var(--a-accent); margin-top:auto; padding-top:var(--sp-1);}

/* ---------- Legend ---------- */
.legend-wrap{
  padding:var(--sp-4) var(--sp-5);
  border-bottom:1px solid var(--a-border-soft);
  background:var(--a-panel);
}
.legend-title{
  font-family:var(--font-mono);
  font-size:var(--fs-label);
  letter-spacing:.12em;
  text-transform:uppercase;
  color:var(--a-text-faint);
  margin:0 0 var(--sp-2);
}
.legend{display:flex; flex-wrap:wrap; gap:var(--sp-2);}
.atlas-scope .chip{
  display:flex; align-items:center; gap:6px;
  border:1px solid var(--a-border);
  background:var(--a-panel2);
  padding:5px var(--sp-3) 5px 7px;
  border-radius:var(--r-pill);
  cursor:pointer;
  font-size:var(--fs-label);
  color:var(--a-text-dim);
  transition:all var(--dur) var(--ease);
  user-select:none;
}
.atlas-scope .chip:hover{border-color:var(--a-text-faint); color:var(--a-text);}
.atlas-scope .chip.active{background:var(--brand-soft); border-color:var(--a-accent); color:var(--a-text);}
.atlas-scope .chip .dotc{width:9px;height:9px;border-radius:50%;flex:none;}
.legend-reset{
  font-family:var(--font-mono);
  font-size:var(--fs-label);
  color:var(--a-text-faint);
  border:1px dashed var(--a-border);
  border-radius:var(--r-pill);
  padding:5px var(--sp-3);
  cursor:pointer;
  background:transparent;
}
.legend-reset:hover{color:var(--a-accent-warm);border-color:var(--a-accent-warm);}

/* ---------- Progress panel ---------- */
.progress-wrap{
  padding:var(--sp-4) var(--sp-5) var(--sp-3);
  border-bottom:1px solid var(--a-border-soft);
  display:flex;
  gap:var(--sp-3);
  align-items:stretch;
  flex-wrap:wrap;
  background:var(--a-bg);
}
.stat-block{
  flex:1 1 150px;
  border:1px solid var(--a-border-soft);
  border-radius:var(--r-md);
  background:var(--a-panel);
  padding:var(--sp-3) var(--sp-4);
  min-width:150px;
}
.stat-block.stat-primary{border-color:var(--a-accent); background:color-mix(in srgb, var(--a-accent) 6%, var(--a-panel));}
.stat-block.stat-progress{flex:1 1 170px; display:flex; flex-direction:column; justify-content:center; gap:var(--sp-2);}
.pt-label{
  font-family:var(--font-mono);
  font-size:var(--fs-micro); letter-spacing:.1em; text-transform:uppercase;
  color:var(--a-text-faint); margin-bottom:5px;
}
.pt-value{
  font-family:var(--font-mono);
  font-size:var(--fs-lg); font-weight:700; color:var(--a-text);
  line-height:1.25;
}
.pt-value.tier{color:var(--a-accent); font-size:var(--fs-body);}
.progress-bar-outer{height:8px; border-radius:var(--r-pill); background:var(--a-border-soft); overflow:hidden; position:relative; margin-top:2px;}
.progress-bar-inner{height:100%; background:linear-gradient(to right,var(--a-tier-b),var(--a-tier-i),var(--a-tier-a),var(--a-tier-e)); width:0%; transition:width .3s ease;}
.progress-text-wrap{padding:0 var(--sp-5) var(--sp-4); border-bottom:1px solid var(--a-border-soft); background:var(--a-bg);}
.progress-text{font-size:var(--fs-sm); color:var(--a-text-dim); line-height:var(--lh-body); max-width:900px;}
.progress-text b{color:var(--a-text);}
.progress-reset{
  flex:0 0 auto; align-self:center;
  font-family:var(--font-mono);
  font-size:var(--fs-label); color:var(--a-text-faint);
  border:1px dashed var(--a-border); border-radius:var(--r-pill);
  padding:var(--sp-2) var(--sp-3); cursor:pointer; background:transparent; height:fit-content;
}
.progress-reset:hover{color:var(--a-accent-warm); border-color:var(--a-accent-warm);}

.suggest-wrap{
  padding:var(--sp-3) var(--sp-5) var(--sp-4);
  border-bottom:1px solid var(--a-border-soft);
  background:color-mix(in srgb, var(--warning) 5%, transparent);
  display:none;
}
.suggest-wrap.show{display:block;}
.suggest-label{
  font-family:var(--font-mono); font-size:var(--fs-label); letter-spacing:.1em; text-transform:uppercase;
  color:var(--a-accent); margin-bottom:var(--sp-2); display:flex; align-items:center; gap:7px;
}
.suggest-label::before{content:"➜"; font-size:11px;}
.suggest-chips{display:flex; flex-wrap:wrap; gap:10px;}
.suggest-chip{
  flex:0 0 auto; min-width:200px; max-width:280px;
  background:var(--a-panel); border:1px solid var(--a-border); border-left:3px solid var(--v-color,var(--a-accent));
  border-radius:var(--r-md); padding:9px var(--sp-3); cursor:pointer;
  transition:transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.suggest-chip:hover{transform:translateY(-2px); box-shadow:var(--shadow-md);}
.suggest-chip .sc-top{display:flex; justify-content:space-between; align-items:baseline; gap:8px; margin-bottom:3px;}
.suggest-chip .sc-label{font-family:var(--font-mono); font-weight:700; font-size:var(--fs-sm); color:var(--a-text);}
.suggest-chip .sc-vendor{font-family:var(--font-mono); font-size:9px; letter-spacing:.06em; text-transform:uppercase; color:var(--v-color,var(--a-accent));}
.suggest-chip .sc-reason{font-size:var(--fs-label); color:var(--a-text-dim); line-height:1.4;}

/* ---------- Chart shell -----------------------------------------------
   Deliberately NOT snapped to the --sp-* / --fs-* scale below this point:
   every pixel here is either load-bearing chart geometry (JS computes
   --ch/--boxh/--head-h and positions cert boxes/connector lines against
   them directly -- see certification-atlas.js's layoutChart()) or a
   micro-label sized for a genuinely denser context than the rest of the
   app, the same way a sparkline's axis ticks are legitimately smaller
   than body text elsewhere in a coherent design system. Colors are
   still fully aliased to the real tokens; only the geometry is
   independent. ---------- */
.scroll-shell{overflow-x:auto; overflow-y:hidden; padding-bottom:4px;}
.chart{display:flex; min-width:2100px; padding:26px 32px 10px; position:relative;}
.axis{flex:0 0 56px; position:relative; margin-right:6px;}
.axis-head-spacer{height:var(--head-h);}
.axis-track{position:relative; height:var(--ch); border-radius:8px; overflow:hidden;
  background:linear-gradient(to top,
    var(--a-tier-b) 0%, var(--a-tier-b) 24%,
    var(--a-tier-i) 24%, var(--a-tier-i) 49%,
    var(--a-tier-a) 49%, var(--a-tier-a) 74%,
    var(--a-tier-e) 74%, var(--a-tier-e) 100%);
  opacity:var(--a-axis-op);
}
.axis-labels{position:absolute; left:0; right:0; top:var(--head-h); height:var(--ch); pointer-events:none;}
.axis-label{
  position:absolute; left:0; right:0;
  font-family:var(--font-mono);
  font-size:10px; letter-spacing:.14em; text-transform:uppercase;
  color:var(--a-text-faint);
  writing-mode:vertical-rl; transform:rotate(180deg);
  text-align:center; display:flex; align-items:center; justify-content:center;
  pointer-events:none;
}
.axis-tick{position:absolute; left:56px; right:0; border-top:1px dashed var(--a-border-soft); pointer-events:none;}
.axis-tick span{position:absolute; left:-52px; top:-7px; font-family:var(--font-mono); font-size:9px; color:var(--a-text-faint);}

.columns{display:flex; flex:1; gap:14px;}
.col{flex:1; min-width:225px; position:relative;}
.col-head{
  height:64px; display:flex; flex-direction:column; justify-content:flex-end;
  padding-bottom:10px; border-bottom:2px solid var(--col-color,var(--a-accent)); margin-bottom:10px;
}
.col-head .tag{font-family:var(--font-mono); font-size:9.5px; letter-spacing:.12em; color:var(--col-color,var(--a-accent)); text-transform:uppercase; margin-bottom:4px;}
.col-head .name{font-size:13px; font-weight:700; color:var(--a-text); line-height:1.25;}
.col-body{
  position:relative; height:var(--ch);
  border-left:1px solid var(--a-border-soft); border-right:1px solid var(--a-border-soft);
  border-radius:4px;
  background:repeating-linear-gradient(to top, transparent 0, transparent calc(25% - 1px), var(--a-grid) calc(25% - 1px), var(--a-grid) 25%);
}
.col-connectors{position:absolute; inset:0; pointer-events:none; z-index:1;}
.connector-overlay{position:absolute; top:0; left:0; pointer-events:none; z-index:1; overflow:visible;}
.conn-line{opacity:0; transition:opacity .15s ease;}
.conn-line.active{opacity:0.7;}

/* ---------- Cert boxes ---------- */
.cert{
  position:absolute; left:6px; right:6px; height:var(--boxh);
  border-radius:5px; background:var(--a-panel2);
  border:1px solid var(--a-border);
  border-left:3px solid var(--v-color,var(--a-accent));
  display:flex; align-items:center; padding:0 22px 0 30px;
  cursor:pointer; z-index:2;
  transition:transform .12s ease, box-shadow .12s ease, border-color .12s ease, opacity .2s ease, background .15s ease;
  overflow:hidden;
}
.cert:hover{
  transform:translateX(2px) scale(1.02);
  border-color:var(--v-color,var(--a-accent));
  box-shadow:0 4px 18px -4px rgba(0,0,0,0.45), 0 0 0 1px var(--v-color,var(--a-accent));
  z-index:6;
}
.cert.dim{opacity:0.14;}
.cert.dim:hover{opacity:0.55;}
.cert.fade-out{opacity:0.08;}
.cert.search-dim{opacity:0.08;}
.cert.search-match{outline:2px solid var(--a-accent); outline-offset:1px;}
.cert.owned{background:color-mix(in srgb, var(--v-color,var(--a-accent)) 14%, var(--a-panel2)); border-color:var(--v-color,var(--a-accent));}
.cert.comparing{outline:2px solid var(--a-accent-warm); outline-offset:1px;}
.cert.suggest-flash{animation:suggestPulse 1.5s ease;}
@keyframes suggestPulse{
  0%,100%{box-shadow:0 0 0 0 rgba(0,0,0,0);}
  15%,45%{box-shadow:0 0 0 4px var(--v-color,var(--a-accent)); transform:translateX(2px) scale(1.03);}
}
.cert-label{
  font-family:var(--font-mono); font-weight:600; font-size:11.5px;
  color:var(--a-text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.cert-check{
  position:absolute; left:6px; top:50%; transform:translateY(-50%);
  width:19px; height:19px; border-radius:50%;
  border:2px solid var(--a-text-faint);
  z-index:3; display:flex; align-items:center; justify-content:center;
  font-size:12px; font-weight:700; color:transparent; background:var(--a-panel);
  transition:transform .1s ease, border-color .1s ease;
}
.cert-check:hover{border-color:var(--a-accent); transform:translateY(-50%) scale(1.12);}
.cert.owned .cert-check{background:var(--v-color,var(--a-accent)); border-color:var(--v-color,var(--a-accent)); color:#08111f;}

.cert-compare{
  position:absolute; right:4px; top:50%; transform:translateY(-50%);
  width:15px; height:15px; border-radius:3px;
  border:1.5px solid var(--a-text-faint); background:var(--a-panel);
  z-index:3; display:flex; align-items:center; justify-content:center;
  font-size:9px; color:var(--a-text-faint); opacity:0; cursor:pointer;
  transition:opacity .12s ease, border-color .12s ease, background .12s ease, color .12s ease;
}
.cert:hover .cert-compare{opacity:1;}
.cert-compare:hover{border-color:var(--a-accent-warm); color:var(--a-accent-warm);}
.cert.comparing .cert-compare{opacity:1; background:var(--a-accent-warm); border-color:var(--a-accent-warm); color:#1a1206;}

/* Touch devices: no hover, so the compare icon must always be visible, and tap
   targets get a bit larger for easier fingertip accuracy. */
@media (hover: none), (pointer: coarse){
  .cert-compare{opacity:0.85; width:18px; height:18px;}
  .cert-check{width:22px; height:22px;}
  .cert{padding-left:32px; padding-right:24px;}
}

.cert-status-dot{
  position:absolute; top:3px; right:3px; width:6px; height:6px; border-radius:50%;
  z-index:4; box-shadow:0 0 0 1.5px var(--a-panel2);
}
.cert-status-dot.status-new{background:var(--a-tier-b);}
.cert-status-dot.status-updated{background:var(--a-tier-i);}
.cert-status-dot.status-changing{background:var(--a-tier-a);}
.cert-status-dot.status-retired{background:var(--a-tier-e);}

/* ---------- Tooltip ---------- */
#tooltip{
  position:fixed; z-index:999; max-width:310px;
  background:var(--a-panel); border:1px solid var(--a-border); border-top:3px solid var(--v-color,var(--a-accent));
  border-radius:var(--r-md); padding:13px 15px;
  box-shadow:var(--shadow-lg);
  pointer-events:none; opacity:0; transform:translateY(4px);
  transition:opacity .12s ease, transform .12s ease;
}
#tooltip.show{opacity:1; transform:translateY(0);}
#tooltip .tt-vendor-row{display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;}
#tooltip .tt-vendor{font-family:var(--font-mono); font-size:10px; letter-spacing:.1em; text-transform:uppercase; color:var(--v-color,var(--a-accent)); margin-bottom:0;}
#tooltip .tt-status-badge{
  font-family:var(--font-mono); font-size:9px; letter-spacing:.04em; text-transform:uppercase;
  padding:2px 7px; border-radius:var(--r-pill); background:var(--a-panel2); color:var(--a-text-faint); border:1px solid var(--a-border);
}
#tooltip .tt-status-badge.status-new{color:var(--a-tier-b); border-color:var(--a-tier-b);}
#tooltip .tt-status-badge.status-updated{color:var(--a-tier-i); border-color:var(--a-tier-i);}
#tooltip .tt-status-badge.status-changing{color:var(--a-tier-a); border-color:var(--a-tier-a);}
#tooltip .tt-status-badge.status-retired{color:var(--a-tier-e); border-color:var(--a-tier-e);}
#tooltip .tt-name{font-size:var(--fs-sm); font-weight:700; color:var(--a-text); margin-bottom:8px; line-height:1.3;}
#tooltip .tt-row{display:flex; gap:8px; font-size:var(--fs-label); color:var(--a-text-dim); margin-bottom:5px; line-height:1.4;}
#tooltip .tt-row b{color:var(--a-text-faint); font-family:var(--font-mono); font-size:9.5px; letter-spacing:.06em; text-transform:uppercase; flex:0 0 62px; padding-top:1px;}
#tooltip .tt-metrics{margin-top:9px; padding-top:9px; border-top:1px solid var(--a-border-soft);}
#tooltip .tt-diff{display:flex; align-items:center; gap:8px; margin-bottom:6px;}
#tooltip .tt-diff:last-child{margin-bottom:0;}
#tooltip .tt-metric-label{font-family:var(--font-mono); font-size:9px; letter-spacing:.06em; text-transform:uppercase; color:var(--a-text-faint); flex:0 0 46px;}
#tooltip .tt-bar{flex:1; height:5px; border-radius:3px; background:var(--a-border); overflow:hidden;}
#tooltip .tt-bar i{display:block; height:100%; background:linear-gradient(to right, var(--a-tier-b), var(--a-tier-i), var(--a-tier-a), var(--a-tier-e));}
#tooltip .tt-bar.demand i{background:linear-gradient(to right, var(--a-text-faint), var(--a-accent));}
#tooltip .tt-diffnum{font-family:var(--font-mono); font-size:11px; color:var(--a-text); flex:none; width:38px; text-align:right;}
#tooltip .tt-cta{margin-top:9px; font-family:var(--font-mono); font-size:10.5px; color:var(--a-accent);}
#tooltip .tt-note{margin-top:7px; font-size:10.5px; color:var(--a-text-faint); font-style:italic; line-height:1.4;}

.atlas-scope footer{padding:var(--sp-4) var(--sp-5) var(--sp-8); color:var(--a-text-faint); font-size:var(--fs-label); line-height:var(--lh-body); max-width:1020px;}
.atlas-scope footer b{color:var(--a-text-dim);}

/* ---------- Compare panel ---------- */
.compare-panel{
  position:fixed; left:0; right:0; bottom:0; z-index:200;
  background:var(--a-panel); border-top:2px solid var(--a-accent-warm);
  box-shadow:var(--shadow-lg);
  transform:translateY(100%); transition:transform .22s ease;
  max-height:62vh; display:flex; flex-direction:column;
}
.compare-panel.show{transform:translateY(0);}
.compare-head{
  display:flex; justify-content:space-between; align-items:center;
  padding:var(--sp-3) var(--sp-5); border-bottom:1px solid var(--a-border-soft); flex:none;
}
.compare-title{font-family:var(--font-mono); font-size:var(--fs-label); letter-spacing:.06em; text-transform:uppercase; color:var(--a-accent-warm); font-weight:700;}
.compare-close{
  font-family:var(--font-mono); font-size:var(--fs-label); color:var(--a-text-faint);
  border:1px dashed var(--a-border); border-radius:var(--r-pill); padding:5px var(--sp-3); cursor:pointer; background:transparent;
}
.compare-close:hover{color:var(--a-accent-warm); border-color:var(--a-accent-warm);}
.compare-body{padding:var(--sp-4) var(--sp-5) var(--sp-5); overflow-y:auto; flex:1;}
.compare-hint{color:var(--a-text-faint); font-size:var(--fs-sm); font-style:italic;}
.compare-grid{display:grid; grid-template-columns:110px 1fr 1fr; gap:1px; background:var(--a-border-soft); border:1px solid var(--a-border-soft); border-radius:var(--r-md); overflow:hidden;}
.compare-grid > div{background:var(--a-panel); padding:9px 12px; font-size:var(--fs-sm); color:var(--a-text-dim);}
.compare-grid .cg-label{
  font-family:var(--font-mono); font-size:9.5px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--a-text-faint); background:var(--a-panel2); display:flex; align-items:center;
}
.compare-grid .cg-head{
  font-family:var(--font-mono); font-weight:700; font-size:var(--fs-body); color:var(--a-text);
  border-top:3px solid var(--v-color,var(--a-accent)); background:var(--a-panel2);
}
.compare-grid .cg-head small{display:block; font-family:var(--font-sans); font-weight:400; font-size:10.5px; color:var(--a-text-faint); margin-top:2px; text-transform:none; letter-spacing:0;}
.compare-grid .cg-bar{display:flex; align-items:center; gap:7px;}
.compare-grid .cg-bar-outer{flex:1; height:5px; border-radius:3px; background:var(--a-border); overflow:hidden;}
.compare-grid .cg-bar-outer i{display:block; height:100%; background:linear-gradient(to right,var(--a-tier-b),var(--a-tier-i),var(--a-tier-a),var(--a-tier-e));}
.compare-grid .cg-bar-num{font-family:var(--font-mono); font-size:11px; flex:none;}
.compare-remove{
  font-family:var(--font-mono); font-size:9px; color:var(--a-text-faint); border:1px solid var(--a-border);
  border-radius:var(--r-pill); padding:1px 7px; cursor:pointer; background:var(--a-panel); margin-top:6px; display:inline-block;
}
.compare-remove:hover{color:var(--a-accent-warm); border-color:var(--a-accent-warm);}

/* ---------- Floating search ---------- */
.float-search{
  position:fixed; right:28px; bottom:26px; z-index:250;
  display:flex; align-items:center; gap:6px;
  background:var(--a-panel); border:1.5px solid var(--a-border); border-radius:var(--r-pill);
  padding:9px 10px 9px 14px; box-shadow:var(--shadow-md);
  transition:border-color .15s ease, box-shadow .15s ease, bottom .22s ease;
  width:230px;
}
body.compare-open .float-search{ bottom:calc(62vh + 20px); }
.float-search:focus-within{border-color:var(--a-accent); box-shadow:var(--shadow-md), 0 0 0 3px color-mix(in srgb, var(--a-accent) 18%, transparent);}
.float-search .fs-icon{color:var(--a-text-faint); font-size:15px; flex:none;}
.float-search input{
  border:none; outline:none; background:transparent; color:var(--a-text);
  font-family:var(--font-sans); font-size:var(--fs-sm); flex:1; min-width:0;
}
.float-search input::placeholder{color:var(--a-text-faint);}
.fs-count{font-family:var(--font-mono); font-size:10px; color:var(--a-text-faint); flex:none; white-space:nowrap;}
.fs-clear{
  flex:none; border:none; background:transparent; color:var(--a-text-faint); cursor:pointer; font-size:12px;
  display:none; padding:2px;
}
.fs-clear.show{display:block;}
.fs-clear:hover{color:var(--a-accent-warm);}

.scroll-shell{-webkit-overflow-scrolling:touch;}

@media (max-width:720px){
  .atlas-scope header{padding:22px 16px 16px; flex-direction:column; gap:14px;}
  .header-actions{width:100%; justify-content:space-between;}
  .role-switch-btn{flex:1; justify-content:center;}
  .legend-wrap{padding:12px 16px;}
  .legend-title{margin-top:0 !important;}
  .progress-wrap{padding:12px 16px 10px; gap:10px;}
  .stat-block{flex-basis:calc(50% - 5px);}
  .stat-block.stat-primary, .stat-block.stat-progress{flex-basis:100%;}
  .progress-text-wrap{padding:0 16px 12px;}
  .suggest-wrap{padding:12px 16px 14px;}
  .suggest-chip{min-width:160px; flex-basis:calc(50% - 5px);}
  .atlas-scope main, .atlas-scope footer{padding-left:16px; padding-right:16px;}
  .atlas-scope footer{padding-top:16px; padding-bottom:100px;}
  .chart{padding:18px 16px 10px;}
  .col{min-width:172px;}
  .axis{flex-basis:38px;}
  .axis-tick{left:38px;}
  .axis-tick span{left:-34px; font-size:8px;}
  #tooltip{max-width:calc(100vw - 32px);}
  .float-search{right:12px; bottom:14px; width:168px; padding:8px 8px 8px 12px;}
  .float-search input{font-size:12px;}
  .compare-grid{grid-template-columns:80px 1fr 1fr; font-size:10.5px;}
  .compare-panel{max-height:70vh;}
  .role-gate{padding:36px 16px;}
  .role-gate-sub{margin-bottom:28px;}
  .role-grid{grid-template-columns:1fr;}
}

@media (max-width:420px){
  .float-search{width:auto; flex:0 0 auto;}
  .float-search input{width:90px;}
}

/* ===========================================================================
   PLANS (competitor comparison) -- static page reachable via #/plans, same
   VIEWS/showView mechanism as Dashboard/Atlas. A normal scrolling page like
   Dashboard, so no layout-flush/layout-atlas class is needed for it.
   =========================================================================== */

.plans-scope { max-width: var(--page-max); margin: 0 auto; padding: var(--sp-6) var(--sp-5) var(--sp-8); }

.plans-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-6);
  flex-wrap: wrap;
  margin-bottom: var(--sp-5);
}
.plans-head-copy h2 { margin-top: var(--sp-2); max-width: 22ch; }
.plans-head-note {
  color: var(--text-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  max-width: 34ch;
  margin: 0;
  flex-shrink: 0;
}

.plans-table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--shadow-sm);
}

.plans-table { width: 100%; min-width: 1040px; border-collapse: collapse; }

.plans-table th,
.plans-table td {
  padding: var(--sp-4);
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--border-subtle);
}
.plans-table tbody tr:last-child th,
.plans-table tbody tr:last-child td { border-bottom: none; }

.plans-table thead th {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-3);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.plans-table thead .plans-row-head { color: var(--text-3); }

.plans-table tbody th {
  font-weight: 600;
  color: var(--text);
  font-size: var(--fs-sm);
  min-width: 200px;
}

.plans-row-sub {
  display: block;
  font-weight: 400;
  font-size: var(--fs-label);
  color: var(--text-3);
  margin-top: 3px;
  line-height: var(--lh-snug);
}

/* Per-vendor price, under the name in each header cell -- distinct from
   .plans-row-sub (which annotates a row's criterion, not a column's
   vendor) so it can override thead th's own uppercase/letter-spacing,
   which a price string like "$0/mo" shouldn't inherit. */
.plans-col-price {
  display: block;
  font-weight: 400;
  font-size: var(--fs-label);
  text-transform: none;
  letter-spacing: normal;
  color: var(--text-3);
  margin-top: 3px;
}
.plans-table thead .plans-col-us .plans-col-price { color: var(--success); opacity: 0.8; }

.plans-table-footnote {
  margin-top: var(--sp-3);
  font-size: var(--fs-label);
  color: var(--text-3);
}

/* Faultline's own column reads as the "home" column, same idea as the
   reference comparison's shaded column -- a soft brand tint plus a bolder
   header, so the eye lands there first without needing a legend. */
.plans-table .plans-col-us {
  background: var(--success-soft);
}
.plans-table thead .plans-col-us {
  color: var(--success);
  font-weight: 700;
}

.plans-mark {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: var(--fs-sm);
  white-space: nowrap;
}
.plans-mark.yes { color: var(--success); font-weight: 650; }
.plans-mark.partial { color: var(--warning); font-weight: 650; }
.plans-mark.no { color: var(--text-3); font-weight: 500; }

.plans-mark .plans-ico { flex: none; }
.plans-mark .plans-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.plans-mark .plans-dash {
  font-family: var(--font-mono);
  flex: none;
}

@media (max-width: 720px) {
  .plans-head { flex-direction: column; align-items: flex-start; }
  .plans-head-note { max-width: none; }
}

/* ---------------------------------------------------------------------
   PLANS -- pricing tier cards (potential future subscription plans,
   shown above the competitor comparison table). Billing doesn't exist
   yet -- every CTA opens the same "coming soon" auth-modal as the
   appbar's Sign in button (see app.js), so this stays an honest preview
   of the tier shape rather than a working checkout.
   --------------------------------------------------------------------- */

.plans-tiers-head { margin-bottom: var(--sp-5); }
.plans-tiers-head h2 { margin-top: var(--sp-2); max-width: 30ch; }
.plans-tiers-sub { color: var(--text-3); font-size: var(--fs-sm); max-width: 60ch; margin: var(--sp-2) 0 0; }

.plans-tiers {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.12fr) minmax(0, 1fr);
  gap: var(--sp-5);
  margin-bottom: var(--sp-8);
  align-items: stretch;
}
.plans-tiers > .plans-tier { min-height: 100%; }
.plans-tiers > .plans-tier.featured { min-height: calc(100% + 12px); }

.plans-tier {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  position: relative;
}
/* The middle "Pro" tier reads as the recommended plan -- lifted slightly
   and outlined in brand colour, the same "this is the one" cue the
   reference pricing mock gives its centre column. */
.plans-tier.featured {
  border-color: var(--brand-500);
  box-shadow: var(--shadow-lg);
  transform: translateY(-6px) scale(1.04);
  z-index: 1;
}
.plans-tier-flag {
  position: absolute;
  top: var(--sp-3);
  right: var(--sp-3);
  background: var(--brand-500);
  color: var(--brand-on);
  font-size: var(--fs-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 3px 9px;
  border-radius: var(--r-pill);
}

.plans-tier-band {
  padding: var(--sp-3) var(--sp-5);
  font-weight: 700;
  font-size: var(--fs-lg);
  color: var(--text);
  background: var(--surface-2);
  border-bottom: 1px solid var(--border-subtle);
}
.plans-tier.featured .plans-tier-band { background: var(--brand-soft); color: var(--brand-400); }

.plans-tier-body { padding: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-4); flex: 1; }

.plans-tier-price { display: flex; align-items: baseline; gap: 4px; }
.plans-tier-amount { font-family: var(--font-mono); font-size: var(--fs-display); font-weight: 700; line-height: 1; color: var(--text); }
.plans-tier-amount-strike { font-family: var(--font-mono); font-size: var(--fs-lg); font-weight: 700; line-height: 1; color: var(--text-3); text-decoration: line-through; margin-right: 2px; }
.plans-tier-period { color: var(--text-3); font-size: var(--fs-sm); }
.plans-tier-badge {
  align-self: flex-start;
  background: var(--brand-soft);
  color: var(--brand-400);
  font-size: var(--fs-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 2px 8px;
  border-radius: var(--r-pill);
}

.plans-tier-features { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); flex: 1; }
.plans-tier-features li { display: flex; align-items: center; gap: 8px; font-size: var(--fs-sm); }
.plans-tier-features li.yes { color: var(--text); }
.plans-tier-features li.yes .plans-ico { color: var(--success); flex: none; }
.plans-tier-features li.partial { color: var(--text-2); }
.plans-tier-features li.partial .plans-dot { background: var(--warning); width: 6px; height: 6px; border-radius: 50%; flex: none; }
.plans-tier-features li.no { color: var(--text-3); }
.plans-tier-features li.no .plans-dash { font-family: var(--font-mono); flex: none; }

.plans-tier-cta { width: 100%; justify-content: center; margin-top: auto; }

@media (max-width: 900px) {
  .plans-tiers { grid-template-columns: 1fr; }
  .plans-tiers > .plans-tier.featured { min-height: 0; }
  .plans-tier.featured { transform: none; }
}

/* ---------------------------------------------------------------------
   BLUEPRINT

   Two surfaces: the index (a plain list of saved topologies, ordinary
   scrolling page) and the editor, which is viewport-locked exactly like
   the workspace -- the canvas IS the page, so it takes the height rather
   than sitting inside a document that scrolls out from under a drag.

   Everything here reads the app's own tokens. The canvas is the one place
   that needed new colours at all, and even those are derived from the
   existing ramp with color-mix rather than being a private palette, so
   light/dark switching costs nothing.
   --------------------------------------------------------------------- */

/* --- index ---------------------------------------------------------- */
.bp-index-list { display: grid; gap: var(--sp-3); }

.bp-card {
  display: flex;
  align-items: center;
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-lg);
  padding-right: var(--sp-4);
}
.bp-card:hover { border-color: var(--border); background: var(--surface-2); }
.bp-card-open {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex: 1;
  min-width: 0;
  padding: var(--sp-4) var(--sp-5);
  text-decoration: none;
  color: inherit;
}
.bp-card-delete { color: var(--text-3); }
.bp-card-delete:hover { color: var(--danger); background: var(--danger-soft); }
.bp-card-icon {
  display: flex;
  color: var(--brand-400);
  background: var(--brand-soft);
  border-radius: var(--r-sm);
  padding: 9px;
}
.bp-card-body { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.bp-card-body strong { font-size: var(--fs-body); }
.bp-card-body em { font-style: normal; font-size: var(--fs-label); color: var(--text-3); }
.bp-card-go { color: var(--text-3); display: flex; }

.bp-index-empty { text-align: center; display: grid; gap: var(--sp-3); justify-items: center; padding: var(--sp-7) var(--sp-5); }
.bp-index-empty h3 { margin: 0; }
.bp-index-empty p { color: var(--text-2); max-width: 56ch; margin: 0; }
.bp-index-empty svg { color: var(--text-3); }

/* --- editor shell ---------------------------------------------------- */
body.view-blueprint-editor { height: 100vh; overflow: hidden; }

.bp-editor { display: flex; flex-direction: column; flex: 1; min-height: 0; gap: var(--sp-3); }

.bp-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--border-subtle);
}
.bp-name-input { width: 260px; font-weight: 600; }
.bp-vendor { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-label); color: var(--text-3); }
.bp-vendor .input { width: auto; padding: 7px 10px; }

.bp-status { font-size: var(--fs-label); color: var(--text-3); min-width: 96px; text-align: right; }
.bp-status.dirty { color: var(--warning); }
.bp-status.error { color: var(--danger); }

.bp-body {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 216px minmax(0, 1fr) 312px;
  gap: var(--sp-3);
}

/* --- palette --------------------------------------------------------- */
.bp-palette {
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  overflow: auto;
}
.bp-palette-head { margin: 0 0 var(--sp-1); font-size: var(--fs-micro); letter-spacing: .08em; text-transform: uppercase; color: var(--text-3); }
.bp-palette-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  text-align: left;
  padding: var(--sp-3);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: inherit;
  font: inherit;
  cursor: grab;
}
.bp-palette-item:hover { border-color: var(--brand-500); background: var(--surface-3); }
.bp-palette-item:active { cursor: grabbing; }
.bp-palette-item span:last-child { display: flex; flex-direction: column; gap: 2px; }
.bp-palette-item strong { font-size: var(--fs-sm); }
.bp-palette-item em { font-style: normal; font-size: var(--fs-micro); color: var(--text-3); }
.bp-palette-icon { display: flex; color: var(--brand-400); }
.bp-palette-hint { margin: var(--sp-3) 0 0; font-size: var(--fs-micro); line-height: 1.6; color: var(--text-3); }
.bp-palette-hint kbd {
  font-family: var(--font-mono);
  font-size: 10px;
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0 4px;
}

/* --- canvas ---------------------------------------------------------- */
.bp-canvas-wrap {
  position: relative;
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-lg);
  background-color: var(--surface-1);
  /* A faint dot grid, the way every diagram tool signals "this is a
     canvas, things go here". Decorative only -- nothing snaps to it. */
  background-image: radial-gradient(circle, color-mix(in srgb, var(--border) 55%, transparent) 1px, transparent 1px);
  background-size: 22px 22px;
  overflow: hidden;
}
.bp-canvas { width: 100%; height: 100%; display: block; cursor: grab; touch-action: none; }
.bp-canvas:active { cursor: grabbing; }

.bp-canvas-empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--sp-3);
  pointer-events: none;
  color: var(--text-3);
}
.bp-canvas-empty p { margin: 0; font-size: var(--fs-sm); }

.bp-zoom { position: absolute; right: var(--sp-3); bottom: var(--sp-3); display: flex; gap: var(--sp-1); }

/* Nodes. Fill/stroke rather than CSS box styling, since these are SVG. */
.bp-node { cursor: grab; }
.bp-node:active { cursor: grabbing; }
.bp-node-box { fill: var(--surface-2); stroke: var(--border); stroke-width: 1.4; }
.bp-node:hover .bp-node-box { stroke: var(--border-strong); }
.bp-node.selected .bp-node-box { stroke: var(--brand-400); stroke-width: 2.2; }
.bp-node.error .bp-node-box { stroke: var(--danger); }
.bp-node.warning .bp-node-box { stroke: var(--warning); }
.bp-node-icon { color: var(--brand-400); }
.bp-node.switch .bp-node-icon { color: var(--info); }
.bp-node.host .bp-node-icon { color: var(--text-2); }
.bp-node-name { fill: var(--text); font-size: 13px; font-weight: 650; }
.bp-node-type { fill: var(--text-3); font-size: 10.5px; letter-spacing: .04em; text-transform: uppercase; }
/* An addressed port with no cable in it -- the one piece of addressing
   that has no link label to live on. Sits under the box, deliberately
   quieter than a real cable label. */
.bp-node-stranded { fill: var(--text-3); font-size: 9.5px; font-family: var(--font-mono); }
.bp-node-flag.error { fill: var(--danger); }
.bp-node-flag.warning { fill: var(--warning); }
.bp-node text { pointer-events: none; user-select: none; }

/* The cable handle: invisible until the device is hovered or selected,
   so a canvas of ten devices isn't a canvas of ten floating buttons. */
.bp-handle { opacity: 0; cursor: crosshair; }
.bp-node:hover .bp-handle,
.bp-node.selected .bp-handle { opacity: 1; }
.bp-handle-hit { fill: transparent; }
.bp-handle-dot { fill: var(--brand-500); stroke: var(--surface-1); stroke-width: 1.5; }
.bp-handle-plus { stroke: var(--brand-on); stroke-width: 1.6; stroke-linecap: round; }

/* Links. The hit line is a wide transparent stroke under the visible one
   -- a 2px line is close to impossible to click. */
.bp-link-hit { stroke: transparent; stroke-width: 16; cursor: pointer; }
.bp-link-line { stroke: var(--border-strong); stroke-width: 2; }
.bp-link.selected .bp-link-line { stroke: var(--brand-400); stroke-width: 3; }
.bp-link.error .bp-link-line { stroke: var(--danger); }
.bp-link.warning .bp-link-line { stroke: var(--warning); }
.bp-link-ghost { stroke: var(--brand-400); stroke-width: 2; stroke-dasharray: 5 4; pointer-events: none; }
/* Cable labels: a background pill behind the text, EVE-NG's own
   convention and the same one the lab's topology exhibit uses -- without
   one, text this small reads as noise against the link lines, the grid
   dots and other labels crossing behind it. The pill replaced a
   paint-order text halo, which could not cope with a two-line label. */
.bp-link-label { pointer-events: none; user-select: none; }
.bp-link-pill { fill: var(--surface-1); stroke: var(--border-subtle); stroke-width: 1; }
.bp-link-iface { fill: var(--text-2); font-size: 9.5px; font-family: var(--font-mono); }
.bp-link-ip { fill: var(--text-3); font-size: 8.5px; font-family: var(--font-mono); }
.bp-link.selected .bp-link-pill { stroke: var(--brand-400); }
.bp-link.selected .bp-link-iface { fill: var(--text); }

/* --- inspector -------------------------------------------------------- */
.bp-inspector {
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.bp-insp-head { display: flex; align-items: center; gap: var(--sp-3); }
.bp-insp-head h3 { margin: 0; font-size: var(--fs-lg); flex: 1; min-width: 0; overflow-wrap: anywhere; }
.bp-insp-section { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-2); }
.bp-insp-section h4 { margin: 0; flex: 1; font-size: var(--fs-label); letter-spacing: .08em; text-transform: uppercase; color: var(--text-3); }
.bp-insp-hint { margin: 0; font-size: var(--fs-micro); line-height: 1.6; color: var(--text-3); }

.bp-field { display: flex; flex-direction: column; gap: var(--sp-1); font-size: var(--fs-label); color: var(--text-2); }
.bp-field span em { font-style: normal; color: var(--text-3); }
.bp-check { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-micro); color: var(--text-3); }

.bp-facts { display: grid; gap: var(--sp-1); margin: 0; }
.bp-facts > div { display: flex; justify-content: space-between; gap: var(--sp-3); font-size: var(--fs-sm); }
.bp-facts dt { color: var(--text-3); }
.bp-facts dd { margin: 0; color: var(--text); font-variant-numeric: tabular-nums; }

.bp-port {
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-sm);
  padding: var(--sp-3);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  background: var(--surface-2);
}
.bp-port-head { display: flex; align-items: center; gap: var(--sp-2); }
.bp-port-head strong { font-size: var(--fs-sm); font-family: var(--font-mono); }
.bp-port-peer { flex: 1; font-size: var(--fs-micro); color: var(--text-3); }

.bp-vlan-row { display: flex; align-items: center; gap: var(--sp-2); }
.bp-vlan-id { font-size: var(--fs-micro); color: var(--text-3); white-space: nowrap; }

.bp-insp-issues { display: grid; gap: var(--sp-2); }

/* --- issue bar --------------------------------------------------------- */
.bp-issues { display: flex; flex-wrap: wrap; gap: var(--sp-2); min-height: 30px; align-items: center; }
.bp-issue {
  font-size: var(--fs-label);
  line-height: 1.4;
  padding: 5px 10px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-subtle);
  background: var(--surface-2);
  color: var(--text-2);
  text-align: left;
  font-family: inherit;
}
button.bp-issue { cursor: pointer; }
button.bp-issue:hover { border-color: var(--border-strong); }
.bp-issue.error { background: var(--danger-soft); border-color: color-mix(in srgb, var(--danger) 40%, transparent); color: var(--danger); }
.bp-issue.warning { background: var(--warning-soft); border-color: color-mix(in srgb, var(--warning) 40%, transparent); color: var(--warning); }
.bp-issue.ok { background: var(--success-soft); border-color: color-mix(in srgb, var(--success) 35%, transparent); color: var(--success); }

/* The free-build note that replaces the Objectives list in a Blueprint
   lab -- see applyBlueprintLabChrome(). */
.task.bp-free-build .task-marker { color: var(--brand-400); display: flex; align-items: center; justify-content: center; }

/* A launch refused for topology reasons lists one issue per line, so the
   error modal has to keep the newlines it is given. */
#error-modal-message { white-space: pre-line; }

@media (max-width: 1180px) {
  /* Below this the three-column editor squeezes the canvas to nothing.
     The canvas keeps the full width and the two side panels sit under it
     as an ordinary scrolling page -- the same trade the workspace makes
     at its own breakpoint. */
  body.view-blueprint-editor { height: auto; overflow: visible; }
  .bp-body { grid-template-columns: 1fr; }
  .bp-canvas-wrap { height: 60vh; }
  .bp-inspector { max-height: none; }
}

/* A Blueprint (free-build) lab has no objectives, so #ws-panel is hidden
   and the topology takes the whole left column instead -- see
   applyBlueprintLabChrome(). The card is the same live #topo-card the
   normal layout parks under the terminal, so it needs its .term-col
   sizing undone rather than a second set of rules: fill the column,
   no top margin, and let the terminal have the entire right-hand side. */
body.lab-blueprint .ws-left > .topo-card {
  flex: 1 1 auto;
  min-height: 0;
  margin-top: 0;
  border: none;
  border-radius: 0;
  background: transparent;
}
body.lab-blueprint .ws-left > .topo-card > .topo-body { flex: 1 1 auto; min-height: 0; }

/* Terminal 60 / scratchpad 40 down the right-hand column. An explicit
   basis on both, not flex:1 on the terminal: xterm sizes itself from the
   box it is given (fitTerminal()), so "whatever is left over" would make
   the terminal's own row count depend on how the notes box happened to
   measure that frame. */
body.lab-blueprint .term-col > .terminal-pane { flex: 0 0 60%; min-height: 160px; }
body.lab-blueprint .term-col > .notes-card {
  flex: 1 1 0;
  min-height: 120px;
  margin-top: var(--sp-4);
}

.notes-card {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-md);
  overflow: hidden;
}
.notes-head { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4) var(--sp-2); }
.notes-status { font-size: var(--fs-micro); color: var(--text-3); }
.notes-text {
  flex: 1 1 auto;
  min-height: 0;
  resize: none;
  border: none;
  outline: none;
  background: transparent;
  color: var(--text);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  line-height: 1.65;
  padding: 0 var(--sp-4) var(--sp-3);
  width: 100%;
}
.notes-text::placeholder { color: var(--text-3); }
/* The pad is where someone writes an addressing plan, so it gets the same
   focus treatment as a real input rather than none at all. */
.notes-card:focus-within { border-color: var(--border); }
