/* TrustCtrl web UI — design system.
   Follows UX-002: an 8px spacing scale (4/8/16/24/32/48/64), a capped type scale (5 sizes),
   three button types, and status expressed as colour + icon + text. Calm, professional, light by
   default with a dark theme via [data-theme="dark"].

   ── The "paper and ink" light theme ──────────────────────────────────────────────────────────
   The light theme is a design draft the owner approved; it was proven first in the CSS lab
   (design/css-lab/, live at /css-lab-product) before it was moved in here. Five moves carry it:

     1. Paper, not screen — a warm off-white page, flat surfaces, no gradients, hairline shadows.
     2. 17px body text, and a "muted" that is genuinely readable (#5f646c, not #8593a8).
     3. A 2px border on everything pressable, because a coloured fill alone is too weak a signal.
     4. Numbers in JetBrains Mono, so digits line up down a column.
     5. Colour only where it means something.

   The DARK theme keeps the platform's own navy — that was a deliberate choice, not an oversight.
   The draft shipped its own dark palette and we are not taking it.

   🔴 But the theme decision is about COLOUR only. Everything in moves 2-4 above — the type scale,
   the 2px borders, the 48px hit areas, the flat surfaces — is theme-independent and applies in
   dark too. Otherwise a customer gets two different products depending on one toggle.

   🔴 The spacing scale is deliberately NOT touched. The draft has its own 8-step scale whose
   values collide with ours (its --sp-5 is 20px, ours is 32px), and spacing is not where this
   design lives. Rewriting it would move every gap on every page for no visible gain. Components
   that need the draft's finer steps (12/20/26px) say so in their own rule. */

/* ── Typefaces ──────────────────────────────────────────────────────────────────────────────
   🔴 These MUST be served from here. The Content-Security-Policy says `font-src 'self'`, so an
   @import from fonts.googleapis.com is blocked without a single error message — the page simply
   falls back to the system font and you believe you have tested the design. We have made that
   mistake once already. Variable files, 84 KB in total, in static/fonts/.

   🔴 JetBrains Mono is declared 400-500, not 400-700. The design asks for font-weight:700 on
   every number; with no 700 cut the browser draws a real 700 with wider digits, which reads as
   "a different font size" and cannot be spotted by comparing px values, because they are equal. */
@font-face {
  font-family: "Instrument Sans"; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url("fonts/instrument-sans-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Instrument Sans"; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url("fonts/instrument-sans-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
                 U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
                 U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 500; font-display: swap;
  src: url("fonts/jetbrains-mono-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}
@font-face {
  font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 500; font-display: swap;
  src: url("fonts/jetbrains-mono-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
                 U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
                 U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --font-sans: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;

  /* Minimum hit areas. 48px is not arbitrary — it is the smallest frame a thumb hits reliably. */
  --hit: 48px;
  --hit-sm: 44px;
  /* Spacing scale — the only permitted step values. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 16px;
  --sp-4: 24px;
  --sp-5: 32px;
  --sp-6: 48px;
  --sp-7: 64px;

  /* Type scale — five sizes, no more.
     🔴 Raised for the paper theme: body is 17px, not 15px, and "small" is 15.5px, not 13px. This
     is the change with the widest blast radius in the whole stylesheet, because ~118 fixed pixel
     widths and 21 grid-column definitions elsewhere in this file were measured against 13px text
     and will clip at 15.5px. They are corrected at their own rules, not here. */
  --fs-xs: 14px;
  --fs-sm: 15.5px;
  --fs-md: 17px;
  --fs-lg: 21px;
  --fs-xl: 28px;

  --radius-sm: 8px;
  --radius: 10px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --content-max: 1320px;
  /* Two widths, and `--sidebar-w` points at whichever is in force. Everything that has to line up
     with the rail — the shell grid, the mobile drawer's cap — reads the pointer, so collapsing is
     one token swap on :root rather than a rule per affected element. */
  --sidebar-w-open: 260px;
  --sidebar-w-min: 76px;
  --sidebar-w: var(--sidebar-w-open);
  --topbar-h: 64px;

  /* ── The navigation rail's own palette ───────────────────────────────────
     🔴 The rail is dark in BOTH themes. On paper that is the one deliberate inversion in the
     design: the page is where the work is and it is light, the rail is where you are and it is
     not. Without it the sidebar and the page are the same white and the eye has to find the
     border to know which is which — which is exactly what the old shell felt like.

     These are literals, not the page tokens. The page tokens flip with the theme; if the rail
     read from them it would go white on paper, and the whole point is that it does not. */
  --nav-bg: #161b24;
  --nav-edge: #0e121a;
  --nav-text: #c1c7d0;
  --nav-text-strong: #ffffff;
  --nav-hover: #242c38;
  --nav-label: #8f97a6;
  --nav-sep: #2b3340;
  --nav-outline: #39414f;
  --nav-outline-hover: #8f97a6;
  --nav-rail: #7ba4f5;
  /* Dot colours for a product's state. Lighter than the page's severity colours on purpose —
     these sit on near-black, where the page's #b3261e reads as a dark smudge. */
  --nav-dot-bad: #ff8f86;
  --nav-dot-warn: #e5b45f;
  --nav-dot-none: #4a5462;

  /* Light theme — "paper and ink" */
  --bg-body: #f4f3ef;
  --bg-surface: #ffffff;
  --bg-surface-2: #faf9f6;
  --bg-hover: #f1f0ec;
  --bg-topbar: #ffffff;
  --bg-sidebar: #ffffff;

  /* The inverse surface: a filled dark bar carrying a light active chip (the Simple/Technical
     toggle). It flips in dark mode — see the note there; that flip is deliberate. */
  --surface-inverse: #1b1f27;
  --surface-inverse-hover: #333944;

  --text-primary: #1b1f27;
  --text-strong: #14181f;
  --text-secondary: #3a4049;
  /* 🔴 Prose belongs in --text-secondary. --text-muted is for metadata beside it, and it is now
     dark enough to actually read — the old #8593a8 sat at the edge of legibility on paper. */
  --text-muted: #5f646c;
  --text-inverse: #ffffff;
  --text-inverse-2: #c8ccd3;

  --border: rgba(20, 24, 32, 0.12);
  /* A hairline for rows inside a card, where a full --border would draw a ladder. */
  --border-soft: rgba(20, 24, 32, 0.08);
  --border-strong: #8b919b;
  /* The unfilled part of a meter or progress bar. */
  --track: #e6e4de;

  --accent: #2f62d9;
  --accent-hover: #254fb4;
  --accent-soft: #eef3fd;
  /* Link blue is darker than button blue: it has to hold contrast against paper at 17px. */
  --accent-text: #23509f;
  --accent-text-hover: #16376f;
  --accent-border: #b8ccf5;

  --ok: #0f5f42;
  --ok-soft: #eaf6f0;
  --warn: #7a5a12;
  --warn-soft: #fbf4e2;
  --attention: #9a4c00;
  --attention-soft: #fdf1e4;
  --danger: #b3261e;
  --danger-soft: #fdeceb;
  --info: #23509f;
  --info-soft: #eef3fd;

  /* Paper does not float. Shadows are hairlines that separate, not depth that lifts. */
  --shadow-sm: 0 1px 2px rgba(20, 24, 32, 0.04);
  --shadow-md: 0 2px 10px rgba(20, 24, 32, 0.07);
  --shadow-lg: 0 12px 32px rgba(20, 24, 32, 0.16);

  color-scheme: light;
}

/* The platform's own navy — kept on purpose when the paper theme landed. The draft shipped a dark
   palette of its own and we are not taking it; this is the theme customers already know.
   Only the tokens the paper theme introduced are added below, so the two themes stay in step. */
[data-theme="dark"] {
  --bg-body: #0b1220;
  --bg-surface: #131c2b;
  --bg-surface-2: #0f1826;
  --bg-hover: #1b2638;
  --bg-topbar: #101a29;
  --bg-sidebar: #101a29;

  /* The rail is dark in both themes, so only its hue moves — to the platform's own navy, which
     the user chose to keep. The geometry, the weights and the rail marker are identical, so a
     customer who flips the theme changes the colour of one surface, not the product. */
  --nav-bg: #101a29;
  --nav-edge: #060d18;
  --nav-hover: #1b2638;
  --nav-sep: #22314a;
  --nav-outline: #2a3b57;
  --nav-outline-hover: #7d8ba3;

  /* 🔴 This flips, and the flip is the design, not a bug. The filled bar behind the
     Simple/Technical toggle is dark on paper and light in navy — in both themes it is the
     surface the page is NOT, which is what makes the active chip read as chosen. */
  --surface-inverse: #e8eef6;
  --surface-inverse-hover: #cbd6e6;

  --text-primary: #e8eef6;
  --text-strong: #ffffff;
  --text-secondary: #a7b3c6;
  --text-muted: #7383a0;
  --text-inverse: #0b1220;
  --text-inverse-2: #51607a;

  --border: #223047;
  --border-soft: rgba(255, 255, 255, 0.06);
  --border-strong: #2d3f5c;
  --track: #1b2638;

  --accent: #3b82f6;
  --accent-hover: #60a5fa;
  --accent-soft: rgba(59, 130, 246, 0.16);
  --accent-text: #93c5fd;
  --accent-text-hover: #bfdbfe;
  --accent-border: rgba(59, 130, 246, 0.34);

  --ok: #4ade80;
  --ok-soft: rgba(74, 222, 128, 0.14);
  --warn: #fbbf24;
  --warn-soft: rgba(251, 191, 36, 0.14);
  --attention: #fb923c;
  --attention-soft: rgba(251, 146, 60, 0.16);
  --danger: #f87171;
  --danger-soft: rgba(248, 113, 113, 0.14);
  --info: #93c5fd;
  --info-soft: rgba(59, 130, 246, 0.16);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 6px 20px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.5);

  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
}

body {
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  line-height: 1.6;
  color: var(--text-primary);
  background: var(--bg-body);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent-text); text-decoration: none; }
a:hover { color: var(--accent-text-hover); text-decoration: underline; }
/* In running prose the underline is the only signal that survives colour blindness and a
   black-and-white printout, so links inside text carry it without being hovered. */
p a, li a { text-decoration: underline; text-underline-offset: 3px; }

h1, h2, h3 { margin: 0; font-weight: 700; line-height: 1.2; letter-spacing: -0.018em; }
p { text-wrap: pretty; }

/* One focus ring for the whole platform, thick enough to find on paper. */
:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; border-radius: 6px; }
button:focus-visible, a:focus-visible, summary:focus-visible { outline-color: var(--text-strong); }
::selection { background: var(--accent-soft); }

/* ── Brand ─────────────────────────────────────────────────────────────── */
.brand { display: flex; align-items: center; gap: var(--sp-2); }
.brand-mark {
  width: 26px; height: 26px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  background: linear-gradient(135deg, #60a5fa, #2563eb);
  color: #fff;
}
.brand-word { font-size: var(--fs-md); font-weight: 700; letter-spacing: -0.01em; }
.brand-word .accent { color: var(--accent); }
[data-theme="dark"] .brand-word .accent { color: var(--accent-text); }

/* ── App shell ─────────────────────────────────────────────────────────── */
/* Five permanent areas (UX-003 §3): top bar, left nav, main content, and the docked AI panel.

   🔴 One deliberate departure from the paper theme in here: the top bar and the left nav keep
   HAIRLINE borders, not the 2px the rest of the platform puts on anything pressable. The 2px rule
   exists so a control stands out from the page it sits on; the chrome is not on the page, it is
   around it, and a row of heavy outlines up there competes with the page's own controls for the
   same attention. The hit areas are the part that matters and they are raised to 44px throughout. */
/* The rail now runs the FULL height and the top bar starts to its right. It used to be the other
   way round — one bar across the top, the rail hung underneath it — and that ordering said the bar
   owned the window. It does not: the rail is where you are in the product, the bar is what you do
   to the page you are on. Putting the brand at the top of the rail and letting the bar begin at the
   content's left edge makes the two columns read as "place" and "page" instead of one nested in the
   other. It is also what makes collapsing legible: the whole left edge narrows, not a panel. */
.app-shell { display: grid; grid-template-columns: var(--sidebar-w) 1fr auto; grid-template-rows: var(--topbar-h) 1fr; min-height: 100vh; }

.topbar {
  grid-column: 2 / 4;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3);
  padding: 0 var(--sp-4);
  background: var(--bg-topbar);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 20;
}
/* 🔴 Both sides may shrink, and every control in them may not.
   Moving the bar out of column 1 cost it the rail's 260px, and between 901px and ~980px the row
   no longer fitted: the whole document grew a 41px horizontal scrollbar. Nothing LOOKED broken in
   a screenshot — the bar itself was intact, the page under it had simply been pushed sideways —
   which is why this was found by measuring and not by looking.
   `min-width: 0` lets the two halves give; `flex: none` on the buttons stops the give landing on
   the hit areas, which were being squeezed from 44px to 42px. The search field is the one part
   that may absorb it, because a shorter search field is still a search field. */
.topbar-left { display: flex; align-items: center; gap: var(--sp-4); min-width: 0; }
.topbar-right { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.topbar-left > *, .topbar-right > * { flex: none; }

/* Tenant selector */
.tenant-selector {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: var(--hit-sm); padding: 0 var(--sp-3);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--bg-surface-2); color: var(--text-secondary);
  font-size: var(--fs-sm); font-weight: 600; font-family: inherit; cursor: pointer;
  max-width: 220px;
}
.tenant-selector { white-space: nowrap; }
.tenant-selector:hover { background: var(--bg-hover); color: var(--text-strong); }
/* 🔴 `min-width: 0` is what makes the ellipsis work. A flex item defaults to `min-width: auto`,
   which means it refuses to shrink below its content — so the name ignored the button's max-width
   and ran out over the notification bell on a phone instead of truncating. The icon and the caret
   are pinned so the name is the only part that gives. Unlike an asset name, a tenant name is
   orientation rather than identity, and the full one is a click away in the menu. */
.tenant-selector > svg, .tenant-selector .caret { flex: none; }
/* Structural, not `#tenantName`: layout that depends on an id breaks the moment the markup is
   reused or previewed somewhere else, and it did — this rule was written against the id and had no
   effect in the shell preview next door. */
.tenant-selector > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tenant-selector .caret { color: var(--text-muted); font-size: 10px; }

/* Popover menu (profile) */
.menu-pop { position: absolute; right: 0; top: 40px; width: 224px; padding: var(--sp-2); display: none; z-index: 30; }
.menu-head { padding: var(--sp-2); border-bottom: 1px solid var(--border); margin-bottom: var(--sp-2); }

/* Topbar panels (notifications + task center) — fixed popovers anchored under their icon */
.panel-pop {
  position: fixed; width: 400px; max-width: calc(100vw - 16px);
  max-height: min(520px, calc(100vh - 72px)); overflow-y: auto;
  padding: 0; display: none; z-index: 35;
}
.panel-pop.open { display: block; }
.panel-head {
  position: sticky; top: 0; z-index: 1;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  padding: var(--sp-3); border-bottom: 1px solid var(--border);
  background: var(--bg-surface); font-weight: 650; font-size: var(--fs-sm);
}
.panel-list { padding: var(--sp-1); }
.panel-section {
  padding: var(--sp-2) var(--sp-2) var(--sp-1);
  font-size: var(--fs-xs); font-weight: 650; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .04em;
}
.panel-item {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  width: 100%; padding: var(--sp-2); border: 0; border-radius: var(--radius-sm);
  background: transparent; color: inherit; text-align: left; cursor: pointer;
  font: inherit;
}
.panel-item:hover { background: var(--bg-hover); }
.panel-item.unread { background: var(--accent-soft); }
.panel-item.unread:hover { background: var(--bg-hover); }
.panel-item-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.panel-item-title { font-size: var(--fs-sm); font-weight: 600; }
.panel-item-text { font-size: var(--fs-sm); color: var(--text-secondary); }
.panel-item-time { font-size: var(--fs-xs); color: var(--text-muted); }
.unread-dot { flex: none; width: 8px; height: 8px; border-radius: 999px; background: var(--accent); margin-top: 6px; }

/* Count badge on topbar icon buttons */
.count-badge {
  position: absolute; top: -4px; right: -4px;
  min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 999px; background: var(--danger); color: #fff;
  font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
}

.btn-link { border: 0; background: none; color: var(--accent); font: inherit; font-size: var(--fs-xs); cursor: pointer; padding: 0; }
.btn-link:hover { text-decoration: underline; }
/* Still a real hit area — 44px is the floor, not 26px. */
/* 🔴 `.btn-sm` was used 23 times across products.js, assets.js and addmonitor.js — and was NOT
   DEFINED. Every "Delete", "Edit", "Run now", "Copy", "Scan now" and "Fix DNS →" inside a table row
   was therefore a full-size button: 48px tall, 26px of side padding, 17px text, sitting next to a
   domain name. That is the whole of why MailControl's table looked wrong.

   A phantom class fails exactly like a phantom token — silently, and it looks like a design
   decision rather than a missing rule. `CssTokenContractTest` catches `var(--x)` with no `--x:`;
   it does not catch a class name with no rule, and this is the argument for extending it.

   Same treatment as .btn-xs, which is the row-action size this was reaching for. 44px, not smaller:
   it is still a button a finger has to hit, and that floor is the design's, not a suggestion. */
.btn-xs, .btn-sm { min-height: var(--hit-sm); height: auto; padding: 0 var(--sp-3); font-size: var(--fs-sm); }

/* Modal */
.modal-backdrop { position: fixed; inset: 0; background: rgba(15,27,45,.4); z-index: 40; display: flex; align-items: center; justify-content: center; padding: var(--sp-4); }

.global-search {
  display: flex; align-items: center; gap: var(--sp-2);
  /* Shrinks from 320px down to 150px before anything else in the bar gives. Below that it stops
     being a field and becomes a mystery box, so the keyboard hint drops out first (see the
     narrow-desktop rule at the end of the file) rather than the field itself vanishing. */
  min-height: var(--hit-sm); flex: 0 1 320px; min-width: 150px; max-width: 320px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--border-strong); border-radius: 999px;
  background: var(--bg-surface-2); color: var(--text-muted);
  font-size: var(--fs-sm); cursor: text;
}
.global-search > svg, .global-search kbd { flex: none; }
.global-search > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.global-search kbd {
  margin-left: auto; font-family: inherit; font-size: var(--fs-xs);
  padding: 1px 6px; border: 1px solid var(--border-strong); border-radius: 4px; color: var(--text-muted);
}

.icon-btn {
  position: relative;
  width: var(--hit-sm); height: var(--hit-sm); display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: transparent; color: var(--text-secondary); cursor: pointer;
}
.icon-btn:hover { background: var(--bg-hover); color: var(--text-primary); }

.avatar {
  width: 40px; height: 40px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent-soft); color: var(--accent-text); border: 1px solid var(--accent-border);
  font-size: var(--fs-sm); font-weight: 700;
}

/* ── Sidebar nav ───────────────────────────────────────────────────────── */
.sidebar {
  grid-row: 1 / 3; grid-column: 1;
  /* Sticky and exactly one screen tall, so a long product list scrolls inside the rail instead of
     dragging the brand off the top of the window. */
  position: sticky; top: 0; align-self: start; height: 100vh;
  display: flex; flex-direction: column;
  background: var(--nav-bg);
  border-right: 1px solid var(--nav-edge);
  padding: var(--sp-3) 12px var(--sp-3);
  /* The rail itself does not scroll — its list does (see .nav-list). That is what keeps the brand
     pinned at the top and Administration/Help pinned at the bottom no matter how many products a
     tenant has installed; scrolling the whole rail would push both off. */
  overflow: hidden;
  z-index: 25;
}

/* Brand + the collapse control, pinned above the list. */
.sidebar-head {
  display: flex; align-items: center; gap: 10px;
  margin-bottom: var(--sp-3); padding: 0 4px; flex: none;
}
/* The wordmark is a link to Mission Control — the one place every product's logo goes, and the
   only nav target that survives the rail collapsing to icons. */
.sidebar-head .brand { flex: 1; min-width: 0; border-radius: 9px; text-decoration: none; }
.sidebar-head .brand:hover { text-decoration: none; }
.sidebar-head .brand:focus-visible { outline: 3px solid var(--nav-rail); outline-offset: 3px; }
.sidebar .brand-mark {
  width: 34px; height: 34px; border-radius: 9px;
  background: var(--accent); color: #fff;
}
.sidebar .brand-word { color: var(--nav-text-strong); white-space: nowrap; }
/* The page's --accent is #2f62d9; on near-black that reads as a dark smudge rather than a colour,
   so the wordmark's second half gets the rail's own lighter blue. */
.sidebar .brand-word .accent,
[data-theme="dark"] .sidebar .brand-word .accent { color: var(--nav-rail); }

.nav-toggle {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px;
  border: 2px solid var(--nav-outline); border-radius: 9px;
  background: transparent; color: var(--nav-text);
  font: inherit; font-size: 15px; line-height: 1; cursor: pointer;
}
.nav-toggle:hover { border-color: var(--nav-outline-hover); color: var(--nav-text-strong); }
.nav-toggle:focus-visible { outline: 3px solid var(--nav-rail); outline-offset: 2px; }

.nav-section { margin-bottom: var(--sp-4); }
.nav-label {
  font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--text-muted); padding: 0 var(--sp-2); margin-bottom: var(--sp-2);
}
/* 🔴 Scoped to .sidebar throughout. `.nav-item` is also the row in the profile and tenant popovers,
   which sit on a light card — giving the class itself the rail's colours turned those unreadable
   once. Base rule stays neutral; the rail dresses its own copies. */
.nav-item {
  position: relative;
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: var(--hit-sm); padding: var(--sp-2) var(--sp-2) var(--sp-2) 13px;
  border-radius: var(--radius);
  color: var(--text-secondary); font-size: 16px; font-weight: 600;
  cursor: pointer; user-select: none; margin-bottom: 2px;
}
.nav-item:hover { background: var(--bg-hover); color: var(--text-strong); text-decoration: none; }
/* The active item wears a rail, the same shape a KPI tile, a finding row and a CodeControl section
   use to say "this one". Reusing it here means the sidebar joins the system instead of inventing a
   third way to mark a state. */
