/* Perpendis design tokens — cloud-console idiom (GCP/AWS-like):
   dense but airy, thin borders, one accent, semantic status colors.
   Every page imports this; no page redefines a token. */

:root {
  /* Tell the browser which native rendering (scrollbars, select popups,
     autofill, selection) matches the page. `light dark` follows the OS here;
     the explicit-choice blocks below pin it so a stored toggle that disagrees
     with the OS does not leave light widgets on a dark page, or vice versa. */
  color-scheme: light dark;

  --bg: #f8f9fa;
  --surface: #ffffff;
  --surface-2: #f1f3f4;
  --border: #dadce0;
  --ink: #202124;
  --sub: #5f6368;

  /* Light-theme inks are darkened past the Google-console defaults so that
     11-14px text clears WCAG 4.5:1 on the surfaces it actually sits on. The
     LIVE / Sealed chips are honesty markers (ARCHITECTURE §6) and must not be
     the least legible text on the page. Measured: #0b57d0 on --blue-bg 5.57:1,
     on --bg 6.06:1, on --surface-2 5.74:1; #0d652d on --ok-bg 6.34:1;
     #c5221f on --err-bg 4.92:1; #b05a00 on --warn-bg 4.55:1. */
  --blue: #0b57d0;
  --blue-bg: #e8f0fe;
  --blue-ink: #0b57d0;

  --ok: #0d652d;
  --ok-bg: #e6f4ea;
  --warn: #b05a00;
  --warn-bg: #fef7e0;
  --err: #c5221f;
  --err-bg: #fce8e6;

  --on-accent: #ffffff;
  --shadow: 0 1px 2px rgba(60, 64, 67, .1), 0 1px 3px rgba(60, 64, 67, .08);
  --mono: ui-monospace, "Cascadia Code", Consolas, Menlo, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --radius: 8px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #202124; --surface: #292a2d; --surface-2: #35363a; --border: #3c4043;
    --ink: #e8eaed; --sub: #9aa0a6;
    --blue: #8ab4f8; --blue-bg: #1f3352; --blue-ink: #8ab4f8;
    --ok: #81c995; --ok-bg: #1e3325; --warn: #fdd663; --warn-bg: #33301a;
    --err: #f28b82; --err-bg: #3c2422;
    --on-accent: #202124;
    --shadow: 0 1px 2px rgba(0,0,0,.3), 0 1px 3px rgba(0,0,0,.25);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #202124; --surface: #292a2d; --surface-2: #35363a; --border: #3c4043;
  --ink: #e8eaed; --sub: #9aa0a6;
  --blue: #8ab4f8; --blue-bg: #1f3352; --blue-ink: #8ab4f8;
  --ok: #81c995; --ok-bg: #1e3325; --warn: #fdd663; --warn-bg: #33301a;
  --err: #f28b82; --err-bg: #3c2422;
  --on-accent: #202124;
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #f8f9fa; --surface: #ffffff; --surface-2: #f1f3f4; --border: #dadce0;
  --ink: #202124; --sub: #5f6368;
  /* Keep in step with the :root defaults above — same measured ratios. */
  --blue: #0b57d0; --blue-bg: #e8f0fe; --blue-ink: #0b57d0;
  --ok: #0d652d; --ok-bg: #e6f4ea; --warn: #b05a00; --warn-bg: #fef7e0;
  --err: #c5221f; --err-bg: #fce8e6;
  --on-accent: #ffffff;
}

* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 13px/1.55 var(--sans);
}

/* --- app bar --- */
.appbar {
  height: 52px; background: var(--surface); border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: 14px; padding: 0 20px;
  position: sticky; top: 0; z-index: 5;
}
.brand { font-size: 16px; font-weight: 500; text-decoration: none; color: var(--ink); }
.brand span { color: var(--blue); }
.brand small { color: var(--sub); font-weight: 400; margin-left: 8px; font-size: 12px; }
.appbar .right { margin-left: auto; display: flex; gap: 8px; align-items: center; }

