/* Design tokens. Light is the default; dark mirrors the previous theme.

   The governing rule for colour: neutral is the resting state, and colour is
   only spent where it carries information somebody has to act on. "Nothing is
   wrong" is the most common state in this app, so it is the quietest — green
   survives only inside word diffs, where it distinguishes added from removed
   text and therefore earns its ink. */
:root {
  --bg-main: #f6f7f9;
  --bg-second: #ffffff;
  --bg-third: #eceef2;
  --bg-muted: #dfe2e8;
  --text-main: #101115;
  --text-second: #5a5f6a;
  --text-faint: #8a8f99;
  --text-on-solid: #ffffff;

  --accent: #ffda35;
  --text-on-accent: #0f1115;

  /* Not a severity: this is the colour of a link, and nothing else. */
  --link: #35708f;

  /* The five states an item on this dashboard can be in, and the only five
     colours that ever mean something: alert (broken, act now), review (changed,
     worth a look), accepted (intentional, quiet on purpose), ok/clean (nothing
     to report — no colour, see --ok's comment below). Any new component reads
     status from these tokens rather than picking a new hue. */
  /* Cinnabar, from the client's own reference palette (mystic / sinbad / san
     marino / pickled bluewood / cinnabar) — replaces the old alert red with
     the same hue in both themes; it already clears WCAG contrast against
     both a white and a dark card (4.0:1 / 4.3:1). */
  --alert: #e2474b;
  /* A fresh, more openly "yellow" gold — distinct from --accent (the brand
     mark's lemon yellow) on purpose, same reasoning as the file's original
     review colour: warmer and dimmer than the accent so the two never read
     as the same colour. This dark a gold is only needed where it paints
     small text (pills, badges) and has to clear 4.5:1 on a white card —
     any brighter and it fails AA. See --review-vivid below for every other
     use, where that darkness just reads as brown. */
  --review: #96690a;
  /* Same hue, lit up: for large bold numerals (which only need 3:1 as
     "large text"), icons, ring strokes and cell/bar fills — none of which
     are small body text — where --review's darkness for AA reads as mud
     rather than the yellow it is supposed to be. */
  --review-vivid: #b8860b;
  /* A finding somebody has agreed is meant to be there. Deliberately quiet: it
     must be visible without competing with the things that still need doing. */
  --accepted: #6b7280;
  /* Reserved for word-diff additions, never for a clean page or a good number —
     see the file header comment. */
  --ok: #5f8300;

  --alert-soft: rgba(226, 71, 75, 0.1);
  --review-soft: rgba(150, 105, 10, 0.12);
  --accepted-soft: rgba(107, 114, 128, 0.12);
  --ok-soft: rgba(95, 131, 0, 0.12);

  /* Border-strength tints: enough to read as "this container is about a
     problem" without the full-strength hue shouting from an outline. */
  --alert-line: rgba(226, 71, 75, 0.32);
  --review-line: rgba(150, 105, 10, 0.3);

  --add: #4a7c00;
  --del: #c0392b;

  /* Decorative categorical palette: used only where colour is telling apart
     unrelated categories that carry no severity of their own (defect-type
     bars, KPI icon badges), never to grade a page's own status — that job
     stays with the five state tokens above, and OK/clean pages never touch
     this palette. All four non-red steps of the client's reference palette
     (mystic / sinbad / san marino / pickled bluewood) feed this ramp — not
     just the two darkest — so the chart reads as an actual sequence of
     their colours instead of one hue tinted three times. Sinbad is darkened
     for use here since at full brightness it fails contrast as text on a
     white card; mystic is reserved for -soft backgrounds for the same
     reason. Red/yellow are left out on purpose — they are --alert/--review —
     so a defect-type bar never gets mistaken for a severity signal. */
  --cat-0: #406d96; /* san marino */
  --cat-1: #2f3a56; /* pickled bluewood */
  --cat-2: #5c7278; /* sinbad, darkened for contrast */
  --cat-3: color-mix(in srgb, var(--cat-0) 50%, var(--cat-1) 50%);
  --cat-4: color-mix(in srgb, var(--cat-2) 60%, var(--cat-1) 40%);
  --cat-0-soft: color-mix(in srgb, var(--cat-0) 14%, transparent);
  --cat-1-soft: color-mix(in srgb, var(--cat-1) 14%, transparent);
  --cat-2-soft: color-mix(in srgb, var(--cat-2) 14%, transparent);

  /* A numeric delta genuinely improving or worsening (fewer/more alerts than
     last run) is not the same claim as "this page is fine" — it is allowed
     to say so in colour without touching --ok, which stays reserved for word
     diffs per the file header. */
  --delta-good: #2e9e5b;
  --delta-bad: var(--alert);

  --overlay: rgba(16, 17, 21, 0.45);

  /* No dedicated highlight for the active nav row in light theme yet — it
     reuses the same elevated surface as everything else, which is correct
     here since light theme has no separate reference to match. */
  --nav-highlight: var(--bg-third);

  --radius-md: 6px;
  --radius-lg: 8px;
  --radius-xl: 12px;
  --radius-pill: 999px;

  --mono: ui-monospace, "SF Mono", "Geist Mono", Menlo, "Cascadia Mono",
    "Segoe UI Mono", Consolas, monospace;

  /* Elevation is reserved for things that actually float above the page. A card
     already separates itself with a border and a background step; a shadow on
     top of that is a third separation nobody asked for. */
  --shadow-card: none;
  --shadow-float: 0 4px 12px rgba(16, 17, 21, 0.08), 0 1px 3px rgba(16, 17, 21, 0.06);
  --shadow-pop: 0 18px 48px rgba(16, 17, 21, 0.18), 0 2px 8px rgba(16, 17, 21, 0.08);
}

[data-theme="dark"] {
  --bg-main: #101115;
  --bg-second: #181c24;
  --bg-third: #222834;
  --bg-muted: #313847;
  --text-main: #ffffff;
  --text-second: #8b8f98;
  --text-faint: #6b6f78;
  --text-on-solid: #ffffff;

  /* Same value Gitterdone (Buena's own task tracker, same design system)
     uses for its active nav row — one step lighter than --bg-third so
     "where am I" reads as its own surface, not just another elevated
     panel. */
  --nav-highlight: #2c3038;

  --link: #6493b0;

  /* Same cinnabar as light theme — it already clears contrast against both
     dark backgrounds (4.3:1 / 4.7:1), so unlike the old alert red it needs no
     separate dark-theme weight. */
  --alert: #e2474b;
  /* Was the same yellow as --accent, so "needs review" and the brand mark were
     literally indistinguishable. Brighter, more openly yellow than the old
     gold, still warmer and dimmer than the accent. */
  --review: #f0c04a;
  /* Already bright enough on a dark card to read as yellow everywhere, so
     unlike light theme it does not need a separate, darker AA-safe weight. */
  --review-vivid: var(--review);
  --accepted: #8b93a3;
  --ok: #a2c52d;

  --alert-soft: rgba(226, 71, 75, 0.16);
  --review-soft: rgba(240, 192, 74, 0.14);
  --accepted-soft: rgba(139, 147, 163, 0.16);
  --ok-soft: rgba(162, 197, 45, 0.16);

  --alert-line: rgba(226, 71, 75, 0.38);
  --review-line: rgba(240, 192, 74, 0.34);

  --add: #a2c52d;
  --del: #de594f;

  /* Same reference palette as light theme, mirrored: mystic and sinbad read
     against a dark card the way san marino and pickled bluewood read
     against a white one, so the two darkest reference steps get lightened
     instead for the other two slots — same four-colour ramp as light theme,
     not just sinbad tinted three times. */
  --cat-0: #d8e8e8; /* mystic */
  --cat-1: #a8d0da; /* sinbad */
  --cat-2: #96afc5; /* san marino, lightened for contrast */
  --cat-3: #8d93a2; /* pickled bluewood, lightened for contrast */
  --cat-4: color-mix(in srgb, var(--cat-2) 50%, var(--cat-1) 50%);
  --cat-0-soft: color-mix(in srgb, var(--cat-0) 18%, transparent);
  --cat-1-soft: color-mix(in srgb, var(--cat-1) 18%, transparent);
  --cat-2-soft: color-mix(in srgb, var(--cat-2) 18%, transparent);

  --delta-good: #3ecf72;
  --delta-bad: var(--alert);

  --overlay: rgba(0, 0, 0, 0.55);

  --shadow-card: none;
  --shadow-float: 0 4px 14px rgba(0, 0, 0, 0.4);
  --shadow-pop: 0 18px 48px rgba(0, 0, 0, 0.55);
}

/* Type scale. Ten steps replacing seventeen ad-hoc sizes, several of which
   differed by half a pixel and so read as accidents rather than decisions. */
:root {
  --fs-2xs: 11px;
  --fs-xs: 12px;
  --fs-sm: 13px;
  /* Body/reading text floor: 14px, so the smallest text a user actually
     reads (not decorative labels/badges) never dips below the desktop
     minimum. */
  --fs-base: 14px;
  --fs-md: 16px;
  --fs-lg: 18px;
  --fs-xl: 22px;
  --fs-2xl: 27px;
  --fs-3xl: 33px;
  /* Used once, for the health score: the single flagship number of the whole
     dashboard, a step above every KPI value it outranks. */
  --fs-4xl: 42px;

  --track-caps: 0.06em;
  --track-tight: -0.02em;
  --lh-tight: 1.25;
  --lh-normal: 1.5;

  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 14px;
  --sp-4: 18px;
  --sp-5: 24px;
  --sp-6: 28px;
  --sp-8: 40px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: "Satoshi", system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--bg-main);
  color: var(--text-main);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* app.js builds most titles with el("strong", ...) and el("b", ...), so the
   browser default of 700 was setting the weight for half the interface. In a
   geometric face like Satoshi, 500 at 13px already reads as hierarchy; 700 at
   10.5px only reads as noise. Weight 700 is now reserved for the large
   numbers, and every case that needs it says so explicitly. */
strong, b { font-weight: 500; }

/* Counters must not change width between runs. Same rule for the font: these
   are stats to read at a glance, not code, so they stay in the body face
   (Satoshi) instead of --mono — mono is reserved for things that are
   literally code-like (paths, dates, selectors). */
.kpi-value, .rv-item strong, .bar-count, .bar-pct, .col-chart-value,
.coverage-key b, .badge, .nav-badge, .scope-stat strong, .st-stat strong,
.kpi-delta, .mini-bar-points, .why-score-points, .why-score-total {
  font-variant-numeric: tabular-nums;
}

/* Every interactive element here is a <button>; leaning on the UA ring over a
   custom-coloured card looks like a rendering accident rather than focus. */
:where(a, button, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--text-main);
  outline-offset: 2px;
  border-radius: inherit;
}
:where(a, button, summary):focus:not(:focus-visible) { outline: none; }

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

.hidden { display: none !important; }

/* ------------------------------------------------------------------- icons */
/* stroke, fill and the stroke-* properties are all inherited, so they reach
   into the shadow tree a <use> creates and one rule dresses the whole set. */
.icon {
  width: 16px; height: 16px; flex: none; display: block;
  fill: none; stroke: currentColor;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
}
.icon-sm { width: 14px; height: 14px; }
.icon-2xs { width: 11px; height: 11px; stroke-width: 2; }

/* A disclosure chevron drawn from two borders, shared by every <details> in the
   app. It rotates rather than swapping ▸ for ▾, so the control moves instead of
   blinking — and it inherits colour and optical weight from its own text. */
.dx-check summary::before,
.dx-evidence summary::before,
.mark-log summary::before,
.text-expand summary::before,
.why-score summary::before,
.bar-list-more summary::before,
.scope-check summary::after {
  content: ""; display: inline-block; width: 5px; height: 5px; flex: none;
  border-right: 1.4px solid currentColor; border-bottom: 1.4px solid currentColor;
  transform: rotate(-45deg); transform-origin: 60% 60%;
  transition: transform 0.16s ease;
  opacity: 0.6;
}
.dx-check summary::before, .dx-evidence summary::before, .mark-log summary::before, .text-expand summary::before, .why-score summary::before, .bar-list-more summary::before { margin-right: 8px; vertical-align: 1px; }
.dx-check[open] summary::before,
.dx-evidence[open] summary::before,
.mark-log[open] summary::before,
.text-expand[open] summary::before,
.why-score[open] summary::before,
.bar-list-more[open] summary::before,
.scope-check[open] summary::after { transform: rotate(45deg); }