.nav-item.active { background: var(--accent-soft); color: var(--accent-text); }
.nav-item.active::before {
  content: ""; position: absolute; left: 0; top: 6px; bottom: 6px; width: 4px;
  border-radius: 0 3px 3px 0; background: var(--accent);
}
.nav-item .nav-ico { width: 18px; height: 18px; flex-shrink: 0; }
.nav-sep { height: 1px; background: var(--border); margin: var(--sp-2) var(--sp-2); }
.nav-sub { margin: 2px 0 var(--sp-1) 30px; }
.nav-sub .nav-item { font-size: var(--fs-sm); min-height: 40px; padding: 5px var(--sp-2) 5px 13px; color: var(--text-muted); }

/* ── The rail's own dress ──────────────────────────────────────────────── */
/* 🔴 `min-height: 0` is the load-bearing half. A flex item's default `min-height: auto` means it
   refuses to shrink below its content, so without this the list simply grew past the rail and
   pushed the footer out of the window — and with the shrink allowed but no `overflow`, the rows
   squashed into each other instead. Both were visible before this line existed. */
.sidebar .nav-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; }
.sidebar .nav-item {
  gap: 13px;
  min-height: var(--hit); padding: 0 12px;
  border-radius: 9px;
  color: var(--nav-text); font-size: 16.5px; font-weight: 500;
}
.sidebar .nav-item:hover { background: var(--nav-hover); color: var(--nav-text-strong); }
.sidebar .nav-item.active { background: var(--nav-hover); color: var(--nav-text-strong); font-weight: 700; }
/* The rail sits INSIDE the item, not bled out to the window edge. 🔴 It was `left: -12px` and
   looked better — but the list scrolls, and a scroll container computes `overflow-x: visible` to
   `auto`, so the bleed both clipped and put a stray horizontal scrollbar in the rail. Inset is
   also what the draft draws. */
.sidebar .nav-item.active::before { background: var(--nav-rail); left: 0; }
.sidebar .nav-item:focus-visible { outline: 3px solid var(--nav-rail); outline-offset: -3px; }
/* A fixed slot, not "whatever the glyph is wide". The icons, the product dots and the favourite
   star all sit in the same 22px column, so the labels start on one line — and when the rail
   collapses to icons that column is the only thing left, which is why it has to be exact. */
.sidebar .nav-item .nav-ico,
.sidebar .nav-item .nav-dot,
.sidebar .nav-item .fav-star {
  flex: none; width: 22px; display: flex; align-items: center; justify-content: center;
}
.sidebar .nav-item .nav-ico { height: 20px; width: 22px; }
.sidebar .nav-sep { background: var(--nav-sep); margin: var(--sp-2) 4px; }
.sidebar .nav-label, .sidebar .nav-head { color: var(--nav-label); padding-inline: 14px; }

/* Products sit flush under their parent rather than indented 30px. The indent was carrying the
   hierarchy on its own; now the smaller row (44px against 48px) and the dot in place of an icon
   say the same thing, and the dots stay readable when the rail is 76px wide — an indented list
   would have had nothing left to show there. */
.sidebar .nav-sub { margin: 2px 0 var(--sp-1); }
.sidebar .nav-sub .nav-item {
  min-height: var(--hit-sm); padding: 0 12px; font-size: 16px; color: var(--nav-text);
}
/* 🔴 Labels are addressed by a class, not by "any span that is not an icon". The :not() chain
   this replaces read `span:not(.nav-dot):not(.fav-star)` — and the count badge is a span that is
   neither, so collapsing the rail hid the one marker the collapsed rail exists to keep. A list of
   exceptions only ever grows, and it fails silently every time something new is added. */
.sidebar .nav-text, .sidebar .fav-label {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* A product's dot. 🔴 It never claims "clean". Filled amber/red means we found something; a hollow
   ring means nothing is open, which is not the same statement as "checked and verified"; and the
   default — no data yet, or the count could not be loaded — is a dim ring that reads as "not
   known". Painting an unmeasured product green would be the one lie this rail could tell. */
.nav-dot::before {
  content: ""; width: 10px; height: 10px; border-radius: 50%;
  border: 2px solid var(--nav-dot-none); background: transparent;
}
.nav-dot[data-state="none"]::before { border-color: var(--nav-label); }
.nav-dot[data-state="warn"]::before { border-color: var(--nav-dot-warn); background: var(--nav-dot-warn); }
.nav-dot[data-state="bad"]::before { border-color: var(--nav-dot-bad); background: var(--nav-dot-bad); }

/* The open count, right-aligned. Mono so seven numbers form a column you can compare down. */
.sidebar .nav-count {
  margin-left: auto; flex: none;
  font-family: var(--font-mono); font-size: 14px; color: var(--nav-label);
}
/* The one exception: a count of things needing attention on a main item is a badge, not a number.
   Same red the findings table uses, because it is the same fact. */
.sidebar .nav-badge {
  margin-left: auto; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 30px; min-height: 26px; padding: 0 8px;
  border-radius: 999px; background: var(--danger); border: 2px solid var(--danger);
  font-family: var(--font-mono); font-size: 13.5px; font-weight: 700; color: #fff;
}

.sidebar .nav-foot {
  margin-top: auto; flex: none;
  padding-top: var(--sp-3); border-top: 1px solid var(--nav-sep);
}
.sidebar .nav-foot .nav-item { min-height: var(--hit-sm); font-size: 16px; }

/* Favorites in the left nav (UX-003 §12) */
.nav-head {
  font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--text-muted); padding: var(--sp-2) var(--sp-2) var(--sp-1);
}
.nav-fav .fav-star { color: var(--warn); flex-shrink: 0; }
.fav-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── The collapsed rail ────────────────────────────────────────────────────
   🔴 Deliberately inside a min-width query, not hung off :root.nav-collapsed alone. Below 900px
   the rail is not a rail, it is an off-canvas drawer — and a drawer with the labels stripped out
   is unusable. Scoping by width means the preference simply does not exist on a phone, rather
   than existing and having to be beaten by a later rule. (Media queries add no specificity; the
   drawer rules would have lost.)

   What survives the collapse: the 22px icon column, the product dots, the badge counts and the
   rail on the active item. What goes: every label, the sub-list's own indent, the section
   headings. Every item keeps a `title`, so the label is one hover away — and the icon column was
   built to a fixed width precisely so this state lines up instead of drifting. */
@media (min-width: 901px) {
  :root.nav-collapsed { --sidebar-w: var(--sidebar-w-min); }

  .nav-collapsed .sidebar { padding-inline: 12px; }
  .nav-collapsed .sidebar-head { flex-direction: column; gap: var(--sp-2); padding: 0; }
  .nav-collapsed .sidebar .brand-word { display: none; }
  .nav-collapsed .sidebar .nav-text,
  .nav-collapsed .sidebar .fav-label,
  .nav-collapsed .sidebar .nav-count { display: none; }
  .nav-collapsed .sidebar .nav-item { justify-content: center; padding-inline: 0; gap: 0; }
  .nav-collapsed .sidebar .nav-sub { margin-inline: 0; }

  /* The badge cannot keep its width in a 52px column, so it becomes the marker it always was:
     a red disc on the icon. The number itself is in the tooltip and on the page it links to. */
  .nav-collapsed .sidebar .nav-badge {
    position: absolute; top: 6px; right: 8px; margin: 0;
    min-width: 10px; min-height: 10px; width: 10px; height: 10px;
    padding: 0; font-size: 0; border-width: 2px;
  }

  /* A heading with nothing under it to head. Kept as a spacer rather than removed, so the groups
     do not run together into one list of fourteen. */
  .nav-collapsed .sidebar .nav-label,
  .nav-collapsed .sidebar .nav-head { font-size: 0; padding: 0; margin-bottom: var(--sp-2); }
  .nav-collapsed .sidebar .nav-label::after,
  .nav-collapsed .sidebar .nav-head::after {
    content: "···"; display: block; text-align: center;
    font-size: var(--fs-sm); line-height: 1; letter-spacing: 0.1em;
  }
}

/* Section header inside popover menus (e.g. "Seneste" in the profile menu) */
.menu-sect {
  font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--text-muted); padding: var(--sp-2) var(--sp-2) var(--sp-1);
}

/* Favorite toggle on detail pages */
.star-btn.on { color: var(--warn); border-color: var(--warn); }

/* ── Content ───────────────────────────────────────────────────────────── */
.content { grid-row: 2; grid-column: 2; padding: var(--sp-5) var(--sp-6); overflow-y: auto; }
.content-inner { max-width: var(--content-max); margin: 0 auto; }

.breadcrumb { font-size: var(--fs-sm); color: var(--text-muted); margin-bottom: var(--sp-2); }

.hero { margin-bottom: var(--sp-5); }
.hero h1 { font-size: var(--fs-xl); letter-spacing: -0.02em; }
.hero p { margin: var(--sp-2) 0 0; color: var(--text-secondary); font-size: var(--fs-md); }

/* ── Cards & KPI ───────────────────────────────────────────────────────── */
.card {
  background: var(--bg-surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
  padding: var(--sp-4);
}
.card + .card { margin-top: var(--sp-3); }
.card-title { font-size: var(--fs-md); font-weight: 650; margin-bottom: var(--sp-3); }

.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-3); margin-bottom: var(--sp-5); }
.kpi-card {
  background: var(--bg-surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
  padding: var(--sp-4); cursor: pointer; transition: box-shadow .12s, transform .12s;
}
.kpi-card:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
.kpi-label { font-size: var(--fs-sm); color: var(--text-secondary); display: flex; align-items: center; gap: var(--sp-2); }
.kpi-value { font-size: var(--fs-xl); font-weight: 700; letter-spacing: -0.02em; margin-top: var(--sp-2); }
.kpi-sub { font-size: var(--fs-xs); color: var(--text-muted); margin-top: var(--sp-1); }

/* ── Buttons (three types) ─────────────────────────────────────────────── */
/* Border before fill. Everything pressable carries a 2px edge, because a coloured surface on its
   own is too weak a signal on paper — and `min-height` rather than `height`, so a button that
   wraps to two lines grows instead of clipping its own label. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: var(--hit); padding: 0 26px;
  border-radius: var(--radius); border: 2px solid transparent;
  font-size: var(--fs-md); font-weight: 700; cursor: pointer; font-family: inherit;
  transition: background .12s, border-color .12s, color .12s;
}
.btn:disabled { opacity: .55; cursor: not-allowed; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); text-decoration: none; }
.btn-secondary { background: var(--bg-surface); color: var(--text-primary); border-color: var(--border-strong); font-weight: 600; }
.btn-secondary:hover:not(:disabled) { border-color: var(--text-strong); color: var(--text-strong); text-decoration: none; }
.btn-ghost { background: transparent; color: var(--accent-text); }
.btn-ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--accent-text-hover); text-decoration: none; }
.btn-danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn-danger:hover:not(:disabled) { filter: brightness(1.1); text-decoration: none; }
.btn-block { width: 100%; }
.tenant-list { display: flex; flex-direction: column; gap: var(--sp-2); margin: var(--sp-3) 0; }
.trust-trend { margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
.sparkline { display: block; margin: var(--sp-1) 0; }

/* ── Forms ─────────────────────────────────────────────────────────────── */
.field { margin-bottom: var(--sp-3); }
.field label { display: block; font-size: var(--fs-sm); font-weight: 600; margin-bottom: var(--sp-2); color: var(--text-secondary); }
.input {
  width: 100%; min-height: 54px; height: auto; padding: 12px var(--sp-3);
  border: 2px solid var(--border-strong); border-radius: var(--radius-md);
  background: var(--bg-surface); color: var(--text-primary); font-size: var(--fs-md); font-family: inherit;
}
/* The border alone carries focus; the ring is drawn by :focus-visible, so a mouse click into a
   field does not paint a halo the keyboard user needs. */
.input:focus { outline: none; border-color: var(--accent); box-shadow: none; }
.input:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.input::placeholder { color: var(--text-muted); opacity: 1; }

/* ── Status badges (colour + icon + text) ──────────────────────────────── */
/* Outline AND word. Colour is never the only carrier of meaning, so the badge keeps a 2px edge in
   its own colour — that survives colour blindness and a black-and-white printout. */
/* 🔴 A badge is a MARKER, not a control, and it was sized as one — 36px tall with a 2px border,
   the same as the status pill beside a page title. That is right for one pill standing alone and
   wrong the moment there are seven: a CodeControl repository row carries GitHub · private ·
   MALWARE · EXPLOITED · SECRET · EOL · COPYLEFT, and at pill size they stacked six deep and made
   the row 200px tall. Nothing was clipping, so the measuring harness passed it; it simply looked
   shouted.

   The rule the rest of this design follows: 2px borders and 44px+ hit areas are for things you can
   PRESS. A badge cannot be pressed. It gets the marker scale — 26px, a hairline-and-a-half border,
   and the 14px floor this design sets for readable text, no smaller. The standalone status pill
   keeps its own size and lives in .ip-pill. */
.badge {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 26px; font-size: var(--fs-xs); font-weight: 700;
  padding: 0 9px; border-radius: var(--radius-sm); border: 1.5px solid currentColor;
  white-space: nowrap;
}
/* The dot is there so severity is never carried by colour alone. A muted badge has no severity to
   carry — grey means "we have no answer" — so its dot says nothing and only adds width. On
   MailControl that was eleven columns of meaningless dots in every row. */
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 999px; background: currentColor; flex: 0 0 auto; }
.badge-muted::before { display: none; }
.badge-ok { color: var(--ok); background: var(--ok-soft); }
.badge-warn { color: var(--warn); background: var(--warn-soft); }
.badge-danger { color: var(--danger); background: var(--danger-soft); }
.badge-info { color: var(--info); background: var(--info-soft); }
.badge-muted { color: var(--text-muted); background: var(--bg-hover); }
/* Severity (UX-002 §10): critical=red, high=orange, medium=amber, low=blue, info=neutral. */
.sev-critical { color: var(--danger); background: var(--danger-soft); }
.sev-high { color: var(--attention); background: var(--attention-soft); }
.sev-medium { color: var(--warn); background: var(--warn-soft); }
.sev-low { color: var(--info); background: var(--info-soft); }
.sev-info { color: var(--text-muted); background: var(--bg-hover); }

/* ── Trust Score & meters ──────────────────────────────────────────────── */
.trust-card { display: flex; gap: var(--sp-5); align-items: flex-start; flex-wrap: wrap; }
.trust-score { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); min-width: 140px; }
.trust-score .num { font-size: 52px; font-weight: 750; letter-spacing: -0.03em; line-height: 1; }
.trust-score .num small { font-size: var(--fs-lg); color: var(--text-muted); font-weight: 600; }
.trust-explain { flex: 1; min-width: 220px; }
.trust-explain p { margin: 0 0 var(--sp-3); color: var(--text-secondary); }

.meter-row { display: grid; grid-template-columns: 150px 1fr 44px; gap: var(--sp-3); align-items: center; padding: var(--sp-2) 0; }
.meter-row .m-label { font-size: var(--fs-sm); color: var(--text-secondary); }
.meter-row .m-val { font-size: var(--fs-sm); font-weight: 650; text-align: right; }
.meter { height: 8px; border-radius: 999px; background: var(--bg-hover); overflow: hidden; }
.meter > span { display: block; height: 100%; border-radius: 999px; background: var(--accent); }
.meter.good > span { background: var(--ok); }
.meter.fair > span { background: var(--attention); }
.meter.poor > span { background: var(--danger); }

/* Severity breakdown row */
.sev-breakdown { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.sev-chip { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: var(--fs-sm); }
.sev-chip .sev-count { font-weight: 700; font-size: var(--fs-md); }
.sev-dot { width: 9px; height: 9px; border-radius: 999px; }

/* Two-column section layout */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
@media (max-width: 1100px) { .grid-2 { grid-template-columns: 1fr; } }

/* Timeline */
.timeline { list-style: none; margin: 0; padding: 0; }
.timeline li { display: flex; gap: var(--sp-3); padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); font-size: var(--fs-sm); }
.timeline li:last-child { border-bottom: none; }
.timeline .t-type { font-weight: 600; }
.timeline .t-time { margin-left: auto; color: var(--text-muted); font-size: var(--fs-xs); white-space: nowrap; }
/* The reason someone gave when they acknowledged or closed a finding. Quoted and italic because it
   is their words, not ours — it must never read as system copy. */
.timeline .fe-note { color: var(--text-secondary); font-style: italic; min-width: 0; overflow-wrap: anywhere; }
.mono { font-family: var(--font-mono); font-size: var(--fs-xs); }

/* ── Filter bar (sticky above tables, UX-002 §9) ───────────────────────── */
.filter-bar {
  position: sticky; top: 0; z-index: 10;
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) 0; margin-bottom: var(--sp-2);
  background: var(--bg-body);
}
.search-box {
  display: flex; align-items: center; gap: var(--sp-2);
  height: 38px; padding: 0 var(--sp-3); min-width: 260px; flex: 1 1 260px; max-width: 420px;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--bg-surface); color: var(--text-muted);
}
.search-box input { border: none; background: transparent; outline: none; width: 100%; font-size: var(--fs-sm); color: var(--text-primary); font-family: inherit; }
.input.select { min-height: var(--hit); height: auto; width: auto; min-width: 130px; padding: 0 var(--sp-3); border-radius: var(--radius); cursor: pointer; }
.row-count { font-size: var(--fs-xs); padding: var(--sp-2) 0; }

/* Context navigation tabs (UX-003 §8) */
.tabs { display: flex; gap: var(--sp-1); border-bottom: 1px solid var(--border); margin-bottom: var(--sp-4); }
.tab {
  padding: var(--sp-2) var(--sp-3); font-size: var(--fs-sm); font-weight: 600; color: var(--text-secondary);
  border-bottom: 2px solid transparent; margin-bottom: -1px; text-decoration: none;
}
.tab:hover { color: var(--text-primary); text-decoration: none; }
.tab.active { color: var(--accent-text); border-bottom-color: var(--accent); }

/* Relationship rows */
.rel-row { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); font-size: var(--fs-sm); }
.rel-row:last-child { border-bottom: none; }
.rel-arrow { color: var(--text-muted); font-weight: 700; }

/* Key-value rows (detail pages) */
.kv { display: grid; grid-template-columns: 180px 1fr; gap: var(--sp-3); padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); font-size: var(--fs-sm); }
.kv:last-child { border-bottom: none; }
.kv > span:first-child { color: var(--text-muted); }

/* ── Table ─────────────────────────────────────────────────────────────── */
/* 🔴 A wide table scrolls — and now it SAYS so.
   Measured on the CodeControl repository list at a 1180px window: the six data columns need 718px
   between them, and four of the six are sized by their HEADER, not by anything in them —
   "Dependencies" is 151px wide to hold the number 964. There is no arrangement of seven columns
   that fits a 1280px laptop, so the table has to scroll. What was wrong was not the scrolling; it
   was that a cut-off column looked identical to a broken one.

   The two `local` gradients are painted with the content and slide away as you scroll; the two
   `scroll` shadows stay pinned to the edges. The pair cancel exactly at each end, so the shadow
   appears only on the side that still has something hidden — no JavaScript, and nothing shows at
   all when the table fits. */
.table-wrap {
  overflow-x: auto;
  --tw-bg: var(--bg-surface);
  background:
    linear-gradient(to right, var(--tw-bg) 40%, transparent) left center / 28px 100% no-repeat local,
    linear-gradient(to left, var(--tw-bg) 40%, transparent) right center / 28px 100% no-repeat local,
    linear-gradient(to right, rgba(20, 24, 32, 0.14), transparent) left center / 14px 100% no-repeat scroll,
    linear-gradient(to left, rgba(20, 24, 32, 0.14), transparent) right center / 14px 100% no-repeat scroll;
}
.ip-card .table-wrap { --tw-bg: var(--ip-surface); }
[data-theme="dark"] .table-wrap {
  background-image:
    linear-gradient(to right, var(--tw-bg) 40%, transparent),
    linear-gradient(to left, var(--tw-bg) 40%, transparent),
    linear-gradient(to right, rgba(0, 0, 0, 0.45), transparent),
    linear-gradient(to left, rgba(0, 0, 0, 0.45), transparent);
}
/* The shared data table. It carries the CodeControl repository list, the certificate list and a
   dozen others, so what is written here shows up in all of them.

   🔴 The two `nowrap`s are the whole fix for "nothing sits straight". A `<table>` distributes width
   by content, so a two-word header and a timestamp were each being folded onto three lines while
   the column that actually needed room — the repository name and its markers — was squeezed. A
   header is a label and a timestamp is one value; neither is prose, and neither should ever wrap.
   The first cell is exempt, because that IS the one that must be free to wrap. */