/* --- chips --- */
.chip {
  display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 500;
  border-radius: 12px; padding: 2px 10px; white-space: nowrap;
}
.chip.ok { background: var(--ok-bg); color: var(--ok); }
.chip.warn { background: var(--warn-bg); color: var(--warn); }
.chip.err { background: var(--err-bg); color: var(--err); }
.chip.blue { background: var(--blue-bg); color: var(--blue-ink); }
.chip.gray { background: var(--surface-2); color: var(--sub); }
.chip .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

/* --- buttons --- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  border: 0; border-radius: 4px; padding: 8px 20px;
  font: 500 13px var(--sans); cursor: pointer;
  background: var(--blue); color: var(--on-accent); text-decoration: none;
}
.btn:hover { filter: brightness(.95); }
.btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.btn.text { background: transparent; color: var(--blue-ink); padding: 8px 12px; }
.btn.outline { background: transparent; color: var(--blue-ink); border: 1px solid var(--border); }
.btn.danger { background: var(--err); }
.btn[disabled] { opacity: .4; cursor: default; pointer-events: none; }

/* --- layout --- */
.shell { display: grid; grid-template-columns: 216px 1fr; min-height: calc(100vh - 52px); }
.nav { background: var(--surface); border-right: 1px solid var(--border); padding: 12px 0; }
.nav .grp { font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--sub); padding: 14px 20px 6px; }
/* `min-width: 0` + `overflow-wrap: anywhere`: a system name with no space in it
   (db.js accepts 120 characters) used to lay out at its min-content width and
   paint straight across the 1fr content column — measured 481px of link text
   outside a 216px sidebar at 1280 and 1920. It now wraps inside the sidebar. */
.nav a { display: block; padding: 7px 20px; color: var(--ink); text-decoration: none;
  font-size: 13px; border-left: 3px solid transparent;
  min-width: 0; overflow-wrap: anywhere; }
.nav a:hover { background: var(--surface-2); }
.nav a.on { background: var(--blue-bg); color: var(--blue-ink);
  border-left-color: var(--blue); font-weight: 500; }
.content { padding: 24px 32px 64px; max-width: 1060px; }

/* Below the sidebar breakpoint the nav becomes a horizontal strip under the app
   bar. It used to be display:none with no replacement, which hid the only link
   back to the current evidence pack on every tablet and phone. */
@media (max-width: 820px) {
  .shell { grid-template-columns: 1fr; }
  .nav {
    border-right: 0; border-bottom: 1px solid var(--border); padding: 6px 8px;
    display: flex; align-items: center; gap: 4px; overflow-x: auto;
    -webkit-overflow-scrolling: touch; scrollbar-width: thin;
  }
  .nav .grp { padding: 0 6px 0 10px; white-space: nowrap; flex-shrink: 0; }
  .nav .grp:first-child { padding-left: 4px; }
  .nav a {
    padding: 6px 10px; border-left: 0; border-radius: 4px; white-space: nowrap;
    flex-shrink: 0;
  }
  .nav a.on { border-left: 0; }
  .content { padding: 20px 16px 56px; }
}

.crumb { font-size: 12px; color: var(--sub); margin-bottom: 6px; }
.crumb b { color: var(--ink); font-weight: 500; }
h1 { font-size: 20px; font-weight: 400; margin: 0 0 4px; text-wrap: balance; }

/* User-supplied strings (system name, base model, filenames) land in headings,
   breadcrumbs and table cells. Without this an unbroken 120-character name sets
   the min-content width of its container and the WHOLE PAGE scrolls sideways —
   measured scrollWidth 1015 against clientWidth 360 on the console and on the
   unauthenticated share page. Break inside the word rather than overflow. */
h1, h2, h3, .crumb, .crumb b, td, th { min-width: 0; overflow-wrap: anywhere; }