/* A long piece of copied page text (a heading, a paragraph) collapsed behind
   its own truncated preview, so a single long finding does not push every
   other finding on the page out of view. Same disclosure chevron as every
   other <details> above. */
.text-expand summary {
  cursor: pointer; list-style: none; color: var(--text-faint);
  font-size: var(--fs-sm); line-height: 1.5;
}
.text-expand summary::-webkit-details-marker { display: none; }
.text-expand summary:hover { color: var(--text-second); }
.text-expand p { margin-top: 6px !important; }
.text-expand[open] summary { color: var(--text-second); margin-bottom: 2px; }

/* ------------------------------------------------------------------ shell */
.shell { display: flex; min-height: 100vh; }

.sidebar {
  width: 232px;
  flex-shrink: 0;
  border-right: 1px solid var(--bg-third);
  background: var(--bg-second);
  padding: 20px 14px;
  display: flex;
  flex-direction: column;
  gap: 22px;
  position: sticky;
  top: 0;
  height: 100vh;
}

/* Every left edge in this 232px column sits on 12px. There used to be five
   different insets here (6 / 4 / 10 / 11 / 12); nothing looked broken, but the
   column never resolved into a single vertical line either. */
.brand { display: flex; align-items: center; gap: 12px; padding: 0 var(--sp-3); }
/* The same mark as the browser tab, so the favicon is recognisably this product
   rather than a default globe. */
.logo-mark {
  display: grid; place-items: center;
  width: 32px; height: 32px; flex: none;
  border-radius: var(--radius-lg);
  background: var(--accent);
  color: var(--text-on-accent);
}
.logo-mark svg { width: 32px; height: 32px; display: block; }
.brand-text { display: flex; flex-direction: column; min-width: 0; }
.brand-text strong { font-size: var(--fs-base); font-weight: 500; letter-spacing: -0.01em; }
.site { font-size: var(--fs-xs); color: var(--text-second); text-decoration: none; }
.site:hover { text-decoration: underline; }

/* ------------------------------------------------------- domain switcher */
.site-switch { display: flex; flex-direction: column; gap: 6px; }
.side-label {
  font-size: var(--fs-2xs); font-weight: 500;
  text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--text-faint); padding: 0 var(--sp-3);
}
.site-tabs { display: flex; flex-direction: column; gap: 3px; }
.site-tab {
  position: relative;
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px 26px 8px var(--sp-3);
  border: 1px solid transparent; border-radius: var(--radius-lg);
  background: transparent; color: var(--text-second);
  font-family: inherit; text-align: left; cursor: pointer;
  transition: background 0.18s ease, border-color 0.18s ease;
}
.site-tab:hover { background: var(--bg-third); }
.site-tab.active {
  background: var(--bg-main); border-color: var(--bg-muted); color: var(--text-main);
}
.st-main { display: flex; align-items: center; gap: 6px; min-width: 0; }
.st-name {
  font-size: var(--fs-base); font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Sentence case, not caps: this holds the word "staging", which is content.
   Uppercase + tracking is reserved for system labels. */
.st-flag {
  font-size: var(--fs-2xs);
  padding: 1px 6px; border-radius: var(--radius-pill);
  background: var(--bg-muted); color: var(--text-second); flex-shrink: 0;
}
.st-host {
  font-size: var(--fs-2xs); color: var(--text-faint); font-family: var(--mono);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.st-edit {
  position: absolute; top: 9px; right: 9px;
  display: flex; color: var(--text-faint);
  opacity: 0; transition: opacity 0.18s ease;
}
.site-tab:hover .st-edit, .site-tab.active .st-edit,
.site-tab:focus-within .st-edit { opacity: 1; }
.st-edit:hover { color: var(--text-main); }
.site-add {
  display: flex; align-items: center; gap: 8px;
  padding: 7px var(--sp-3);
  border: 1px dashed var(--bg-muted); border-radius: var(--radius-lg);
  background: transparent; color: var(--text-second);
  font-family: inherit; font-size: var(--fs-sm); text-align: left; cursor: pointer;
}
.site-add:hover { background: var(--bg-third); color: var(--text-main); }

.nav { display: flex; flex-direction: column; gap: 2px; }
.nav-item {
  display: flex; align-items: center; gap: 10px;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-lg);
  font-size: var(--fs-base); font-weight: 400;
  color: var(--text-second);
  text-decoration: none;
  transition: background 0.18s ease, color 0.18s ease;
}
.nav-item .icon { opacity: 0.7; transition: opacity 0.18s ease; }
.nav-item:hover { background: var(--bg-third); color: var(--text-main); }
.nav-item:hover .icon { opacity: 1; }
/* The current view is the only nav row that carries the full text colour and a
   fill; everything else stays one step back so the eye finds "where am I"
   without reading the list. */
.nav-item.active {
  background: var(--nav-highlight); color: var(--text-main); font-weight: 500;
}
.nav-item.active .icon { opacity: 1; }
.nav-badge {
  margin-left: auto;
  font-size: var(--fs-2xs); font-weight: 500;
  padding: 2px 7px; border-radius: var(--radius-pill);
  background: var(--bg-muted); color: var(--text-second);
}
.nav-badge.alert { background: var(--alert-soft); color: var(--alert); }
.nav-badge:empty { display: none; }

.sidebar-foot { margin-top: auto; display: flex; flex-direction: column; gap: 10px; }
.mini-card {
  background: var(--bg-main);
  border: 1px solid var(--bg-third);
  border-radius: var(--radius-lg);
  padding: 10px var(--sp-3);
  display: flex; flex-direction: column; gap: 3px;
}
.mini-label {
  font-size: var(--fs-2xs); font-weight: 500;
  text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--text-faint);
}
.mini-card strong { font-size: var(--fs-sm); font-family: var(--mono); font-weight: 500; }

.btn-ghost {
  display: flex; align-items: center; gap: 8px;
  padding: var(--sp-2) var(--sp-3);
  background: transparent; border: 1px solid var(--bg-third);
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
  border-radius: var(--radius-lg);
  color: var(--text-second); font-family: inherit; font-size: var(--fs-base);
  cursor: pointer;
}
.btn-ghost:hover { background: var(--bg-third); color: var(--text-main); }

/* A quiet toggle for re-ordering a list (severity vs. date) that sits inside a
   card/drawer header, not the page chrome — smaller and borderless at rest,
   the same hover affordance as any other .btn-ghost so it reads as clickable. */
.sort-toggle {
  border-color: transparent; padding: 4px var(--sp-2);
  font-size: var(--fs-sm); color: var(--text-faint); white-space: nowrap;
}
.sort-toggle:hover { color: var(--text-main); }
.drawer-sort-row { display: flex; justify-content: flex-end; margin: -4px 0 10px; }
/* Sits in a .priority-head row (which wraps left-to-right: label, kind tag,
   page-count tag) — pushed to the far right so it reads as the row's own
   action, not another tag in the same line. */
.priority-bulk { margin-left: auto; }

/* -------------------------------------------------------------------- main */
.main { flex: 1; min-width: 0; padding: 0 30px 60px; max-width: 1680px; }

/* The run status and "check now" are the two controls you reach for from any
   view, so the header follows you down the page instead of scrolling away. */
.topbar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 22px; padding: 28px 0 20px; margin-bottom: 10px;
  background: var(--bg-main);
  border-bottom: 1px solid var(--bg-third);
}
.topbar-text { min-width: 0; }
.topbar h1 { margin: 0; font-size: var(--fs-2xl); font-weight: 500; letter-spacing: var(--track-tight); }
.sub { margin: 5px 0 0; color: var(--text-second); font-size: var(--fs-base); }
.actions { display: flex; align-items: center; gap: 12px; flex: none; }

/* ------------------------------------------------------------------- views */
/* One view at a time. Everything used to be stacked into a single column eight
   screens tall, which meant the sidebar was a table of contents for a document
   rather than navigation for an app. */
.view { display: none; padding-top: var(--sp-4); }
.view.active { display: block; animation: view-in 0.16s ease; }
@keyframes view-in { from { opacity: 0; transform: translateY(3px); } }

/* The strip above a bare view: whatever narrows or explains what is below it. */
.view-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap; margin-bottom: var(--sp-4);
}
.view-sub { margin: 0; font-size: var(--fs-base); color: var(--text-second); }
.view-bar-right { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }

/* A view that is one list gets a panel instead of a card: same surface, no
   redundant heading repeating the title already in the header. */
.panel {
  background: var(--bg-second); border: 1px solid var(--bg-third);
  border-radius: var(--radius-xl); padding: 2px var(--sp-5);
}

.pill {
  font-size: var(--fs-sm); font-weight: 500;
  padding: 6px 13px; border-radius: var(--radius-pill);
  background: var(--bg-third); color: var(--text-second);
  display: inline-flex; align-items: center; gap: 7px;
}
.pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
/* A clean run is not an achievement to celebrate, it is the expected state. */
.pill.ok { background: var(--bg-third); color: var(--text-second); }
.pill.review { background: var(--review-soft); color: var(--review); }
.pill.alert { background: var(--alert-soft); color: var(--alert); }
/* Three beats on load, then it stops. A dot pulsing forever in a tab that
   stays open all day stops being a signal and becomes a tic. */
.pill.alert::before { animation: pulse 1.4s ease-in-out 3; }
@keyframes pulse { 50% { opacity: 0.35; } }

/* "Why this score" (PRD-design-ai, Phase 3): a real disclosure of the design
   score's per-finding deduction, not a summary — every bucket weight has to
   be readable here, never just implied by the total. Reuses the same
   disclosure chevron as .text-expand. */
.drawer-design-score { margin-bottom: var(--sp-3); }
/* At-a-glance version of "Why this score?" below: one thin bar per category
   this page loses points to, sized to each other (not to 100) since the
   point is comparing this page's own categories, not restating the score
   itself. Same hue-N colour language as the site-wide bar charts so a
   category reads as the same colour everywhere it appears. */
.mini-bar-list { display: flex; flex-direction: column; gap: 6px; margin: var(--sp-2) 0 var(--sp-3); }
.mini-bar-row { display: flex; align-items: center; gap: var(--sp-2); }
.mini-bar-label {
  flex: 0 0 132px; font-size: var(--fs-xs); color: var(--text-second);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mini-bar-track { flex: 1; height: 6px; border-radius: var(--radius-pill); background: var(--bg-muted); overflow: hidden; }
.mini-bar-fill { height: 100%; border-radius: var(--radius-pill); transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1); }
.mini-bar-fill.hue-0 { background: var(--cat-0); }
.mini-bar-fill.hue-1 { background: var(--cat-1); }
.mini-bar-fill.hue-2 { background: var(--cat-2); }
.mini-bar-fill.hue-3 { background: var(--cat-3); }
.mini-bar-fill.hue-4 { background: var(--cat-4); }
.mini-bar-points { flex: 0 0 auto; font-size: var(--fs-2xs); font-weight: 700; color: var(--text-faint); }
.why-score { margin-top: var(--sp-2); }
.why-score summary {
  cursor: pointer; list-style: none; color: var(--text-faint);
  font-size: var(--fs-sm); line-height: 1.5;
}
.why-score summary::-webkit-details-marker { display: none; }
.why-score summary:hover { color: var(--text-second); }
.why-score[open] summary { color: var(--text-second); margin-bottom: var(--sp-2); }
.why-score-list {
  display: flex; flex-direction: column; gap: 6px;
  padding: var(--sp-3); border-radius: var(--radius-lg);
  background: var(--bg-second); border: 1px solid var(--bg-third);
}
.why-score-item {
  display: flex; align-items: baseline; gap: var(--sp-2);
  font-size: var(--fs-sm);
}
.why-score-bucket {
  flex: none; font-size: var(--fs-2xs); font-weight: 500;
  padding: 1px 7px; border-radius: var(--radius-pill);
  background: var(--bg-third); color: var(--text-faint);
}
.why-score-label { flex: 1; color: var(--text-second); }
.why-score-points { color: var(--alert); flex: none; }
.why-score-total {
  margin-top: var(--sp-2); padding-top: var(--sp-2);
  border-top: 1px dashed var(--bg-third);
  font-size: var(--fs-xs); color: var(--text-faint);
}