table.data { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
/* 12px between columns, 16px at the two outer edges. A two-digit count does not need 32px of air
   around it, and six columns of it were taking 48px away from the only column that had something
   to say. */
table.data th, table.data td { text-align: left; padding: var(--sp-3) 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
table.data th:first-child, table.data td:first-child { padding-left: var(--sp-3); }
table.data th:last-child, table.data td:last-child { padding-right: var(--sp-3); }
table.data th { color: var(--text-muted); font-weight: 700; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; }
table.data td:not(:first-child) { white-space: nowrap; }
/* The identity column takes what is left, and its markers wrap as a row of chips rather than as
   text with spaces in it — which is what put one badge per line. */
/* `width: 100%` takes whatever the fixed columns leave; the floor is what stops "whatever is left"
   from being 250px. A CodeControl row carries the repository name plus up to seven risk markers,
   and below this the markers stacked four deep. Under the floor the table scrolls sideways inside
   .table-wrap — which is what that wrapper has always been for, and is a better answer than a row
   as tall as a paragraph.

   200px, down from 260 once the CodeControl risk markers moved into their own column: what is left
   here is a name and at most two identity chips, and giving it more would only take width from the
   column that now carries six. The chips wrap under the name rather than the name truncating —
   `jpjp11/trustctrl` and `jpjp11/CertWatchPro` differ at the end, and an ellipsis there would make
   two repositories look like one. */
table.data td:first-child { width: 100%; min-width: 190px; }
table.data .cell-marks { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
/* A column of markers is the one non-first cell that must be allowed to wrap — the `nowrap` above
   would otherwise lay six chips out in a single 600px line and take the width back from the name.
   The floor keeps two chips per line instead of one; below it the row grows taller than it is
   worth and the table should scroll instead. */
table.data td.cell-risks { white-space: normal; min-width: 232px; }
table.data th.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
table.data th.sortable:hover { color: var(--text-primary); }
table.data tbody tr:hover { background: var(--bg-surface-2); }
table.data tbody tr.row-link { cursor: pointer; }
table.data tr.clickable { cursor: pointer; }

/* ── Empty / error / skeleton states ───────────────────────────────────── */
.empty-state { text-align: center; padding: var(--sp-7) var(--sp-4); color: var(--text-muted); }
.empty-state .empty-ico { font-size: 30px; margin-bottom: var(--sp-3); opacity: .7; }
.empty-state h3 { font-size: var(--fs-lg); color: var(--text-secondary); margin-bottom: var(--sp-2); }

.skeleton { background: linear-gradient(90deg, var(--bg-hover) 25%, var(--bg-surface-2) 37%, var(--bg-hover) 63%); background-size: 400% 100%; animation: shimmer 1.3s ease infinite; border-radius: var(--radius-sm); }
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
.skeleton-line { height: 12px; margin-bottom: var(--sp-2); }
.skeleton-kpi { height: 96px; }

.alert { padding: var(--sp-3); border-radius: var(--radius-sm); font-size: var(--fs-sm); margin-bottom: var(--sp-3); }
.alert-danger { background: var(--danger-soft); color: var(--danger); }
.alert-ok { background: var(--ok-soft); color: var(--ok); }
.alert-warning { background: var(--warn-soft); color: var(--warn); }

/* ── AI Assistant panel (docked right, collapsed by default) ───────────── */
.ai-panel { grid-row: 2; grid-column: 3; display: flex; border-left: 1px solid var(--border); background: var(--bg-sidebar); }
.ai-panel.collapsed .ai-body { display: none; }
.ai-panel:not(.collapsed) .ai-rail { display: none; }
.ai-rail {
  width: 48px; display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  padding-top: var(--sp-3); border: none; background: transparent; cursor: pointer; color: var(--text-secondary);
}
.ai-rail:hover { color: var(--accent); }
.ai-rail-label { writing-mode: vertical-rl; font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.08em; }
.ai-body { width: 340px; max-width: 80vw; display: flex; flex-direction: column; }
.ai-head { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-3); border-bottom: 1px solid var(--border); font-weight: 650; font-size: var(--fs-sm); }
.ai-content { flex: 1; padding: var(--sp-3); overflow-y: auto; }
.ai-content .muted { font-size: var(--fs-sm); }

/* Explain AI output in the panel */
.ai-block { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-3); background: var(--bg-surface); }
.ai-q { font-weight: 650; font-size: var(--fs-sm); margin-bottom: var(--sp-2); }
.ai-a { font-size: var(--fs-sm); }
.ai-a p { margin: 0 0 var(--sp-2); }
.ai-a p:last-child { margin-bottom: 0; }
.ai-meta {
  margin-top: var(--sp-2); padding-top: var(--sp-2); border-top: 1px dashed var(--border);
  font-size: var(--fs-xs); color: var(--text-muted);
}
.btn-ai { white-space: nowrap; }
.ai-input { padding: var(--sp-3); border-top: 1px solid var(--border); }

/* ── Global search (command palette) ───────────────────────────────────── */
.search-overlay { position: fixed; inset: 0; z-index: 60; background: rgba(15,27,45,.4); display: flex; justify-content: center; align-items: flex-start; padding: 12vh var(--sp-4) var(--sp-4); }
.search-panel { width: 100%; max-width: 600px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-lg); overflow: hidden; display: flex; flex-direction: column; max-height: 70vh; }
.search-input-row { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border); color: var(--text-muted); }
.search-input-row input { flex: 1; border: none; outline: none; background: transparent; font-size: var(--fs-lg); color: var(--text-primary); font-family: inherit; }
.search-input-row kbd { font-family: inherit; font-size: var(--fs-xs); padding: 2px 6px; border: 1px solid var(--border-strong); border-radius: 4px; color: var(--text-muted); }

/* Bare <kbd> elsewhere (e.g. the Help view's shortcut list) gets the same look */
kbd { font-family: inherit; font-size: var(--fs-xs); padding: 2px 6px; border: 1px solid var(--border-strong); border-radius: 4px; color: var(--text-muted); }
.search-results { overflow-y: auto; padding: var(--sp-2); }
.search-group { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); padding: var(--sp-3) var(--sp-2) var(--sp-2); }
.search-result { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-sm); cursor: pointer; }
.search-result.active { background: var(--accent-soft); }
.search-result-main { min-width: 0; flex: 1; }
.search-result-label { font-size: var(--fs-sm); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-result-sub { font-size: var(--fs-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-result-tag { flex-shrink: 0; }
.search-empty { padding: var(--sp-6) var(--sp-4); text-align: center; font-size: var(--fs-sm); }
.search-foot { display: flex; justify-content: space-between; align-items: center; padding: var(--sp-2) var(--sp-4); border-top: 1px solid var(--border); font-size: var(--fs-xs); color: var(--text-muted); }

/* ── Auth (login / signup / reset) pages ───────────────────────────────────
   A split screen: a full-bleed photograph on the left, the form on the right.
   These pages are the one surface a customer meets before the app's density
   rules apply, so the type here runs larger than the app's five-step scale —
   deliberately, and only inside .auth-page. Colour still comes from tokens so
   both themes follow along. */
.auth-page { min-height: 100vh; display: grid; grid-template-columns: 44% 1fr; background: var(--bg-body); }

.auth-visual { position: relative; overflow: hidden; background: #050b18; }
.auth-visual img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; }

.auth-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-4); padding: var(--sp-6) var(--sp-5); }
.auth-shell { width: 100%; max-width: 560px; }

.auth-card { width: 100%; max-width: none; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 18px; box-shadow: var(--shadow-lg); padding: var(--sp-6) var(--sp-6) var(--sp-5); }
.auth-card .brand { justify-content: center; gap: 12px; margin-bottom: var(--sp-5); }
.auth-card .brand-mark { width: 40px; height: 46px; background: none; border-radius: 0; }
.auth-card .brand-word { font-size: 32px; letter-spacing: -0.03em; }
.auth-card h1 { font-size: 30px; line-height: 1.15; letter-spacing: -0.025em; text-align: left; margin-bottom: var(--sp-2); }
.auth-card .subtitle { text-align: left; color: var(--text-secondary); font-size: var(--fs-md); margin-bottom: var(--sp-4); }

/* Controls sit taller here than in the app — this is a one-decision page. */
.auth-card .field { margin-bottom: var(--sp-4); }
.auth-card .field label { color: var(--text-primary); }
.auth-card .input { height: 52px; border-radius: var(--radius); border-color: var(--border); font-size: var(--fs-md); }
.auth-card .btn { height: 56px; border-radius: var(--radius); font-size: var(--fs-md); }

/* Icon inside the field, and the show/hide control for passwords. */
.input-affix { position: relative; display: block; }
.input-affix .input { padding-left: 46px; }
.input-affix .input-icon { position: absolute; left: 16px; top: 50%; transform: translateY(-50%); display: flex; color: var(--text-muted); pointer-events: none; }
.input-affix .input-icon svg { display: block; }
.input-affix .input[data-has-toggle] { padding-right: 48px; }
.pw-toggle {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; padding: 0;
  border: 0; border-radius: var(--radius-sm); background: none;
  color: var(--text-muted); cursor: pointer;
}
.pw-toggle:hover { color: var(--text-primary); background: var(--bg-hover); }
.pw-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.auth-row-end { display: flex; justify-content: flex-end; margin: 0 0 var(--sp-4); font-size: var(--fs-md); }
.auth-links { display: flex; justify-content: space-between; margin-top: var(--sp-3); font-size: var(--fs-md); }

.auth-divider { display: flex; align-items: center; gap: var(--sp-3); margin: var(--sp-4) 0; color: var(--text-muted); font-size: var(--fs-sm); }
.auth-divider::before, .auth-divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }

.auth-alt { text-align: center; font-size: var(--fs-md); color: var(--text-secondary); }

.auth-assurance { display: flex; gap: var(--sp-3); margin-top: var(--sp-4); padding: var(--sp-3) var(--sp-4); background: var(--bg-surface-2); border: 1px solid var(--border); border-radius: 12px; }
.auth-assurance svg { flex: none; margin-top: 2px; color: var(--text-muted); }
.auth-assurance strong { display: block; font-size: var(--fs-md); margin-bottom: var(--sp-1); }
.auth-assurance p { font-size: var(--fs-md); line-height: 1.5; color: var(--text-secondary); margin: 0; }

.auth-footer { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: var(--sp-3); margin-top: var(--sp-4); font-size: var(--fs-sm); color: var(--text-muted); }
.auth-footer a { color: var(--text-muted); }
.auth-footer span { color: var(--border-strong); }

/* Below the split, the photograph steps aside rather than shrinking to a strip. */
@media (max-width: 980px) {
  .auth-page { grid-template-columns: 1fr; }
  .auth-visual { display: none; }
  .auth-panel { padding: var(--sp-5) var(--sp-4); }
}
@media (max-width: 560px) {
  .auth-panel { padding: var(--sp-4) var(--sp-3); }
  .auth-card { padding: var(--sp-5) var(--sp-4) var(--sp-4); border-radius: 14px; }
  .auth-card .brand-word { font-size: 26px; }
  .auth-card h1 { font-size: 25px; }
  .auth-links { flex-direction: column; gap: var(--sp-2); }
}
.muted { color: var(--text-muted); }
.hidden { display: none !important; }
.spinner { width: 15px; height: 15px; border: 2px solid rgba(255,255,255,.5); border-top-color: #fff; border-radius: 999px; animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Mobile navigation (UX-003 §16): drawer replaces the sidebar, bottom nav for primary items ──

   The bottom bar carries FOUR items, not five, and that is a decision rather than a trim.

   It used to be Dashboard · Findings · Search · Notifications · Profile. Two of those already sit
   in the top bar on mobile — the bell and the avatar are both still there — so they were asking for
   the same thumb twice. Search is not: the global search field IS hidden on mobile, so the bottom
   bar is its only way in, and it stays.

   The cost of five was the label. Five items across a 380px screen leaves 76px each, and
   "Notifications" only fits that at 10px — under the floor this design sets for readable text, in
   the one place the user is holding the device at arm's length. Four items give 95px, which takes
   the 14px label the rest of the platform uses.

   So the rule the bar now follows: the bottom bar is where you GO (Dashboard, Findings, Assets)
   plus the one thing you cannot otherwise reach (Search). What you DO — notifications, profile,
   help — stays top-right, where it already was and where it is conventionally looked for. */
.nav-burger { display: none; }
.drawer-backdrop { display: none; }
.bottom-nav { display: none; }
/* The wordmark sits at the top of the rail on a desktop; a second copy in the bar two centimetres
   to its right is the same word twice. The markup keeps both because the phone needs the bar's
   one — the media query above turns it back on there. */
.topbar .brand { display: none; }

@media (max-width: 900px) {
  body { overflow-x: hidden; }
  .app-shell { grid-template-columns: 1fr; }
  /* 🔴 The bar spans columns 2–4 on a desktop, because the rail owns column 1. Here there is only
     one column — and a `grid-column: 2 / 4` on a one-column grid does not clamp, it CREATES the
     two implicit columns it was promised. The page then sat in column 1 with 45px to itself at
     480px wide. Measured, not guessed; nothing looked wrong in the screenshot because the bar
     itself was fine. */
  .topbar { grid-column: 1; }
  .ai-panel { display: none; }
  .content { grid-column: 1; padding: var(--sp-4); padding-bottom: calc(80px + var(--sp-4)); }
  .global-search { display: none; }
  /* Scoped to the bar. The bar has no room for the word on a phone — but the drawer does, and it
     is the drawer's only piece of identity, so an unscoped rule left it as a bare mark. */
  .topbar .brand-word { display: none; }
  /* Fluid, not a fixed step. A flat 110px cap fitted at 390px and overflowed between 361 and
     385px — measured — because the rest of the bar is fixed-size and only this chip can give. A vw
     term makes it yield exactly as much as the window took away. */
  .tenant-selector { max-width: min(110px, 24vw); }
  .topbar { padding: 0 var(--sp-3); }
  .topbar-left { gap: var(--sp-2); min-width: 0; }
  /* Activity rows wrap rather than push: a row is a dot, a title, an id and a time, and on a phone
     those four cannot share one line. Measured at 390px before this. */
  .ip-act { flex-wrap: wrap; }
  .ip-act-title { min-width: 0; overflow-wrap: anywhere; }
  .ip-act-time { margin-left: auto; }
  .kpi-grid { grid-template-columns: 1fr 1fr; }
  /* The band follows the gutter — see the note on .ip-head. */
  .ip-head { margin: calc(var(--sp-4) * -1) calc(var(--sp-4) * -1) var(--sp-4); padding: var(--sp-4); }

  .nav-burger { display: inline-flex; }

  /* Sidebar becomes an off-canvas drawer.
     It now starts at the very top rather than under the bar, because the brand moved into it —
     a drawer that opens below the bar would have covered the page but left a strip of chrome it
     does not own. The button in its head becomes the way out; the backdrop is the other. */
  .sidebar {
    position: fixed; top: 0; bottom: 0; left: 0; height: auto; z-index: 45;
    width: min(var(--sidebar-w-open), 84vw);
    transform: translateX(-105%);
    transition: transform 0.2s ease;
    box-shadow: var(--shadow-lg);
  }
  body.drawer-open .sidebar { transform: translateX(0); }
  body.drawer-open .drawer-backdrop {
    display: block; position: fixed; inset: 0;
    background: rgba(15, 27, 45, 0.4); z-index: 44;
  }
  /* Identity lives in the bar on a phone, where the drawer is shut nearly all the time. On the
     desktop it lives in the rail and the bar's copy is redundant — see the rule below. */
  .topbar .brand { display: flex; }

  /* Bottom navigation: Dashboard, Findings, Assets, Search */
  .bottom-nav {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
    height: calc(72px + env(safe-area-inset-bottom, 0px));
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: var(--bg-topbar); border-top: 1px solid var(--border);
  }
  .bn-item {
    position: relative;
    flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: var(--sp-1); border: 0; background: none; cursor: pointer;
    color: var(--text-muted); font: inherit; font-size: var(--fs-xs); font-weight: 600;
    text-decoration: none;
  }
  .bn-item:hover { text-decoration: none; color: var(--text-strong); }
  .bn-item.active { color: var(--accent-text); }
  /* The same rail as the sidebar and the KPI tiles, turned to sit along the edge the bar is
     attached to. Colour alone would be the only marker otherwise, and at arm's length on a phone
     that is the weakest signal available. */
  .bn-item.active::before {
    content: ""; position: absolute; top: 0; left: 22%; right: 22%; height: 3px;
    border-radius: 0 0 3px 3px; background: var(--accent);
  }
}

/* The narrow phone. Headings step down so a page title cannot eat the first screenful, and the
   page gutter halves — 24px of padding on a 380px screen is 13% of it spent on nothing. */
@media (max-width: 720px) {
  .content { padding: var(--sp-3); padding-bottom: calc(80px + var(--sp-3)); }
  .ip-head { margin: calc(var(--sp-3) * -1) calc(var(--sp-3) * -1) var(--sp-4); padding: var(--sp-4) var(--sp-3); }
  .ip-title h1 { font-size: 28px; }
  .ip-title p { font-size: var(--fs-md); }
  .ip-hero-top h2 { font-size: 22px; }
  .ip-card-head, .ip-fchips, .ip-tr, .ip-asset, .ip-activity, .ip-empty-row { padding-inline: var(--sp-3); }
  .kpi-grid { grid-template-columns: 1fr; }
  /* The controls under a page title stop being a wrapped huddle and become a stack of full-width
     targets — on a phone they are the page's primary actions, not its trim. */
  .ip-controls { width: 100%; }
  .ip-controls > * { flex: 1 1 auto; }
  .ip-controls .input.select { width: 100%; }
}

/* The narrowest phone still in use (320px). The top bar is the only thing that genuinely runs out
   of room there: a burger, a brand mark, the tenant chip, two icon buttons and an avatar do not fit
   once each is a 44px target. The tenant chip gives way — it is orientation, and the full name is
   in the drawer — rather than shrinking every hit area back below the floor. */
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* ── Print: strip the app chrome so a report view prints/exports as a clean PDF ── */
@media print {
  /* Interactive chrome has no meaning on paper. The product tab strip and the hero's action
     buttons/menus go too — SiteControl's Overview IS the printable report since the separate
     report page was merged into it, so printing must not carry navigation into the PDF. */
  .topbar, .sidebar, .no-print, .bottom-nav,
  .product-tabs, .hero .btn, .hero .action-menu { display: none !important; }
  .app-shell { display: block !important; }
  .content { grid-column: 1 !important; padding: 0 !important; overflow: visible !important; }
  .content-inner { max-width: none !important; }
  body { background: #fff !important; }
  .card { box-shadow: none !important; border: 1px solid #ccc !important; break-inside: avoid; page-break-inside: avoid; }
  a[href]::after { content: ""; }
  .report-doc .card:first-child h1 { color: #000; }
  /* "What we checked" separates its rows by banding, and a browser drops backgrounds when it prints:
     on paper the list would arrive as fifty lines of text with nothing between them. Forcing the
     bands through with print-color-adjust would spend ink on fifty grey slabs, so the rows take
     their hairlines back for print, and the ones needing attention keep a left bar — the wash that
     carries that on screen is exactly what the printer throws away. The caret is an affordance for
     a click nobody can make on paper. */
  .check-rows > * { border-top: 1px solid #ddd !important; background: none !important; box-shadow: none !important; }
  .check-rows > [data-s="WARN"], .check-rows > [data-s="FAIL"] { border-left: 3px solid #666 !important; }
  .check-group-head, .check-body { background: none !important; }
  .check-row > summary::after { display: none !important; }
}

/* Dashboard refresh indicator — gentle attention pulse (respects reduced-motion). */
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}
@media (prefers-reduced-motion: reduce) {
  [style*="animation:pulse"] { animation: none !important; }
  .acc-chev, .kpi-card, .subwrap { transition: none !important; }
}

/* ── Overview redesign: score gauge, health categories, severity buckets ─────
   Layout & information hierarchy inspired by a best-in-class SEO health console;
   colours are TrustCtrl's own tokens. Every number here is real platform data
   (trust score, domain scores, findings-by-severity) — no decorative metrics. */

/* Circular Trust Score gauge — replaces the bare number on the Trust card. The
   arc length is set in JS from the real score; colour follows the trust band. */
.gauge { position: relative; width: 150px; height: 150px; flex: none; }
.gauge svg { transform: rotate(-90deg); display: block; }
.gauge .gauge-arc { transition: stroke-dashoffset .6s ease; }
@media (prefers-reduced-motion: reduce) { .gauge .gauge-arc { transition: none; } }
.gauge .gauge-mid { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.gauge .g-score { font-size: 42px; font-weight: 750; letter-spacing: -0.03em; line-height: 1; font-variant-numeric: tabular-nums; }
.gauge .g-delta { font-size: var(--fs-sm); font-weight: 700; min-height: 18px; }
.gauge .g-delta.up { color: var(--ok); }
.gauge .g-delta.down { color: var(--attention); }
.gauge .g-lbl { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 4px; }

/* Product overview panel: score gauge + band on the left, severity breakdown on the right. */
.score-panel { display: flex; gap: var(--sp-4); align-items: center; flex-wrap: wrap; }
.score-panel .sp-gauge { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); flex: none; }
.score-panel .sp-body { flex: 1; min-width: 240px; }

/* Domain-health category cards (driven by real domainScores). */
.health-cats { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: var(--sp-3); }
.health-cat { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-surface-2); }
.health-cat .hc-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.health-cat .hc-label { font-size: var(--fs-sm); font-weight: 600; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.health-cat .hc-val { font-size: var(--fs-lg); font-weight: 750; font-variant-numeric: tabular-nums; }

/* Severity buckets (real findings-by-severity counts). */
.sev-buckets { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.sev-bucket { flex: 1 1 76px; text-align: center; padding: var(--sp-3) var(--sp-2); border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-surface-2); text-decoration: none; color: inherit; transition: box-shadow .12s, transform .12s; }
.sev-bucket:hover { box-shadow: var(--shadow-sm); transform: translateY(-1px); text-decoration: none; }
.sev-bucket .sb-n { font-size: var(--fs-xl); font-weight: 750; font-variant-numeric: tabular-nums; line-height: 1; }
.sev-bucket .sb-k { font-size: var(--fs-xs); color: var(--text-muted); display: flex; align-items: center; justify-content: center; gap: 5px; margin-top: var(--sp-2); }
@media (prefers-reduced-motion: reduce) { .sev-bucket { transition: none; } }

/* ── Findings list: expandable rows with affected pages inline ──────────────
   Rows for rules that carry an affected-page list expand in place (progressive
   disclosure) instead of forcing a click-through; the URLs are the real ones
   resolved from the linked observation. Other rows still open the detail page. */
.acc-chev { display: inline-block; width: 14px; color: var(--text-muted); transition: transform .15s ease; }
table.data tr.frow.expandable { cursor: pointer; }
table.data tr.frow.open { background: var(--bg-surface-2); }
table.data tr.frow.open .acc-chev { transform: rotate(90deg); }
.eff { display: flex; align-items: center; gap: var(--sp-2); }
.eff .meter { width: 72px; flex: none; }
.eff .eff-lbl { font-size: var(--fs-xs); color: var(--text-secondary); white-space: nowrap; }
.eff .meter.strong > span { background: var(--attention); }
table.data tr.subrow > td { padding: 0; background: var(--bg-surface-2); }
.subwrap { padding: var(--sp-2) var(--sp-4) var(--sp-4); }
.subwrap .sub-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-3); }
.subwrap .sub-head h4 { margin: 0; font-size: var(--fs-md); }
.subwrap .sub-pages { list-style: none; margin: 0; padding: 0; max-height: 320px; overflow-y: auto; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-surface); }
.subwrap .sub-pages li { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border); font-size: var(--fs-sm); }
.subwrap .sub-pages li:last-child { border-bottom: none; }
.subwrap .sub-pages .sp-url { flex: 1; min-width: 0; overflow-wrap: anywhere; }

/* Text-length findings: each affected page shows its current title/description with a character
   counter, and the part beyond Google's limit struck through in red (the "too long" case). */
.len-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.len-offender { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-3); background: var(--bg-surface-2); }
.len-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); margin-bottom: var(--sp-2); flex-wrap: wrap; }
.len-text { font-size: var(--fs-sm); line-height: 1.6; overflow-wrap: anywhere; color: var(--text-secondary); }
.len-text s { color: var(--danger); text-decoration-thickness: 2px; }