/* The same strings are echoed into running prose — most sharply in the
   delete-confirm copy, where the system name appears in <b> three times. Two of
   those measured 918 px and 956 px inside a 328 px card at 360 px wide and set
   the document's scrollWidth to 989 on their own, after the heading itself had
   been fixed. Prose breaks the token, not the layout. */
p, li, dt, dd, b, summary, .lede, .note, .hint, .sub { min-width: 0; overflow-wrap: anywhere; }
.lede { color: var(--sub); margin: 0 0 20px; max-width: 70ch; }

/* --- cards, tables, disclosure --- */
.card { background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px 20px; margin-bottom: 16px; }
.card h2 { font-size: 14px; font-weight: 500; margin: 0 0 10px; }
.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.row3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.row4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
@media (max-width: 900px) {
  .row2, .row3 { grid-template-columns: 1fr; }
  .row4 { grid-template-columns: 1fr 1fr; }
}

table { border-collapse: collapse; width: 100%; font-size: 13px; }
th { text-align: left; font-size: 11px; font-weight: 500; color: var(--sub);
  border-bottom: 1px solid var(--border); padding: 8px 10px; }
td { padding: 9px 10px; border-bottom: 1px solid var(--border);
  font-variant-numeric: tabular-nums; }
tbody tr:hover td { background: var(--surface-2); }
tbody tr:last-child td { border-bottom: 0; }
.scroll { overflow-x: auto; }

details { border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); margin-bottom: 12px; }
details summary { cursor: pointer; padding: 12px 16px; font-size: 13px; font-weight: 500;
  list-style: none; display: flex; align-items: center; gap: 8px; }