.btn-primary {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--accent); color: var(--text-on-accent);
  border: none; border-radius: var(--radius-lg);
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-base); font-weight: 500; font-family: inherit;
  cursor: pointer;
  transition: transform 0.18s ease, filter 0.2s ease;
}
.btn-primary:hover { filter: brightness(1.06); transform: translateY(-1px); }
.btn-primary:disabled { opacity: 0.55; cursor: progress; transform: none; }
/* Same shape as the confirming button everywhere else, so a destructive
   confirmation is not a different control — only a differently coloured one. */
.btn-primary.danger { background: var(--alert); color: var(--text-on-solid); }
/* A run in progress: the icon turns while the button is disabled. */
.btn-primary:disabled .icon { animation: spin 1.1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

.btn-icon {
  display: grid; place-items: center;
  width: 30px; height: 30px; padding: 0;
  border-radius: var(--radius-md);
  background: var(--bg-second); border: 1px solid var(--bg-third);
  color: var(--text-second); cursor: pointer; line-height: 1;
}
/* Without this, themeToggle (.btn-ghost + .btn-icon together) keeps
   .btn-ghost's 8px/12px padding, leaving a content box smaller than the
   16px icon inside a 30px button. Grid's "center" then falls back to safe/
   start alignment rather than overflowing evenly, which pinned the icon to
   the button's top-left corner — read as "the moon isn't centered," but the
   icon itself was fine; the button had no room to center anything in. */
.btn-icon:hover { background: var(--bg-third); color: var(--text-main); }

/* ------------------------------------------------------------ draft alert */
/* This used to say "alert" four times over — red border, red fill, red heading
   and a red context. One signal is enough when it is the right one: the tinted
   outline and the gutter dot carry the severity, and the words carry the rest. */
.draft-alert {
  position: relative;
  border: 1px solid var(--alert-line);
  background: var(--bg-second);
  border-radius: var(--radius-xl);
  padding: var(--sp-4) 20px var(--sp-4) 32px;
  margin-bottom: 20px;
}
.draft-alert::before {
  content: ""; position: absolute; top: 22px; left: 16px;
  width: 6px; height: 6px; border-radius: 50%; background: var(--alert);
}
.draft-alert h2 { margin: 0 0 6px; font-size: var(--fs-md); font-weight: 500; color: var(--text-main); }
.draft-alert ul { margin: var(--sp-2) 0 0; padding-left: var(--sp-4); font-size: var(--fs-base); }
.draft-alert li { margin-bottom: 3px; line-height: var(--lh-normal); }
.draft-alert li::marker { color: var(--text-faint); }
.draft-alert a { color: var(--text-main); }

/* A discarded run is not a defect in the site, so it does not get the alert
   colour — it is the tool reporting on itself. Review weight, same gutter dot
   as everything else, and it says plainly that nothing was lost. */
.run-notice {
  position: relative;
  border: 1px solid var(--review-line);
  background: var(--bg-second);
  border-radius: var(--radius-xl);
  padding: var(--sp-3) 20px var(--sp-3) 32px;
  margin-bottom: 20px;
}
.run-notice::before {
  content: ""; position: absolute; top: 17px; left: 16px;
  width: 6px; height: 6px; border-radius: 50%; background: var(--review);
}
/* "It ran and everything is fine" is the most common ending and the least
   urgent, so it gets no colour at all — just enough outline to be read as a
   deliberate statement rather than a leftover. */
.run-notice.ok { border-color: var(--bg-muted); }
.run-notice.ok::before { background: var(--text-faint); }
.run-notice h2 { margin: 0 0 4px; font-size: var(--fs-base); font-weight: 500; color: var(--text-main); }
.run-notice p { margin: 0; font-size: var(--fs-sm); color: var(--text-second); line-height: var(--lh-normal); max-width: 78ch; }

/* -------------------------------------------------------------------- kpis */
.kpis {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px; margin-bottom: 20px;
}
.kpi {
  background: var(--bg-second); border: 1px solid var(--bg-third);
  border-radius: var(--radius-xl); padding: var(--sp-4);
  box-shadow: var(--shadow-card);
  display: flex; flex-direction: column; gap: 4px;
  /* Appears once per full render, staggered by position — not a shadow, not a
     number counting up, just the block settling into place. backwards keeps it
     invisible for the length of its own delay instead of flashing full-opacity
     first. */
  animation: kpi-in 0.32s ease backwards;
}
.kpi:nth-child(1) { animation-delay: 0ms; }
.kpi:nth-child(2) { animation-delay: 40ms; }
.kpi:nth-child(3) { animation-delay: 80ms; }
.kpi:nth-child(4) { animation-delay: 120ms; }
.kpi:nth-child(5) { animation-delay: 160ms; }
@keyframes kpi-in { from { opacity: 0; transform: translateY(4px); } }
.kpi-head { display: flex; align-items: center; gap: 8px; }
/* Growth or shrinkage in pages monitored is information, not a verdict — one
   neutral pill for both directions, the arrow carries the direction. The only
   KPI delta the data can back honestly (see renderKpis), so it does not
   pretend the other four cards also have a "vs last run" number. */
.kpi-delta {
  margin-left: auto; display: inline-flex; align-items: center; gap: 3px;
  font-size: var(--fs-2xs); color: var(--text-second);
  background: var(--bg-third); border-radius: var(--radius-pill); padding: 2px 6px 2px 5px;
}
/* A delta on a problem count is allowed to say "this got worse/better" — a
   different claim from "this page is fine", which is the one thing colour
   never gets to say in this app. */
.kpi-delta.bad { color: var(--delta-bad); background: color-mix(in srgb, var(--delta-bad) 14%, var(--bg-third)); }
.kpi-delta.good { color: var(--delta-good); background: color-mix(in srgb, var(--delta-good) 14%, var(--bg-third)); }
.kpi-icon {
  /* A 24px box against a ~8.5px cap height does not optically centre. */
  margin-top: -1px;
  width: 34px; height: 34px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: var(--bg-third); color: var(--text-second);
}
.kpi-icon.alert { background: var(--alert-soft); color: var(--alert); }
.kpi-icon.review { background: var(--review-soft); color: var(--review-vivid); }
.kpi-icon.ok { background: var(--bg-third); color: var(--text-second); }
/* Decorative only: these three KPIs are not a severity, so their badge borrows
   a categorical colour instead of grey, for the same reason a bar chart gives
   unrelated series different hues. */
.kpi-icon.violet { background: var(--cat-0-soft); color: var(--cat-0); }
.kpi-icon.blue { background: var(--cat-1-soft); color: var(--cat-1); }
.kpi-icon.teal { background: var(--cat-2-soft); color: var(--cat-2); }
.kpi-label { font-size: var(--fs-sm); color: var(--text-second); }
.kpi-value { font-size: var(--fs-2xl); font-weight: 700; letter-spacing: var(--track-tight); line-height: 1.1; }
.kpi-note { font-size: var(--fs-xs); color: var(--text-faint); }
.kpi.alert .kpi-value { color: var(--alert); }
.kpi.review .kpi-value { color: var(--review-vivid); }
/* A good number is still just a number: it does not need to be green. */
.kpi.ok .kpi-value { color: var(--text-main); }
.kpi-clickable { cursor: pointer; }
.kpi-clickable:hover { border-color: var(--text-faint); }

/* --------------------------------------------------------------------- seo */
.seo-group {
  border: 1px solid var(--bg-third); border-radius: var(--radius-lg);
  padding: 11px 13px; margin-bottom: 8px;
}
.seo-group summary {
  cursor: pointer; list-style: none; font-weight: 500; font-size: var(--fs-base);
}
.seo-group summary::-webkit-details-marker { display: none; }
.seo-rows { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
.seo-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 7px 9px; border-radius: var(--radius-md); background: var(--bg-main);
}
.seo-row-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.seo-row-path { font-family: var(--mono); font-size: var(--fs-sm); overflow-wrap: anywhere; }
.seo-row-detail { font-size: var(--fs-xs); color: var(--text-faint); }

/* ------------------------------------------------------------------- cards */
.card {
  background: var(--bg-second); border: 1px solid var(--bg-third);
  border-radius: var(--radius-xl); padding: var(--sp-5);
  box-shadow: var(--shadow-card);
  margin-bottom: var(--sp-5);
}
/* Baseline, not centre: the left block is two lines and the right is one, so
   centring floated the note to the midpoint of the pair instead of sitting it
   on the heading's baseline. .staging-group-head already did this correctly. */
.card-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; margin-bottom: 16px;
}
.card-head h2 { margin: 0; font-size: var(--fs-lg); font-weight: 500; letter-spacing: -0.01em; }
.card-sub { margin: 2px 0 0; font-size: var(--fs-sm); color: var(--text-faint); }
.card-note { font-size: var(--fs-sm); color: var(--text-faint); }
.card-title-row { display: flex; align-items: center; gap: 7px; }
.card-title-row h2 { margin: 0; }

/* Temporary marker for what shipped this week — small and brand-coloured
   rather than an alert/review hue, since nothing here needs action. Meant to
   be pulled once the features stop being new, not a permanent UI element. */
.tag-new {
  font-size: 9px; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase;
  padding: 1px 6px; border-radius: var(--radius-pill);
  background: var(--accent); color: var(--text-on-accent);
  flex: none;
}

/* Overview stacks charts, a priority list and a section grid one after another
   with identical card styling — nothing told the eye where one topic ends and
   the next begins, so the whole page read as one continuous wall. A quiet
   uppercase label (same treatment as the sidebar's "Domains" eyebrow) ahead of
   each row is just a scan anchor: no colour, no rule, no weight change on the
   cards themselves. */
.zone-label {
  font-size: var(--fs-2xs); font-weight: 500;
  text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--text-faint); margin: 0 0 var(--sp-3);
}

.grid-2 { display: grid; grid-template-columns: 1fr 1.15fr; gap: 22px; align-items: start; }
/* Three analytics cards on one row. They are all short, so stretching them to a
   shared height fills the row instead of leaving a tall empty column below. */
.grid-main { display: grid; grid-template-columns: 1.4fr 0.85fr 1.1fr; gap: 22px; }
.grid-main > .card { margin-bottom: 0; display: flex; flex-direction: column; }
.grid-main { margin-bottom: 22px; }
@media (max-width: 1240px) {
  .grid-main { grid-template-columns: 1fr 1fr; }
  .grid-main > .card:first-child { grid-column: 1 / -1; }
}
@media (max-width: 860px) {
  .grid-2, .grid-main { grid-template-columns: 1fr; }
  .grid-main > .card:first-child { grid-column: auto; }
}

/* Coverage used to be one card stretched to match its taller row-siblings,
   which left a dead gap inside it. Splitting it into two content-sized cards
   fixed that gap, but pinning the stack to the top of the row just moved the
   same leftover height below the stack instead — a blank grey rectangle next
   to two cards still shorter than their row-siblings. The stack now stretches
   with the row like the other two cards do, and both cards share the leftover
   height equally (rather than dumping it all into one), centring their own
   content within their own share — reads as two cards with breathing room,
   not as background. */
.card-stack { display: flex; flex-direction: column; gap: var(--sp-5); }
.card-stack > .card { margin-bottom: 0; flex: 1; display: flex; flex-direction: column; justify-content: center; }

/* Two related Overview cards side by side, each taking half the row, instead
   of stacking both full-width — "By category" and "Worst design scores" are
   short enough on their own that a full-width row for either one left a lot
   of empty horizontal space next to a narrow column of content. */