/* ── Findings: product filter tiles + per-product groups (product+category structure) ───────────── */
.ptile-strip { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-3); margin-bottom: var(--sp-3); }
@media (max-width: 1200px) { .ptile-strip { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 760px) { .ptile-strip { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .ptile-strip { grid-template-columns: 1fr; } }
.ptile { display: flex; flex-direction: column; align-items: center; gap: 9px; text-align: center;
  padding: 16px var(--sp-2) 14px; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg-surface); box-shadow: var(--shadow-sm); cursor: pointer; font: inherit; color: inherit;
  transition: border-color .12s, transform .12s, box-shadow .12s; }
.ptile:hover { border-color: var(--border-strong); transform: translateY(-1px); }
.ptile.active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow-sm); }
.ptile .ring { --pct: 0; --rc: var(--accent); width: 58px; height: 58px; border-radius: 50%; flex: none;
  background: conic-gradient(var(--rc) calc(var(--pct) * 1%), var(--bg-hover) 0); display: grid; place-items: center; position: relative; }
.ptile .ring::before { content: ""; position: absolute; inset: 5px; border-radius: 50%; background: var(--bg-surface); }
.ptile .ring > .rv { position: relative; font-weight: 800; font-size: var(--fs-md); font-variant-numeric: tabular-nums; line-height: 1; }
.ptile .ring.ok { --rc: var(--ok); } .ptile .ring.warn { --rc: var(--attention); } .ptile .ring.bad { --rc: var(--danger); } .ptile .ring.accent { --rc: var(--accent); }
.ptile .pt-name { font-weight: 700; font-size: var(--fs-sm); display: flex; align-items: center; gap: 6px; }
.ptile .pt-name .pt-ico { font-size: 15px; line-height: 1; }
.ptile .pt-count { font-size: var(--fs-xs); color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.ptile .pt-count b { color: var(--text-primary); font-weight: 700; }
.ptile .pt-count.clean { color: var(--ok); font-weight: 600; }
.ptile-note { font-size: var(--fs-sm); color: var(--text-secondary); margin: 0 0 var(--sp-3); }
.ptile-note b { color: var(--text-primary); }
.linkbtn { background: none; border: none; color: var(--accent-text); cursor: pointer; font: inherit; font-weight: 600; padding: 0; text-decoration: underline; }

/* "Focused on one website" banner, shown on both dashboards when a site is picked in the selector. */
.focus-banner { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  background: var(--bg-hover); border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-3); font-size: var(--fs-sm); color: var(--text-secondary); }
.focus-banner b { color: var(--text-primary); }

.pgroup { padding: 0; margin-bottom: var(--sp-3); overflow: hidden; }
.pgroup-head { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border); }
.pgroup-head .pg-ico { width: 34px; height: 34px; border-radius: 9px; background: var(--accent-soft); display: grid; place-items: center; font-size: 17px; flex: none; }
.pgroup-head .pg-name { font-weight: 700; }
.pgroup-head .pg-sub { font-size: var(--fs-xs); color: var(--text-muted); }
.pgroup-head .pg-right { margin-left: auto; display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; justify-content: flex-end; }
.pgroup-head .pg-health { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); color: var(--text-secondary); }
.pgroup-head .pg-health .meter { display: inline-block; vertical-align: middle; }
.pgroup-head .pg-health b { color: var(--text-primary); font-variant-numeric: tabular-nums; }
.pgroup .table-wrap { border: 0; margin: 0; }
.pg-empty { padding: var(--sp-4); }

/* Rule sections inside a product card. Rules that raise one finding per affected package can put a
   few hundred rows in one card; each collapses to a counted summary line so the card still reads at
   a glance, and opens to the individually-triageable rows. */
.rsec { border-bottom: 1px solid var(--border); }
.rsec:last-child { border-bottom: 0; }
.rsec-head { display: flex; align-items: center; gap: var(--sp-2); width: 100%; padding: var(--sp-3) var(--sp-4);
  background: none; border: 0; font: inherit; color: inherit; text-align: left; cursor: pointer; }
.rsec-head:hover { background: var(--bg-subtle, rgba(127, 127, 127, 0.06)); }
.rsec-chev { color: var(--text-muted); font-size: var(--fs-xs); width: 12px; flex: none; }
.rsec-name { font-weight: 600; }
.rsec-sev { display: inline-flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  font-size: var(--fs-xs); color: var(--text-secondary); }
.rsec-open { margin-left: auto; font-size: var(--fs-xs); color: var(--text-muted); flex: none; }
.rsec-body .table-wrap { border-top: 1px solid var(--border); }
/* A long coordinate must not stretch the table — the full value is in the row's title attribute. */
.frow-problem { display: inline-block; max-width: 34ch; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; vertical-align: bottom; }

/* "By product" rows (per-site drill-downs still use them) — health bar + open-finding count. */
.prod-row { display: grid; grid-template-columns: 26px 1fr auto 80px 46px; gap: var(--sp-2); align-items: center;
  padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); text-decoration: none; color: inherit; }
.prod-row:last-child { border-bottom: 0; }
.prod-row:hover { color: inherit; text-decoration: none; }
.prod-row:hover .pr-name { color: var(--accent-text); }
.prod-row .pr-ico { font-size: 16px; }
.prod-row .pr-name { font-weight: 600; }
.prod-row .pr-open { font-size: var(--fs-xs); color: var(--text-secondary); font-variant-numeric: tabular-nums; text-align: right; }
.prod-row .pr-open b { color: var(--text-primary); }

/* Phishing Explorer: clickable screenshot thumbnail, external-site link, and full-size lightbox. */
.shot-thumb { padding: 0; border: 0; background: none; cursor: zoom-in; display: inline-block; line-height: 0; }
.shot-thumb img { height: 72px; max-width: 150px; object-fit: cover; object-position: top;
  border-radius: 6px; border: 1px solid var(--border); transition: box-shadow .12s, transform .12s; }
.shot-thumb:hover img { box-shadow: var(--shadow-sm); transform: scale(1.03); }
.ext-link { font-size: var(--fs-sm); text-decoration: none; opacity: .7; }
.ext-link:hover { opacity: 1; }
.shot-lightbox { position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,.72);
  display: flex; align-items: center; justify-content: center; padding: var(--sp-4); }
.shot-lightbox .shot-box { background: var(--bg-surface); border-radius: var(--radius); border: 1px solid var(--border);
  max-width: min(1100px, 96vw); max-height: 94vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: var(--shadow-lg, 0 20px 60px rgba(0,0,0,.4)); }
.shot-lightbox .shot-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.shot-lightbox .shot-actions { display: flex; align-items: center; gap: var(--sp-2); }
.shot-lightbox .shot-img { overflow: auto; background: var(--bg-hover); }
.shot-lightbox .shot-img img { display: block; max-width: 100%; height: auto; margin: 0 auto; }

/* Findings explorer (per-website report): page-size selector + result footer. */
.fe-size { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); color: var(--text-secondary); white-space: nowrap; }
.fe-foot { padding: var(--sp-2) var(--sp-4) var(--sp-3); font-size: var(--fs-sm); }
.fe-foot b { color: var(--text-primary); }
.prod-row .pr-open .clean { color: var(--ok); font-weight: 600; }
.prod-row .pr-h { font-variant-numeric: tabular-nums; font-size: var(--fs-sm); text-align: right; }

/* ── Simple findings view (plain-language default; findings toggle Simple/Technical) ─────────── */
/* A quiet grey track carrying one raised chip. The toggle is a mode you set once, not this page's
   call to action, so it must not outshout the primary button next to it — the raised surface plus
   border is what reads as "chosen", no inverse surface needed.

   🔴 This shape is reserved for CHOOSING A VIEW of the same page. Page navigation is deliberately
   given a different treatment (a bordered pill, further down): the design draft used one bar for
   both, and in the product they would be indistinguishable — "where am I" and "what am I looking
   at" are not the same question and must not answer with the same control. */
.view-toggle {
  display: inline-flex; gap: 2px; padding: 4px;
  border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-hover);
}
.view-toggle button {
  border: 1px solid transparent; background: none; min-height: 40px; padding: 0 18px; cursor: pointer;
  border-radius: var(--radius-sm); font-family: inherit; font-size: 16px; font-weight: 600;
  color: var(--text-secondary); transition: background .12s, color .12s;
}
.view-toggle button:hover { color: var(--text-primary); }
.view-toggle button.active {
  background: var(--bg-surface); color: var(--text-primary); cursor: default;
  border-color: var(--border); box-shadow: var(--shadow-sm);
}

.sv-verdict { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.sv-verdict .ring { --pct: 0; --rc: var(--accent); width: 84px; height: 84px; border-radius: 50%; flex: none;
  background: conic-gradient(var(--rc) calc(var(--pct) * 1%), var(--bg-hover) 0);
  display: grid; place-items: center; position: relative; }
.sv-verdict .ring::before { content: ""; position: absolute; inset: 7px; border-radius: 50%; background: var(--bg-surface); }
.sv-verdict .ring > .rv { position: relative; font-weight: 800; font-size: var(--fs-lg); font-variant-numeric: tabular-nums; }
.sv-verdict .ring.ok { --rc: var(--ok); } .sv-verdict .ring.warn { --rc: var(--attention); } .sv-verdict .ring.bad { --rc: var(--danger); }
.sv-verdict-text { flex: 1; min-width: 260px; }
.sv-verdict-text h3 { margin: 0 0 var(--sp-1); font-size: var(--fs-lg); }
.sv-verdict-text p { margin: 0; color: var(--text-secondary); font-size: var(--fs-sm); max-width: 62ch; }
.sv-reassure { color: var(--text-muted) !important; margin-top: var(--sp-1) !important; font-size: var(--fs-xs) !important; }

.sv-concerns { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-2); margin-bottom: var(--sp-3); }
@media (max-width: 900px) { .sv-concerns { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .sv-concerns { grid-template-columns: 1fr; } }
.sv-concern { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; text-align: left;
  background: var(--bg-surface); border: 1px solid var(--border); border-top: 3px solid var(--border-strong);
  border-radius: var(--radius); padding: var(--sp-3); cursor: pointer; font-family: inherit; box-shadow: var(--shadow-sm); }
.sv-concern:hover { border-color: var(--border-strong); }
.sv-concern.active { box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow-sm); border-color: var(--accent); }
.sv-concern.st-ok { border-top-color: var(--ok); } .sv-concern.st-warn { border-top-color: var(--warn); } .sv-concern.st-bad { border-top-color: var(--danger); }
.sv-concern-name { font-weight: 700; font-size: var(--fs-sm); color: var(--text-primary); display: flex; align-items: center; gap: 7px; }
.sv-concern .st-dot { width: 8px; height: 8px; border-radius: 999px; flex: none; background: var(--border-strong); }
.sv-concern.st-ok .st-dot { background: var(--ok); } .sv-concern.st-warn .st-dot { background: var(--warn); } .sv-concern.st-bad .st-dot { background: var(--danger); }
.sv-status-word { font-size: var(--fs-xs); font-weight: 700; color: var(--text-secondary); }
.sv-concern.st-ok .sv-status-word { color: var(--ok); } .sv-concern.st-warn .sv-status-word { color: var(--warn); } .sv-concern.st-bad .sv-status-word { color: var(--danger); }
.sv-concern-line { font-size: var(--fs-xs); color: var(--text-secondary); }

.sv-bucket { margin-top: var(--sp-4); }
.sv-bucket-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-1); }
.sv-bucket-head .dot { width: 10px; height: 10px; border-radius: 999px; flex: none; }
.sv-bucket-head h4 { margin: 0; font-size: var(--fs-md); }
.sv-count { font-size: var(--fs-xs); color: var(--text-muted); font-weight: 700; font-variant-numeric: tabular-nums; }
.sv-bucket-sub { margin: 0 0 var(--sp-2); font-size: var(--fs-xs); color: var(--text-muted); }

.sv-card { display: flex; gap: var(--sp-3); align-items: flex-start; background: var(--bg-surface);
  border: 1px solid var(--border); border-left-width: 4px; border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-2); box-shadow: var(--shadow-sm); cursor: pointer; }
.sv-card:hover { border-color: var(--border-strong); }
.sv-card.sv-now { border-left-color: var(--danger); }
.sv-card.sv-soon { border-left-color: var(--warn); }
.sv-card-main { flex: 1; min-width: 0; }
.sv-card-main h5 { margin: 0 0 2px; font-size: var(--fs-md); font-weight: 650; }
.sv-consequence { margin: 0 0 var(--sp-2); color: var(--text-secondary); font-size: var(--fs-sm); max-width: 68ch; }
.sv-related { color: var(--text-muted); }
/* The affected package/file on a per-item card. Set apart from the plain-language headline: it is a
   literal identifier, so it must be copyable-looking and never mistaken for prose. */
.sv-item { display: block; margin-top: 2px; font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--fs-sm); font-weight: 500; color: var(--text-secondary); overflow-wrap: anywhere; }
.sv-chips { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.sv-chip { display: inline-flex; align-items: center; font-size: var(--fs-xs); font-weight: 600;
  border-radius: 999px; padding: 2px 10px; background: var(--bg-surface-2); border: 1px solid var(--border);
  color: var(--text-secondary); white-space: nowrap; }
.sv-chip.who { background: var(--accent-soft); color: var(--accent-text); border-color: transparent; }
.sv-card-cta { flex: none; align-self: center; }
@media (max-width: 640px) {
  .sv-card { flex-direction: column; }
  .sv-card-cta { align-self: stretch; }
  .sv-card-cta .btn { width: 100%; justify-content: center; }
}

.sv-minor { border: 1px dashed var(--border-strong); border-radius: var(--radius); background: var(--bg-surface-2); }
.sv-minor summary { cursor: pointer; padding: var(--sp-3) var(--sp-4); font-size: var(--fs-sm); font-weight: 600;
  color: var(--text-secondary); list-style: none; display: flex; align-items: center; gap: var(--sp-2); }
.sv-minor summary::-webkit-details-marker { display: none; }
.sv-minor summary::before { content: "▸"; }
.sv-minor[open] summary::before { content: "▾"; }
.sv-minor-body { padding: 0 var(--sp-4) var(--sp-3); }
.sv-minor-row { display: flex; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-2) 0;
  border-top: 1px solid var(--border); font-size: var(--fs-sm); color: var(--text-secondary); cursor: pointer; }
.sv-minor-row:hover span:first-child { color: var(--text-primary); }
.sv-minor-row span:last-child { color: var(--text-muted); font-size: var(--fs-xs); white-space: nowrap; }

.sv-qa { display: grid; gap: var(--sp-3); }
.sv-qa-block h6 { margin: 0 0 var(--sp-1); font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.07em;
  text-transform: uppercase; color: var(--text-muted); }
.sv-qa-block p { margin: 0; font-size: var(--fs-sm); color: var(--text-secondary); max-width: 68ch; }
.sv-qa-block p + p { margin-top: var(--sp-2); }
.sv-qa-block b { color: var(--text-primary); }
.sv-tech { color: var(--text-muted) !important; font-size: var(--fs-xs) !important; }
.sv-tech span { font-family: var(--font-mono); }
/* The instruction itself, above the line about who does it and how long it takes. It reads first
   because it is the answer to the question the heading asks — "who should do it" is context. */
.sv-todo { margin-bottom: var(--sp-3); font-size: var(--fs-sm); color: var(--text-secondary); max-width: 68ch; }
.sv-todo .seo-step-lead { color: var(--text-primary); font-weight: 600; }
.sv-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-3); }
.sv-footlink { margin: var(--sp-4) 0 0; font-size: var(--fs-sm); color: var(--text-secondary); }

/* ── Simple dashboard (plain-language Mission Control) ────────────────────────────────────────── */
.sv-verdict-cta { margin-left: auto; align-self: center; }
.sv-delta { font-weight: 700; }
.sv-delta.up { color: var(--ok); }
.sv-delta.down { color: var(--danger); }
.sv-seeall { margin: var(--sp-1) 0 0; font-size: var(--fs-xs); color: var(--text-muted); }

.sv-site-row { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--sp-2) var(--sp-4); margin-bottom: var(--sp-2); box-shadow: var(--shadow-sm); }
.sv-site-name { font-weight: 650; font-size: var(--fs-sm); min-width: 150px; }
.sv-site-chips { display: flex; gap: var(--sp-2); flex-wrap: wrap; flex: 1; }
.sv-sitechip { font-size: var(--fs-xs); font-weight: 600; border-radius: 999px; padding: 2px 10px; white-space: nowrap; }
.sv-sitechip.ok { background: var(--ok-soft); color: var(--ok); }
.sv-sitechip.warn { background: var(--warn-soft); color: var(--warn); }
.sv-sitechip.bad { background: var(--danger-soft); color: var(--danger); }

.sv-digest { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-2); }
@media (max-width: 760px) { .sv-digest { grid-template-columns: repeat(2, 1fr); } }
.sv-digest-stat { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--sp-3); display: flex; flex-direction: column; gap: 2px; box-shadow: var(--shadow-sm); }
.sv-digest-stat b { font-size: var(--fs-xl); font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.sv-digest-stat span { font-size: var(--fs-xs); color: var(--text-secondary); }

/* ── Advanced dashboards refresh: the Simple design language at technical density ──────────────
   Stat tiles (the digest-stat look, with a tone colour when the number is a signal), a verdict
   headline inside the product overview panel, compact all-clear rows instead of towering empty
   states, and a tab strip that replaces the hero button-soup on the product pages. */

.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: var(--sp-3); margin-bottom: var(--sp-3); }
.stat { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm); padding: var(--sp-3) var(--sp-4); display: flex; flex-direction: column; gap: 2px;
  color: inherit; text-decoration: none; transition: box-shadow .12s, transform .12s; }
a.stat:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); text-decoration: none; }
.stat b { font-size: var(--fs-xl); font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; line-height: 1.15; }
.stat .st-label { font-size: var(--fs-sm); font-weight: 600; color: var(--text-secondary); }
.stat .st-sub { font-size: var(--fs-xs); color: var(--text-muted); }
.stat.tone-bad b { color: var(--danger); }
.stat.tone-warn b { color: var(--attention); }
.stat.tone-ok b { color: var(--ok); }
.stat.tone-accent b { color: var(--accent-text); }
@media (max-width: 760px) { .stat-grid { grid-template-columns: repeat(2, 1fr); } }

/* Verdict headline inside the product overview panel (score-panel) — same voice as sv-verdict. */
.sp-body .sp-headline { margin: 0 0 var(--sp-1); font-size: var(--fs-lg); }
.sp-body .sp-line { margin: 0 0 var(--sp-3); color: var(--text-secondary); font-size: var(--fs-sm); max-width: 70ch; }
.sp-body .sp-line b { color: var(--text-primary); }

/* Compact all-clear row — replaces a towering empty state when a list is simply empty & healthy. */
.all-clear { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-sm); color: var(--text-secondary); }
.all-clear .ac-dot { width: 9px; height: 9px; border-radius: 999px; background: var(--ok); flex: none; }
.all-clear b { color: var(--text-primary); }

/* Product tab strip under the hero — the sub-pages live here, not as a wall of hero buttons. */
.product-tabs { margin: calc(-1 * var(--sp-3)) 0 var(--sp-4); }

/* Hero action menu (e.g. CertControl's import options) — one button, small popover. */
.action-menu { position: relative; display: inline-flex; }
.action-menu .menu-pop { top: 42px; width: 240px; }
.action-menu.open .menu-pop { display: block; }
.menu-item { display: flex; align-items: center; gap: var(--sp-2); width: 100%; padding: var(--sp-2);
  border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--text-primary);
  font: inherit; font-size: var(--fs-sm); font-weight: 550; text-align: left; cursor: pointer; }
.menu-item:hover { background: var(--bg-hover); }

/* Day separators in event streams — a quiet label row that chunks the table into days. */
table.data tr.day-sep td { padding: var(--sp-3) var(--sp-3) var(--sp-1);
  font-size: var(--fs-xs); font-weight: 650; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .05em; border-bottom: none; }
table.data tr.day-sep:hover, table.data tbody tr.day-sep:hover { background: transparent; }

/* "By product" tiles on the technical dashboard — the findings-page ptile, four across. */
.ptile-strip.cols-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1100px) { .ptile-strip.cols-4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .ptile-strip.cols-4 { grid-template-columns: 1fr; } }

/* ── Simple SiteControl: per-website cards with a small health ring ───────────────────────────── */
.sv-card .ring.sv-site-ring { --pct: 0; --rc: var(--border-strong); width: 56px; height: 56px; border-radius: 50%;
  flex: none; align-self: center; background: conic-gradient(var(--rc) calc(var(--pct) * 1%), var(--bg-hover) 0);
  display: grid; place-items: center; position: relative; }