details summary::-webkit-details-marker { display: none; }
details summary::before { content: "▸"; color: var(--sub); transition: transform .15s; }
details[open] summary::before { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { details summary::before { transition: none; } }
details .body { padding: 0 16px 14px; color: var(--sub); font-size: 12.5px; }

/* --- forms --- */
label.field { display: block; margin-bottom: 12px; }
label.field span { display: block; font-size: 12px; color: var(--sub); margin-bottom: 4px; }
input[type="text"], input[type="email"], input[type="password"], input[type="url"],
select, textarea {
  width: 100%; padding: 9px 11px; border: 1px solid var(--border); border-radius: 4px;
  background: var(--surface); color: var(--ink); font: 13px var(--sans);
}
input:focus, select:focus, textarea:focus { outline: 2px solid var(--blue); outline-offset: -1px; }

/* Plain links and disclosure toggles had no authored focus style, so keyboard
   focus looked materially different between the landing page (which defines its
   own rule) and the console. One ring for every focusable thing. */
a:focus-visible, summary:focus-visible {
  outline: 2px solid var(--blue); outline-offset: 2px; border-radius: 3px;
}

/* --- misc --- */
/* minmax(0, 1fr), not a bare 1fr. `1fr` is minmax(auto, 1fr) and that `auto`
   floors the track at the widest value's MIN-CONTENT width, so one unbroken
   token (a sha256 pin, an OCI digest, a 120-character system name) widened
   EVERY row of the list and pushed the page sideways — measured 2296 px cells
   at a 1265 px viewport, and silently clipped on paper, where a max-width
   media query can never help because the print box is ~794 px. */
.kv { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 4px 16px;
  font-size: 12.5px; margin: 0; }
.kv dt { color: var(--sub); min-width: 0; overflow-wrap: anywhere; }
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.mono { font-family: var(--mono); font-size: 11.5px; }
pre.code { background: var(--surface-2); border-radius: 6px; padding: 12px;
  font-family: var(--mono); font-size: 11px; white-space: pre-wrap; word-break: break-all;
  max-height: 200px; overflow: auto; margin: 8px 0; }
.empty { text-align: center; padding: 48px 20px; color: var(--sub); }
.toast { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
  background: var(--ink); color: var(--bg); padding: 10px 20px; border-radius: 6px;
  font-size: 13px; opacity: 0; pointer-events: none; transition: opacity .2s;
  z-index: 20; box-shadow: var(--shadow); }
.toast.show { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .toast { transition: none; } }
.spinner { width: 14px; height: 14px; border: 2px solid var(--border);
  border-top-color: var(--blue); border-radius: 50%; animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }

/* ==================================================================
   SCANNING LAYER — icons, markers, metrics, rows, fact blocks.
   These exist so a page can carry a fact with a glyph plus four words
   where it used to carry it with a paragraph. They add structure only;
   no honesty label (ARCHITECTURE §6) is ever replaced by an icon, and
   nothing here hides text behind an interaction.
   ================================================================== */

/* --- icons (markup comes from /css/icons.js) --- */
/* icons.js already writes width/height attributes; these rules stop a
   flex or grid parent from stretching the box, and drop the glyph onto
   the text baseline instead of letting it ride high beside 13px type.
   The 16px on `.icon` itself is the FLOOR, not the norm: an <svg> with no
   intrinsic size and no CSS size lays out at the SVG default 300x150, so a
   glyph that arrives without a size class — a sprite <use> on the landing
   page, a hand-written slot — degrades to a text-height icon instead of
   detonating the layout. The size classes below all follow this rule and
   have equal specificity, so each one still wins for its own element. */
.icon { flex: 0 0 auto; vertical-align: -.15em; width: 16px; height: 16px; }
.icon-12 { width: 12px; height: 12px; vertical-align: -.11em; }
.icon-14 { width: 14px; height: 14px; vertical-align: -.13em; }
.icon-16 { width: 16px; height: 16px; }
.icon-18 { width: 18px; height: 18px; vertical-align: -.19em; }
.icon-20 { width: 20px; height: 20px; vertical-align: -.22em; }
.icon-24 { width: 24px; height: 24px; vertical-align: -.28em; }
/* An icon that is context rather than status reads at --sub; one that IS
   the status inherits its meaning colour from .marker or .chip. */
.icon-muted { color: var(--sub); }
/* Only for a refresh/loading glyph that replaces a .spinner in place. */
.icon-spin { animation: spin .9s linear infinite; transform-origin: 50% 50%; }
@media (prefers-reduced-motion: reduce) { .icon-spin { animation: none; } }

/* A chip is 11px type; a 16px glyph would out-weigh the word it labels,
   and the chip's own 5px gap already spaces it. */
.chip .icon { width: 12px; height: 12px; vertical-align: -.12em; }
/* Pull the leading glyph back into the button's optical padding. */
.btn .icon { margin-left: -2px; }
/* summary and card headings are flex rows already — just fix the size. */
details summary .icon, .card h2 .icon { flex: 0 0 auto; }

/* --- status marker: icon + colour + word, never colour alone --- */
/* Colour is the third signal, not the only one: a red dot is invisible to
   a red-green reader, prints grey, and survives no screenshot pasted into
   an underwriting file. Shape and text carry the meaning without it. */
.marker { display: inline-flex; align-items: baseline; gap: 6px;
  font-size: 12.5px; font-weight: 500; min-width: 0; overflow-wrap: anywhere; }
.marker .icon { align-self: center; }
.marker.ok { color: var(--ok); }
.marker.warn { color: var(--warn); }
.marker.err { color: var(--err); }
.marker.live { color: var(--blue-ink); }
.marker.muted { color: var(--sub); }
/* The trailing qualifier — "sample", "not verified yet", "unsigned" — is
   part of the marker, so it can never drift away from the status it
   qualifies, and it stays the same size class as the marker itself. */
.marker small { font-weight: 400; font-size: 11.5px; color: var(--sub); }
.marker.block { display: flex; }

/* --- metric: one number, one label, optional glyph and trend --- */
/* Named .metric, not .stat: app.html and demo/index.html already own a
   local .stat and a shared box rule would silently restyle both. */
.metric { display: grid; gap: 3px; padding: 12px 14px; min-width: 0;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); }
.metric-label { display: flex; align-items: center; gap: 6px;
  font-size: 11px; color: var(--sub); letter-spacing: .04em;
  text-transform: uppercase; min-width: 0; }