.grid-half { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; margin-bottom: 22px; align-items: start; }
/* Deliberately NOT stretched to match the taller sibling: "Worst design
   scores" grows with its row list (more rows on a bigger site = a taller
   card), while "By category"'s chart has a sensible height cap of its own
   (see .category-chart .scatter-svg) so bubbles don't spread thin across a
   mostly-empty canvas. Stretching the shorter card to match would leave a
   dead, empty slab below its capped-height chart — worse than the two cards
   simply having different heights, which is normal for a dashboard row. */
.grid-half > .card { margin-bottom: 0; display: flex; flex-direction: column; }
@media (max-width: 860px) {
  .grid-half { grid-template-columns: 1fr; }
}

/* Shared legend swatches. */
.legend { display: flex; gap: 12px; align-self: center; }
.lg { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--text-second); }
.sw { width: 9px; height: 9px; border-radius: 3px; background: var(--review-vivid); flex: none; }
.sw.alert { background: var(--alert); }
/* Matches the coverage grid: clean is grey, not green. */
.sw.ok { background: var(--bg-muted); }
.sw.accepted { background: var(--accepted); }

/* ------------------------------------------------------ health: rings+trend */
.health-row { display: flex; flex-direction: column; gap: var(--sp-4); flex: 1; min-width: 0; }
.rings-block { display: flex; align-items: center; gap: 24px; align-self: center; }
/* Two concentric progress rings (outer = Health score, inner = Design score)
   replacing the two side-by-side half-arc gauges: same shared centre reads
   "these are two readings of the same site" without a click. Colour still
   only encodes severity (ok = text colour, review/alert = their existing
   tokens) — the two metrics are told apart by radius and the legend beside
   them, never by a second, decorative colour pair (color solo donde hay que
   actuar: no brand hues invented just to tell the rings apart). */
.rings { position: relative; width: 172px; height: 172px; flex: none; }
.rings-svg { width: 100%; height: 100%; overflow: visible; }
.ring-track { fill: none; stroke: var(--bg-muted); }
.ring-arc {
  fill: none; stroke-linecap: round;
  transform: rotate(-90deg); transform-origin: 50% 50%;
  transition: stroke-dashoffset 0.9s cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: stroke; cursor: default;
}
/* A healthy score draws in the text colour, not in green: the number already
   says the site is fine, and a green ring says it a second time. */
.ring-arc.ok { stroke: var(--text-main); }
.ring-arc.review { stroke: var(--review-vivid); }
.ring-arc.alert { stroke: var(--alert); }
.rings-value {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  pointer-events: none;
}
.rv-item { display: flex; align-items: baseline; gap: 3px; }
.rv-item strong { font-size: var(--fs-3xl); font-weight: 700; letter-spacing: var(--track-tight); line-height: 1; }
.rv-item span { font-size: var(--fs-sm); color: var(--text-faint); }
/* The design score sits visually "inside" the health score, same as its ring —
   smaller, not a competing headline. */
.rv-inner strong { font-size: var(--fs-lg); }
.rv-inner span { font-size: var(--fs-xs); }

.rings-legend { display: flex; flex-direction: column; gap: 10px; }
/* Mirrors each ring's own severity colour exactly (not the unrelated grey used
   for "clean" elsewhere, e.g. the coverage grid) — the swatch and the ring
   right next to it must always match. */
.ring-sw { width: 9px; height: 9px; border-radius: 3px; flex: none; display: inline-block; background: var(--review); }
.ring-sw.ok { background: var(--text-main); }
.ring-sw.review { background: var(--review-vivid); }
.ring-sw.alert { background: var(--alert); }

.trend { min-width: 0; }
.trend-svg { width: 100%; height: 136px; display: block; }
/* The viewBox is 560 wide but the SVG renders at ~460, so every stroke came out
   at 0.82x: the 1px grid lines landed on sub-pixels and the line weight changed
   at every breakpoint. */
.trend-svg :where(path, line, circle) { vector-effect: non-scaling-stroke; }
.grid-line { stroke: var(--bg-third); stroke-width: 1; }
.trend-line {
  fill: none; stroke: var(--alert); stroke-width: 3; stroke-linejoin: round; stroke-linecap: round;
}
/* Draws the line once, left to right, when the chart first mounts. The dash
   array is set inline (to the path's own length) so this rule only supplies
   the timing; re-renders on poll do not replay it because .draw-in is added
   fresh each time and the browser only animates a property that changes. */
.trend-line.draw-in {
  animation: trend-draw 0.7s ease-out forwards;
}
@keyframes trend-draw {
  from { stroke-dashoffset: var(--len, 0); }
  to { stroke-dashoffset: 0; }
}
.trend-fill {
  transition: opacity 0.5s ease;
}
.trend-fill.draw-in { opacity: 0; }
.trend-dot {
  fill: var(--bg-second); stroke: var(--alert); stroke-width: 2.5;
  transition: r 0.18s ease, stroke-width 0.18s ease;
}
.trend-dot.on { r: 7; stroke-width: 3; }
/* The most recent run, when it is the one in alert: three beats on load, same
   restraint as .pill.alert — a signal that stops, not a tic that runs all day. */
.trend-pulse {
  fill: none; stroke: var(--alert); stroke-width: 2; opacity: 0;
  animation: trend-pulse 1.4s ease-out 2;
}
@keyframes trend-pulse {
  0% { r: 4; opacity: 0.5; }
  100% { r: 11; opacity: 0; }
}
/* Design score, on its own 0-100 scale sharing the same chart area. Dashed and
   neutral on purpose: it is a secondary reference line, not another alert
   signal, so it must not compete in color or weight with the alerts area. */
.trend-score-line {
  fill: none; stroke: var(--text-second); stroke-width: 1.5; stroke-dasharray: 4 3;
  stroke-linejoin: round; stroke-linecap: round;
}
.trend-score-dot { fill: var(--text-second); }
/* Invisible full-height columns, one per run: the whole column is hoverable
   instead of only the dot. */
.trend-band { fill: transparent; cursor: pointer; }
.trend-guide { stroke: var(--text-faint); stroke-width: 1; stroke-dasharray: 3 3; pointer-events: none; }
.trend-guide.off { opacity: 0; }
.tg-top { stop-color: var(--alert); stop-opacity: 0.34; }
.tg-mid { stop-color: var(--alert); stop-opacity: 0.08; }
.tg-bottom { stop-color: var(--alert); stop-opacity: 0; }
.trend-axis {
  display: flex; justify-content: space-between;
  font-size: var(--fs-xs); color: var(--text-faint); margin-top: 2px;
}

/* ----------------------------------------------------------- coverage grid */
.coverage {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(12px, 1fr));
  gap: 4px; margin-top: var(--sp-3); margin-bottom: 0; align-content: start;
}
/* Roughly nine of every ten cells here are clean, so whatever colour the clean
   state takes becomes the largest colour surface in the app. Grey keeps the
   map's shape while letting the handful of problems be the only thing the eye
   catches — which is the entire job of this grid. */
.cov-cell {
  aspect-ratio: 1; border: 0; padding: 0; border-radius: 3px; cursor: pointer;
  background: var(--bg-third); outline: 1px solid transparent;
  transition: outline-color 0.18s ease, background-color 0.2s ease, transform 0.15s ease;
}
/* A page whose findings are all intentional is not clean and not work either:
   it reads as a filled cell, one step darker than clean. */
.cov-cell.accepted { background: var(--bg-muted); }
.cov-cell.review { background: var(--review-vivid); opacity: 0.55; }
.cov-cell.alert { background: var(--alert); opacity: 0.9; }
/* A small scale instead of only an outline: a 12px cell growing past its own
   box (via transform, which does not reflow) reads as "picked up" without the
   ripple a real size change caused in neighbouring cells. */
.cov-cell:hover {
  outline-color: var(--text-main); outline-offset: 1px;
  transform: scale(1.15); position: relative; z-index: 1;
}
.coverage-key { display: flex; gap: 14px; flex-wrap: wrap; }
.coverage-key b { color: var(--text-main); }
/* A single segmented bar above the key: the same four counts as the grid,
   read as one proportion instead of ~100 individual cells. A different shape
   again — grid above, straight proportion bar below — rather than a second
   ring competing with the gauge and the bar list. */
.cov-bar {
  display: flex; height: 8px; border-radius: var(--radius-pill); overflow: hidden;
  background: var(--bg-third); margin-bottom: 12px; flex: none;
}
.cov-bar-seg { height: 100%; width: 0%; transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1); }
.cov-bar-seg.alert { background: var(--alert); }
.cov-bar-seg.review { background: var(--review-vivid); }
.cov-bar-seg.accepted { background: var(--accepted); }
/* Clean pages stay the same grey as the grid and the legend swatch — no ink
   spent on "nothing to report" here either. */
.cov-bar-seg.ok { background: var(--bg-muted); }

/* ------------------------------------------------- things to fix: bar list */
/* A ranked horizontal bar chart, not a donut: the health gauge is already a
   ring, and a second ring for a different metric reads as a repeated chart
   rather than a distinct one. Bars also have no centre hole to run out of
   room in when a category name is long. */
/* Rows pack from the top now, not centred: centring used to spend a stretched
   card's entire leftover height as padding above and below the row group —
   the tallest thing in the row (this list) was dictating extra air in itself
   on top of forcing its shorter siblings to stretch to match it. Tighter row
   spacing shrinks that natural height directly instead. */
.bar-list { display: flex; flex-direction: column; gap: 8px; flex: 1; justify-content: flex-start; }
.bar-row {
  display: flex; flex-direction: column; gap: 4px; border: 0; background: none;
  font-family: inherit; color: inherit; cursor: pointer; text-align: left; padding: 3px 8px;
  border-radius: var(--radius-md); transition: background 0.18s ease;
}
.bar-row:hover { background: var(--bg-third); }
.bar-row-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.bar-name { font-size: var(--fs-base); color: var(--text-main); font-weight: 500; }
.bar-meta { display: flex; align-items: baseline; gap: 7px; flex: none; }
.bar-count { font-size: var(--fs-md); font-weight: 700; }
.bar-pct { font-size: var(--fs-xs); color: var(--text-faint); }
/* A pill, not a thin rule: the previous 6px bar read as an underline, not a
   chart. 10px with a rounded end and a lit-to-dim gradient along its own
   length gives it enough body to actually look like a bar. */