.sv-card .ring.sv-site-ring::before { content: ""; position: absolute; inset: 5px; border-radius: 50%; background: var(--bg-surface); }
.sv-card .ring.sv-site-ring > .rv { position: relative; font-weight: 800; font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.sv-card .ring.sv-site-ring.ok { --rc: var(--ok); } .sv-card .ring.sv-site-ring.warn { --rc: var(--attention); } .sv-card .ring.sv-site-ring.bad { --rc: var(--danger); }

/* ── "What we checked" rows: compact by default, detail on expand ──────────────────────────────
   This list is the product's shop window — the counterweight to a findings list that by its nature
   only shows failures — and on SiteControl it runs to fifty-odd rows. It has to survive being
   scanned, not just read, and three things stopped it.

   The status used to be a full-colour emoji. Fifty of those stack into a bar of saturated green,
   amber and red down the left edge, which puts the loudest object on the page next to the row's
   least surprising fact; and an emoji is a bitmap, so it ignores the theme and the dark canvas got
   the same fluorescent green as the light one. The "—" for NOT_CHECKED was a text glyph 4px
   narrower than an emoji, so every un-run row's label sat 4px out of the column — a zigzag down
   fifty-eight rows. The mark is drawn here now, from the same severity tokens as the rest of the
   product, so a pass is the quietest thing in the column and only the exceptions carry a fill. It
   is still a shape per state, never colour alone, and the status word rides along in aria-label.

   The caret asked for `var(--muted)`, which is not a token — only the `.muted` class is. The
   declaration was therefore invalid and `color`, being inherited, fell back to the full text
   colour: fifty carets at 17:1 down the right edge, five times the contrast of the result they
   point at. The loudest mark in every row was the one carrying the least information.

   That got the marks under control but left the list's real problem: fifty-odd rows, each one a
   hairline followed by a line of text, is a wall. Three changes give it structure a scanning eye can
   hold on to.

   FIRST, the row is a grid, not a flex line — a fixed status gutter, the label, a fixed-width result
   column, the caret — so all four align down the whole list. The result used to be a flex item that
   started wherever the label ended, which meant fifty answers at fifty different left edges; it now
   ends against a hairline that makes it visibly a column. It is monospaced for the same reason the
   instrument panel's figures are: "17 crawled page(s)" and "2 of 48" only line up if the digits do.

   SECOND, the separator is banding rather than a rule per row. A hairline between every pair of rows
   is fifty-eight lines drawn to say "these are separate", which is the one thing nobody was confused
   about; alternating surfaces say it without adding a mark. Rules survive only where the structure
   actually changes — under a group heading, under the column header.

   THIRD, rows needing attention carry a wash and a left bar. Thirteen amber marks in a column of
   fifty are findable if you look down the gutter, but the row itself was identical to its neighbours,
   so "find the thirteen" meant reading. The tinted band and the bar make them objects you can see
   from across the card — still never colour alone, since the mark and the word are both still there.

   Row height went from ~30px to 52px. The list is long, and a long list read at a glance needs air
   more than it needs to fit on one screen; it never fitted on one screen anyway. */
.check-group {
  --check-gutter: 52px;
  --check-result-w: 300px;
  --check-cols: var(--check-gutter) minmax(0, 1fr) var(--check-result-w) 32px;
  margin-top: var(--sp-5);
}
/* Banding is only legible if the band reaches the card's edge — stopping it short of the padding
   turns each band into a floating slab. The rows bleed out to the card's inner edge and carry the
   padding themselves, which is also what lets the group header read as a header rather than a
   paragraph in a box. */
.check-group > * { margin-left: calc(var(--sp-4) * -1); margin-right: calc(var(--sp-4) * -1); }
.check-group-head {
  padding: var(--sp-3) var(--sp-4);
  background: var(--bg-surface-2);
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
.check-group-head > div { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-3); }
.check-group-head b { font-size: var(--fs-md); font-weight: 600; }

/* The column header. Small, quiet, and worth repeating per group: the groups are far enough apart
   that by the fourth one the reader has lost what the right-hand column is. */
.check-cols {
  display: grid; grid-template-columns: var(--check-cols); align-items: center;
  padding: var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--border);
  font-family: var(--ip-mono, var(--font-mono));
  font-size: 11px; letter-spacing: 0.09em; text-transform: uppercase; color: var(--text-muted);
}
.check-cols > :nth-child(3) { text-align: right; padding-right: 18px; }

.check-rows > * { position: relative; }
/* The band. Placed on the row itself, so an open row's summary and its expansion sit on one surface
   rather than the expansion looking like a separate object that drifted under the wrong line. */
.check-rows > :nth-child(even) { background: var(--bg-surface-2); }
/* The two states somebody has to act on. The bar is inset rather than a border so it cannot shift
   the row's grid by 3px and put the label column out of line with the rows above it. */
.check-rows > [data-s="WARN"] { background: color-mix(in srgb, var(--attention) 9%, transparent);
  box-shadow: inset 3px 0 0 var(--attention); }
.check-rows > [data-s="FAIL"] { background: color-mix(in srgb, var(--danger) 9%, transparent);
  box-shadow: inset 3px 0 0 var(--danger); }

.check-row > summary::-webkit-details-marker { display: none; }
.check-row > summary::marker { content: ""; }
/* The caret is a grid item in the fourth column, so it lands in the same place on every row instead
   of trailing whatever the result happened to be. */
.check-row > summary::after {
  content: "›"; color: var(--text-muted); font-size: var(--fs-md); justify-self: end;
  transition: transform .12s;
}
.check-row[open] > summary::after { transform: rotate(90deg); }
/* Semi-transparent, so hovering a row that needs attention does not wipe out the amber that says so. */
.check-row > summary:hover { background: color-mix(in srgb, var(--text-primary) 5%, transparent); }
.check-row > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.check-row > summary, .check-line {
  display: grid; grid-template-columns: var(--check-cols); align-items: center;
  gap: var(--sp-2); min-height: 52px; padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-md);
}
.check-row > summary { cursor: pointer; list-style: none; }

/* The status mark: one fixed 18px box whatever the state, so the label column has ONE left edge. */
.check-ico {
  --ck: var(--text-muted);
  flex: 0 0 auto; align-self: center;
  width: 18px; height: 18px; border-radius: 999px;
  display: inline-grid; place-items: center;
  font-size: 11px; font-weight: 700; line-height: 1; color: var(--ck);
}
/* A pass is the answer on most rows on most sites, so it gets a tick and nothing else — a filled
   chip forty times over is the noise this list is trying not to make. */
.check-ico[data-s="PASS"] { --ck: var(--ok); }
.check-ico[data-s="INFO"] { --ck: var(--info); }
/* The two states somebody has to act on are the only ones with a fill, so scanning the column finds
   them without reading a word. */
.check-ico[data-s="WARN"] { --ck: var(--attention); background: color-mix(in srgb, var(--ck) 15%, transparent); }
.check-ico[data-s="FAIL"] { --ck: var(--danger);    background: color-mix(in srgb, var(--ck) 15%, transparent); }
/* "Nobody checked" must never look like "checked and clean" — the distinction this product exists
   to make. An empty ring, the way the unknown state is drawn everywhere else on the platform. */
.check-ico[data-s="NOT_CHECKED"] { box-shadow: inset 0 0 0 1px var(--border-strong); }

/* The row's right-hand answer — "97.9% in raw HTML", "2 of 48 fail". This is the payload of the row
   and it was set in --text-muted: 3.1:1 on the light surface, the least legible thing on the line.
   Only an un-run row dims now, which is what the renderer's `dim` was always meant to say.

   The hairline on its right edge is what makes this a column rather than text that happens to end
   near the margin, and it is the reason the width is fixed: a column whose edge moves row to row is
   not one. */
.check-result {
  font-family: var(--ip-mono, var(--font-mono));
  font-size: var(--fs-sm); color: var(--text-secondary); font-variant-numeric: tabular-nums;
  text-align: right; padding-right: 18px; border-right: 1px solid var(--border);
  align-self: stretch; display: flex; align-items: center; justify-content: flex-end;
  /* The cell is pulled out through the row's vertical padding so the rule runs edge to edge. Left
     inside it, the line breaks for 16px at every row boundary and fifty of those read as a dashed
     border — a decoration — rather than as the edge of a column. */
  margin-top: calc(var(--sp-2) * -1); margin-bottom: calc(var(--sp-2) * -1);
}
.check-result.muted { color: var(--text-muted); }

/* The expansion. It is inset to the label column and takes the recessed surface with a rail down its
   left edge, so at a glance it is plainly the open row's contents and not a gap in the list — which
   is what a full-width block of prose between two banded rows reads as. `why` is the sentence
   explaining what we tested and why it matters — the reason a row is worth opening at all — and it
   was --text-muted at 13px: 3.1:1 in light, under the floor for body copy, and visibly fainter than
   the verdict below it, so the row read back to front. Same fix as `.ip-hint`: prose somebody has to
   read is --text-secondary. The measure cap matters as much as the colour — the card is 1300px wide
   and these sentences ran the whole way across it. */
.check-body {
  margin: 0 var(--sp-4) var(--sp-3) calc(var(--sp-4) + var(--check-gutter));
  padding: var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-3);
  background: var(--bg-body); border-left: 2px solid var(--border-strong);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.check-why, .check-intro, .check-note, .check-verdict { max-width: 78ch; text-wrap: pretty; }
.check-why { margin: 0; font-size: 13.5px; color: var(--text-secondary); line-height: 1.6; }
.check-why b, .check-why strong, .check-note b, .check-note strong { color: var(--text-primary); font-weight: 600; }
.check-intro { margin: var(--sp-1) 0 var(--sp-2); font-size: 13.5px; color: var(--text-secondary); line-height: 1.6; }
.check-verdict { margin: var(--sp-2) 0 0; font-size: var(--fs-sm); line-height: 1.6; }
/* The honest note about what a check cannot see. A caveat, so it stays one step down — but a step,
   not a whisper: 12px muted is the setting for a table sub-label, not for a sentence. */
.check-note {
  margin: var(--sp-2) 0 0; font-size: var(--fs-sm); color: var(--text-secondary); line-height: 1.6;
  border-left: 3px solid var(--border-strong); padding-left: var(--sp-2);
}
/* The label. min-width:0 because a grid item's automatic minimum is its content, so without it a
   long check name pushes the result column off its fixed edge instead of wrapping. */
.check-label { min-width: 0; overflow-wrap: anywhere; }
.check-group-head .check-intro { margin: var(--sp-2) 0 0; max-width: 70ch; }

/* The card's own header. The counts are the reader's first question — how much of this ran, and how
   much of it is a problem — so they are set as figures in the monospace face rather than folded into
   a sentence, and the two that carry a verdict are toned. The words stay: never colour alone. */
.check-counts {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2);
  font-family: var(--ip-mono, var(--font-mono));
  font-size: var(--fs-xs); color: var(--text-muted);
}
.check-counts > i { color: var(--border-strong); font-style: normal; }
.check-counts > .ok { color: var(--ok); }
.check-counts > .warn { color: var(--attention); }
.check-headline { margin: var(--sp-2) 0 0; max-width: 78ch; color: var(--text-secondary); text-wrap: pretty; }

/* Narrow viewports. The result column is the first thing to give: it shrinks, then drops under the
   label rather than squeezing both into two unreadable ribbons. The status gutter never moves —
   it is the one column the list is scanned down. */
@media (max-width: 1100px) { .check-group { --check-result-w: 220px; } }
@media (max-width: 820px) {
  .check-group { --check-cols: 40px minmax(0, 1fr) 24px; --check-gutter: 40px; }
  /* Every cell is placed by hand here: with the result moved to a second row, auto-placement would
     carry the caret down with it and hang it off the bottom of the row it belongs to. */
  .check-row > summary, .check-line { padding-top: var(--sp-2); padding-bottom: var(--sp-2); align-items: start; }
  .check-ico { grid-column: 1; grid-row: 1; }
  .check-label { grid-column: 2; grid-row: 1; }
  .check-cols > :nth-child(3) { display: none; }
  .check-result {
    grid-column: 2; grid-row: 2; justify-content: flex-start; text-align: left;
    padding-right: 0; border-right: 0; align-self: auto; margin-top: 0; margin-bottom: 0;
  }
  .check-row > summary::after { grid-column: 3; grid-row: 1; }
}

/* ── SEO task cards — "Fix this first" and the full task list ───────────────────────────────────
   One card shape for the two places a task appears, because it is the same object: the overview
   shows the top three, the Tasks page shows all of them.

   The card used to be a `.card` with everything at one weight — heading, prose, an evidence table,
   the page's current title/meta, a recipe and two buttons, each a paragraph following the last. The
   reader had to read it to find out whether it was worth reading. It now has four bands the eye can
   land on in order: what it is (title + tags), the numbers behind the claim (a strip of figures),
   what to do about it (prose), and what the page says today (a panel). Nothing was removed; the
   same content is arranged so a scan gets you to the decision.

   The rail carries the score, not the row's position: the list is sorted, so an ordinal rail would
   fade to nothing 20 rows down the Tasks page and say something the order already says. */
/* 4px at full strength, every card. Two attempts at grading the rail by score produced rails that
   could not be seen — 38% of the accent on a 3px edge is a rail you have to go looking for, and a
   marking nobody notices is not a marking. The score already has a home: the pill on the same line
   as the title, in words and out of ten. */
.seo-task {
  border-left: 4px solid var(--accent);
  border-radius: 0 14px 14px 0;
  padding: 24px 28px;
}
.seo-task + .seo-task { margin-top: var(--sp-3); }
.seo-task-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap; margin-bottom: var(--sp-3);
}
.seo-task-title { margin: 0; font-size: 18px; font-weight: 600; letter-spacing: -0.01em; text-wrap: pretty; }
.seo-task-tags { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
/* Not a `.badge`: a badge is a status, carries a dot and reads as a verdict. Effort and score are
   metadata about the task, and dressing them as statuses put two false verdicts on every card. */
.seo-pill {
  display: inline-flex; align-items: center; padding: 4px 11px; border-radius: 999px;
  font-size: var(--fs-xs); white-space: nowrap;
  color: var(--text-secondary); background: color-mix(in srgb, var(--text-primary) 5%, transparent);
  border: 1px solid var(--border);
}
.seo-pill.on {
  color: var(--accent-text); font-weight: 500;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
}

/* The numbers the claim rests on. Grouped into one block rather than run through the sentence: a
   figure you can compare is worth more than the same figure buried in prose, and this is the part a
   customer checks us on. Monospaced for the same reason as everywhere else — digits that line up. */
.seo-stats {
  display: flex; flex-wrap: wrap; gap: var(--sp-4);
  padding: 14px 18px; margin-bottom: var(--sp-3);
  background: color-mix(in srgb, var(--text-primary) 3%, transparent);
  border-radius: 10px; width: fit-content; max-width: 100%;
}
.seo-stats dt {
  font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-muted); font-weight: 600; margin-bottom: 5px;
}
.seo-stats dd {
  margin: 0; font-size: 17px; color: var(--text-primary);
  font-family: var(--ip-mono, var(--font-mono));
  font-variant-numeric: tabular-nums;
}

.seo-task-body { margin: 0 0 var(--sp-3); font-size: var(--fs-md); line-height: 1.65; max-width: 78ch; text-wrap: pretty; }
/* The competing-pages table on a cannibalisation task is one more band in the card, so it keeps the
   same gap as the others rather than butting against the panel below it. */
.seo-task .table-wrap { margin-bottom: var(--sp-3); }
.seo-task-body b, .seo-task-body strong { font-weight: 600; }

/* An instruction written as steps. The list is the point: each line is one thing to do, so the
   marker sits outside the text and every step starts on the same left edge — a customer working
   down the list has to be able to see where one ends and the next begins. The lead-in line stays
   tight against the list it introduces; the gap belongs after the last step, not inside. */
.seo-do > b { display: block; margin-bottom: 6px; font-weight: 600; }
.seo-step-lead { margin: 0 0 8px; }
.seo-step-lead:last-child { margin-bottom: 0; }
.seo-steps { margin: 0 0 8px; padding-left: 1.25em; list-style: disc; }
.seo-steps:last-child { margin-bottom: 0; }
.seo-steps li { margin-bottom: 6px; padding-left: 2px; }
.seo-steps li:last-child { margin-bottom: 0; }
/* A closing line after the list ("If you took the page down on purpose, nothing needs doing.")
   is a remark about the steps, not a fourth step — it gets air above it to say so. */
.seo-steps + .seo-step-lead { margin-top: 10px; }

/* What the page says right now, next to the advice about it. A panel rather than two more lines of
   prose: this is quoted evidence from the customer's own site, and it reads as evidence only if it
   is visibly set apart from our commentary about it. */
.seo-now { border: 1px solid var(--border); border-radius: 11px; overflow: hidden; margin-bottom: var(--sp-3); }
.seo-now-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap;
  padding: 10px 18px; border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-primary) 3%, transparent);
  font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-secondary); font-weight: 600;
}
.seo-now-head a { font-size: var(--fs-xs); letter-spacing: 0; text-transform: none; font-weight: 400; }
/* `auto`, not a fixed width: the label column has to fit its widest label, and "Description" set in
   uppercase with letter-spacing is wider than any number picked by eye — at 82px it ran straight
   into the value beside it. */
.seo-now-body { padding: 16px 18px; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 12px var(--sp-3); align-items: baseline; }
.seo-now-body dt { font-size: var(--fs-xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); font-weight: 600; }
.seo-now-body dd { margin: 0; font-size: 14.5px; line-height: 1.5; color: var(--text-secondary); }
.seo-now-len { font-size: var(--fs-xs); white-space: nowrap; }
.seo-now-len.ok { color: var(--ok); }
.seo-now-len.off { color: var(--attention); }

/* ── "What got better" — the wins ───────────────────────────────────────────────────────────────
   The half of the story a task list can never tell. It used to be a KPI strip followed by a card of
   text rows, which read as two unrelated objects: the count said "4" and the list below it named
   four searches, and nothing on the page said those were the same four.

   Now the count and the searches it counts sit side by side in one row — the number is the claim,
   the list is the receipt. Green is doing real work here: it is the only place in SiteControl where
   a colour means "this got better", and the tile, the caption and the improved figure all take it,
   so the three parts read as one statement. */
.seo-wins {
  display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 2fr);
  gap: var(--sp-3); align-items: stretch; margin-bottom: var(--sp-3);
}
@media (max-width: 900px) { .seo-wins { grid-template-columns: 1fr; } }

.seo-win-tile {
  border-left: 3px solid var(--ok); border-radius: 0 14px 14px 0;
  padding: 24px 28px; display: flex; flex-direction: column; justify-content: center;
}
.seo-win-n {
  font-size: 52px; line-height: 1; font-weight: 500; letter-spacing: -0.03em;
  color: var(--ok); margin-bottom: 14px;
  font-family: var(--ip-mono, var(--font-mono));
  font-variant-numeric: tabular-nums;
}
.seo-win-l { font-size: 17px; font-weight: 600; margin-bottom: 4px; }
.seo-win-s { font-size: var(--fs-sm); color: var(--text-muted); }

.seo-win-list { border-radius: 14px; padding: 20px 28px 22px; }
.seo-win-cap {
  font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ok); font-weight: 700; margin-bottom: 4px;
}
.seo-win-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-4); padding: 13px 0; border-bottom: 1px solid var(--border);
}
.seo-win-row:last-child { border-bottom: 0; padding-bottom: 0; }
.seo-win-q { font-size: var(--fs-md); font-weight: 600; }
.seo-win-v {
  font-size: 14px; color: var(--text-secondary); white-space: nowrap;
  font-family: var(--ip-mono, var(--font-mono));
  font-variant-numeric: tabular-nums;
}
/* Only the part that improved is green. Colouring the whole line would make the number it moved
   FROM look like good news too, which is the one thing it is not. */
.seo-win-v .up { color: var(--ok); }
.seo-wins-note { margin: 0 0 var(--sp-3); font-size: var(--fs-sm); color: var(--text-secondary); }

/* ── Value: what the search traffic produced ────────────────────────────────────────────────────
   The one line on this page that has to be read is the one that says a number is ESTIMATED. Google
   has never published which search term produced which sale and never will, so the keyword figures
   are an allocation — and an allocation printed in the same weight as a measurement is the easiest
   lie this product could tell. So it gets a marker on the heading, in the accent, and the basis is
   prose above the table rather than fine print below it. */
.seo-est {
  display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 999px;
  font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 700;
  color: var(--accent-text); background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  vertical-align: middle; margin-left: var(--sp-2);
}
/* The same marker at table scale. A competitor's position is MEASURED when we read it off a live
   Google page ourselves, and ESTIMATED when it comes from the supplier's own index — and for a long
   while both were printed identically, which made the weaker number borrow the stronger one's
   authority. It sits next to the figure rather than in a footnote, because a reader who scans a
   column of numbers never reaches the footnote. */
.seo-src {
  display: inline-block; margin-left: 6px; font-size: 10px; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--accent-text);
  vertical-align: middle;
}

.seo-val-note { margin: 0; padding: var(--sp-3) var(--sp-4); font-size: var(--fs-sm); color: var(--text-secondary); text-wrap: pretty; }
.seo-val-note + .seo-cols { border-top: 1px solid var(--border); }
/* A dash means "we do not know", and it must never be mistaken for a small number. */
.seo-n.unknown { color: var(--text-muted); }

/* ── Google's index: what the index holds for the pages we asked about ──────────────────────────
   Two numbers that are two halves of one whole, so they are drawn as one: the split bar under the
   tiles is the same two figures as a single length, and it makes the ratio readable without doing
   the arithmetic. In / out earn colour because one of them is the thing to act on; the reason
   breakdown below does not — a reason is a category, and colouring seven of them would turn a list
   into a warning. */
.seo-idx { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); margin-bottom: var(--sp-3); }
@media (max-width: 720px) { .seo-idx { grid-template-columns: 1fr; } }
.seo-idx-tile { border-left: 3px solid var(--idx, var(--text-muted)); border-radius: 0 14px 14px 0; padding: 24px 28px; }
.seo-idx-n {
  font-size: 48px; line-height: 1; font-weight: 500; letter-spacing: -0.03em;
  color: var(--idx, var(--text-primary)); margin-bottom: 14px;
  font-family: var(--ip-mono, var(--font-mono));
  font-variant-numeric: tabular-nums;
}
.seo-idx-l { font-size: 17px; font-weight: 600; margin-bottom: 4px; }
.seo-idx-s { font-size: var(--fs-sm); color: var(--text-secondary); }
.seo-idx-split { display: flex; height: 8px; gap: 2px; margin-bottom: var(--sp-2); }
.seo-idx-split i { border-radius: 2px; }
.seo-idx-split i:first-child { border-radius: 5px 2px 2px 5px; background: var(--ok); }
.seo-idx-split i:last-child { border-radius: 2px 5px 5px 2px; background: var(--attention); }
.seo-idx-split i:only-child { border-radius: 5px; }
.seo-idx-note { margin: 0 0 var(--sp-5); font-size: var(--fs-sm); color: var(--text-secondary); max-width: 88ch; text-wrap: pretty; }

.seo-idx-row {
  display: grid; grid-template-columns: minmax(0, 260px) minmax(0, 1fr) 70px;
  align-items: center; gap: var(--sp-4); padding: 12px var(--sp-4);
  border-bottom: 1px solid var(--border);
}
.seo-idx-row:last-child { border-bottom: 0; }
.seo-idx-row > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.seo-idx-bar { height: 6px; border-radius: 4px; background: var(--bg-hover); overflow: hidden; }
.seo-idx-bar i { display: block; height: 100%; border-radius: 4px; background: var(--idx, var(--text-muted)); }
.seo-idx-row > span:last-child {
  text-align: right;
  font-family: var(--ip-mono, var(--font-mono));
  font-variant-numeric: tabular-nums;
}
@media (max-width: 720px) {
  .seo-idx-row { grid-template-columns: minmax(0, 1fr) 60px; }
  .seo-idx-bar { grid-column: 1 / -1; }
}