/* 22px reads as a number to scan rather than a heading to read; tabular
   figures keep a column of metrics from jittering as values change. */
.metric-value { font-size: 22px; line-height: 1.2; font-weight: 400;
  font-variant-numeric: tabular-nums; min-width: 0; overflow-wrap: anywhere; }
.metric-value small { font-size: 12px; color: var(--sub); font-weight: 400; }
.metric-value.mono { font-family: var(--mono); font-size: 13px; }
/* The footer is where the caveat lives — "sample", "unsigned", the unit —
   so every metric has a place to say what its number is NOT. */
.metric-foot { display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-size: 11.5px; color: var(--sub); min-width: 0; }
.metric-trend { display: inline-flex; align-items: center; gap: 4px;
  font-variant-numeric: tabular-nums; }
.metric-trend.up { color: var(--ok); }
.metric-trend.down { color: var(--err); }
.metric-trend.flat { color: var(--sub); }
/* auto-fit rather than a fixed count: a metric strip should not need a
   different class per page, and it collapses to one column at 360px. */
.metric-grid { display: grid; gap: 12px; margin-bottom: 16px;
  grid-template-columns: repeat(auto-fit, minmax(min(184px, 100%), 1fr)); }

/* --- rows: a scannable list, denser than a stack of cards --- */
/* A card costs 16px of padding and a border per item; ten of them is a
   page of scrolling. A row is one line, one glyph, one trailing status. */
.rows { border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); overflow: hidden; margin-bottom: 16px; }
.rows > .row { display: flex; align-items: center; gap: 10px;
  padding: 9px 14px; border-top: 1px solid var(--border); min-width: 0; }
.rows > .row:first-child { border-top: 0; }
.rows > a.row { color: var(--ink); text-decoration: none; }
.rows > .row:hover { background: var(--surface-2); }
.rows > a.row:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; }
/* The lead glyph is a fixed 20px gutter so titles align down the list
   even when one row's icon is a wide silhouette (cpu) and another's a dot. */
.row-lead { flex: 0 0 20px; display: flex; justify-content: center;
  color: var(--sub); }
.row-main { flex: 1 1 auto; min-width: 0; }
.row-title { font-size: 13px; min-width: 0; overflow-wrap: anywhere; }
.row-sub { font-size: 11.5px; color: var(--sub); min-width: 0;
  overflow-wrap: anywhere; }
.row-end { flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
  margin-left: auto; }
/* Below 520px the trailing status would squeeze the title to two words,
   so the row wraps and the status drops under it, still attached. */
@media (max-width: 520px) {
  .rows > .row { flex-wrap: wrap; }
  .row-end { margin-left: 30px; width: 100%; }
}
.rows-head { display: flex; align-items: center; gap: 8px;
  padding: 8px 14px; border-bottom: 1px solid var(--border);
  background: var(--surface-2); font-size: 11px; font-weight: 500;
  color: var(--sub); letter-spacing: .04em; text-transform: uppercase; }

/* --- kv refinements for dense fact blocks --- */
/* .dense narrows the label column and tightens the leading for blocks of
   ten-plus facts (receipt fields, share metadata) where the default
   150px/4px rhythm turns a table of facts into a page of scrolling. */
.kv.dense { grid-template-columns: 124px minmax(0, 1fr); gap: 2px 12px;
  font-size: 12px; }
/* .wide is the opposite case: sentence-length labels ("Signature covers")
   that would otherwise wrap to three lines against a 150px column. */
.kv.wide { grid-template-columns: 210px minmax(0, 1fr); }
/* Hairlines make a long fact block scannable by row instead of by column.
   The rule sits on dd and dt so it survives either being the taller cell. */
.kv.lines dt, .kv.lines dd { border-top: 1px solid var(--border);
  padding: 5px 0; }