.bar-track { height: 8px; border-radius: var(--radius-pill); background: var(--bg-muted); overflow: hidden; }
.bar-fill {
  height: 100%; width: var(--w, 0%); border-radius: var(--radius-pill);
  transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.bar-row.hue-0 .bar-count { color: var(--cat-0); }
.bar-row.hue-1 .bar-count { color: var(--cat-1); }
.bar-row.hue-2 .bar-count { color: var(--cat-2); }
.bar-row.hue-3 .bar-count { color: var(--cat-3); }
.bar-row.hue-4 .bar-count { color: var(--cat-4); }
.bar-fill.hue-0 { background: var(--cat-0); }
.bar-fill.hue-1 { background: var(--cat-1); }
.bar-fill.hue-2 { background: var(--cat-2); }
.bar-fill.hue-3 { background: var(--cat-3); }
.bar-fill.hue-4 { background: var(--cat-4); }

/* Ranked bar lists (fix chart, SEO breakdown) only show their top rows by
   default — ten simultaneous percentages is a lot to scan at once — and tuck
   the rest behind the same disclosure every other long list in the app uses. */
.bar-list-more { margin-top: 2px; }
.bar-list-more summary {
  display: flex; align-items: center; cursor: pointer; list-style: none;
  color: var(--text-faint); font-size: var(--fs-sm); padding: 3px 8px;
}
.bar-list-more summary::-webkit-details-marker { display: none; }
.bar-list-more summary:hover { color: var(--text-second); }
.bar-list-more .bar-list { margin-top: 4px; }

/* Bubble chart: used by the "By category" design card, which lives on its
   own full-width row (not squeezed into a narrow stacked column like the
   bar-lists above). One bubble per category — x = pages reached, y = total
   impact (pages × severity weight), size = severity weight — crossed at the
   median of each axis into a priority quadrant, same viewBox/axis shape as
   .scatter-svg right next to it in "Worst design scores", plus a clickable
   legend below since close bubbles can overlap and the exact count still
   needs to read.
   Colour marks PRIORITY, not category identity: a bubble is --alert only when
   it falls in the frequent + high-impact quadrant (fix first); every other
   bubble stays the quiet --accepted grey — same "colour only where there is
   something to act on" rule as the rest of the app, no per-category rainbow.
   The legend label + tooltip carry identity instead of colour. */
.category-chart-box { display: flex; flex-direction: column; min-height: 0; padding-top: var(--sp-2); }
/* Plain-language answer to what the scatter plot below it is for, so reading
   the quadrant chart is optional rather than required to get the takeaway. */
.chart-callout { font-size: var(--fs-sm); color: var(--text-second); line-height: var(--lh-normal); margin: 0 0 var(--sp-3); }
.category-chart { display: flex; flex-direction: column; gap: var(--sp-2); }
/* A comfortable fixed-ish height range for the plot itself — not tied to the
   sibling card (see .grid-half, which no longer stretches cards to match one
   another) so a handful of bubbles never has to spread thin across a mostly-
   empty canvas just because the list next to it happens to be tall. The JS
   measures this rendered box before drawing so the viewBox can match it
   pixel-for-pixel; a mismatched viewBox is what makes circles stretch into
   ellipses. */
.category-chart .scatter-svg { height: 160px; max-height: 220px; flex: 1; min-width: 0; }
/* Row: a vertical y-axis (total impact) next to the plot itself, mirroring
   the horizontal x-axis (pages reached) already below it — without this, only
   one of the two axes being measured had any label at all. */
.category-plot { display: flex; gap: var(--sp-2); align-items: stretch; }
.scatter-axis-y {
  display: flex; flex-direction: column; justify-content: space-between; align-items: center;
  flex: none; width: 14px; padding: 2px 0;
  font-size: var(--fs-2xs); color: var(--text-faint); text-align: center;
}
.scatter-axis-y-title {
  writing-mode: vertical-rl; transform: rotate(180deg);
  text-transform: uppercase; letter-spacing: var(--track-caps); white-space: nowrap;
}
.category-dot {
  cursor: pointer; fill-opacity: 0.62; stroke-width: 2;
  transition: fill-opacity 180ms ease, stroke-width 180ms ease;
}
.category-dot:hover { fill-opacity: 0.88; stroke-width: 3; }
/* The bubble's own count, shown once it is big enough to hold it — same
   weight rule as every other headline number in the app (700 reserved for
   the number itself, not the label around it). */
.category-dot-label {
  font-size: 11px; font-weight: 700; fill: var(--text-main);
  text-anchor: middle; dominant-baseline: central;
}
.category-dot.hot { fill: var(--alert); stroke: var(--alert); }
.category-dot.cold { fill: var(--accepted); stroke: var(--accepted); }
.category-legend { display: flex; flex-wrap: wrap; gap: 6px; }
.category-legend-btn {
  display: inline-flex; align-items: center; gap: 6px; border: 0;
  background: var(--bg-third); font-family: inherit; cursor: pointer;
  padding: 5px 10px; border-radius: var(--radius-pill); transition: background 0.18s ease;
}
.category-legend-btn:hover { background: var(--bg-muted); }
.category-legend-label { font-size: var(--fs-2xs); color: var(--text-second); }
.category-legend-count { font-size: var(--fs-2xs); font-weight: 700; }
.category-legend-count.hot { color: var(--alert); }
.category-legend-count.cold { color: var(--accepted); }

/* An empty state is a state, not an error. Left-aligned, measured and given a
   container, so "nothing here yet" reads as considered rather than as a stray
   grey sentence floating in a card. */
.empty {
  display: flex; align-items: flex-start; gap: 10px;
  padding: var(--sp-6) var(--sp-4); max-width: 44ch; text-align: left;
  border: 1px dashed var(--bg-muted); border-radius: var(--radius-md);
  color: var(--text-second); font-size: var(--fs-sm); line-height: var(--lh-normal);
}
.empty .icon { flex: none; margin-top: 1px; opacity: 0.55; }
/* Inside a grid an empty state is not one of the cells — it stands in for all
   of them, so it spans the row instead of sitting in the first column. */
.preview-grid > .empty, .page-grid > .empty, .scope-stats > .empty,
.preview-grid > .empty-panel, .page-grid > .empty-panel { grid-column: 1 / -1; }

/* A feature that has not run yet, not a filtered-out list — see emptyPanel()
   in app.js. Same dashed, colourless container as .empty, roomier because it
   carries a reason and, often, the one button that fixes it. */
.empty-panel {
  padding: var(--sp-6) var(--sp-4); max-width: 46ch; text-align: left;
  border: 1px dashed var(--bg-muted); border-radius: var(--radius-md);
}
.empty-panel-title { font-weight: 600; margin-bottom: 4px; }
.empty-panel-desc { color: var(--text-second); font-size: var(--fs-sm); line-height: var(--lh-normal); margin: 0 0 10px; }
.empty-panel .btn-inline { font-size: var(--fs-sm); }

/* ------------------------------------------------------------- skeletons
   The gap between the page appearing and the first response landing. A flat
   grey block, not a shimmer sweeping forever — the same restraint as the rest
   of the app's motion: a placeholder announces "loading", it does not perform
   it. Each rule targets :empty, so the instant a render function fills the
   container with real content, the skeleton simply has nothing left to match. */
.kpi.sk { gap: 8px; }
.sk-block { display: block; background: var(--bg-third); border-radius: var(--radius-md); }
.sk-w60 { width: 60%; height: 10px; }
.sk-w80 { width: 40%; height: 8px; }
.sk-h24 { width: 45%; height: 22px; border-radius: var(--radius-md); }
.trend:empty, .coverage:empty {
  background: var(--bg-third); border-radius: var(--radius-md); min-height: 170px;
}
.bar-list:empty { min-height: 170px; position: relative; }
.bar-list:empty::before {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(var(--bg-third) 0 18px, transparent 0 32px),
    linear-gradient(var(--bg-third) 0 18px, transparent 0 32px),
    linear-gradient(var(--bg-third) 0 18px, transparent 0 32px);
  background-repeat: no-repeat;
  background-position: 0 0, 0 40px, 0 80px;
  background-size: 100% 18px;
  border-radius: var(--radius-md);
}

@media (max-width: 640px) {
  /* Left-aligned at every width. The layout should not change its mind at a
     breakpoint about where the page's edge is. */
  .health-row { align-items: flex-start; }
  .rings-block { align-self: flex-start; flex-direction: column; align-items: flex-start; gap: 12px; }
}

/* ------------------------------------------------------------- group grid */
.group-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 10px; }
.group-tile {
  border: 1px solid var(--bg-third); border-radius: var(--radius-lg);
  padding: 12px; background: var(--bg-main);
  cursor: pointer; text-align: left; font-family: inherit; color: inherit;
  display: flex; flex-direction: column; gap: 6px;
  transition: border-color 0.15s ease, transform 0.08s ease;
}
.group-tile:hover { border-color: var(--text-faint); transform: translateY(-1px); }
.group-tile.has-alert { border-color: var(--alert-line); }
.group-name { font-size: var(--fs-base); font-weight: 500; text-transform: capitalize; }
.group-meta { font-size: var(--fs-xs); color: var(--text-second); }
.dots { display: flex; gap: 3px; }
.dot { width: 6px; height: 6px; border-radius: 50%; }
.dot.ok { background: var(--bg-muted); }
.dot.review { background: var(--review-vivid); }
.dot.alert { background: var(--alert); }
.dot.accepted { background: var(--accepted); }

/* ----------------------------------------------------------- change previews */
/* A grid, not a carousel: this is the one view whose whole purpose is seeing
   every change at once, and a horizontal scroller showed three of them. */
.preview-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: var(--sp-4);
}
.slide {
  border: 1px solid var(--bg-third); border-radius: var(--radius-xl);
  background: var(--bg-second); overflow: hidden;
  display: flex; flex-direction: column;
  padding: 0; text-align: left; font-family: inherit; color: inherit; cursor: pointer;
  transition: transform 0.15s ease, border-color 0.15s ease;
}
.slide:hover { transform: translateY(-1px); border-color: var(--text-faint); }
.slide.alert { border-color: var(--alert-line); }
.shot-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--bg-third); }
.shot-pair.single { grid-template-columns: 1fr; }
.shot { position: relative; background: var(--bg-second); aspect-ratio: 16 / 11; overflow: hidden; cursor: zoom-in; }
.shot img { width: 100%; height: 100%; object-fit: cover; object-position: top center; display: block; }
/* Sentence case: this label carries a date, and uppercasing a date is a plain
   legibility loss for no gain. */
.shot-tag {
  position: absolute; top: 6px; left: 6px;
  font-size: var(--fs-2xs); font-family: var(--mono);
  background: rgba(0, 0, 0, 0.66); color: var(--text-on-solid);
  padding: 2px 6px; border-radius: var(--radius-md);
}
.slide-body { padding: 11px 13px; display: flex; flex-direction: column; gap: 5px; }
.slide-title { font-size: var(--fs-base); font-weight: 500; }
.slide-path { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-faint); }
/* Clamped by line, not by height: a max-height cut the third line through its
   own x-height, which looks like a rendering bug rather than a truncation. */
.slide-detail {
  font-size: var(--fs-sm); color: var(--text-second); line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}

/* -------------------------------------------------------------- page cards */
.filters { display: flex; gap: 6px; flex-wrap: wrap; }
.card-head .filters { align-self: center; }
/* Shape and weight match Gitterdone's own filter chips (same design system):
   a soft rounded rectangle, not a full pill, sitting on nothing until it
   needs to say "this one is active" or "the pointer is here". */
.chip {
  border: 1px solid var(--bg-third); background: transparent;
  border-radius: var(--radius-lg); padding: 5px 10px;
  font-size: var(--fs-base); font-family: inherit; color: var(--text-second); cursor: pointer;
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.chip:hover { color: var(--text-main); border-color: var(--text-faint); }
.chip.active { background: var(--text-main); color: var(--bg-second); border-color: var(--text-main); }

/* ------------------------------------------------ status: the gutter dot
   Four components across the app used to carry a 3px coloured stripe down their
   left edge. Twelve of them, at full saturation, on lists where most rows are
   fine — so the loudest thing on screen was the frame rather than the finding.

   They share one mechanism now: a left gutter and a small dot inside it. The
   card itself stays completely neutral, the dot is the only ink spent, and the
   title, path and tags all hang off a single vertical axis instead of being
   pushed sideways by a border. A page with nothing wrong declares no --sev at
   all, so its dot is transparent: clean pages cost no colour whatsoever. */
.page-card, .issue, .mark-row, .tip, .priority-row, .worst-design-row { position: relative; }
.page-card::before, .issue::before, .mark-row::before, .tip::before, .priority-row::before, .worst-design-row::before {
  content: ""; position: absolute; border-radius: 50%;
  background: var(--sev, transparent);
}

.page-card.alert, .issue.alert, .mark-row.stale, .tip.alert, .priority-row.alert, .worst-design-row.alert { --sev: var(--alert); }
.page-card.review, .issue.review, .tip.review, .priority-row.review, .worst-design-row.review { --sev: var(--review); }
.page-card.accepted, .issue.accepted, .mark-row.on { --sev: var(--accepted); }
/* "gone" reuses --review (not a new colour) so it reads as its own state at a
   glance instead of the same muted grey as "on" — before this it shared
   --bg-muted with the card's own background tint, so both were indistinguishable. */
.mark-row.gone { --sev: var(--review); }
/* .ok deliberately declares nothing. */

.page-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 11px; }
/* Same "ghost row" idea Gitterdone (Buena's own task tracker, same design
   system) uses for its list items: the border is transparent at rest, so a
   clean run of cards reads as one quiet surface, and only shows a frame
   where the pointer actually is. Structural containers (.card, .kpi) stay
   bordered as before — this only applies one level down, to individual
   items inside them. */