.seo-idx-pages { --idx-cols: minmax(0, 1fr) minmax(0, 260px) 96px; }
.seo-idx-head, .seo-idx-page { display: grid; grid-template-columns: var(--idx-cols); gap: var(--sp-4); }
.seo-idx-head {
  padding: 10px var(--sp-4); border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-primary) 3%, transparent);
}
.seo-idx-head span {
  font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-muted); font-weight: 700;
}
.seo-idx-head span:last-child { text-align: right; }
.seo-idx-page { align-items: center; padding: 11px var(--sp-4); border-bottom: 1px solid var(--border); }
.seo-idx-page:last-child { border-bottom: 0; }
.seo-idx-page:hover { background: var(--bg-hover); }
.seo-idx-page > a {
  font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--ip-mono, var(--font-mono));
}
/* The reason IS a status — Google's verdict on that page — so it keeps the dot the rest of the
   product uses for one, and the colour that goes with it. */
.seo-idx-why { display: inline-flex; align-items: baseline; gap: 7px; min-width: 0; font-size: var(--fs-sm); color: var(--why, var(--text-secondary)); }
.seo-idx-why i { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--why, var(--text-muted)); align-self: center; }
.seo-idx-why span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.seo-idx-when { text-align: right; font-size: var(--fs-xs); color: var(--text-muted); white-space: nowrap; }
@media (max-width: 820px) {
  .seo-idx-pages { --idx-cols: minmax(0, 1fr) minmax(0, 180px); }
  .seo-idx-head span:last-child, .seo-idx-when { display: none; }
}

/* ── Content ideas: topics, and what to do with the page that half-covers them ──────────────────
   Every card answers one question — write something new, or deepen what is already there — so the
   verb leads and the left edge carries it: accent for "expand what exists", green for "make a new
   page". Two kinds of work, told apart before the title is read.

   Coverage is stated once as a figure and drawn once as a bar. The bar carries the colour because
   a 4px line is read as a length, not a word; the figure stays plain so the same judgement is not
   asserted twice in one line. */
.seo-idea { border-left: 3px solid var(--idea, var(--accent)); border-radius: 0 14px 14px 0; padding: 24px 28px 22px; }
.seo-idea + .seo-idea { margin-top: var(--sp-3); }
.seo-idea-do {
  font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 700;
  color: var(--idea, var(--accent)); margin-bottom: var(--sp-3);
}
/* The heading is a phrase somebody typed into Google, and nothing on the card used to say so — a
   reader could just as reasonably have taken it for a title we were proposing, or a tag, or a word
   they were meant to sprinkle somewhere. The label and the quotation marks are the whole fix. */
.seo-idea-what {
  font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-muted); font-weight: 600; margin-bottom: 4px;
}
.seo-idea-t { margin: 0 0 var(--sp-2); font-size: 19px; font-weight: 600; letter-spacing: -0.01em; text-wrap: pretty; }
.seo-idea-origin { margin: 0 0 var(--sp-3); max-width: 88ch; color: var(--text-secondary); text-wrap: pretty; }
.seo-cov { max-width: 340px; height: 4px; border-radius: 3px; background: var(--bg-hover); overflow: hidden; margin-bottom: var(--sp-3); }
.seo-cov i { display: block; height: 100%; border-radius: 3px; background: var(--cov, var(--text-muted)); }
.seo-idea-body { margin: 0 0 var(--sp-3); max-width: 88ch; color: var(--text-secondary); text-wrap: pretty; }
.seo-also {
  padding: 12px 16px; border-radius: var(--radius-sm); max-width: 88ch; margin-bottom: var(--sp-3);
  background: color-mix(in srgb, var(--text-primary) 3%, transparent);
}
/* The page the instruction refers to. Its own line, monospace, clickable — the sentence above says
   what to do to it, and a 55-character URL wedged into that sentence is placed at the hardest
   possible point to read past. */
.seo-idea-page {
  display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap;
  max-width: 88ch; margin-bottom: var(--sp-3);
}
.seo-idea-page > span {
  font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-muted); font-weight: 700; flex: none;
}
.seo-idea-page > a {
  font-size: var(--fs-sm); overflow-wrap: anywhere;
  font-family: var(--ip-mono, var(--font-mono));
}
.seo-also-l { font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 6px; }
.seo-also-v { font-size: var(--fs-sm); color: var(--text-secondary); text-wrap: pretty; }
.seo-idea-foot {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding-top: var(--sp-3); border-top: 1px solid var(--border);
}
@media (max-width: 720px) { .seo-idea { padding: 20px; } }

/* ── Searches: the full query table ────────────────────────────────────────────────────────────
   Four or five numbers per row, hundreds of rows: a table where the eye has to compare down a
   column, not read across a line. So the figures are monospace and tabular — proportional digits
   make 1,247 and 984 the same width and the column stops meaning anything at a glance — and the
   separators are hairlines rather than boxes, because a border around every cell draws more ink
   than the numbers inside it.

   Impressions carry a bar because it is the one column with no ceiling: 5,035 against 71 says
   nothing until you can see the ratio. Clicks, CTR and position are bounded or comparable as
   figures, so a bar there would be decoration. */
.seo-kpis {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px;
  background: var(--border); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden; margin-bottom: var(--sp-3);
}
@media (max-width: 860px) { .seo-kpis { grid-template-columns: repeat(2, 1fr); } }
/* Value's strip is three tiles or five, depending on whether the property reports money at all —
   a fixed four would leave a hole in one case and wrap badly in the other. */
.seo-kpis.fit { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
.seo-kpi { background: var(--bg-surface); padding: 18px 20px 16px; }
.seo-kpi-l {
  font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-muted); font-weight: 700; margin-bottom: 10px;
}
.seo-kpi-v {
  font-size: 30px; line-height: 1; font-weight: 500; letter-spacing: -0.02em; margin-bottom: 8px;
  font-family: var(--ip-mono, var(--font-mono));
  font-variant-numeric: tabular-nums;
}
.seo-kpi-n { font-size: var(--fs-sm); color: var(--text-secondary); }

.seo-tbl-card { padding: 0; overflow: hidden; }
.seo-tbl-head { padding: var(--sp-4) var(--sp-4) var(--sp-3); border-bottom: 1px solid var(--border); }
.seo-tbl-meta {
  margin: 6px 0 0; font-size: var(--fs-xs); color: var(--text-muted);
  font-family: var(--ip-mono, var(--font-mono));
}
.seo-tbl-bar { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-3); }
.seo-tbl-count {
  margin-left: auto; font-size: var(--fs-sm); color: var(--text-muted); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* One control, three states — a segmented switch rather than a dropdown, because the three cuts
   are the question this page is for and a dropdown hides two thirds of it behind a click. */
.seo-seg { display: flex; gap: 2px; padding: 3px; border: 1px solid var(--border); border-radius: 9px; }
.seo-seg button {
  padding: 7px 13px; border: 0; border-radius: 7px; background: none; cursor: pointer;
  font: inherit; font-size: var(--fs-sm); font-weight: 500; color: var(--text-secondary); white-space: nowrap;
}
.seo-seg button:hover { color: var(--text-primary); }
.seo-seg button[aria-pressed="true"] { color: var(--text-primary); background: var(--bg-hover); }

.seo-tbl { --seo-cols: minmax(0, 1fr) 76px 118px 104px 96px; }
/* The gap is not decoration: right-aligned headings in adjacent columns end and begin at the same
   pixel, so CLICKS and CONVERSIONS read as one word without it. */
.seo-cols, .seo-row { display: grid; grid-template-columns: var(--seo-cols); gap: var(--sp-3); }
.seo-cols {
  padding: 10px var(--sp-4); border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--text-primary) 3%, transparent);
}
.seo-cols span {
  font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-muted); font-weight: 700; text-align: right;
}
.seo-cols span:first-child { text-align: left; }
.seo-row { align-items: center; padding: 0 var(--sp-4); border-bottom: 1px solid var(--border); }
.seo-row:last-child { border-bottom: 0; }
.seo-row:hover { background: var(--bg-hover); }
.seo-q { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; padding: 13px var(--sp-3) 13px 0; }
.seo-q > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.seo-q .seo-pill { flex: none; padding: 2px 9px; }
.seo-n {
  padding: 13px 0; text-align: right; font-size: 14px; color: var(--text-secondary);
  font-family: var(--ip-mono, var(--font-mono));
  font-variant-numeric: tabular-nums;
}
/* Zero is not a small number here, it is an absence — a search that was shown and never clicked.
   Dimming it lets the eye skip to the rows where something actually happened. */
.seo-n.none { color: var(--text-muted); }
.seo-n.hit { color: var(--text-primary); }
.seo-n.page1 { color: var(--ok); }
.seo-impr { position: relative; }
/* Sits under the figure, not beside it: the number is the answer and the bar is the shape of it.
   Kept faint on purpose — at full accent it read as an underline, which made every row look like a
   link to somewhere. */
.seo-impr i { position: absolute; right: 0; bottom: 4px; height: 2px; border-radius: 2px;
  background: color-mix(in srgb, var(--accent) 32%, transparent); }
.seo-tbl-note { margin: 0; padding: var(--sp-3) var(--sp-4); font-size: var(--fs-sm); color: var(--text-secondary); }
.seo-tbl-empty { margin: 0; padding: var(--sp-5) var(--sp-4); color: var(--text-secondary); }
@media (max-width: 820px) {
  .seo-tbl { --seo-cols: minmax(0, 1fr) 58px 88px 80px 72px; }
  .seo-cols, .seo-row { padding-left: var(--sp-3); padding-right: var(--sp-3); }
  .seo-tbl-head { padding: var(--sp-3); }
}

/* ── "Did it work?" — the answer four weeks after a task was handled ────────────────────────────
   The third part of one story: "What got better" is everything that moved, the task list is what to
   move next, and this is whether the customer's own change moved anything. It wore a different
   shape from both — bold text, a badge, two grey paragraphs — so the loop we built the product
   around read as a footnote.

   Same language as the other two now: a rail carrying the verdict, the figures in monospace with
   only the improvement coloured, and the sentence that says what the numbers mean underneath. The
   verdict is a real status, so it keeps a `.badge` — colour, dot and word — where effort and score
   on a task card take a plain pill. That distinction is the point of having both. */
.seo-effects { display: flex; flex-direction: column; gap: var(--sp-2); }
.seo-effect {
  border-left: 3px solid var(--eff, var(--border-strong));
  border-radius: 0 10px 10px 0; padding: 14px 18px;
  background: color-mix(in srgb, var(--text-primary) 3%, transparent);
}
.seo-effect-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; }
.seo-effect-q { font-size: var(--fs-md); font-weight: 600; }
.seo-effect-nums {
  margin: var(--sp-2) 0 0; font-size: 14px; color: var(--text-secondary);
  font-family: var(--ip-mono, var(--font-mono));
  font-variant-numeric: tabular-nums;
}
/* Same rule as the wins list: colour the half that moved, never the number it moved from. Down is
   amber rather than red — a quiet four weeks is not a fault, and the sentence below says so. */
.seo-effect-nums .up { color: var(--ok); }
.seo-effect-nums .down { color: var(--attention); }
.seo-effect-note { margin: 6px 0 0; font-size: var(--fs-sm); color: var(--text-muted); }

/* The heading over a run of task cards: what this list is, and the way out of it. Shared so the
   overview's top-three and the full Tasks page are visibly the same list seen from two distances. */
.seo-sec-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap; margin: var(--sp-5) 0 var(--sp-3);
}
.seo-sec-head h2 { margin: 0 0 var(--sp-1); font-size: 24px; font-weight: 600; letter-spacing: -0.015em; }
.seo-sec-head p { margin: 0; font-size: 14.5px; color: var(--text-secondary); max-width: 78ch; text-wrap: pretty; }

.seo-task-actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
/* The recipe is the third item in the action row, not a separate block above it: "how do I do this"
   belongs with "I did it". It opens in place — the steps are the answer, not a link to one. */
.seo-recipe { margin-left: auto; }
.seo-recipe > summary { cursor: pointer; list-style: none; font-size: var(--fs-sm); font-weight: 500; color: var(--accent-text); }
.seo-recipe > summary::-webkit-details-marker { display: none; }
.seo-recipe > summary::marker { content: ""; }
.seo-recipe > summary::after { content: " →"; }
.seo-recipe[open] > summary::after { content: " ↓"; }
.seo-recipe > summary:hover { text-decoration: underline; }
.seo-recipe > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
.seo-recipe ol { margin: var(--sp-2) 0 0 1.25rem; padding: 0; font-size: var(--fs-sm); line-height: 1.6; color: var(--text-secondary); }
.seo-recipe li { margin: var(--sp-1) 0; }
/* The steps must not sit in the flex row that the summary lives in — they belong under the whole
   card, at its full width, not squeezed into the column the link occupies. */
.seo-task-actions > .seo-recipe[open] { flex: 1 0 100%; margin-left: 0; }

@media (max-width: 720px) {
  .seo-task { padding: 20px; }
  .seo-stats { gap: var(--sp-3); }
  .seo-now-body { grid-template-columns: 1fr; gap: 4px var(--sp-3); }
  .seo-now-body dt { margin-top: var(--sp-2); }
}

/* ── CodeControl report sections: every card folds behind its own heading (ccCollapseSections) ── */
.cc-section > summary {
  display: flex; align-items: center; gap: var(--sp-2); list-style: none;
}
.cc-section > summary::-webkit-details-marker { display: none; }
.cc-section > summary::marker { content: ""; }
/* The caret sits after the heading text, not before it: the headings carry emoji and badges, and a
   leading triangle would push those out of the column the eye scans down. */
.cc-section > summary::after { content: "▾"; color: var(--text-muted); font-size: var(--fs-xs); flex: 0 0 auto; }
.cc-section[open] > summary::after { content: "▴"; }
.cc-section > summary:hover { color: var(--accent); }
.cc-section > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cc-section:not([open]) > summary { margin-bottom: 0; }

/* The status rail. Every CodeControl section declares a state and wears it on its left edge.

   Two things this fixes. First, the rail used to be an inline style set only on sections that had
   something wrong, so most of the list carried no rail at all — and scanning down a column where the
   colour stops after the fourth row reads as "the rest are empty", not "the rest are fine". Absence of
   colour is not a state; it is the absence of information, and it was standing in for the most common
   answer we give. Second, the rail was a `border-left` on a card with rounded corners, so the radius
   clipped it top and bottom into a tapered stub rather than a bar.

   So: the rail is square-ended (the card keeps its radius on the other three sides), and a section
   cannot render without choosing a state, because the attribute is what draws it. */
.card[data-state] {
  border-left: 4px solid var(--border-strong);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.card[data-state="danger"] { border-left-color: var(--danger); }
.card[data-state="warn"] { border-left-color: var(--warn); }
.card[data-state="ok"] { border-left-color: var(--ok); }
/* Blue is "here is what is there", not a verdict: inventories, exports, the scan's own details, and
   base images that are merely listed. It reads as neutral information rather than a standing amber. */
.card[data-state="info"] { border-left-color: var(--info); }
/* "Nobody checked" must never look like "checked and clean" — the distinction this product exists to
   make. Dashed rather than solid: visibly not a verdict, at a glance, without reading a word. */
.card[data-state="unknown"] { border-left-color: var(--text-muted); border-left-style: dashed; }
/* This asked for `var(--muted)`, which is not a token — only the `.muted` class is — so the rail
   fell back to currentColor and drew near-black in light and near-white in dark. The dark override
   below was written to tame that; it was treating the symptom. The token is correct now, and the
   override stays because a dashed rail wants to recede against the danger rail beside it. */
[data-theme="dark"] .card[data-state="unknown"] { border-left-color: var(--border-strong); }

/* ── Instrument panel (.ip) — Mission Control and Findings ──────────────────────────────────────
   The shared design language of the operational pages. Drawn for dark first — deep canvas,
   translucent surfaces, monospaced numerals so digits line up down a column — and given light
   equivalents rather than left as a hole in the page for anyone on the light theme. Colour is data
   everywhere in this block: a KPI rail, a product ring and a finding row's edge all read from the
   same severity/health scale, which is what lets these pages be scanned rather than read.

   Every token is set once on `.ip` and inherited, so a new card joins the system by using them
   instead of inventing its own — and so a page joins it by wearing the one class. */
.ip {
  --ip-surface: var(--bg-surface);
  --ip-surface-hover: var(--bg-hover);
  --ip-border: var(--border);
  --ip-border-strong: var(--border-strong);
  --ip-track: var(--track);
  --ip-inset: var(--bg-surface-2);
  --ip-tone: var(--accent);
  --ip-mono: var(--font-mono);
  --ip-radius: var(--radius-lg);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}
/* Dark keeps the navy, but not the gradients: surfaces are flat in BOTH themes now, because the
   flatness is the design and not a property of the light palette. The translucent-white stack and
   the fixed radial canvas wash that used to live here are gone for the same reason. */
[data-theme="dark"] .ip {
  --ip-surface: var(--bg-surface);
  --ip-surface-hover: var(--bg-hover);
  --ip-border: rgba(255, 255, 255, 0.10);
  --ip-border-strong: rgba(255, 255, 255, 0.20);
  --ip-track: rgba(255, 255, 255, 0.07);
  --ip-inset: rgba(0, 0, 0, 0.22);
}
/* Nothing in here is underlined on hover. Half this page's links are whole cards, rows and tiles, and
   the global a:hover underline draws a line straight across them. The affordance is carried by the
   surface instead: the card lifts, the border brightens, the row's background fills. Text links in the
   card headers brighten rather than underline, for the same reason the rest of the page does not. */
.ip a:hover, .ip a:focus, .ip a:focus-visible { text-decoration: none; }
.ip-card-head a:hover { color: var(--accent-hover); }

.ip .tone-ok { --ip-tone: var(--ok); }
.ip .tone-warn { --ip-tone: var(--attention); }
.ip .tone-bad { --ip-tone: var(--danger); }
.ip .tone-info { --ip-tone: var(--accent); }
.ip .tone-muted { --ip-tone: var(--text-muted); }

/* Header: the page says what it is, then hands over the controls that change what it shows.

   The white band is one of the two composition moves that make this design what it is (the other is
   the toned gauge panel in the hero). `.ip-head` sits loose on the page background, so the band is
   made by bleeding out through `.content`'s own padding with negative margins — which means the two
   have to agree, and they are re-stated together at each breakpoint below.

   🔴 It reaches the edge of the CONTENT COLUMN, not the window, once the viewport is wider than
   `--content-max`. That is a deliberate limit rather than a vw-based trick, which breaks the moment
   the sidebar folds away. */
.ip-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-5); flex-wrap: wrap;
  background: var(--bg-surface); border-bottom: 1px solid var(--border);
  margin: calc(var(--sp-5) * -1) calc(var(--sp-6) * -1) var(--sp-4);
  padding: 26px var(--sp-6) 20px;
}
/* Capped so a long subtitle wraps instead of pushing the controls onto a row of their own, where
   they read as loose buttons rather than as this page's controls. */
/* Give room rather than fix a width: a hard max-width fits at exactly one window size, and at every
   other one the controls drop onto a line of their own. */
.ip-title { flex: 1 1 340px; max-width: 620px; min-width: 0; }
.ip-title h1 { margin: 0; font-size: 34px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.15; }
.ip-title p { margin: var(--sp-2) 0 0; color: var(--text-secondary); font-size: 18.5px; line-height: 1.55; max-width: 56ch; }
.ip-controls { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.ip-controls .input.select { padding-top: 9px; padding-bottom: 9px; border-radius: var(--radius); }

/* One badge shape for the whole dashboard: a dot that carries the colour, and words that carry the
   meaning. Status is never colour alone (UX-002 §7). */
.ip-pill {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: 36px; padding: 0 13px; border-radius: 999px; font-size: var(--fs-sm); font-weight: 700;
  color: var(--ip-tone); background: color-mix(in srgb, var(--ip-tone) 12%, var(--ip-surface));
  border: 2px solid var(--ip-tone);
  white-space: nowrap;
}
.ip-pill > i { width: 8px; height: 8px; border-radius: 50%; background: var(--ip-tone); flex: none; }

/* The header's refresh status: a quiet line of text beside the Refresh button, deliberately not a
   badge — the header keeps one loud element (the button). The dot carries the tone, the words carry
   the meaning (UX-002 §7). */
.ip-refresh-status {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-sm); font-weight: 600; color: var(--text-secondary); white-space: nowrap;
}
.ip-refresh-status > i { width: 8px; height: 8px; border-radius: 50%; background: var(--ip-tone); flex: none; }

/* KPI row */
.ip-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-3); }
.ip-kpi {
  position: relative; overflow: hidden; display: flex; flex-direction: column; gap: 3px;
  padding: 20px 22px; border-radius: var(--radius-md); box-shadow: var(--shadow-sm);
  background: var(--ip-surface); border: 1px solid var(--ip-border);
  text-decoration: none; color: inherit; transition: border-color .15s;
}
/* Paper does not lift on hover. The border darkening is the whole affordance. */
.ip-kpi:hover { border-color: var(--ip-border-strong); }
/* The rail glows in dark: on a near-black canvas a 3px bar is easy to miss, and the rail is the
   tile's only carrier of colour. Light gets the bar without the halo — a glow on a white surface
   reads as a printing smudge, not as emphasis. */
.ip-kpi-rail {
  position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
  background: var(--ip-tone);
}
/* The glow went with the gradients: on a flat surface a 5px bar carries itself. */
.ip-kpi-val {
  font-family: var(--ip-mono); font-size: 34px; font-weight: 700; letter-spacing: -0.02em;
  line-height: 1; color: var(--ip-tone);
}
.ip-kpi-label { font-size: 18px; font-weight: 700; color: var(--text-primary); }
.ip-kpi-hint { font-size: var(--fs-sm); color: var(--text-muted); }

/* Trust Score hero: the score, the verdict, what is dragging it down, and how it has moved. */
.ip-hero {
  display: grid; grid-template-columns: 250px minmax(0, 1fr); gap: 0;
  padding: 0; overflow: hidden; border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);
  background: var(--ip-surface); border: 1px solid var(--ip-border);
}
/* The card gives up its own padding so the divider can run edge to edge; the two columns carry
   theirs instead. Without that the dial floats on white and reads as an illustration rather than
   as an instrument. */
.ip-hero-gauge {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-3);
  background: var(--bg-surface-2); padding: var(--sp-5) 26px;
  border-right: 1px solid var(--ip-border);
}
/* 132px with a thicker ring, and the caption moved out from under the arc — see below. */
.ip-donut { position: relative; width: 132px; height: 132px; margin-bottom: 26px; }
.ip-donut svg { width: 132px; height: 132px; transform: rotate(-90deg); }
/* stroke-width is a presentation attribute, so CSS may override it — which means the ring can be
   redrawn from the stylesheet without touching the markup that emits the circles. */