.kv.lines dt:first-of-type, .kv.lines dd:first-of-type { border-top: 0; }
/* A chip or marker in the value column must not stretch the row height. */
.kv dd .chip, .kv dd .marker { vertical-align: baseline; }
.kv dt .icon { margin-right: 4px; }
/* Below ~430px a fixed 150px label column leaves under 200px for a value
   that is often a hash, so the pair stacks instead of squeezing. */
@media (max-width: 430px) {
  .kv, .kv.dense, .kv.wide { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .kv dt { margin-top: 8px; font-size: 11px; }
  .kv dt:first-of-type { margin-top: 0; }
  .kv.lines dd { border-top: 0; padding-top: 0; }
}

/* --- print --- */
@media print {
  /* Paper has no theme. A reader printing from dark mode would otherwise
     get #8ab4f8 strokes and #81c995 markers on white — the §6 markers
     (sample, LIVE, sealed) are exactly the ink that must not go pale.
     These are the light values above, re-pinned for paper, not new ones. */
  :root, :root[data-theme="dark"], :root[data-theme="light"] {
    color-scheme: light;
    --bg: #ffffff; --surface: #ffffff; --surface-2: #f1f3f4; --border: #b0b3b8;
    --ink: #111111; --sub: #444746;
    --blue: #0b57d0; --blue-bg: #e8f0fe; --blue-ink: #0b57d0;
    --ok: #0d652d; --ok-bg: #e6f4ea; --warn: #8a4600; --warn-bg: #fef7e0;
    --err: #c5221f; --err-bg: #fce8e6;
    --on-accent: #ffffff; --shadow: none;
  }
  /* Icons are foreground vector, but a UA that strips "decorative" colour
     can still flatten them; pin the adjustment and the stroke explicitly. */
  .icon { print-color-adjust: exact; -webkit-print-color-adjust: exact;
    stroke: currentColor; }
  .icon-spin { animation: none; }
  /* Backgrounds are the first thing a printer drops. A hairline keeps the
     `sample` and LIVE chips readable as chips even with backgrounds off —
     losing that boundary would leave an illustrative score looking measured. */
  .chip { border: 1px solid currentColor; padding: 1px 8px; font-size: 8pt;
    print-color-adjust: exact; -webkit-print-color-adjust: exact; }
  /* THE canonical paper treatment for status colour. It lives here, once,
     because report.html and share.html are the same evidence pack printed from
     the same link minutes apart: each used to carry its own copy, and the two
     copies drifted until /s/ printed every chip pure black on transparent —
     LIVE, sample, ok and err flattened into one another — while /report kept
     semantic colour. Colour is the third signal after the glyph and the word,
     and an underwriter's filed copy should not be the one that loses it. These
     are the light-theme inks pulled a shade darker for toner; a page may not
     restate them. */
  .chip.blue { background: #e8f0fe !important; color: #174ea6 !important; }
  .chip.gray { background: #eeeeee !important; color: #333333 !important; }
  .chip.ok   { background: #e6f4ea !important; color: #0d652d !important; }
  .chip.warn { background: #fef7e0 !important; color: #8a4500 !important; }
  .chip.err  { background: #fce8e6 !important; color: #a50e0e !important; }
  .chip .dot { background: currentColor !important; }
  /* The chip's own glyph must not be dropped as decoration: `sample` and LIVE
     are the two labels this product cannot afford to print pale. */
  .chip .icon { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
  .marker.ok    { color: #0d652d !important; }
  .marker.err   { color: #a50e0e !important; }
  .marker.warn  { color: #8a4500 !important; }
  .marker.live  { color: #174ea6 !important; }
  .marker.muted { color: #444746 !important; }
  .metric, .rows > .row, .rows-head { break-inside: avoid; page-break-inside: avoid; }
  .metric, .rows { background: transparent; box-shadow: none; }
  .rows > .row:hover { background: transparent; }
  /* Paper is ~794px wide: the .kv stacking breakpoint never fires there,
     but a 210px .wide label column plus a hash still overflows, so pull
     the wide variant back to the default column on paper. */
  .kv.wide { grid-template-columns: 150px minmax(0, 1fr); }
}