.page-card {
  border: 1px solid transparent;
  border-radius: var(--radius-lg); padding: var(--sp-3) var(--sp-3) var(--sp-3) 26px;
  background: var(--bg-second); cursor: pointer;
  display: flex; flex-direction: column; gap: 7px;
  text-align: left; font-family: inherit; color: inherit;
  transition: transform 0.15s ease, border-color 0.15s ease;
}
/* Sits on the x-height of the title, not on the card's centre. */
.page-card::before { top: 15px; left: var(--sp-3); width: 6px; height: 6px; }
.page-card:hover { transform: translateY(-1px); border-color: var(--text-faint); }
.page-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.page-title { font-size: var(--fs-base); font-weight: 500; line-height: 1.3; }
.page-path { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-faint); word-break: break-all; }
.badge {
  font-size: var(--fs-2xs); font-weight: 500;
  padding: 2px 7px; border-radius: var(--radius-pill); flex-shrink: 0;
  background: var(--bg-muted); color: var(--text-second);
}
.badge.alert { background: var(--alert-soft); color: var(--alert); }
.badge.review { background: var(--review-soft); color: var(--review); }
.badge.accepted { background: var(--accepted-soft); color: var(--accepted); }
.tags { display: flex; flex-wrap: wrap; gap: 4px; }
.tag {
  font-size: var(--fs-2xs); padding: 2px 7px; border-radius: var(--radius-md);
  background: var(--bg-third); color: var(--text-second);
}
.tag.alert { background: var(--alert-soft); color: var(--alert); }
.tag.accepted { background: var(--accepted-soft); color: var(--accepted); }
.tag.review { background: var(--review-soft); color: var(--review); }

/* ----------------------------------------------------------------- staging */
.staging-empty { font-size: var(--fs-base); color: var(--text-second); line-height: 1.6; }
.staging-empty code {
  font-family: var(--mono); font-size: var(--fs-sm);
  background: var(--bg-third); padding: 2px 6px; border-radius: var(--radius-md);
}
.diff-row {
  border-top: 1px solid var(--bg-third); padding: 11px 0;
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
}
.diff-row:first-child { border-top: none; }
.diff-info { min-width: 0; }
.diff-info strong { font-size: var(--fs-base); }
.diff-info p { margin: 3px 0 0; font-size: var(--fs-sm); color: var(--text-second); }
.diff-links { display: flex; gap: 8px; flex-shrink: 0; }
.diff-links a { font-size: var(--fs-xs); color: var(--text-second); text-decoration: none; border: 1px solid var(--bg-third); padding: 4px 9px; border-radius: var(--radius-md); }
.diff-links a:hover { color: var(--text-main); border-color: var(--text-faint); }
.diff-path { display: block; margin-top: 3px; font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-faint); }

.btn-inline {
  background: none; border: none; padding: 0;
  font-family: inherit; font-size: inherit;
  color: var(--text-main); text-decoration: underline; cursor: pointer;
}

/* Three headline numbers: what differs and what only exists on one domain. */
.staging-summary {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px; margin-bottom: 18px;
}
.st-stat {
  display: flex; flex-direction: column; gap: 2px;
  padding: 14px 16px;
  border: 1px solid var(--bg-third); border-radius: var(--radius-xl);
  background: var(--bg-second);
}
.st-stat strong { font-size: var(--fs-2xl); font-weight: 700; letter-spacing: var(--track-tight); }
.st-stat span { font-size: var(--fs-xs); color: var(--text-second); }
.st-stat.review strong { color: var(--review-vivid); }
.st-stat.ok strong { color: var(--text-faint); }

.staging-group { margin-top: var(--sp-6); }
.staging-group-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: var(--sp-2); padding: 0 2px;
}
.staging-group-head h3 { margin: 0; font-size: var(--fs-base); font-weight: 500; }

@media (max-width: 720px) {
  .staging-summary { grid-template-columns: 1fr; }
}

/* ------------------------------------------------------------------- modal */
.modal { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; }
.modal-backdrop { position: absolute; inset: 0; background: var(--overlay); }
.modal-panel {
  position: relative;
  width: min(460px, calc(100vw - 32px));
  max-height: calc(100vh - 48px); overflow-y: auto;
  background: var(--bg-second);
  border: 1px solid var(--bg-third);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-pop);
  padding: var(--sp-5);
  display: flex; flex-direction: column; gap: var(--sp-4);
}
.modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.modal-head h3 { margin: 0; font-size: var(--fs-lg); font-weight: 500; letter-spacing: -0.01em; }

.field { display: flex; flex-direction: column; gap: 6px; }
.field > span { font-size: var(--fs-sm); font-weight: 500; color: var(--text-second); }
.field > span b { color: var(--alert); }
.field input {
  padding: 9px 11px;
  border: 1px solid var(--bg-muted); border-radius: var(--radius-lg);
  background: var(--bg-main); color: var(--text-main);
  font-family: inherit; font-size: var(--fs-base);
}
.field input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent; }
.field small { font-size: var(--fs-xs); color: var(--text-faint); line-height: var(--lh-normal); }
.field small code {
  font-family: var(--mono); font-size: var(--fs-2xs);
  background: var(--bg-third); padding: 1px 4px; border-radius: 3px;
}

.modal-error {
  margin: 0; padding: 9px 11px;
  border-radius: var(--radius-lg);
  background: var(--alert-soft); color: var(--alert);
  font-size: var(--fs-sm);
}
.modal-foot { display: flex; align-items: center; gap: 8px; }
.modal-foot .spacer { flex: 1; }

/* --------------------------------------------------------------- repair modal */
.repair-panel { width: min(520px, calc(100vw - 32px)); }
.repair-body { display: flex; flex-direction: column; gap: var(--sp-3); font-size: var(--fs-sm); }
.repair-context {
  padding: 8px 11px; border-radius: var(--radius-md); background: var(--bg-main);
  color: var(--text-second); line-height: var(--lh-normal);
}
.repair-context b { color: var(--text-main); font-weight: 500; }
.repair-loading {
  display: flex; align-items: center; gap: 9px;
  padding: 10px 0; color: var(--text-second);
}
.repair-loading .icon { animation: spin 1.1s linear infinite; }
.repair-suggestion {
  padding: 10px 12px; border-radius: var(--radius-md);
  background: var(--accepted-soft); border: 1px solid transparent;
}
.repair-suggestion a { font-weight: 500; word-break: break-all; }
.repair-suggestion p { margin: 6px 0 0; color: var(--text-second); line-height: var(--lh-normal); }
.repair-none {
  padding: 10px 12px; border-radius: var(--radius-md);
  background: var(--bg-main); color: var(--text-second); line-height: var(--lh-normal);
}
.repair-note { font-size: var(--fs-xs); color: var(--text-faint); }

/* --------------------------------------------------------- ai design review
   Phase 4 (PRD-design-ai): a page-level, click-only section, kept visually
   separate from the Design Score row above it and the itemized findings below
   it — this is judgment, not measurement, and the two must never blur into
   each other. Observation cards reuse .issue/.dx/.tag as-is (see aiObsCard()
   in app.js): a card with no severity class declares no --sev, so its dot
   stays transparent — the same "clean costs no colour" rule real findings
   follow, since a qualitative observation is a suggestion, not a defect. */
.ai-review { margin: var(--sp-5) 0; padding-top: var(--sp-4); border-top: 1px solid var(--bg-third); }
.ai-review-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: var(--sp-3); }
.ai-review-head h4 { margin: 0; font-size: var(--fs-md); font-weight: 500; color: var(--text-main); }
.ai-review-list { display: flex; flex-direction: column; gap: var(--sp-2); }
.ai-review-note {
  margin: 0; padding: 10px 12px; border-radius: var(--radius-md);
  background: var(--bg-main); color: var(--text-second); font-size: var(--fs-sm); line-height: var(--lh-normal);
}
.ai-review-note.hidden { display: none; }

/* --------------------------------------------------------- staging/live */
.plive-tabs { display: flex; gap: 6px; margin-bottom: var(--sp-3); }
.plive-tabs.hidden { display: none; }
.plive-diff { margin-bottom: var(--sp-3); }
.plive-diff.hidden { display: none; }
.plive-diff-title { margin: 0 0 4px; font-size: var(--fs-sm); color: var(--text-second); }
.plive-item {
  display: flex; align-items: center; gap: 8px; padding: 7px 9px;
  border-radius: var(--radius-md); background: var(--bg-main);
}
.plive-url { font-size: var(--fs-sm); color: var(--text-second); word-break: break-all; }
.plive-ok { margin: 0; font-size: var(--fs-sm); color: var(--text-second); }

/* ------------------------------------------------------------- ai suggest */
.ai-suggest { width: 100%; display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
.ai-suggest-result { width: 100%; }
.ai-suggest-draft { font-style: italic; color: var(--text-main); }
.ai-suggest-actions { display: flex; gap: 8px; margin-top: 8px; }
.gen-image-prompt { display: flex; gap: 8px; width: 100%; }
.gen-image-input {
  flex: 1; min-width: 0; padding: 6px 10px; border-radius: var(--radius-md);
  border: 1px solid var(--bg-third); background: var(--bg-main); color: var(--text-main);
  font-size: var(--fs-sm);
}
.gen-image-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.gen-image-cell { display: flex; flex-direction: column; gap: 6px; align-items: center; width: 140px; }
.gen-image-cell img { width: 140px; height: 140px; object-fit: cover; border-radius: var(--radius-md); border: 1px solid var(--bg-third); }

/* Link-preview mockup (what the page looks like pasted into Slack/Twitter/
   etc.) shown next to a title/description SEO finding. Neutral by design —
   same border-only container as the rest of the app, no colour: the point is
   to show the truncation, not to flag severity a second time. */
.share-preview {
  display: flex; gap: 12px; width: 100%; max-width: 420px;
  border: 1px solid var(--bg-third); border-radius: var(--radius-lg); overflow: hidden;
  background: var(--bg-second);
}
.share-preview-image {
  flex: none; width: 96px; height: 96px; display: flex; align-items: center; justify-content: center;
  background: var(--bg-third); color: var(--text-faint); font-size: var(--fs-2xs);
}
.share-preview-image img { width: 100%; height: 100%; object-fit: cover; }
.share-preview-body { flex: 1; min-width: 0; padding: 10px 12px 10px 0; display: flex; flex-direction: column; gap: 3px; justify-content: center; }
.share-preview-domain { font-size: var(--fs-2xs); color: var(--text-faint); letter-spacing: 0.04em; }
.share-preview-title { font-size: var(--fs-sm); font-weight: 500; color: var(--text-main); line-height: var(--lh-tight); }
.share-preview-desc { margin: 0; font-size: var(--fs-xs); color: var(--text-second); line-height: var(--lh-tight); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }

/* The confirm dialog replaces window.confirm(), so it has to carry the sentence
   that explains the consequence — pre-line keeps the paragraph break the copy
   writes, instead of running the warning into the description. */
.dialog-body {
  margin: 0; white-space: pre-line;
  font-size: var(--fs-sm); color: var(--text-second); line-height: var(--lh-normal);
}
.dialog-body:empty { display: none; }
.btn-danger {
  padding: 9px 13px;
  border: 1px solid var(--alert-line); border-radius: var(--radius-lg);
  background: var(--alert-soft); color: var(--alert);
  font-family: inherit; font-size: var(--fs-base); cursor: pointer;
}
.btn-danger:hover { background: var(--alert); color: var(--text-on-solid); }

/* ----------------------------------------------------------------- history */
/* Counts only, above the list — same claim the KPI deltas already make
   ("more/fewer open issues"), reusing --delta-good/--delta-bad rather than a
   new pair of colours, and only shown once a report has lifecycle data (see
   diffLifecycle() in diff.js) instead of a guess for older stored runs. */
.history-summary-bar {
  display: flex; flex-wrap: wrap; gap: var(--sp-6);
  background: var(--bg-second); border: 1px solid var(--bg-third);
  border-radius: var(--radius-xl); padding: var(--sp-4) var(--sp-5);
  margin-bottom: var(--sp-4);
}
.history-stat { display: flex; flex-direction: column; gap: 2px; }
.history-stat-value { font-size: var(--fs-2xl); font-weight: 700; letter-spacing: var(--track-tight); line-height: 1.1; }
.history-stat-value.good { color: var(--delta-good); }
.history-stat-value.bad { color: var(--delta-bad); }
.history-stat-label { font-size: var(--fs-sm); color: var(--text-second); }
.history-stat-note { font-size: var(--fs-xs); color: var(--text-faint); }

.history-date-filter { display: flex; align-items: center; gap: 6px; color: var(--text-faint); font-size: var(--fs-sm); }
.history-date-filter input {
  padding: 5px 8px;
  border: 1px solid var(--bg-muted); border-radius: var(--radius-md);
  background: var(--bg-main); color: var(--text-main);
  font-family: inherit; font-size: var(--fs-sm);
}
.history-date-filter input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent; }