.ip-donut svg circle, .ip-ring svg circle { stroke-width: 11; }
.ip-donut-mid {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px;
}
.ip-donut-score {
  font-family: var(--ip-mono); font-size: 34px; font-weight: 700; letter-spacing: -0.02em; line-height: 1;
}
.ip-donut-delta { font-family: var(--ip-mono); font-size: var(--fs-sm); font-weight: 700; min-height: 18px; }
.ip-donut-delta.up { color: var(--ok); }
.ip-donut-delta.down { color: var(--danger); }
/* Upper case carries the signal, so the eyebrow no longer has to shrink to 11px to say "label". */
.ip-eyebrow {
  font-family: inherit; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted);
  margin-top: var(--sp-1);
}
/* 🔴 The caption sits BELOW the dial, not inside it. It used to be squeezed to 10px with a 92px cap
   to clear the arc; that held at 170px, but the ring is 132px now and the inner chord below centre
   is narrower than the words, so the text cut straight through the stroke. Outside, it can be 14px
   and legible instead of a 10px apology. `.ip-donut-mid` is `inset: 0`, so `top: 100%` lands exactly
   on the dial's bottom edge, and the margin on `.ip-donut` reserves the room so it cannot sit on top
   of the status pill underneath. */
.ip-donut-mid .ip-eyebrow {
  position: absolute; top: calc(100% + 10px); left: 50%; transform: translateX(-50%);
  max-width: none; white-space: nowrap; margin: 0;
  font-size: var(--fs-xs); letter-spacing: 0.1em; line-height: 1;
}
.ip-hero-body { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; padding: var(--sp-5); }
.ip-hero-top {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap;
}
.ip-hero-top h2 { margin: 0; font-size: var(--fs-xl); font-weight: 700; letter-spacing: -0.018em; line-height: 1.2; }
.ip-hero-top p { margin: var(--sp-2) 0 0; color: var(--text-secondary); font-size: 18px; line-height: 1.65; max-width: 78ch; }
.ip-hero .btn-ai { white-space: nowrap; border-radius: 11px; }

.ip-affected { display: flex; flex-direction: column; }
.ip-affected .ip-eyebrow { margin-bottom: var(--sp-2); }
.ip-affected-row {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) 0; border-top: 1px solid var(--border-soft);
  font-size: var(--fs-md); color: var(--text-primary); line-height: 1.6;
}
/* Fixed-width severity chip: the labels line up into a column the eye can run down.
   🔴 104px, not 78px. The old width was measured against 11.5px text and cut "Critical" in half at
   15.5px — the box and the type have to move together, which is the shape of this whole rollout. */
.ip-sev {
  flex: none; width: 104px; min-height: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--fs-sm); font-weight: 700;
  padding: 0 var(--sp-3); border-radius: var(--radius-sm); white-space: nowrap;
  color: var(--sev); background: color-mix(in srgb, var(--sev) 12%, var(--ip-surface));
  border: 2px solid var(--sev);
}

/* Trend: an inset panel, so it reads as evidence under the verdict rather than a second headline. */
.ip-trend { padding: 20px; border-radius: var(--radius-md); background: var(--ip-inset); border: 1px solid var(--ip-border); }
.ip-trend-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  margin-bottom: var(--sp-2); font-size: 16px; color: var(--text-secondary);
}
.ip-delta {
  display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-sm); font-weight: 700;
  padding: var(--sp-1) var(--sp-3); border-radius: 999px; border: 2px solid currentColor;
}
.ip-delta > i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.ip-delta.up { color: var(--ok); background: color-mix(in srgb, var(--ok) 12%, transparent); }
.ip-delta.down { color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, transparent); }
.ip-delta.flat { color: var(--text-muted); background: var(--ip-track); }
.ip-trend-svg { width: 100%; height: 96px; display: block; }
.ip-trend-foot {
  display: flex; justify-content: space-between; margin-top: var(--sp-2);
  font-family: var(--ip-mono); font-size: 15px; color: var(--text-muted);
}

/* Per-product strip.
   🔴 240px, not 215px. The tile has to hold a 54px ring, a 13px gap and the product name at 18px —
   "WatchControl" alone needs 119px — plus 40px of padding, which is 226px before anything else.
   At 215px the grid dropped to two columns around a 450px viewport and squeezed the name past its
   box. The minimum is now derived from the widest thing the tile actually contains. */
.ip-products { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--sp-3); }
.ip-ptile-id { min-width: 0; }
.ip-ptile {
  display: flex; flex-direction: column; gap: var(--sp-3); padding: 20px;
  border-radius: var(--radius-md); box-shadow: var(--shadow-sm);
  background: var(--ip-surface); border: 1px solid var(--ip-border);
  text-decoration: none; color: inherit; transition: border-color .18s;
}
.ip-ptile:hover { border-color: var(--ip-border-strong); }
.ip-ptile-top { display: flex; align-items: center; gap: 13px; min-width: 0; }
.ip-ring { position: relative; width: 54px; height: 54px; flex: none; }
.ip-ring svg { width: 54px; height: 54px; transform: rotate(-90deg); }
.ip-ring > b {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-family: var(--ip-mono); font-size: 17px; font-weight: 700;
}
.ip-ptile-id { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ip-ptile-name { font-size: 18px; font-weight: 700; letter-spacing: -0.01em; }
.ip-ptile-status { font-size: 16px; color: var(--text-secondary); }
.ip-ptile-status.clean { color: var(--ok); font-weight: 700; }
.ip-ptile-foot { display: flex; align-items: center; gap: var(--sp-2); }
/* 10px with its own border, not a 4px hairline: at 4px a small share is a smudge you cannot see,
   and the share is the number the tile exists to show. */
.ip-bar { flex: 1; height: 10px; border-radius: 6px; background: var(--ip-track); border: 1px solid var(--ip-border); overflow: hidden; }
.ip-bar > i { display: block; height: 100%; border-radius: 6px; background: var(--ip-tone); }
.ip-ptile-share { font-family: var(--ip-mono); font-size: 15px; color: var(--text-muted); }

/* Cards: findings, assets, activity */
.ip-card {
  border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);
  background: var(--ip-surface); border: 1px solid var(--ip-border); overflow: hidden;
}
/* 🔴 The heading is a `> span`, not an `h2` — the markup is
   `<div class="ip-card-head"><span>Findings</span>`. An h2 selector here matches nothing and fails
   silently; it cost a full round of the lab to find. */
.ip-card-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  padding: 26px 26px var(--sp-3); font-size: 22px; font-weight: 700; letter-spacing: -0.015em;
  border-bottom: 1px solid var(--ip-border);
}
.ip-card-head > span { font-size: 22px; font-weight: 700; }
.ip-card-head a { font-size: 16.5px; font-weight: 700; }
/* 🔴 `min(400px, 100%)`, not a bare 400px. `minmax(400px, 1fr)` refuses to go below its minimum,
   so on any viewport narrower than 400px the column stayed 400px wide and pushed out of the page —
   measured at 390px and 320px. Wrapping the minimum in min() lets it collapse to the container. */
.ip-grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(400px, 100%), 1fr)); gap: var(--sp-3); }
.ip-card .empty-state, .ip-card .all-clear { margin: 0 24px 24px; }

/* The big severity chip: a count you can read across the room, and a filter you can press. It is
   deliberately NOT the small `.ip-chip` further down — that one is a label inside a row and is tinted
   by its severity at rest, which would leave every filter here looking permanently pressed. This one
   rests on the neutral inset and earns its colour only when it is on. */
.ip-fchips { display: flex; gap: var(--sp-2); padding: var(--sp-3) 26px; flex-wrap: wrap; }
.ip-fchip {
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: var(--hit); padding: 0 var(--sp-3); border-radius: var(--radius);
  background: var(--ip-surface); border: 2px solid var(--ip-border); cursor: pointer;
  text-decoration: none; color: inherit;
  transition: border-color .15s, background .15s;
}
.ip-fchip:hover { border-color: var(--ip-border-strong); }
.ip-fchip.on { background: color-mix(in srgb, var(--sev) 12%, var(--ip-surface)); border-color: currentColor; }
.ip-fchip > b {
  font-family: var(--ip-mono); font-size: var(--fs-lg); font-weight: 700; letter-spacing: -0.02em; color: var(--sev);
}
.ip-fchip > span { display: flex; align-items: center; gap: 7px; font-size: 16px; font-weight: 600; color: var(--text-secondary); }
.ip-fchip > span > i { width: 7px; height: 7px; border-radius: 50%; background: var(--sev); }

.ip-table { border-top: none; }
/* 🔴 The columns are wider than they were: `110px 130px 1fr 110px` was measured against 13px text
   and clips the severity chip at 15.5px. Box and type move together. */
.ip-tr {
  display: grid; grid-template-columns: 118px 150px minmax(0, 1fr) 108px; gap: 20px; align-items: start;
  padding: var(--sp-3) 26px; border-bottom: 1px solid var(--border-soft);
  font-size: var(--fs-md); text-decoration: none; color: inherit;
  box-shadow: inset 5px 0 0 var(--sev, transparent);
  transition: background .15s;
}
a.ip-tr:hover { background: var(--bg-surface-2); }
.ip-th {
  padding-top: 12px; padding-bottom: 12px; box-shadow: none; align-items: center;
  background: var(--bg-surface-2); border-bottom: 1px solid var(--ip-border);
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted);
}
.ip-tr > span:nth-child(3) { line-height: 1.55; text-wrap: pretty; }
.ip-dim { color: var(--text-secondary); }
.ip-status { display: flex; justify-content: flex-end; }
.ip-empty-row { padding: var(--sp-5) 26px; color: var(--text-muted); font-size: 16px; }

.ip-asset {
  display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) 26px;
  border-top: 1px solid var(--border-soft); text-decoration: none; color: inherit; transition: background .15s;
}
.ip-asset:hover { background: var(--bg-surface-2); }
.ip-tick { width: 5px; height: 34px; border-radius: 99px; background: var(--sev); flex: none; }
.ip-asset-id { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
/* An asset name is a technical identity — a domain, a repo, a host — and belongs in mono, where a
   full stop and a hyphen cannot be mistaken for one another. */
/* 🔴 Wraps, and does NOT ellipsis. In mono at 17px a long host no longer fits the column at 900px,
   and the old `text-overflow: ellipsis` answered that by hiding the end of it — but this string is
   an identity, and "mail.certcontrol.pro" cut to "mail.certcontr…" is indistinguishable from a
   different host with the same prefix. A row that grows a line is the cheaper mistake. Measured:
   ellipsis fired at 900px and 380px before this. */
.ip-asset-name {
  font-family: var(--ip-mono); font-size: 17px; font-weight: 700;
  overflow-wrap: anywhere;
}
.ip-asset-id .ip-dim { font-size: 16px; color: var(--text-muted); }
.ip-asset-count { width: 64px; text-align: right; font-family: var(--ip-mono); font-size: var(--fs-lg); font-weight: 700; }

.ip-activity { padding: 0 26px var(--sp-2); }
.ip-act {
  display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) 0;
  border-top: 1px solid var(--border-soft); font-size: 16px;
}
.ip-act-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--ip-tone); flex: none; box-shadow: none; }
.ip-act-title { font-size: 17px; font-weight: 700; }
.ip-act-id {
  font-family: var(--ip-mono); font-size: 15px; color: var(--text-secondary);
  padding: 2px var(--sp-2); border-radius: 6px; background: var(--bg-hover); border: 1px solid var(--ip-border);
}
.ip-act-time { margin-left: auto; color: var(--text-muted); font-size: 16px; white-space: nowrap; }

@media (max-width: 900px) {
  .ip-hero { grid-template-columns: 1fr; }
  .ip-hero-gauge { padding-right: 0; border-right: none; padding-bottom: var(--sp-4); border-bottom: 1px solid var(--ip-border); }
  .ip-tr { grid-template-columns: 92px 1fr; grid-auto-rows: min-content; row-gap: var(--sp-1); }
  .ip-tr > span:nth-child(2) { text-align: right; }
  .ip-tr > span:nth-child(3), .ip-status { grid-column: 1 / -1; }
  .ip-status { justify-content: flex-start; }
  .ip-th { display: none; }
  .ip-title h1 { font-size: 26px; }
}

/* ── Findings (technical) — the same instrument panel, arranged around triage ─────────────────── */
.ip-top { display: grid; grid-template-columns: minmax(300px, 360px) 1fr; gap: var(--sp-3); align-items: stretch; }
/* 🔴 A SECOND .ip-products declaration — the Findings page adds `align-content` — and being later
   it beats the one further up. The minimum has to be raised in both, or the fix is dead code that
   looks right in the diff. Found by measuring: the strip still overflowed at 768px after the first
   one was corrected. */
.ip-products { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--sp-3); align-content: start; }