.history-list {
  display: flex; flex-direction: column;
  background: var(--bg-second); border: 1px solid var(--bg-third);
  border-radius: var(--radius-xl); padding: 2px var(--sp-5);
}
.history-list:has(.empty), .history-list:has(.empty-panel) { background: none; border: 0; padding: 0; }
.history-day-label {
  font-size: var(--fs-2xs); font-weight: 500;
  text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--text-faint); padding: 12px 0 4px; margin: 0;
}
.history-day:first-child .history-day-label { padding-top: 8px; }
.history-row {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 0; border-top: 1px solid var(--bg-third);
  font-size: var(--fs-base);
  width: 100%; background: none; border-left: none; border-right: none; border-bottom: none;
  text-align: left; font-family: inherit; cursor: pointer; color: inherit;
  transition: background-color 180ms ease;
}
.history-day:first-child .history-row:first-child { border-top: none; }
.history-row:hover, .history-row[aria-expanded="true"] { background: var(--bg-third); }
.history-date { font-family: var(--mono); font-size: var(--fs-sm); color: var(--text-second); min-width: 90px; flex: none; }
.history-summary { color: var(--text-second); font-size: var(--fs-sm); flex: 1; min-width: 0; }
.history-lifecycle { display: flex; gap: 6px; flex: none; }
/* Same disclosure chevron as every <details> in the app (see the shared
   selector list above) — this one is a <button>, not a <details>, so it gets
   its own rule instead of joining that list, but draws identically. */
.history-caret {
  flex: none; display: inline-block; width: 6px; height: 6px; margin: 0 2px;
  border-right: 1.4px solid var(--text-faint); border-bottom: 1.4px solid var(--text-faint);
  transform: rotate(-45deg); transition: transform 0.16s ease;
}
.history-row[aria-expanded="true"] .history-caret { transform: rotate(45deg); }

/* One run's full detail, expanded in place under its row. Same building
   blocks as everywhere else in the app (.log-row, word-diff) — this is not a
   new way of showing a change, just this list's own place to show it. */
.history-detail { padding: 0 0 14px 4px; border-top: none; display: flex; flex-direction: column; gap: 2px; }
.history-detail .log-row:first-child { border-top: none; }

/* Left, like everything above it. A centred line under an entirely left-aligned
   page is the one place the layout stops agreeing with itself. */
.foot {
  text-align: left; color: var(--text-faint); font-size: var(--fs-xs);
  border-top: 1px solid var(--bg-third);
  padding-top: var(--sp-4); margin-top: var(--sp-8);
}

/* ------------------------------------------------------------------ drawer */
.drawer { position: fixed; inset: 0; z-index: 50; display: flex; justify-content: flex-end; }
.drawer-backdrop { position: absolute; inset: 0; background: var(--overlay); }
.drawer-panel {
  position: relative; width: min(560px, 92vw); height: 100%;
  background: var(--bg-second); border-left: 1px solid var(--bg-third);
  box-shadow: var(--shadow-pop);
  display: flex; flex-direction: column;
  animation: slide-in 0.18s ease;
}
@keyframes slide-in { from { transform: translateX(24px); opacity: 0.4; } }
.drawer-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 22px 24px; border-bottom: 1px solid var(--bg-third);
}
.drawer-head h3 { margin: 0 0 4px; font-size: var(--fs-lg); font-weight: 500; }
.drawer-body { padding: 22px 24px; overflow-y: auto; flex: 1; }

.issue {
  border: 1px solid var(--bg-third);
  border-radius: var(--radius-lg); padding: var(--sp-3) var(--sp-3) var(--sp-3) 25px; margin-bottom: var(--sp-2);
}
.issue::before { top: 15px; left: var(--sp-3); width: 5px; height: 5px; }
/* A finding somebody has signed off on recedes: it is still fully readable, but
   it should never be the first thing the eye lands on in a list of real work. */
.issue.accepted { opacity: 0.78; }
.issue.accepted:hover { opacity: 1; }
.issue-head { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
.issue-head strong { font-size: var(--fs-base); }
.issue p { margin: 0; font-size: var(--fs-sm); color: var(--text-second); line-height: var(--lh-normal); word-break: break-word; }
.issue-since { font-size: var(--fs-xs); color: var(--text-faint); margin-top: 5px; display: block; }

.bp-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
.bp-side { background: var(--bg-main); border-radius: var(--radius-md); padding: 8px 10px; }
.bp-side span {
  font-size: var(--fs-2xs); font-weight: 500;
  text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--text-faint); display: block; margin-bottom: 3px;
}
.bp-side p { font-size: var(--fs-sm); color: var(--text-main); }

.word-diff { margin-top: 8px; font-size: var(--fs-sm); line-height: 1.6; background: var(--bg-main); padding: 9px 11px; border-radius: var(--radius-md); }
/* The one place green survives: here it is not "everything is fine", it is
   "this text was added", which is information the reader needs. */
.word-diff ins { background: var(--ok-soft); color: var(--add); text-decoration: none; padding: 0 2px; border-radius: var(--radius-md); }
.word-diff del { background: var(--alert-soft); color: var(--del); padding: 0 2px; border-radius: var(--radius-md); }
.word-diff .ctx { color: var(--text-faint); }

.drawer-design-score { margin-bottom: 14px; }
.drawer-shots { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; }
.drawer-shots .shot { aspect-ratio: 3 / 4; border-radius: var(--radius-md); border: 1px solid var(--bg-third); }

/* ------------------------------------------------------- issue diagnosis */
/* Each finding reads top to bottom: what was found, what it means, where it is,
   and what to do. The blocks are tinted differently so the eye can jump to the
   fix without reading the rest. */
.issue-detail { font-family: var(--mono); font-size: var(--fs-sm) !important; }
.issue-target {
  display: block; font-family: var(--mono); font-size: var(--fs-xs); margin-top: 4px;
  color: var(--link); text-decoration: none; word-break: break-all;
}
.issue-target:hover { text-decoration: underline; }

/* --------------------------------------------------------------- priority */
.priority-list { display: flex; flex-direction: column; gap: 8px; }
.priority-row {
  border: 1px solid var(--bg-third); border-radius: var(--radius-lg);
  padding: var(--sp-3) var(--sp-3) var(--sp-3) 25px;
}
.priority-row::before { top: 15px; left: var(--sp-3); width: 5px; height: 5px; }
.priority-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 5px; }
.priority-head strong { font-size: var(--fs-base); }
.priority-detail { font-family: var(--mono); font-size: var(--fs-sm) !important; color: var(--text-second); }
.priority-page-btn {
  display: inline-flex; align-items: center; margin-top: 6px; margin-right: 6px;
  font-size: var(--fs-xs); padding: 3px 9px; border-radius: var(--radius-md);
  background: var(--bg-main); border: 1px solid var(--bg-third); color: var(--link);
  cursor: pointer;
}
.priority-page-btn:hover { text-decoration: underline; }
.priority-pages { margin-top: 6px; }
.priority-pages summary { cursor: pointer; list-style: none; color: var(--text-faint); font-size: var(--fs-sm); }
.priority-pages summary::-webkit-details-marker { display: none; }
.priority-pages summary:hover { color: var(--text-second); }
.priority-page-list { margin-top: 6px; display: flex; flex-wrap: wrap; }

/* "Suggest a replacement URL" on a broken link — see renderLinkFixBox() and
   src/link-fixes.js. Reuses .gen-image-prompt/.gen-image-input for the input
   row so this doesn't invent a second text-field style. */
.link-fix { margin-top: 10px; }
.link-fix-saved {
  margin: 0; font-size: var(--fs-sm); color: var(--text-second);
  display: flex; align-items: baseline; gap: 4px; flex-wrap: wrap;
}
.link-fix-saved strong { color: var(--text-main); font-weight: 500; }
.link-fix-saved a { word-break: break-all; }
.link-fix-actions { display: flex; gap: 8px; margin-top: 6px; }

/* Line chart above the worst-design-list: x = pages ranked worst to best,
   y = design score. Dots reuse the same severity colours as everywhere else;
   "ok" pages (score >= 85 despite having an open finding) stay uncoloured on
   purpose, same rule as every other clean/low-severity state in this app.
   The connecting line itself stays neutral — it runs through mixed
   severities, so it is not one more thing to colour alert/review. Kept
   visually distinct from the bubble plot in "By category" right next to it,
   which the two used to be close enough to read as the same chart. */
.design-scatter { margin-bottom: var(--sp-3); }
.scatter-svg { width: 100%; height: 108px; display: block; }
.scatter-svg :where(line, path, circle) { vector-effect: non-scaling-stroke; }
.score-line { fill: none; stroke: var(--text-second); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.scatter-dot {
  fill-opacity: 0.16; stroke-width: 2; cursor: pointer;
  transition: fill-opacity 0.18s ease, stroke-width 0.18s ease, r 0.18s ease;
}
.scatter-dot:hover { fill-opacity: 0.28; stroke-width: 2.5; r: 6; }
.scatter-dot.alert { fill: var(--alert); stroke: var(--alert); }
.scatter-dot.review { fill: var(--review); stroke: var(--review); }
.scatter-dot.ok { fill: var(--text-faint); stroke: var(--text-faint); }
.scatter-axis {
  display: flex; justify-content: space-between;
  font-size: var(--fs-2xs); color: var(--text-faint); margin-top: 2px;
}
.scatter-axis span:nth-child(2) { text-transform: uppercase; letter-spacing: var(--track-caps); }

/* Overview "worst design scores" card: same row shape as .priority-row, plus
   each page's own mini-bar-list underneath so the top offenders (and why)
   are visible without opening every drawer. */
.worst-design-list { display: flex; flex-direction: column; gap: 6px; }
/* Tighter than .priority-row's padding/gap: this row always carries a
   mini-bar-list underneath, and five of these stack in one card, so the same
   roomy spacing that reads fine for a single-line priority row compounds
   into a card that dwarfs its neighbors once repeated five times. */
.worst-design-row {
  border: 1px solid var(--bg-third); border-radius: var(--radius-lg);
  padding: 10px var(--sp-3) 10px 25px;
  background: var(--bg-second); cursor: pointer; width: 100%;
  display: flex; flex-direction: column; gap: 6px;
  text-align: left; font-family: inherit; color: inherit;
  transition: transform 0.15s ease, border-color 0.15s ease;
}
.worst-design-row::before { top: 13px; left: var(--sp-3); width: 5px; height: 5px; }
.worst-design-row:hover { transform: translateY(-1px); border-color: var(--text-faint); }
.worst-design-row .mini-bar-list { margin: 0; gap: 4px; }

.dx { margin-top: 9px; padding: 8px 11px; border-radius: var(--radius-md); background: var(--bg-main); }
.dx-label {
  display: block; font-size: var(--fs-2xs); font-weight: 500;
  text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--text-faint); margin-bottom: 4px;
}
.dx ul, .dx ol { margin: 0; padding-left: 17px; }
.dx li { font-size: var(--fs-sm); color: var(--text-second); line-height: 1.55; margin-bottom: 3px; }
.dx li::marker { color: var(--text-faint); }
/* The fix block earns its emphasis with a border and darker text rather than a
   green wash: it is the part you act on, not the part that is going well. */