/* Portfolio card: the whole account in one block, and the way back to "all products". */
.ip-portfolio {
  display: flex; flex-direction: column; gap: var(--sp-4); text-align: left; cursor: pointer;
  padding: 26px; border-radius: 22px; color: inherit; font: inherit;
  background: var(--ip-surface); border: 1px solid var(--ip-border); transition: border-color .18s;
}
[data-theme="dark"] .ip-portfolio {
  background: linear-gradient(160deg, rgba(70, 120, 220, 0.16) 0%, rgba(20, 28, 44, 0.9) 55%, rgba(14, 20, 32, 0.95) 100%);
  border-color: rgba(120, 160, 255, 0.16);
}
.ip-portfolio:hover, .ip-portfolio.on { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.ip-portfolio-top { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.ip-portfolio-top > div { display: flex; flex-direction: column; gap: 3px; }
.ip-portfolio-name { font-size: 18px; font-weight: 700; letter-spacing: -0.01em; }
.ip-eyebrow { font-family: var(--ip-mono); }
.ip-ring-lg { width: 78px; height: 78px; }
.ip-ring-lg svg { width: 78px; height: 78px; }
.ip-ring-lg > b { font-size: 22px; letter-spacing: -0.02em; }
.ip-portfolio-count { display: flex; align-items: baseline; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.ip-portfolio-count > b { font-family: var(--ip-mono); font-size: 32px; font-weight: 700; letter-spacing: -0.02em; }
.ip-portfolio-count > span { font-size: var(--fs-sm); color: var(--text-secondary); }
/* The severity mix: proportion, seen rather than read. */
.ip-mix { display: flex; height: 10px; border-radius: 99px; overflow: hidden; background: var(--ip-track); border: 1px solid var(--ip-border); }
.ip-mix > span { display: block; }
.ip-legend { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2) var(--sp-3); margin-top: var(--sp-3); }
.ip-legend-row { display: flex; align-items: center; gap: var(--sp-2); font-size: 15px; color: var(--text-secondary); }
.ip-legend-row > i { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.ip-legend-row > b { margin-left: auto; font-family: var(--ip-mono); color: var(--text-primary); }
.ip-portfolio-note {
  margin: auto 0 0; padding-top: var(--sp-3); border-top: 1px solid var(--ip-border);
  font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5;
}
.ip-portfolio-note b { color: var(--text-primary); }

/* Product tile in Findings: the same tile as Mission Control, plus the product's own icon and a
   pressed state — here it is a filter, not a link. */
.ip-ptile { text-align: left; cursor: pointer; font: inherit; color: inherit; }
.ip-ptile.on {
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
  background: linear-gradient(150deg, color-mix(in srgb, var(--accent) 14%, transparent), var(--ip-inset));
}
.ip-ptile-name { display: flex; align-items: center; gap: 7px; }
.ip-ico { font-size: 15px; line-height: 1; }

/* Filter bar */
.ip-filters {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-2); border-radius: 16px; background: var(--ip-surface); border: 1px solid var(--ip-border);
}
.ip-search { flex: 1 1 240px; display: flex; align-items: center; gap: var(--sp-2); padding: 0 var(--sp-3); color: var(--text-muted); }
.ip-search input {
  flex: 1; background: transparent; border: none; outline: none; padding: 11px 0;
  color: var(--text-primary); font-size: var(--fs-md); font-family: inherit;
}
.ip-filters .input.select { border-radius: var(--radius); padding-top: 9px; padding-bottom: 9px; }

/* Product group */
.ip-group + .ip-group, #pgroups + #qualityCard .ip-card { margin-top: var(--sp-3); }
#pgroups > .ip-card + .ip-card { margin-top: var(--sp-3); }
.ip-group-head {
  display: flex; align-items: center; gap: var(--sp-3); padding: 20px 24px;
  border-bottom: 1px solid var(--ip-border); flex-wrap: wrap;
}
.ip-badge {
  width: 38px; height: 38px; border-radius: 12px; flex: none; font-size: 18px;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 24%, transparent);
}
.ip-group-id { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.ip-group-name { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
.ip-group-id .ip-dim { font-size: var(--fs-xs); color: var(--text-muted); }
.ip-group-right { display: flex; align-items: center; gap: var(--sp-3); }
.ip-progress { display: flex; align-items: center; gap: 9px; }
.ip-progress .ip-bar { width: 96px; height: 5px; }
.ip-progress > b { font-family: var(--ip-mono); font-size: var(--fs-xs); color: var(--text-secondary); font-weight: 500; }

/* A rule row: what the problem is, how bad, how much of it, and how much is still open. */
.ip .rsec + .rsec { border-top: 1px solid var(--ip-border); }
.ip-row {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%;
  padding: 16px 24px; background: none; border: none; cursor: pointer;
  font: inherit; color: inherit; text-align: left; transition: background .15s;
}
.ip-row:hover { background: var(--ip-track); }
.ip-row[aria-expanded="true"] { background: color-mix(in srgb, var(--accent) 6%, transparent); }
.ip-tick { width: 3px; height: 26px; border-radius: 99px; background: var(--sev); flex: none; }
.ip .rsec-chev { font-size: 16px; color: var(--text-muted); transition: transform .18s; flex: none; }
.ip-row[aria-expanded="true"] .rsec-chev { transform: rotate(90deg); }
.ip-row-name { flex: 1; min-width: 0; font-size: 14.5px; font-weight: 500; letter-spacing: -0.005em; text-wrap: pretty; }
.ip-chips { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.ip-chip {
  display: flex; align-items: center; gap: 6px; white-space: nowrap;
  min-height: 32px; padding: 0 var(--sp-3); border-radius: var(--radius-sm);
  font-size: var(--fs-sm); font-weight: 700;
  color: var(--sev); background: color-mix(in srgb, var(--sev) 12%, var(--ip-surface));
  border: 2px solid var(--sev);
}
.ip-chip > b { font-family: var(--ip-mono); opacity: .75; }
.ip-mix-sm { width: 110px; height: 6px; flex: none; }
.ip-row-open { width: 72px; text-align: right; font-family: var(--ip-mono); font-size: var(--fs-xs); color: var(--text-muted); }
.ip .rsec-body { padding: 0 24px 20px; }
.ip .rsec-body .table-wrap { border: 1px solid var(--ip-border); border-radius: 14px; }
.ip-quality .table-wrap { margin: 0 24px 24px; border: 1px solid var(--ip-border); border-radius: 14px; }
.ip-quality p { margin: 0 24px var(--sp-3); }

@media (max-width: 1000px) {
  .ip-top { grid-template-columns: 1fr; }
  /* A rule row runs out of width first: its chips and mini-bar are the second telling of a count the
     row states anyway, so they go. Scoped to the row — chips elsewhere may be the only control on
     the card, and dropping those would take the page's filter with them. */
  .ip-row .ip-chips, .ip-mix-sm { display: none; }
}
/* A product whose rules are one-finding-each renders a flat table instead of sections — it gets the
   card's padding so it does not run edge to edge while its sectioned neighbours are inset. */
.ip-group > .table-wrap { margin: var(--sp-3) 24px 24px; border: 1px solid var(--ip-border); border-radius: 14px; }
.ip-pill > b { font-family: var(--ip-mono); }

/* ── Assets, Reports and Activity on the same panel ───────────────────────────────────────────── */
/* Body copy and tables that sit directly in a card get the card's padding — a table running edge to
   edge under a padded heading reads as a rendering fault, not as a design. */
.ip-card-line { margin: calc(-1 * var(--sp-2)) 24px var(--sp-3); font-size: var(--fs-sm); color: var(--text-secondary); }
/* 🔴 The −8px was closing a gap that stopped existing when the card head grew a border, and it
   pulled the first sentence ON TO the rule — visible on MailControl's "DNS setup guide" and "Who is
   sending as you". Same trap as .ip-card-body one screen up; both are fixed the same way, by
   restoring the space the negative margin eats. Two lines in a row cannot collide either. */
.ip-card-head + .ip-card-line { margin-top: var(--sp-3); }
.ip-card-line + .ip-card-line { margin-top: 0; }
.ip-card > .table-wrap, .ip-card > div > .table-wrap {
  margin: 0 24px 24px; border: 1px solid var(--ip-border); border-radius: 14px;
}
.ip-card > div > .row-count, .ip-card > .row-count { padding: var(--sp-3) 24px 0; }
.ip-card .empty-state { padding: var(--sp-5) var(--sp-4); }
/* A card that is a warning keeps the shape and states why in colour. */
.ip-alert { border-color: color-mix(in srgb, var(--attention) 40%, transparent); }
.ip-alert .ip-card-head { color: var(--attention); }

/* Reports: domain health under the score, one bar per domain. */
.ip-domains { display: flex; flex-direction: column; gap: var(--sp-2); }
.ip-domains .ip-eyebrow { margin-bottom: var(--sp-1); }
.ip-domain-row { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-sm); }
.ip-domain-row > span:first-child { flex: 0 0 150px; color: var(--text-secondary); }
.ip-domain-row .ip-bar { flex: 1; height: 6px; }
.ip-domain-row > b { width: 34px; text-align: right; font-family: var(--ip-mono); font-weight: 500; }
.ip-affected-row { text-decoration: none; color: inherit; }
.ip-open-report { font-size: var(--fs-sm); font-weight: 500; color: var(--accent-text); }

/* Activity: the log rows live inside the card, so they carry its padding. */
.ip-card #activityLog > .row-count { padding: var(--sp-3) 24px var(--sp-1); }
.ip-card #activityLog .day-sep { padding: var(--sp-3) 24px var(--sp-1); font-weight: 600; }
.ip-card #activityLog .kv { padding-left: 24px; padding-right: 24px; }
.ip-card #activityLog .empty-state { padding: var(--sp-5) var(--sp-4); }
/* Keep the AI button on the headline's line: the paragraph takes the slack, the button never wraps
   onto a line of its own where it reads as a stray control. */
.ip-hero-top > div:first-child { flex: 1 1 380px; min-width: 0; }
.ip-hero-top .btn-ai { flex: 0 0 auto; }
.ip-kpi-val.sm { font-size: var(--fs-lg); letter-spacing: -0.02em; }

/* ── The seven product pages on the same panel ─────────────────────────────────────────────────
   A product page is Mission Control scoped to one product: the same header, the same hero with the
   product's own health score, the same KPI rail, and then that product's inventory in panel cards.
   Nothing here is a new shape — it is the existing language given the few pieces a product page
   needs that a dashboard does not (a section tab strip, a filter bar, its own evidence slot). */

/* The section tab strip (CertControl's sub-pages, WatchControl's, BrandControl's). It sits under the
   header as a row of surfaces rather than the app's underlined tabs: an underline reads as a border
   fragment against these cards, and this block has no other underlines in it. */
.ip .tabs { border-bottom: none; gap: var(--sp-2); margin: 0; flex-wrap: wrap; }
/* Level 2: which PAGE inside the group. A bordered pill, on purpose not the filled bar used by the
   Simple/Technical toggle — see the note there. */
.ip .tab {
  display: inline-flex; align-items: center; min-height: var(--hit-sm);
  padding: 0 18px; border-radius: var(--radius); border-bottom: none; font-size: 16px; font-weight: 600;
  background: var(--ip-surface); border: 2px solid var(--ip-border); color: var(--text-secondary);
  transition: border-color .15s, color .15s, background .15s;
}
.ip .tab:hover { border-color: var(--ip-border-strong); color: var(--text-strong); }
.ip .tab.active {
  color: var(--accent-text); border-color: var(--accent-border);
  background: var(--accent-soft);
}

/* The hero's evidence slot: the product's open findings, by severity, each chip a way into the list
   filtered to exactly that. Severities with nothing in them are absent rather than shown as zero. */
.ip-sevchips { display: flex; flex-direction: column; gap: var(--sp-2); }
.ip-sevchips .ip-fchips { padding: 0; }
.ip-hero-note { margin: 0; font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.6; }
.ip-hero .all-clear { margin: 0; }

/* Body content that sits directly in a card gets the card's padding — the same rule as the tables. */
.ip-card-body { padding: 0 24px 24px; }
.ip-card > .ip-card-body:first-child { padding-top: 24px; }
/* 🔴 A card that has a titled head starts its body flush against that head's border — and the
   setup cards write `margin-top: calc(-1 * var(--sp-2))` on their first paragraph to close a gap
   that used to exist. Once the head grew a border, that pull put the sentence ON the rule: look at
   the Azure DevOps card before this line. The inline margin cannot be overridden from a stylesheet,
   so the padding absorbs it instead — 24px of it, leaving the 16px the paragraph was after. */
.ip-card-head + .ip-card-body { padding-top: var(--sp-4); }

/* An inventory's own filter bar, in the filter bar's shape from Findings. */
.ip .filter-bar {
  padding: var(--sp-2); border-radius: 16px; border: 1px solid var(--ip-border);
  background: var(--ip-surface); margin: 0; gap: var(--sp-2);
}
.ip .filter-bar .search-box { border: none; background: transparent; flex: 1 1 240px; }
.ip .filter-bar .input.select { border-radius: var(--radius); padding-top: 9px; padding-bottom: 9px; }

/* A product page's action buttons live in the header, next to the view toggle. */
.ip-controls .action-menu .menu-pop { right: 0; }
/* Long product names keep the header on one line; the subtitle carries the explanation. */
.ip-title h1 .ip-ico { font-size: 26px; margin-right: 10px; }

/* ── Detail pages ──────────────────────────────────────────────────────────────────────────────
   A finding, an asset, a certificate: one thing, explained. This is where the work is actually
   done, and it is shaped differently from the pages that got you here. There is no dial — a single
   finding has no score — so the verdict is carried by the headline and the chips beside it, and
   everything below is a stack of titled sections, each answering one question.

   The rule the shape depends on: a section with nothing to say is absent, not empty. Rules differ in
   how much we know about them, and an empty "How to fix it" card would read as a gap in the product
   rather than as a gap in that rule's guidance. */
.ip-detail { gap: var(--sp-3); }
.ip-detail-head { display: flex; flex-direction: column; gap: var(--sp-3); }
.ip-detail-head .breadcrumb { margin: 0; }
.ip-detail-title { display: flex; flex-direction: column; gap: var(--sp-2); max-width: 900px; }
.ip-detail-title h1 { margin: 0; font-size: 30px; font-weight: 600; letter-spacing: -0.022em; line-height: 1.15; }
.ip-detail-title p { margin: 0; font-size: 14.5px; color: var(--text-secondary); line-height: 1.6; text-wrap: pretty; }

/* The chip row: how bad, where it stands, and what kind of thing it is — before any prose. */
.ip-chiprow { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.ip-tag {
  font-size: 12.5px; font-weight: 600; padding: 5px 12px; border-radius: 999px; white-space: nowrap;
  color: var(--sev, var(--text-secondary));
  background: color-mix(in srgb, var(--sev, var(--text-muted)) 13%, transparent);
  border: 1px solid color-mix(in srgb, var(--sev, var(--text-muted)) 28%, transparent);
}
/* Severity is the filled chip; status and category sit beside it unfilled. Three filled chips in a
   row read as three findings of equal weight — the eye has to be told which one is the severity. */
.ip-tag.subtle { background: transparent; font-weight: 500; }

/* The action bar. `Mark as resolved` is tinted rather than solid: it changes the state of the thing
   you are reading, and a solid button at the top of a page of explanation reads as "submit". */
.ip-actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.ip-actions .btn {
  padding: 10px 16px; border-radius: 11px; font-size: var(--fs-sm); font-weight: 500;
  border: 1px solid var(--ip-border-strong); background: var(--ip-track); color: var(--text-primary);
}
.ip-actions .btn:hover { background: var(--ip-surface-hover); border-color: var(--ip-border-strong); }
.ip-actions .btn-primary {
  padding: 10px 20px; font-weight: 600;
  color: var(--accent-text); background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-color: color-mix(in srgb, var(--accent) 32%, transparent);
}
.ip-actions .btn-primary:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
.ip-actions .btn-ghost {
  padding: 0; border: none; background: none; font-size: 13px; font-weight: 400; color: var(--text-secondary);
}
.ip-actions .btn-ghost:hover { color: var(--text-secondary); background: none; }

/* A section: title, then whatever answers it. */
.ip-sec {
  display: flex; flex-direction: column; gap: 10px; padding: 22px 24px;
  border-radius: var(--ip-radius); background: var(--ip-surface); border: 1px solid var(--ip-border);
}
.ip-sec > h2 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
.ip-sec > h2 > span { font-weight: 400; color: var(--text-muted); }
.ip-prose { margin: 0; font-size: 14px; color: var(--text-secondary); line-height: 1.65; text-wrap: pretty; }
.ip-prose.lead { color: var(--text-primary); line-height: 1.6; }
.ip-prose b, .ip-prose strong { color: var(--text-primary); font-weight: 600; }
/* A hint is a whole sentence — "Low: minor issue, fix when convenient", "Most likely first" — and it
   was set at label size in muted grey because the class started life as a sub-line under a table row.
   On a detail page that is text somebody has to read, and 12.5px muted was under the contrast floor
   for body copy, so it now reads as prose one step below `.ip-prose`. The two places where it really
   is a label — a monospaced identity, a second line inside a table row — keep the smaller setting. */
.ip-hint { font-size: 13.5px; color: var(--text-secondary); line-height: 1.6; }
.ip-hint.mono { font-size: 12.5px; color: var(--text-muted); }
.ip-sec .mono { font-family: var(--ip-mono); }
/* A button inside a section is a secondary route out of it ("View asset"), never the page's action —
   it takes the same quiet treatment as the action bar's secondary buttons. */
.ip-sec .btn {
  padding: 9px 16px; border-radius: 10px; font-size: 12.5px; font-weight: 500;
  border: 1px solid var(--ip-border-strong); background: var(--ip-track); color: var(--text-primary);
}
.ip-sec .btn:hover { background: var(--ip-surface-hover); }

/* The wide/narrow pair: the explanation next to the fact it applies to. */
.ip-duo { display: grid; grid-template-columns: 1.6fr 1fr; gap: var(--sp-3); align-items: stretch; }

/* A cause, an affected page, an evidence sample: a dotted row that can hold a full sentence. */
.ip-list { display: flex; flex-direction: column; }
.ip-list-row {
  display: flex; align-items: flex-start; gap: var(--sp-3); padding: 9px 0;
  border-top: 1px solid var(--ip-border); font-size: 13.5px; line-height: 1.6;
  color: var(--text-secondary); text-wrap: pretty; overflow-wrap: anywhere;
}
.ip-list-row > i {
  width: 6px; height: 6px; border-radius: 99px; background: var(--sev, var(--accent));
  margin-top: 7px; flex: none;
}
.ip-list-row .mono { font-size: 13px; }
.ip-list-row > .grow { flex: 1; min-width: 0; }
.ip-list-scroll { max-height: 340px; overflow-y: auto; }
/* An evidence sample is several pieces on one line — a URL, a status badge, an open link — and the
   same markup serves the expanded table row, so the row lays them out rather than the sample. */
.ip-sample { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.ip-sample .sp-url { flex: 1 1 240px; min-width: 0; overflow-wrap: anywhere; }
/* A length offender carries two lines: the page, then the text being measured. */
.ip-sample .len-head { display: flex; align-items: center; gap: var(--sp-2); width: 100%; flex-wrap: wrap; }
.ip-sample .len-head > a { flex: 1 1 240px; min-width: 0; overflow-wrap: anywhere; }
.ip-sample .len-text { width: 100%; color: var(--text-primary); }

/* Code and evidence keep the monospaced face and sit on the inset surface, so a block of code reads
   as quoted material rather than as another card. */
.ip-code {
  margin: 0; padding: var(--sp-3) var(--sp-4); border-radius: 12px;
  background: var(--ip-inset); border: 1px solid var(--ip-border); overflow-x: auto;
}
.ip-code > code { font-family: var(--ip-mono); font-size: 13px; white-space: pre; color: var(--text-primary); }

/* The raw facts, last: what the machine knows, for anyone who needs to quote it. */
.ip-kv { display: flex; align-items: baseline; gap: var(--sp-3); padding: 9px 0; border-top: 1px solid var(--ip-border); font-size: 13.5px; }
.ip-kv > span:first-child { flex: 0 0 168px; color: var(--text-muted); }
.ip-kv > span:last-child { flex: 1; min-width: 0; overflow-wrap: anywhere; color: var(--text-secondary); }

/* The timeline: what happened to this finding, and who did it. */
.ip-time { display: flex; align-items: baseline; gap: var(--sp-3); padding: 10px 0; border-top: 1px solid var(--ip-border); font-size: 13.5px; flex-wrap: wrap; }
.ip-time > b { font-weight: 500; color: var(--text-primary); }
.ip-time-note { color: var(--text-secondary); font-style: italic; }
.ip-time-when { margin-left: auto; color: var(--text-muted); font-size: var(--fs-xs); white-space: nowrap; }

/* An identity — a URL, a domain, a repository — is set in the monospaced face at title size. It is a
   string that has to be read character by character, not a name; the face says so before anyone has
   read it, and it is the same reasoning that puts every number on this panel in mono. */
.ip-detail-title h1.ip-ident,
.ip-title h1.ip-ident { font-family: var(--ip-mono); font-size: 30px; letter-spacing: -0.02em; overflow-wrap: anywhere; }
.ip-ident-sub { font-family: var(--ip-mono); font-size: 13.5px; color: var(--text-muted); overflow-wrap: anywhere; }

/* A chip carrying a fact about the thing: a dot, then the fact. The dot is filled when the fact is
   affirmative (this IS a website, ownership IS verified) and hollow-grey when it is merely a state. */
.ip-tag.dot { display: inline-flex; align-items: center; gap: 7px; }
.ip-tag.dot > i { width: 6px; height: 6px; border-radius: 99px; background: var(--sev, var(--text-muted)); flex: none; }
/* A chip in a grid cell hugs its text. Grid stretches its items by default, which turns a two-word
   chip into a 150px slab and makes the column look like a button rather than a label. */
.ip-tr > .ip-tag, .ip-rel > .ip-tag { justify-self: start; }

/* Detail-page tabs are underlined, not pills. The pills further up are section navigation between
   pages; these switch what this ONE page is showing, and the underline keeps them subordinate to the
   title above them rather than competing with it. */
/* The border falls back to the global one: this strip is shared by a website's four sub-pages, and
   three of them have not moved onto the panel yet. Without the fallback `--ip-border` is undefined
   outside `.ip` and the strip loses its rule entirely. */
/* Level 1 of three: which GROUP of pages you are in. Underlined tabs, because they sit directly
   under the page title and read as part of it. Level 2 is the bordered pill below; level 3 is the
   step row inside a page. Three levels, three different shapes — that is the point. */
.ip-tabs { display: flex; gap: 26px; border-bottom: 1px solid var(--ip-border, var(--border)); flex-wrap: wrap; }
.ip-tabs > a:hover { text-decoration: none; }
.ip-tabs > a {
  display: flex; align-items: center; min-height: 46px;
  padding: 0 2px; margin-bottom: -1px; font-size: 16px; font-weight: 600; letter-spacing: 0.01em;
  color: var(--text-secondary); border-bottom: 3px solid transparent; white-space: nowrap;
}
.ip-tabs > a:hover { color: var(--text-strong); }
.ip-tabs > a.active { color: var(--text-strong); border-bottom-color: var(--accent); }

/* A data table whose columns the caller sizes: `style="--ip-cols: 130px 100px 1fr"`. The alternative
   is a new modifier class per table, and there is one table per page that needs one. */
.ip-table.cols .ip-tr { grid-template-columns: var(--ip-cols); }
/* A measured column — a count, a duration, a byte size — is monospaced and right-aligned, so the
   digits line up down the column and two numbers can be compared without reading them. */
.ip-tr > .num { text-align: right; font-family: var(--ip-mono); font-size: var(--fs-sm); }
.ip-th > .num { font-family: inherit; font-size: 11px; }
.ip-tr > .num.dim { color: var(--text-muted); }
/* A row whose middle column runs several lines deep: the chip and the category must sit level with
   the finding's first line, not float in the middle of a paragraph they do not belong to. */
.ip-table.top .ip-tr { align-items: start; }
.ip-table.top .ip-tr > span { padding-top: 1px; }
.ip-tags { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.ip-mono { font-family: var(--ip-mono); overflow-wrap: anywhere; }
.ip-tr > b { font-weight: 600; }
.ip-tr .ip-hint { display: block; margin-top: 3px; font-size: 12.5px; color: var(--text-muted); }
.ip-count { font-size: var(--fs-xs); color: var(--text-secondary); padding: 5px 12px; border-radius: 999px; background: var(--ip-track); }
.ip-tr.clickable { cursor: pointer; }
.ip-tr.clickable:hover { background: var(--ip-track); }
/* Evidence under a finding: the exact strings the scanner saw, set apart by a rule rather than a box
   so they read as a citation and not as a second finding. */
.ip-ev { display: flex; flex-direction: column; gap: 1px; margin-top: 7px; padding-left: 10px; border-left: 1px solid var(--ip-border); }
.ip-ev > span { font-family: var(--ip-mono); font-size: 13px; color: var(--text-secondary); overflow-wrap: anywhere; }
.ip-ev > span.more { font-family: inherit; color: var(--text-muted); }

/* ── Legacy markup adopted into the panel ─────────────────────────────────
   Roughly thirty pages predate the design language and still emit `.card`, `.stat-grid`, `.pgroup`
   and plain `<table class="data">`. Rewriting each one's internals would be thirty chances to break
   a working page for no visible gain: the panel restyles them where they stand, so they inherit the
   surface, the radius and the rhythm without touching their logic. Same elements, panel clothes. */
.ip .card {
  background: var(--ip-surface); border: 1px solid var(--ip-border);
  border-radius: var(--ip-radius); box-shadow: none; padding: 22px 24px;
}
.ip .card + .card { margin-top: 0; }          /* the panel's own gap already separates them */
.ip .card-title { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
.ip .stat-grid, .ip .kpi-grid { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); margin-bottom: 0; }
.ip .pgroup { padding: 0; margin-bottom: 0; }
.ip .pgroup-head { padding: 20px 24px; border-bottom-color: var(--ip-border); }
.ip .pgroup-head .pg-ico { border-radius: 11px; }
.ip .tabs.product-tabs { margin-bottom: 0; }
.ip table.data th, .ip table.data td { border-bottom-color: var(--ip-border); }
.ip table.data tbody tr:hover { background: var(--ip-track); }
.ip table.data tbody tr:last-child > td { border-bottom: 0; }
.ip .filter-bar, .ip .toolbar { margin-bottom: 0; }
/* A card whose whole body is a table: the title needs its inset, the table does not. */
.ip .card > .table-wrap { margin: 0 -24px -22px; }
.ip .card > .table-wrap th:first-child, .ip .card > .table-wrap td:first-child { padding-left: 24px; }
.ip .card > .table-wrap th:last-child, .ip .card > .table-wrap td:last-child { padding-right: 24px; }
.ip .pgroup > .table-wrap { margin: 0; }
/* The count-plus-meter that rides in a group's right slot, which used to live under `.pgroup-head`. */
.ip-group-right .pg-health { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); color: var(--text-secondary); }
.ip-group-right .pg-health b { color: var(--text-primary); font-variant-numeric: tabular-nums; }
.ip-group-right .pg-health .meter { display: inline-block; vertical-align: middle; }
/* A ghost button in a table row has no container to sit in, so it needs its own edge — without one
   it reads as bold text and nobody presses it. */
.ip table.data .btn { height: 30px; padding: 0 12px; font-size: var(--fs-xs); }
.ip table.data .btn-ghost { border-color: var(--ip-border); color: var(--text-secondary); }
.ip table.data .btn-ghost:hover { background: var(--ip-track); border-color: var(--ip-border-strong); color: var(--text-primary); }
.ip .pgroup .table-wrap th:first-child, .ip .pgroup .table-wrap td:first-child { padding-left: 24px; }
.ip .pgroup .table-wrap th:last-child, .ip .pgroup .table-wrap td:last-child { padding-right: 24px; }

/* A five-column row for the finding lists that live inside a detail page. */
/* The status column is sized to the widest chip it can hold — "Acknowledged" with its dot measures
   130px — so the chip stays under its own header instead of leaning into the date beside it. */
.ip-table.cols5 .ip-tr { grid-template-columns: 100px 150px 1fr 136px 92px; }
.ip-table.cols5 .ip-tr > span:nth-child(3) { padding-top: 1px; }
.ip-table.cols5 .ip-tr > span:last-child { text-align: right; color: var(--text-muted); font-size: var(--fs-xs); white-space: nowrap; }

/* Relationship rows: what the link is, what it points at, and which product recorded it. */
.ip-rel { display: grid; grid-template-columns: 150px 1fr 140px; gap: var(--sp-3); align-items: center; padding: 13px 0; border-top: 1px solid var(--ip-border); }
.ip-rel > a { font-size: 13.5px; font-family: var(--ip-mono); overflow-wrap: anywhere; }
.ip-rel > span:last-child { text-align: right; font-size: 12.5px; color: var(--text-muted); }

@media (max-width: 900px) {
  .ip-duo { grid-template-columns: 1fr; }
  .ip-detail-title h1 { font-size: 24px; }
  .ip-detail-title h1.ip-ident { font-size: 20px; }
  .ip-kv { flex-direction: column; gap: 2px; }
  .ip-kv > span:first-child { flex: none; }
  .ip-rel { grid-template-columns: 1fr; gap: var(--sp-1); }
  .ip-rel > span:last-child { text-align: left; }
  .ip-table.cols5 .ip-tr { grid-template-columns: 92px 1fr; }
  .ip-table.cols5 .ip-tr > span:nth-child(n+3) { grid-column: 1 / -1; }
  .ip-table.cols5 .ip-tr > span:last-child { text-align: left; }
}

/* ── The narrowest phones ───────────────────────────────────────────────────────────────────
   🔴 These live at the END of the file on purpose. A media query adds no specificity, so an
   override only wins by coming later — and the first version of this block sat in the middle of
   the stylesheet, where the Simple view's own rules 236 lines further down quietly beat it. The
   symptom was a card that kept overflowing by 13px with a fix that looked correct in the diff.

   What actually breaks at this width is never the type; it is hard pixel minimums written for a
   desktop row. There are three: a button's side padding, the verdict card's 260px text column,
   and the top bar's row of 44px targets. */
@media (max-width: 480px) {
  /* An asset row is a tick, an identity, a severity chip and a count. The chip is 104px and the
     count 64px and neither can shrink, so below this width the identity was left 40px — measured.
     The chip and the count drop to their own line instead. */
  .ip-asset { flex-wrap: wrap; }
  .ip-asset-id { flex: 1 1 100%; }
}
@media (max-width: 420px) {
  .btn { padding: 0 var(--sp-3); }
  .sv-verdict-text { min-width: 0; }
  .topbar { gap: var(--sp-2); }
  /* The 110px cap from the 900px rule stays down to here. Tightening it further only bought room
     that was not needed until 360px, and it cost the chip its meaning — "Obarzanek ApS" truncated
     to "O…" tells the reader nothing at all, which is worse than a slightly wide chip. */
}
@media (max-width: 360px) {
  /* The tenant chip gives way last: it is orientation, and the full name is in the drawer. Better
     that than shrinking every hit area back under the 44px floor. */
  .tenant-selector { display: none; }
}

/* ── The narrow desktop (901–1080px) ────────────────────────────────────────────────────────
   The window is wide enough that the rail is still a rail, but the bar has only what is left
   beside it — and at 901px that is 641px for a tenant chip, a search field, three icon buttons
   and an avatar. The keyboard hint is the first thing to go: it is a reminder of a shortcut for
   people who already know the shortcut, and it costs 52px that the search field's own label
   needs. Measured at 901, 930, 950 and 960; below 901 the rail becomes a drawer and the bar gets
   the whole window back. */
@media (min-width: 901px) and (max-width: 1080px) {
  .global-search kbd { display: none; }
}