.dx-fix { background: var(--bg-main); border-left: 1px solid var(--bg-muted); }
.dx-fix .dx-label { color: var(--text-second); }
.dx-fix li { color: var(--text-main); }
.dx-evidence { background: transparent; padding: 4px 0 0; }
.dx-evidence li { font-size: var(--fs-xs); color: var(--text-faint); }
.dx-evidence summary {
  display: block; cursor: pointer; list-style: none; user-select: none;
  font-size: var(--fs-2xs); font-weight: 500; text-transform: uppercase;
  letter-spacing: var(--track-caps); color: var(--text-faint); margin-bottom: 4px;
}
.dx-evidence summary::-webkit-details-marker { display: none; }
.dx-evidence summary:hover { color: var(--text-second); }
.dx-evidence[open] summary { margin-bottom: 4px; }
.dx-evidence ul { margin-top: 4px; }

.dx-check { margin-top: 9px; border-top: 1px solid var(--bg-third); padding-top: 7px; }
.dx-check summary {
  cursor: pointer; font-size: var(--fs-xs); color: var(--text-faint);
  list-style: none; user-select: none;
}
.dx-check summary::-webkit-details-marker { display: none; }
.dx-check summary:hover { color: var(--text-second); }
.dx-check p { margin-top: 6px !important; font-size: var(--fs-sm) !important; }
.dx-check b { color: var(--text-main); }

/* ----------------------------------------------- marking a finding as meant */
/* The decision sits at the bottom of the card, after everything that informs
   it: what was found, what it means, and what fixing it would involve. */
.issue-foot {
  margin-top: 10px; padding-top: 9px; border-top: 1px solid var(--bg-third);
  display: flex; flex-direction: column; gap: 8px; align-items: flex-start;
}
.mark-btn { font-size: var(--fs-xs); padding: 5px 10px; }
.mark-btn.on { color: var(--text-faint); }

.accepted-note, .accepted-stale { width: 100%; padding: 8px 11px; border-radius: var(--radius-md); }
.accepted-note { background: var(--accepted-soft); }
.accepted-note .dx-label { color: var(--accepted); }
.accepted-note small, .accepted-stale small {
  display: block; margin-top: 5px; font-size: var(--fs-xs); color: var(--text-faint);
}
/* A mark that stopped applying is the one thing here that has to be loud: a
   decision was made about something that is no longer the same. */
.accepted-stale { background: var(--alert-soft); }
.accepted-stale .dx-label { color: var(--alert); }

/* --------------------------------------------------- intentional (the list) */
.mark-row {
  border: 1px solid var(--bg-third); background: var(--bg-second);
  border-radius: var(--radius-lg); padding: var(--sp-3) var(--sp-4) var(--sp-3) 27px; margin-bottom: var(--sp-2);
  display: flex; flex-direction: column; gap: 5px; align-items: flex-start;
}
.mark-row::before { top: 17px; left: var(--sp-3); width: 5px; height: 5px; }
.mark-row.gone { opacity: 0.7; }
.mark-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.mark-head strong { font-size: var(--fs-base); }
.mark-row p { margin: 0; font-size: var(--fs-sm); color: var(--text-second); line-height: var(--lh-normal); word-break: break-word; }
.mark-detail { font-family: var(--mono); font-size: var(--fs-xs) !important; }
.mark-now { color: var(--alert) !important; font-size: var(--fs-sm) !important; }
.mark-note { font-style: italic; color: var(--text-main) !important; }
.mark-meta { display: flex; gap: 10px; flex-wrap: wrap; font-size: var(--fs-xs); color: var(--text-faint); }
.mark-path { font-family: var(--mono); color: var(--link); text-decoration: none; }
.mark-path:hover { text-decoration: underline; }

.mark-log { margin-top: 14px; border-top: 1px solid var(--bg-third); padding-top: 10px; }
.mark-log summary {
  cursor: pointer; font-size: var(--fs-sm); color: var(--text-faint);
  list-style: none; user-select: none;
}
.mark-log summary::-webkit-details-marker { display: none; }
.mark-log summary:hover { color: var(--text-second); }
.log-row {
  display: grid; grid-template-columns: 72px 118px 1fr; gap: 10px; align-items: baseline;
  padding: 6px 0; border-bottom: 1px solid var(--bg-third); font-size: var(--fs-xs);
}
.log-act {
  text-transform: uppercase; letter-spacing: var(--track-caps);
  font-size: var(--fs-2xs); font-weight: 500;
}
.log-act.accepted { color: var(--accepted); }
.log-act.unmarked { color: var(--review); }
/* Same claim a KPI delta already makes (more/fewer open issues), not a
   verdict on the page — see --delta-good/--delta-bad in the token file. */
.log-act.new { color: var(--delta-bad); }
.log-act.resolved { color: var(--delta-good); }
.log-when { font-family: var(--mono); color: var(--text-faint); }
.log-what { color: var(--text-second); word-break: break-word; }
.log-note { grid-column: 3; font-style: italic; color: var(--text-faint); }
.page-history-list { margin-top: 8px; }
@media (max-width: 640px) {
  .log-row { grid-template-columns: 1fr; gap: 2px; }
  .log-note, .log-what { grid-column: 1; }
}

/* ------------------------------------------------------------ what we check */
.scope-stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px; margin-bottom: 20px;
}
.scope-stat {
  background: var(--bg-second); border: 1px solid var(--bg-third);
  border-radius: var(--radius-xl); padding: 13px 15px;
  display: flex; flex-direction: column; gap: 1px;
}
.scope-stat strong { font-size: var(--fs-xl); font-weight: 700; letter-spacing: var(--track-tight); }
.ss-label { font-size: var(--fs-sm); color: var(--text-main); }
.ss-note { font-size: var(--fs-xs); color: var(--text-faint); line-height: 1.35; }

.scope-body { display: flex; flex-direction: column; gap: 22px; }
.scope-group-head { margin-bottom: 8px; }
.scope-group-head h3 { margin: 0 0 2px; font-size: var(--fs-base); font-weight: 500; }
.scope-group-head p { margin: 0; }

.scope-check {
  border: 1px solid var(--bg-third); border-radius: var(--radius-lg);
  margin-bottom: 6px; background: var(--bg-second);
}
.scope-check summary {
  display: flex; align-items: center; gap: 9px; padding: 10px 13px;
  cursor: pointer; list-style: none; user-select: none; flex-wrap: wrap;
}
.scope-check summary::-webkit-details-marker { display: none; }
.scope-check summary:hover { background: var(--bg-main); border-radius: var(--radius-lg); }
.sev-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--review-vivid); flex: none; }
.sev-dot.alert { background: var(--alert); }
.sev-dot.info { background: var(--link); }
.sc-name { font-size: var(--fs-base); font-weight: 500; flex: 1 1 140px; }
.sc-flag {
  font-size: var(--fs-2xs); font-family: var(--mono); color: var(--text-faint);
  background: var(--bg-third); padding: 2px 6px; border-radius: var(--radius-md);
}
.sc-state {
  font-size: var(--fs-2xs); font-weight: 500;
  text-transform: uppercase; letter-spacing: var(--track-caps);
}
.sc-state.alert { color: var(--alert); }
.sc-state.review { color: var(--review); }
.sc-state.info { color: var(--link); }
.sc-state.off { color: var(--text-faint); }
/* A check that cannot run on this domain is shown, not hidden: the scope page
   has to be honest about what is and is not covered. */
.scope-check.inactive { opacity: 0.55; }
.sc-body { padding: 0 13px 12px 30px; }
.sc-body p { margin: 0 0 7px; font-size: var(--fs-sm); color: var(--text-second); line-height: 1.6; }
.sc-body b { color: var(--text-main); }
.sc-fix-label { display: block; font-size: var(--fs-sm); margin: 10px 0 4px; }
.sc-body ol { margin: 0; padding-left: 17px; }
.sc-body li { font-size: var(--fs-sm); color: var(--text-second); line-height: 1.55; margin-bottom: 3px; }

/* Printing this app means one thing: handing someone the list of what we check.
   So the chrome goes, every other view goes, and the scope view prints open. */
@media print {
  .sidebar, .topbar, .view-bar, #scopePrint { display: none !important; }
  .view { display: none !important; }
  .view[data-view="scope"] { display: block !important; padding: 0; }
  .main { padding: 0; }
  .scope-check { break-inside: avoid; opacity: 1 !important; }
  .scope-check .sc-body { display: block !important; }
}

/* ------------------------------------------------------------------ tooltip */
/* One shared tooltip for every chart. It is positioned with a transform so it
   never triggers a layout, and it is measured while visible-but-transparent so
   it can flip sides near the edges of the window. */
.tip {
  position: fixed; top: 0; left: 0; z-index: 120;
  max-width: 320px; pointer-events: none;
  background: var(--bg-second); color: var(--text-main);
  border: 1px solid var(--bg-muted);
  border-radius: var(--radius-lg); padding: 10px 12px 10px 24px;
  box-shadow: var(--shadow-float);
  display: flex; flex-direction: column; gap: 5px;
  opacity: 1; transition: opacity 0.1s ease;
}
.tip::before { top: 13px; left: 11px; width: 5px; height: 5px; }
.tip.off { opacity: 0; }
.tip-title { font-size: var(--fs-base); line-height: 1.3; }
.tip-sub { font-size: var(--fs-xs); color: var(--text-faint); font-family: var(--mono); word-break: break-all; }
.tip-rows { display: flex; flex-direction: column; gap: 2px; margin-top: 2px; }
.tip-row { display: flex; justify-content: space-between; gap: 14px; font-size: var(--fs-xs); }
.tip-row span { color: var(--text-second); }
.tip-row b { font-family: var(--mono); }
.tip-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
.tip-note { font-size: var(--fs-xs); color: var(--text-second); line-height: var(--lh-normal); white-space: pre-line; }
.tip-hint {
  font-size: var(--fs-2xs); color: var(--text-faint); border-top: 1px solid var(--bg-third);
  padding-top: 5px; margin-top: 2px;
}

/* --------------------------------------------------------------- lightbox */
.lightbox {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0, 0, 0, 0.85);
  display: grid; place-items: center; padding: 24px; cursor: zoom-out;
}
.lightbox img {
  max-width: 100%; max-height: 100%; object-fit: contain;
  border-radius: var(--radius-md); box-shadow: var(--shadow-pop);
}

/* Below 860px the rail becomes a header. Seven destinations do not fit across
   a narrow screen, so the nav scrolls sideways as one strip rather than
   wrapping into three ragged rows that cost more height than the view below
   them. The bleed and the padding cancel out, so the strip scrolls edge to
   edge while its first item still lines up with the content. */
@media (max-width: 860px) {
  .shell { flex-direction: column; }
  .sidebar {
    width: auto; height: auto; position: static;
    flex-direction: column; align-items: stretch; gap: 12px;
    padding: 14px 16px 10px;
    border-right: 0; border-bottom: 1px solid var(--bg-third);
  }
  .brand { padding-left: 0; }

  .site-switch { flex-direction: row; align-items: center; gap: 8px; }
  .site-switch .side-label { display: none; }
  .site-tabs { flex-direction: row; overflow-x: auto; gap: 8px; }
  .site-tab, .site-add { flex: none; padding-left: 12px; }
  .site-add { width: auto; }

  .nav {
    flex-direction: row; flex-wrap: nowrap;
    overflow-x: auto; scrollbar-width: none;
    gap: 4px; margin: 0 -16px; padding: 0 16px 2px;
  }
  .nav::-webkit-scrollbar { display: none; }
  .nav-item { flex: none; white-space: nowrap; }

  .sidebar-foot { flex-direction: row; align-items: center; gap: 8px; margin-top: 0; }
  .mini-card { flex: 1; }

  .main { padding: 0 16px 44px; }
  .topbar { padding: 16px 0 12px; }
}

/* --------------------------------------------------------------- login.html */
.login-body {
  min-height: 100vh; display: grid; place-items: center; padding: 24px;
}
.login-card {
  width: 100%; max-width: 340px;
  display: flex; flex-direction: column; gap: var(--sp-4);
  background: var(--bg-second); border: 1px solid var(--bg-third);
  border-radius: var(--radius-xl); padding: var(--sp-5);
  box-shadow: var(--shadow-card);
}
.login-brand { padding: 0 0 4px; }
.login-submit { width: 100%; justify-content: center; }
