:root {
    --bg: #0f1115;
    --bg-raised: #151820;
    --bg-card: #171b24;
    --border: #262b36;
    --text: #d7dae0;
    --text-dim: #8a8f99;
    /* Mutted caption color. Bumped from #575c67 → #9198a8 after the
       2026-04-20 a11y audit: the previous value gave WCAG contrast
       ratio ~2.82 against --bg, failing AA for normal-size text in
       ~12 places (card hints, kpi labels, `seen N ago` timestamps,
       italic explanations, toolbar labels, footer attribution).
       New value reaches ~4.6 ratio, clears AA (≥4.5), and is visually
       almost indistinguishable against the dark background. All 27+
       usages through this variable lift together. */
    --text-faint: #9198a8;
    --accent: #5b9cf5;
    --accent-dim: #3d72c4;
    --amber: #f5a623;
    --amber-dim: #b97e19;
    --warn: #ffb454;
    --crit: #ff6b6b;
    --ok: #6bcf76;
    --mono: "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
    --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
    color-scheme: dark light;
}
/* Light palette — explicit toggle (data-theme="light") or OS
   preference when no explicit choice is stored. The JS theme
   toggle sets data-theme on <html> and persists in localStorage. */
html[data-theme="light"] {
    --bg: #f4f6fa;
    --bg-raised: #ebeef3;
    --bg-card: #ffffff;
    --border: #d4d9e2;
    --text: #1e2028;
    --text-dim: #535862;
    --text-faint: #5a6070;
    --accent: #2e6ed4;
    --accent-dim: #1b5ab8;
    --amber: #c07a08;
    --amber-dim: #9e6507;
    --warn: #c48000;
    --crit: #d43b3b;
    --ok: #2a8c35;
}
@media (prefers-color-scheme: light) {
    html:not([data-theme="dark"]) {
        --bg: #f4f6fa;
        --bg-raised: #ebeef3;
        --bg-card: #ffffff;
        --border: #d4d9e2;
        --text: #1e2028;
        --text-dim: #535862;
        --text-faint: #5a6070;
        --accent: #2e6ed4;
        --accent-dim: #1b5ab8;
        --amber: #c07a08;
        --amber-dim: #9e6507;
        --warn: #c48000;
        --crit: #d43b3b;
        --ok: #2a8c35;
    }
}

* { box-sizing: border-box; }

/* Screen-reader-only landmark heading. The visual design uses the
   brand row as the de-facto title, but a11y tooling (Lighthouse, VoiceOver)
   expects an <h1> for the page's main topic. */
.visually-hidden {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; overflow: hidden;
    clip: rect(0 0 0 0); border: 0; white-space: nowrap;
}
html, body {
    margin: 0; padding: 0; height: 100%;
    background: var(--bg);
    color: var(--text);
    font-family: var(--sans);
    font-size: 14px;
    line-height: 1.45;
}

.top {
    /* Bar spans full viewport for the background + border-bottom band,
       but inner content aligns with <main> at max-width 1280px. */
    padding: 14px 0;
    border-bottom: 1px solid var(--border);
    background: var(--bg-raised);
}
.top-inner {
    max-width: 1280px;
    margin: 0 auto;
    padding: 0 24px;
    display: flex; align-items: center; justify-content: space-between;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand .logo { color: var(--accent); font-size: 18px; flex: 0 0 auto; }
/* ``nowrap`` on each piece prevents the mobile header from breaking
   words mid-string ("monad-" / "ops") when the flex row runs out of
   space. On mobile the mobile media-query below also stacks the node
   name onto its own line, so "monad-testnet" sits directly under the
   logo/name rather than competing for horizontal space. */
.brand .name { font-weight: 600; letter-spacing: 0.02em; white-space: nowrap; }
.brand .sep { color: var(--text-faint); }
.brand .node { color: var(--text-dim); font-family: var(--mono); font-size: 12.5px; white-space: nowrap; }
.brand-link { color: inherit; text-decoration: none; display: inline-flex; align-items: center; gap: 10px; }
.brand-link:hover .name { color: var(--accent); }

/* Header nav: compact links next to the status pill (e.g. "alerts →"
   cross-link from the dashboard to the alert-history page). */
.nav-links { display: flex; gap: 12px; align-items: center; }
.nav-links a {
    color: var(--text-dim); text-decoration: none;
    font-family: var(--mono); font-size: 12px;
    /* Tap target: 10×12 padding gives visible box ≈ 32h; the ::before
       layer below extends the hit area to ≥44×44 (WCAG 2.5.5) without
       visually chunking up the header. */
    position: relative;
    padding: 10px 12px; border-radius: 4px;
    border: 1px solid var(--border);
    white-space: nowrap;
}
.nav-links a::before {
    content: ""; position: absolute; inset: -6px;
    /* Transparent hit area — expands the clickable surface on touch
       devices past the visible border by 6 px on every side. */
}
.nav-links a:hover { color: var(--accent); border-color: var(--accent-dim); }
.footlink { color: var(--text-dim); text-decoration: none; }
.footlink:hover { color: var(--accent); }

.status { display: flex; align-items: center; gap: 8px; color: var(--text-dim); font-size: 12.5px; }
.status .dot {
    width: 8px; height: 8px; border-radius: 50%;
    background: var(--text-faint);
    box-shadow: 0 0 0 2px rgba(255,255,255,0.04);
}
.status.ok  .dot { background: var(--ok); }
.status.warn .dot { background: var(--warn); }
.status.crit .dot { background: var(--crit); }

/* Health pill is now an anchor — operators who see CRITICAL in the
   header instinctively click it expecting to learn what's wrong.
   Before iter-6 it was a non-interactive <div>. Now it's an <a>
   with a stable href to /alerts?severity=critical as a fallback; a
   JS click-handler (app.js) intercepts on the dashboard and smooth-
   scrolls to the incidents card instead, so the user stays in context
   when the critical is visible on the same page. Keyboard: Tab + Enter
   works for free via anchor semantics. */
a.health-pill-link {
    /* Anchor base reset — no underline, inherit color from .status */
    text-decoration: none; color: var(--text-dim);
    cursor: pointer;
    position: relative;
    padding: 6px 10px; border-radius: 4px;
    transition: background 120ms linear;
}
a.health-pill-link::before {
    /* Tap-area expansion; matches the pattern used for nav links */
    content: ""; position: absolute; inset: -6px;
}
a.health-pill-link:hover { background: rgba(138,143,153,0.08); }
a.health-pill-link:focus-visible {
    outline: 1px solid var(--accent-dim); outline-offset: 2px;
}

main { max-width: 1280px; margin: 0 auto; padding: 24px; }

.grid { display: grid; gap: 16px; }
.grid.grid-4 { grid-template-columns: repeat(4, 1fr); }
.grid.grid-2 { grid-template-columns: repeat(2, 1fr); margin-top: 16px; }
.grid.grid-1 { grid-template-columns: 1fr; margin-top: 16px; }
@media (max-width: 960px) {
    .grid.grid-4 { grid-template-columns: repeat(2, 1fr); }
    .grid.grid-2 { grid-template-columns: 1fr; }
}

.card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 16px 18px;
}
/* Brief border-glow when a KPI value updates. Triggered from JS by
   adding .tile-flash to the card; class is removed/re-added on each
   change so consecutive updates restart the animation cleanly. Solid
   accent ring + soft blurred glow, both fade to transparent. */
@keyframes tile-flash-anim {
    0%   { box-shadow: 0 0 0 1.5px rgba(91,156,245,0.85), 0 0 14px 2px rgba(91,156,245,0.45); }
    60%  { box-shadow: 0 0 0 1.5px rgba(91,156,245,0.55), 0 0 14px 2px rgba(91,156,245,0.20); }
    100% { box-shadow: 0 0 0 1.5px rgba(91,156,245,0),    0 0 14px 2px rgba(91,156,245,0); }
}
.card.tile-flash { animation: tile-flash-anim 0.7s ease-out; }
@media (prefers-reduced-motion: reduce) {
    .card.tile-flash { animation: none; }
}
.card.tall { min-height: 240px; }
/* Cards that host charts must clip: Chart.js sometimes overshoots by
   a sub-pixel during DPR rounding, and the stacked exec-breakdown chart
   on narrow viewports briefly becomes wider than its container during
   resize before it converges. Both show up as a transient horizontal
   scrollbar inside the card (audit 2026-04-20, mobile section). */
.card.chart { padding-bottom: 10px; overflow: hidden; }
/* Chart.js with maintainAspectRatio:false requires a sized parent.
   Without this the canvas grows unbounded on mobile viewports. The
   min-width:0 ensures the box can shrink below its content's intrinsic
   width inside a flex/grid parent, preventing layout-time overflow. */
.chart-box { position: relative; height: 200px; width: 100%; min-width: 0; overflow: hidden; }
.chart-box canvas {
    display: block;
    /* The `max-width: 100%` constraint is the defense-in-depth companion
       to the `width: 100% !important` Chart.js honors internally — under
       certain DPR/zoom ratios Chart.js produces a canvas 1-2 px wider
       than the parent box, which briefly manifests as a scrollbar on
       the card. Capping keeps the canvas within bounds unconditionally. */
    width: 100% !important; height: 100% !important;
    max-width: 100%;
}
.card-header { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 10px; }
.card-title { font-weight: 600; font-size: 13px; letter-spacing: 0.02em; text-transform: uppercase; color: var(--text-dim); }
.card-hint { font-family: var(--mono); color: var(--text-faint); font-size: 11.5px; }

.kpi { }
.kpi-label { font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-faint); }
.kpi-value { font-family: var(--mono); font-size: 28px; font-weight: 600; color: var(--accent); margin-top: 4px; }
.kpi-sub { font-size: 12px; color: var(--text-dim); margin-top: 4px; font-family: var(--mono); }

/* Severity color utilities — applied to numbers that have a meaning on
   a green→red scale. Thresholds for each metric are in app.js and shown
   to the operator in the `.legend` strip under the KPI row. */
.val-ok   { color: var(--ok); }
.val-mid  { color: var(--accent); }
.val-warn { color: var(--warn); }
.val-crit { color: var(--crit); }

/* At-a-glance explanation of what the colored numbers mean. Rendered as
   a single compact line directly under the KPI row. */
.legend {
    max-width: 1280px; margin: 12px auto 0; padding: 8px 24px 0;
    font-family: var(--mono); font-size: 11px; color: var(--text-faint);
    display: flex; flex-wrap: wrap; gap: 4px 14px; align-items: baseline;
}
.legend .group { display: inline-flex; gap: 6px; align-items: baseline; }
.legend .group .name { color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.06em; }
.legend .chip {
    font-family: var(--mono); font-size: 10.5px; font-weight: 600;
    padding: 1px 5px; border-radius: 3px;
}
.legend .chip.ok   { color: var(--ok);   background: rgba(107,207,118,0.10); }
.legend .chip.mid  { color: var(--accent); background: rgba(91,156,245,0.10); }
.legend .chip.warn { color: var(--warn); background: rgba(255,180,84,0.10); }
.legend .chip.crit { color: var(--crit); background: rgba(255,107,107,0.10); }
.legend .note { color: var(--text-faint); font-style: italic; }

.probes { list-style: none; padding: 0; margin: 0; }
.probes li {
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
    font-family: var(--mono); font-size: 12.5px; color: var(--text-dim);
    display: grid; grid-template-columns: 90px 170px 1fr;
    gap: 12px; align-items: center;
}
.probes li:last-child { border-bottom: none; }
.probes li.empty { color: var(--text-faint); text-align: center; grid-template-columns: 1fr; font-style: italic; }
.probe-name { color: var(--text); }
.probe-summary { color: var(--text-dim); }
.status {
    text-transform: uppercase; font-weight: 600; letter-spacing: 0.06em;
    font-size: 11px; padding: 2px 8px; border-radius: 4px; width: fit-content;
}
.status.ok       { color: var(--ok);   background: rgba(107,207,118,0.12); }
.status.warn     { color: var(--warn); background: rgba(255,180,84,0.12); }
.status.critical { color: var(--crit); background: rgba(255,107,107,0.12); }
.status.unknown  { color: var(--text-dim); background: rgba(138,143,153,0.12); }
.status.update   { color: var(--info, #6ea8ff); background: rgba(110,168,255,0.14); }

.version-row {
    padding: 12px 0;
    font-family: var(--mono); font-size: 13px; color: var(--text-dim);
    display: grid; grid-template-columns: 90px auto 1fr; gap: 12px;
    align-items: center; cursor: pointer; outline: none;
    border-radius: 6px;
    transition: background-color 120ms ease;
}
.version-row:hover, .version-row:focus { background: var(--surface-hover, rgba(255,255,255,0.03)); }
a.version-row { text-decoration: none; color: var(--text-dim); }
a.version-row:hover, a.version-row:focus { color: var(--text-dim); text-decoration: none; }

.profile-grid {
    display: grid; gap: 16px;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    margin-top: 8px;
}
.profile-cell {
    padding: 12px 14px;
    border-radius: 8px;
    background: var(--surface-hover, rgba(255,255,255,0.03));
    display: flex; flex-direction: column; gap: 4px;
}
.profile-label {
    font-family: var(--mono); font-size: 11.5px;
    color: var(--text-faint); text-transform: lowercase;
    letter-spacing: 0.02em;
}
.profile-val {
    font-family: var(--mono); font-size: 22px; font-weight: 600;
    color: var(--text); font-variant-numeric: tabular-nums;
}
.profile-sub {
    font-family: var(--mono); font-size: 11.5px; color: var(--text-faint);
}
.version-line {
    color: var(--text); font-variant-numeric: tabular-nums;
    letter-spacing: 0.01em;
}
.version-line .sep { color: var(--text-faint); margin: 0 6px; }
.version-line .v-installed { color: var(--text-dim); }
.version-line .v-latest    { color: var(--text); font-weight: 600; }
.version-line.update .v-latest { color: var(--info, #6ea8ff); }
.version-extras { color: var(--text-faint); font-size: 11.5px; text-align: right; }

.alerts { list-style: none; padding: 0; margin: 0; }
.alerts li { padding: 10px 0; border-bottom: 1px solid var(--border); font-family: var(--mono); font-size: 12.5px; color: var(--text-dim); display: grid; grid-template-columns: 110px 190px 180px 1fr; gap: 12px; align-items: baseline; }
.alerts li:last-child { border-bottom: none; }
.alerts li.empty { color: var(--text-faint); text-align: center; grid-template-columns: 1fr; font-style: italic; }
.alert-ts { color: var(--text-faint); font-size: 11.5px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.sev { text-transform: uppercase; font-weight: 600; letter-spacing: 0.06em; font-size: 11px; padding: 2px 8px; border-radius: 4px; width: fit-content; }
.sev.info      { color: var(--text-dim); background: rgba(138,143,153,0.12); }
.sev.warn      { color: var(--warn); background: rgba(255,180,84,0.12); }
.sev.critical  { color: var(--crit); background: rgba(255,107,107,0.12); }
.sev.recovered { color: var(--ok);   background: rgba(107,207,118,0.12); }

/* Severity + Foundation colour-code chip pair. They share one grid cell
   so the base grid stays 4-column; flex-wrap lets the code-color chip
   drop to a second line on narrow cards rather than pushing out. */
.sev-cc, .kind-cc { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: baseline; width: fit-content; }

/* Code-color chip. Matches the Foundation's 2026-03-26 colour-code
   vocabulary — "CODE RED / ORANGE / GREEN" — so operators read local
   alerts in the same language as Foundation announcements. Visually
   thinner than .sev (no background fill, just a coloured border) to
   avoid two heavy chips side-by-side. */
.cc { text-transform: uppercase; font-weight: 600; letter-spacing: 0.06em; font-size: 10px; padding: 1px 6px; border-radius: 4px; border: 1px solid transparent; background: transparent; white-space: nowrap; }
.cc-red    { color: var(--crit); border-color: rgba(255,107,107,0.45); }
.cc-orange { color: var(--warn); border-color: rgba(255,180,84,0.45); }
.cc-green  { color: var(--ok);   border-color: rgba(107,207,118,0.45); }

.rule { color: var(--text); }
.detail { color: var(--text-dim); }

.bot {
    max-width: 1280px; margin: 0 auto; padding: 16px 24px 32px;
    color: var(--text-faint); font-family: var(--mono); font-size: 11.5px;
    display: flex; gap: 12px;
}
.bot .sep { color: var(--text-faint); }

/* ---- epoch progress card -------------------------------------------- */
/* Full-width card sitting between the KPI strip and the legend. Shows
   the current epoch number, empirically-learned progress through it,
   and a projected ETA. Data comes from /api/state.epoch, fed by a
   sidecar probe that scans monad-bft journal every 15s. */
.epoch-card { padding: 14px 18px 16px; }
.epoch-head {
    display: flex; align-items: baseline; gap: 18px;
    flex-wrap: wrap; margin-bottom: 10px;
}
.epoch-number-block { display: flex; align-items: baseline; gap: 10px; }
.epoch-number {
    font-family: var(--mono); font-size: 28px; font-weight: 600;
    color: var(--accent); letter-spacing: 0.02em;
    font-variant-numeric: tabular-nums;
}
.epoch-sub {
    color: var(--text-dim); font-family: var(--mono);
    font-size: 12px; margin-left: auto;
    font-variant-numeric: tabular-nums;
}
.epoch-progress {
    position: relative; height: 10px; width: 100%;
    background: rgba(138,143,153,0.18);
    border: 1px solid rgba(138,143,153,0.22);
    border-radius: 3px; overflow: hidden;
}
.epoch-progress-fill {
    position: absolute; left: 0; top: 0; bottom: 0; width: 0%;
    background: linear-gradient(90deg, rgba(91,156,245,0.75), var(--accent));
    transition: width 320ms ease;
}
@media (max-width: 720px) {
    .epoch-head { gap: 8px; }
    .epoch-sub { margin-left: 0; width: 100%; font-size: 11px; }
    .epoch-number { font-size: 22px; }
}

/* ---- charts period selector ----------------------------------------- */
/* Single toolbar governs all three chart panels (retry_pct / tx/block /
   exec breakdown). Selection persists in localStorage + URL so a page
   reload (or a link share) keeps the same view. */
.charts-toolbar {
    display: flex; align-items: center; gap: 14px;
    margin: 0 0 10px; padding: 4px 0;
    flex-wrap: wrap;
    font-family: var(--mono); font-size: 11.5px;
    color: var(--text-faint); text-transform: uppercase;
    letter-spacing: 0.06em;
}
.charts-toolbar-label { color: var(--text-faint); }
.charts-toolbar-hint {
    color: var(--text-faint); text-transform: none;
    letter-spacing: 0; margin-left: auto;
}
.charts-range { display: inline-flex; gap: 6px; flex-wrap: wrap; }
.charts-range .range-btn {
    font-family: var(--mono); font-size: 11.5px;
    /* Tap target: min-height 32 + ::before inset -6 reaches the
       WCAG 2.5.5 minimum of 44×44. Visible pill size kept compact
       so the toolbar still reads as a selector strip. */
    position: relative;
    min-height: 32px;
    padding: 8px 12px; border-radius: 4px;
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    cursor: pointer;
    text-transform: uppercase; letter-spacing: 0.04em;
    transition: background 120ms linear, border-color 120ms linear, color 120ms linear;
}
.charts-range .range-btn::before {
    content: ""; position: absolute; inset: -6px;
    /* Transparent hit-area expander; no visual change. */
}
.charts-range .range-btn:hover {
    color: var(--text); border-color: rgba(138,143,153,0.45);
}
.charts-range .range-btn.is-active {
    color: var(--text);
    border-color: var(--accent);
    background: rgba(91,156,245,0.14);
}
.charts-range .range-btn:focus-visible {
    outline: 1px solid var(--accent-dim); outline-offset: 1px;
}
/* Pan buttons — compact arrow-icon look that visually anchors the
   period group on either side. Same hit-target expander applies via
   the existing ::before, so 44×44 WCAG floor still met. */
.charts-range .range-btn.pan-btn {
    padding: 8px 9px;
    font-size: 12px;
    color: var(--text-faint);
    letter-spacing: 0;
}
.charts-range .range-btn.pan-btn:hover { color: var(--text); }
.charts-range .range-btn.pan-btn:disabled,
.charts-range .range-btn.pan-btn[aria-disabled="true"] {
    opacity: 0.35;
    cursor: not-allowed;
    color: var(--text-faint);
    border-color: var(--border);
    background: transparent;
}
/* Stress-event quick-jump row. Sits under the period row, auto-
   populated from /api/stress_events. Each button mirrors .range-btn
   shape so the toolbar reads as one cohesive control surface. Hidden
   entirely (.hidden) when the API returns no events. */
.charts-stress {
    margin-top: 8px;
    display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
}
.charts-stress.hidden { display: none; }
.charts-stress .stress-list { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.charts-stress .stress-btn {
    font-family: var(--mono); font-size: 11.5px;
    position: relative;
    min-height: 32px;
    padding: 6px 10px; border-radius: 4px;
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    cursor: pointer;
    text-transform: none; letter-spacing: 0;
    transition: background 120ms linear, border-color 120ms linear, color 120ms linear;
    display: inline-flex; align-items: center; gap: 6px;
}
.charts-stress .stress-btn::before {
    content: ""; position: absolute; inset: -6px;
}
.charts-stress .stress-btn:hover {
    color: var(--text); border-color: rgba(138,143,153,0.45);
}
.charts-stress .stress-btn:focus-visible {
    outline: 1px solid var(--accent-dim); outline-offset: 1px;
}
.charts-stress .stress-btn .stress-dot {
    width: 7px; height: 7px; border-radius: 50%; background: var(--text-faint);
}
.charts-stress .stress-btn.is-live {
    color: var(--text);
    border-color: rgba(255,107,107,0.55);
    background: rgba(255,107,107,0.08);
}
.charts-stress .stress-btn.is-live .stress-dot {
    background: #ff6b6b;
    box-shadow: 0 0 6px rgba(255,107,107,0.8);
    animation: stress-live-pulse 1.4s ease-in-out infinite;
}
@keyframes stress-live-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.4; }
}
@media (prefers-reduced-motion: reduce) {
    .charts-stress .stress-btn.is-live .stress-dot { animation: none; }
}
/* Custom-range panel — appears inline under the preset buttons when
   the "custom" pill is toggled. Two datetime-locals + apply. Max span
   enforced in JS; server clamps too, so we never ship a 30-day scan. */
.charts-custom {
    display: flex; align-items: center; gap: 10px;
    flex-basis: 100%; flex-wrap: wrap;
    padding: 10px 0 4px;
    border-top: 1px dashed rgba(138,143,153,0.18);
    margin-top: 4px;
    color: var(--text-dim); text-transform: none; letter-spacing: 0;
}
.charts-custom.hidden { display: none; }
.charts-custom label {
    display: inline-flex; align-items: center; gap: 6px;
    font-family: var(--mono); font-size: 11.5px;
    color: var(--text-faint);
}
.charts-custom input[type="datetime-local"] {
    font-family: var(--mono); font-size: 11.5px;
    padding: 2px 6px;
    background: rgba(138,143,153,0.08);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 3px;
    color-scheme: dark;
}
html[data-theme="light"] .charts-custom input[type="datetime-local"] { color-scheme: light; }
.charts-custom input[type="datetime-local"]:focus-visible {
    outline: 1px solid var(--accent-dim); outline-offset: 1px;
    border-color: var(--accent);
}
.charts-custom-msg {
    font-family: var(--mono); font-size: 11px;
    color: var(--crit);
}
.custom-to-wrap {
    display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
.custom-tz {
    font-size: 9.5px; letter-spacing: 0.06em;
    color: var(--text-faint); opacity: 0.65;
    padding: 1px 4px;
    border: 1px solid rgba(138,143,153,0.25); border-radius: 3px;
}
.custom-to-wrap input[type="datetime-local"]:disabled {
    opacity: 0.45; cursor: not-allowed;
}
.custom-to-live {
    display: inline-flex; align-items: center; gap: 5px;
    font-family: var(--mono); font-size: 11px;
    color: var(--text-faint); text-transform: none; letter-spacing: 0;
    padding: 2px 8px;
    border: 1px solid var(--border); border-radius: 3px;
    cursor: pointer;
}
.custom-to-live:hover { color: var(--text-dim); border-color: rgba(138,143,153,0.45); }
.custom-to-live input[type="checkbox"] {
    accent-color: var(--accent);
    margin: 0; cursor: pointer;
}
.charts-toolbar-hint.frozen::before {
    content: "◼ frozen · ";
    color: var(--text-faint);
}

@media (max-width: 720px) {
    .charts-toolbar { gap: 8px; }
    .charts-toolbar-hint { margin-left: 0; width: 100%; }
    /* On mobile the tap-target must NOT shrink below WCAG 2.5.5 minimum
       (44×44). Previous rule `padding: 2px 7px; font-size: 10.5px`
       collapsed the hit area to ~22 px which audit flagged. Keep the
       desktop sizing here; with `gap: 6px` each pill still stays distinct
       from its neighbors. */
    .charts-range .range-btn { font-size: 11px; padding: 8px 11px; }
    .charts-custom { gap: 6px 10px; }
    .charts-custom label { width: 100%; }
    .charts-custom input[type="datetime-local"] { flex: 1; min-width: 0; }
}

/* ---- top contracts panel --------------------------------------------- */
/* Loading overlay: dimmed card content + top-edge progress bar while a
   fresh query is in flight. Cached hits bypass this entirely (instant
   render). Controls disable themselves via the .disabled attribute set
   in _setContractsLoading; here we just provide the visual cue. */
#contracts-card { position: relative; }
#contracts-card.loading #contracts-body { opacity: 0.55; pointer-events: none; }
#contracts-card.loading .contracts-controls select { cursor: wait; }
#contracts-card::before {
    /* Indeterminate top-edge progress bar, only visible while .loading. */
    content: "";
    position: absolute; top: 0; left: 0; right: 0; height: 2px;
    background: linear-gradient(90deg,
        transparent 0%, var(--accent) 50%, transparent 100%);
    background-size: 200% 100%;
    opacity: 0; transition: opacity 120ms linear;
    pointer-events: none;
    border-top-left-radius: inherit; border-top-right-radius: inherit;
}
#contracts-card.loading::before {
    opacity: 1;
    animation: contracts-progress 1.1s linear infinite;
}
@keyframes contracts-progress {
    0%   { background-position: 100% 0; }
    100% { background-position: -100% 0; }
}
.contracts-controls {
    display: flex; gap: 18px; align-items: center; flex-wrap: wrap;
    margin: 0 0 12px; font-size: 11.5px; color: var(--text-faint);
    text-transform: uppercase; letter-spacing: 0.06em;
}
.contracts-controls label { display: flex; gap: 8px; align-items: center; }
.contracts-controls select {
    background: var(--bg-raised); color: var(--text);
    border: 1px solid var(--border); border-radius: 4px;
    padding: 3px 6px; font-family: var(--mono); font-size: 11.5px;
    text-transform: none; letter-spacing: 0;
}
.contracts-controls select:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

table.contracts {
    width: 100%; border-collapse: collapse;
    font-family: var(--mono); font-size: 12px; color: var(--text-dim);
}
table.contracts thead th {
    text-align: left; font-weight: 600; color: var(--text-faint);
    text-transform: uppercase; letter-spacing: 0.06em; font-size: 10.5px;
    padding: 8px 10px; border-bottom: 1px solid var(--border);
}
table.contracts thead th.num { text-align: right; }
table.contracts thead th.bar-col { width: 180px; }
table.contracts tbody td {
    padding: 8px 10px; border-bottom: 1px solid var(--border);
    vertical-align: middle;
}
table.contracts tbody td.num { text-align: right; color: var(--text); }
table.contracts tbody tr:last-child td { border-bottom: none; }
table.contracts tbody tr.empty td {
    color: var(--text-faint); font-style: italic; text-align: center;
}

.contract-cell { display: flex; flex-direction: column; gap: 3px; }
.contract-label {
    color: var(--text); font-size: 12.5px;
    /* ``flex-wrap: wrap`` lets the category chip drop to its own line
       when the contract column narrows on mobile. Without it, the
       anonymous flex-item that holds the label text wrapped between
       words instead, creating a 2-line-tall text block next to a
       vertically-centred chip — which read as an awkward overlap.
       ``align-items: flex-start`` pins the chip to the top of the
       label so when the text wraps to 2+ lines, the chip stays next
       to the first line (e.g. "Staking" / "Precompile" has [SYSTEM]
       next to "Staking", not to "Precompile"). */
    display: flex; flex-wrap: wrap; align-items: flex-start; gap: 3px 8px;
}
.contract-name { /* explicit span so word-wrap stays within the name */
    min-width: 0; word-break: break-word;
}
.contract-label .cat {
    font-size: 9.5px; padding: 1px 6px; border-radius: 3px;
    background: rgba(91,156,245,0.12); color: var(--accent);
    text-transform: uppercase; letter-spacing: 0.05em;
    white-space: nowrap;
}
.contract-addr {
    color: var(--text-faint); font-size: 11px; user-select: all;
}
.contract-addr.unlabeled { color: var(--text); font-size: 12.5px; }

/* Ratio cell: bar + percent sit side-by-side so the percent never
   occludes the bar's end (the 92% vs 95% visual confusion that the
   earlier in-bar chip caused when fills were >85%). */
.ratio-cell {
    display: flex; align-items: center; gap: 10px;
    min-width: 140px;
}
.ratio-bar {
    position: relative; height: 12px; flex: 1; min-width: 80px;
    background: rgba(138,143,153,0.18);
    border: 1px solid rgba(138,143,153,0.22);
    border-radius: 3px; overflow: hidden;
}
.ratio-bar .fill {
    position: absolute; left: 0; top: 0; bottom: 0;
    /* Color and width are assigned via CSSOM from JS (see renderContracts)
       because CSP style-src 'self' drops inline style="" attributes set
       via innerHTML. */
}
.ratio-pct {
    font-family: var(--mono); font-size: 11.5px; font-weight: 600;
    color: var(--text);
    min-width: 38px; text-align: right;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
}

/* ---- tip-lag panel ----------------------------------------------- */
.lag-row {
    display: flex; align-items: baseline; gap: 20px;
    flex-wrap: wrap; margin-top: 6px;
}
.lag-block { display: inline-flex; flex-direction: column; gap: 2px; }
.lag-label {
    font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em;
    color: var(--text-faint);
}
.lag-val {
    font-family: var(--mono); font-size: 22px; font-weight: 600;
    color: var(--text);
}
.lag-val.val-ok   { color: var(--ok); }
.lag-val.val-mid  { color: var(--accent); }
.lag-val.val-warn { color: var(--warn); }
.lag-val.val-crit { color: var(--crit); }
.lag-val.val-unknown { color: var(--text-faint); }
.lag-sep {
    font-family: var(--mono); font-size: 16px; color: var(--text-faint);
    padding: 0 2px;
}
@media (max-width: 720px) {
    .lag-row { gap: 12px; }
    .lag-val { font-size: 18px; }
    .lag-sep { font-size: 14px; }
}

/* ---- parallelism / exec breakdown -------------------------------- */
/* Contextual microcopy for the "effective tps peak" row — operators
   new to Monad's semantics often read "peak 48000 tps/s" as sustained;
   this line disambiguates without needing a separate docs page. */
.parallelism-note {
    max-width: 1280px; margin: 4px auto 0; padding: 4px 24px 0;
    font-family: var(--mono); font-size: 11px; color: var(--text-faint);
    font-style: italic; line-height: 1.5;
}
@media (max-width: 720px) {
    .parallelism-note { padding: 4px 12px 0; font-size: 10.5px; }
}
/* Inline legend for the stacked execution-time chart. Chart.js default
   legend is too loud; a hand-rolled strip under the chart keeps visual
   noise down while still explaining the three phases. */
.chart-legend-inline {
    display: flex; gap: 14px; padding: 6px 4px 0;
    font-family: var(--mono); font-size: 11px; color: var(--text-dim);
    align-items: center;
}
.chart-legend-inline .swatch {
    display: inline-block; width: 12px; height: 12px; border-radius: 2px;
    margin-right: 4px; vertical-align: -1px;
}
.chart-legend-inline .swatch[data-phase="sr"] { background: rgba(91,156,245,0.75); }
.chart-legend-inline .swatch[data-phase="te"] { background: rgba(107,207,118,0.75); }
.chart-legend-inline .swatch[data-phase="cm"] { background: rgba(255,180,84,0.75); }
.chart-box.tall { height: 260px; }

/* ---- chain integrity panel --------------------------------------- */
/* Lifetime reorg counter: green when 0 (positive signal), neutral when
   non-zero (single divergences are background noise on testnet —
   colouring the card on lifetime > 0 would mean it stays coloured
   forever and loses signal value). The card flips amber based on the
   *recent* (24h) count instead — see has-recent-reorg-warn. No red
   path: post-2026-05-03 reframe, no rule takes reorg events to
   CRITICAL, so the card severity ladder tops out at amber/WARN. */
#integrity-card.has-recent-reorg-warn {
    border-color: rgba(255,200,87,0.45);
    box-shadow: 0 0 0 1px rgba(255,200,87,0.15) inset;
}
.integrity-body {
    display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap;
    padding: 4px 2px 2px;
}
.integrity-counter {
    display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
}
.integrity-num {
    font-family: var(--mono); font-size: 28px; font-weight: 600;
    color: var(--ok);
}
#integrity-card.has-reorg .integrity-num { color: var(--text); }
#integrity-card.has-recent-reorg-warn .integrity-num { color: var(--warn); }
.integrity-label {
    font-family: var(--mono); font-size: 12px; color: var(--text-dim);
}
.integrity-recent {
    font-family: var(--mono); font-size: 11px;
    padding: 2px 8px; border-radius: 999px;
    border: 1px solid var(--border);
    color: var(--text-dim);
    background: transparent;
}
#integrity-card.has-recent-reorg-warn .integrity-recent {
    color: var(--warn);
    border-color: rgba(255,200,87,0.55);
    background: rgba(255,200,87,0.08);
}
.integrity-detail {
    font-family: var(--mono); font-size: 12px; color: var(--text-faint);
    word-break: break-all;
}
#integrity-card.has-reorg .integrity-detail { color: var(--text); }
/* Explanatory microcopy so first-time viewers understand why "0" is
   the positive signal. Typography matches the parallelism-note pattern. */
.integrity-note {
    margin-top: 12px; padding-top: 10px;
    border-top: 1px solid var(--border);
    font-family: var(--mono); font-size: 11px; color: var(--text-faint);
    line-height: 1.55; font-style: italic;
    max-width: 820px;
}

/* ---- alerts history page -------------------------------------------- */
.mono-time { font-family: var(--mono); font-size: 11px; color: var(--text-dim); white-space: nowrap; }
.alert-title { color: var(--text); font-size: 12px; }
.alert-detail { color: var(--text-dim); font-size: 11.5px; margin-top: 2px; word-break: break-word; }
.detail-cell { max-width: 520px; }

/* Reorg trace-download link on alerts page. Unobtrusive ghost button
   that appears only on rule=reorg rows (see alerts.js render). Links
   to /api/reorgs/{block_number} — public level by default. */
.alert-trace-btn {
    display: inline-block;
    margin-top: 4px;
    padding: 1px 6px;
    font-family: var(--mono);
    font-size: 11px;
    color: var(--accent);
    background: rgba(91,156,245,0.08);
    border: 1px solid rgba(91,156,245,0.25);
    border-radius: 3px;
    cursor: pointer;
    text-decoration: none;
}
.alert-trace-btn:hover { background: rgba(91,156,245,0.16); }

/* Inline entity links inside alert details. Both reorg-link (Block #N
   in reorg alerts → opens reorg trace popup) and block-link (block #N
   in any other alert rule → opens block detail popup) share styling
   so the dashboard reads consistently regardless of alert type. */
.reorg-link, .block-link {
    color: var(--accent);
    text-decoration: none;
    border-bottom: 1px dotted rgba(91,156,245,0.45);
}
.reorg-link:hover, .block-link:hover { border-bottom-style: solid; }
.reorg-link:focus-visible, .block-link:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* History-table column widths. Set via classes rather than inline
   `style="width:…"` attributes because the site's CSP `style-src 'self'`
   drops inline styles on HTML attributes (iter-4 audit §1). */
table.contracts.history .col-time { width: 140px; }
table.contracts.history .col-sev  { width: 90px; }
table.contracts.history .col-rule { width: 170px; }
@media (max-width: 720px) {
    table.contracts.history .col-time,
    table.contracts.history .col-sev,
    table.contracts.history .col-rule { width: auto; }
}

/* ---- critical incidents panel --------------------------------------- */
/* Card shifts border color when incidents are present — at-a-glance
   operator signal without needing to read the list. */
#incidents-card.has-incidents {
    border-color: rgba(255,107,107,0.45);
    box-shadow: 0 0 0 1px rgba(255,107,107,0.15) inset;
}
.incidents {
    list-style: none; padding: 0; margin: 0;
    font-family: var(--mono); font-size: 12.5px;
}
.incidents li {
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
    display: grid; grid-template-columns: 190px 120px 1fr auto;
    gap: 12px; align-items: center; color: var(--text-dim);
}
.incidents li:last-child { border-bottom: none; }
.incidents li.empty-ok {
    color: var(--ok); font-style: normal; text-align: left;
    grid-template-columns: 1fr; padding: 14px 4px;
    font-size: 13px; letter-spacing: 0.02em;
}
.incidents li.empty-ok a {
    color: var(--text-dim); text-decoration: none; border-bottom: 1px dotted currentColor;
}
.incidents li.empty-ok a:hover { color: var(--text); }
.incidents .kind {
    text-transform: uppercase; font-weight: 600; letter-spacing: 0.06em;
    font-size: 11px; padding: 2px 8px; border-radius: 4px; width: fit-content;
}
.incidents .kind.critical  { color: var(--crit);  background: rgba(255,107,107,0.12); }
.incidents .kind.warn      { color: var(--warn);  background: rgba(255,180,84,0.12); }
.incidents .kind.recovered { color: var(--ok);    background: rgba(107,207,118,0.12); }
.incidents .location { color: var(--text); font-size: 11.5px; word-break: break-all; }
.incidents .detail { color: var(--text-dim); word-break: break-word; }
.incidents .ago { color: var(--text-faint); font-size: 11px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.incidents .ago .alert-ts { color: var(--text-faint); }
.recurrence {
    color: var(--text-faint); font-size: 10.5px;
    margin-left: 6px; letter-spacing: 0.02em;
    padding: 1px 6px; border-radius: 3px;
    background: rgba(255,107,107,0.08);
    white-space: nowrap;
}

@media (max-width: 720px) {
    /* Reclaim horizontal space on narrow viewports — 24px main padding
       + 18px card padding was eating 84px/side, leaving ~220px for
       content on a 390px screen. */
    main { padding: 12px; }
    .card { padding: 12px; overflow-x: auto; }
    .top { padding: 10px 0; }
    .top-inner { padding: 0 12px; }

    /* Header brand on narrow screens: stack node name under the logo
       line so "monad-ops" and "monad-testnet" each sit on their own
       row instead of competing for horizontal space with nav-links +
       status pill (which pushed the brand into mid-word wraps like
       "monad-" / "ops"). The `·` separator is only meaningful on the
       inline desktop layout; hide it when we stack. */
    .brand { flex-wrap: wrap; row-gap: 2px; }
    .brand .sep { display: none; }
    .brand .node { flex-basis: 100%; padding-left: 28px; }

    /* Card header on narrow screens: default `justify-content: space-between`
       runs the hint text right up against the title with no visible gap
       (they read as one glued string, e.g. "RETRY_PCTavg rtp · p95 …").
       Stack title and hint vertically so the descriptor becomes its own
       line with breathing room. Desktop keeps the tight inline layout. */
    .card-header { flex-direction: column; align-items: flex-start; gap: 4px; }
    .card-hint { font-size: 11px; }

    .incidents li { grid-template-columns: 1fr; gap: 4px; }
    .incidents .ago { justify-self: start; }
    /* Mobile: stack card-style. Time and severity share a top row so the
       card reads as a compact timestamp-badge header, then rule+detail. */
    .alerts li {
        grid-template-columns: 1fr;
        grid-template-areas:
            "head"
            "rule"
            "detail";
        gap: 4px;
    }
    .alerts li .sev-cc { grid-area: head; align-self: start; }
    .alerts li .alert-ts {
        grid-area: head;
        justify-self: end;
        align-self: center;
    }
    .alerts li .rule   { grid-area: rule; }
    .alerts li .detail { grid-area: detail; }
    .sev { font-size: 10px; padding: 2px 6px; }
    .cc  { font-size: 9px;  padding: 1px 5px; }

    /* Alert-history table: re-flow as stacked cards on narrow screens.
       The 4-column table collapsed the `detail` column to ~80px, wrapping
       every word to its own line — unreadable. Cards mirror the main
       page's RECENT ALERTS layout: time+severity share a header row,
       rule and detail each get their own row.
       Scoped to .contracts.history so the main top-retried table
       (.contracts, no .history) keeps its tabular display. */
    table.contracts.history,
    table.contracts.history tbody { display: block; width: 100%; }
    table.contracts.history thead { display: none; }
    table.contracts.history tr {
        display: grid;
        grid-template-areas:
            "time sev"
            "rule rule"
            "detail detail";
        grid-template-columns: 1fr auto;
        gap: 4px 10px;
        padding: 10px 0;
        border-bottom: 1px solid var(--border);
        align-items: baseline;
    }
    table.contracts.history tr:last-child { border-bottom: none; }
    table.contracts.history td {
        display: block;
        padding: 0;
        border: none;
    }
    table.contracts.history td.mono-time      { grid-area: time; font-size: 11.5px; }
    table.contracts.history td:nth-child(2)   { grid-area: sev; justify-self: end; }
    table.contracts.history td.rule           { grid-area: rule; color: var(--text); font-size: 12px; }
    table.contracts.history td.detail-cell    { grid-area: detail; max-width: 100%; }
    table.contracts.history tr.empty td {
        grid-column: 1 / -1;
        text-align: center;
        padding: 16px 4px;
    }

    /* contracts table: compact and drop the TX column (least useful in
       a high-conflict ranking, rightmost so layout breaks least). */
    table.contracts { font-size: 10.5px; }
    table.contracts thead th,
    table.contracts tbody td { padding: 6px 5px; }
    table.contracts thead th.bar-col { width: auto; min-width: 56px; }
    table.contracts thead th:nth-child(6),
    table.contracts tbody td:nth-child(6) { display: none; }
    .ratio-cell { gap: 6px; min-width: 90px; }
    .ratio-bar { height: 10px; min-width: 50px; }
    .ratio-pct { font-size: 10.5px; min-width: 32px; }
    .contract-label { font-size: 11px; gap: 4px; }
    .contract-label .cat { font-size: 8.5px; padding: 1px 4px; }
    /* Keep the `0xabcd…ef01` hash on one line. `word-break: break-all`
       let it split mid-ellipsis at narrow widths, pushing the last char
       onto a second line. nowrap + ellipsis avoids that without needing
       a wider column. */
    .contract-addr { font-size: 9.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .contract-addr.unlabeled { font-size: 10.5px; }
    /* When there's no label, the contract cell is just the address — let
       it shrink so the progress bar has breathing room on 390px screens. */
    table.contracts tbody td:first-child { max-width: 100px; }
    .contracts-controls { flex-wrap: wrap; gap: 8px 14px; }

    /* KPI value slightly smaller so the "last block" number doesn't
       force a wider card than the viewport. */
    .kpi-value { font-size: 22px; }
    .kpi-label, .kpi-sub { font-size: 10.5px; }

    .legend { padding: 6px 12px 0; font-size: 10px; gap: 4px 10px; }
    .legend .chip { font-size: 9.5px; padding: 1px 4px; }
}

/* ---- 404 page --------------------------------------------------------
   Minimal branded 404. Replaces the FastAPI default JSON "detail": "Not
   Found" with an HTML page that points the visitor back at / or /alerts.
   Shares the dashboard card styling so the register stays consistent. */
main.page-404 {
    display: flex; align-items: center; justify-content: center;
    min-height: calc(100vh - 48px);
    padding: 40px 24px;
}
.card-404 {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 36px 40px;
    max-width: 520px;
    text-align: center;
}
.code-404 {
    font-family: var(--mono);
    font-size: 56px;
    font-weight: 600;
    color: var(--accent);
    line-height: 1;
}
.title-404 {
    font-family: var(--mono);
    font-size: 14px;
    text-transform: uppercase; letter-spacing: 0.08em;
    color: var(--text-dim);
    margin-top: 10px;
}
.hint-404 {
    font-size: 13px;
    color: var(--text-faint);
    margin-top: 14px;
    word-break: break-all;
}
.hint-404 code {
    font-family: var(--mono);
    font-size: 12.5px;
    background: rgba(138,143,153,0.08);
    padding: 1px 6px;
    border-radius: 3px;
    color: var(--text-dim);
}
.links-404 {
    margin-top: 24px;
    display: flex; align-items: center; justify-content: center;
    gap: 10px;
    font-family: var(--mono);
    font-size: 12.5px;
}
.links-404 .sep { color: var(--text-faint); }

/* ---- /api docs page ------------------------------------------------- */
.api-docs {
    max-width: 1100px;
    margin: 0 auto;
    padding: 20px 16px 40px;
    display: flex; flex-direction: column; gap: 16px;
}
.api-docs h2 {
    margin: 0 0 10px 0;
    font-size: 16px;
    color: var(--text);
    font-weight: 600;
}
.api-docs h3.api-section {
    margin: 20px 0 8px 0;
    font-size: 13.5px;
    color: var(--text);
    font-weight: 600;
}
.api-docs p { color: var(--text-dim); margin: 0 0 10px 0; }
.api-docs code { font-family: var(--mono); color: var(--text); }
.api-docs .base-url {
    background: rgba(138,143,153,0.08);
    padding: 2px 8px;
    border-radius: 3px;
}
.api-notes, .api-fields {
    list-style: none; padding: 0; margin: 0 0 4px 0;
    display: flex; flex-direction: column; gap: 6px;
    color: var(--text-dim);
}
.api-notes li, .api-fields li {
    padding-left: 14px;
    position: relative;
    line-height: 1.5;
}
.api-notes li::before, .api-fields li::before {
    content: "·";
    position: absolute; left: 4px;
    color: var(--text-faint);
}
.api-table {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--mono);
    font-size: 12.5px;
}
.api-table th, .api-table td {
    text-align: left;
    padding: 8px 12px;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}
.api-table th {
    color: var(--text-dim);
    font-weight: 500;
    background: rgba(138,143,153,0.04);
}
.api-table td:first-child { white-space: nowrap; }
.api-table td:last-child { color: var(--text-dim); line-height: 1.5; }
.example { margin: 14px 0; }
.example-label {
    font-size: 12.5px;
    color: var(--text-dim);
    margin-bottom: 6px;
}
.example-code {
    margin: 0;
    padding: 12px 14px;
    background: rgba(138,143,153,0.06);
    border: 1px solid var(--border);
    border-radius: 4px;
    font-family: var(--mono);
    font-size: 12.5px;
    overflow-x: auto;
    white-space: pre;
    color: var(--text);
}
.example-code .u {
    color: var(--accent);
}
@media (max-width: 720px) {
    .api-table { font-size: 11.5px; }
    .api-table th, .api-table td { padding: 6px 8px; }
    .example-code { font-size: 11.5px; padding: 10px; }
}

/* Introduction block on the dashboard landing. */
.intro {
    max-width: 1280px;
    margin: 0 0 4px;
    font-size: 13px;
    color: var(--text-dim);
    line-height: 1.55;
}
.intro a { color: var(--accent); text-decoration: none; }
.intro a:hover { text-decoration: underline; }
.intro code { font-family: var(--mono); font-size: 12px; color: var(--text); }

/* Table scroll wrapper for narrow viewports (C12). */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Small viewport: let grid-4 stack single-column (C11). */
@media (max-width: 420px) {
    .grid.grid-4 { grid-template-columns: 1fr; }
}

/* Respect reduced-motion preference (C2). */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* Copy button for curl examples on /api page (D2.6). */
.example { position: relative; }
.example .copy-btn {
    position: absolute; top: 28px; right: 8px;
    background: var(--bg-raised); color: var(--text-dim);
    border: 1px solid var(--border); border-radius: 4px;
    font-family: var(--mono); font-size: 10.5px;
    padding: 2px 8px; cursor: pointer;
    opacity: 0; transition: opacity 120ms;
}
.example:hover .copy-btn,
.example .copy-btn:focus-visible { opacity: 1; }
.example .copy-btn.copied { color: var(--ok); }

/* Severity detail panel on /alerts page (D2.2). */
details.severity-info {
    margin: 0 0 14px;
    font-size: 12.5px;
    color: var(--text-dim);
}
details.severity-info summary {
    cursor: pointer;
    color: var(--text-faint);
    font-size: 11.5px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
details.severity-info dl {
    margin: 8px 0 0;
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 4px 14px;
}
details.severity-info dt {
    font-family: var(--mono);
    color: var(--text);
    font-weight: 500;
}
details.severity-info dd { margin: 0; }

/* Theme toggle button (G2). */
/* Sparkline mini-charts inside KPI cards (G5). */
.sparkline {
    height: 20px; margin-top: 4px;
}
.sparkline svg { width: 100%; height: 100%; display: block; }
.sparkline polyline {
    fill: none; stroke: var(--accent); stroke-width: 1.5;
    vector-effect: non-scaling-stroke;
}
.sparkline .spark-fill {
    fill: var(--accent); fill-opacity: 0.18; stroke: none;
}

/* Deep-link copy button in chart toolbar (G7). */
.charts-toolbar .copy-btn {
    background: none; border: 1px solid var(--border); border-radius: 4px;
    color: var(--text-dim); font-family: var(--mono); font-size: 10.5px;
    padding: 2px 8px; cursor: pointer; margin-left: auto;
}
.charts-toolbar .copy-btn:hover { color: var(--text); border-color: var(--text-dim); }
.charts-toolbar .copy-btn.copied { color: var(--ok); border-color: var(--ok); }

/* Alerts export bar (G4). */
.alerts-export {
    display: flex; justify-content: flex-end;
    margin: 0 0 8px;
}

/* Touch-device tooltip popover (G8). On `(hover: none)` devices native
   `title` attributes never appear, so a delegated tap-handler renders
   this popover next to the tapped element. Desktop keeps the native
   tooltip — we only intercept when hover is unavailable. */
.tap-tip {
    position: absolute; z-index: 1000;
    max-width: min(260px, calc(100vw - 24px));
    padding: 6px 10px;
    background: var(--panel); color: var(--text);
    border: 1px solid var(--border); border-radius: 6px;
    box-shadow: 0 4px 14px rgba(0,0,0,0.35);
    font-family: var(--mono); font-size: 12px; line-height: 1.4;
    pointer-events: none;
    opacity: 0; transform: translateY(-2px);
    transition: opacity 120ms ease, transform 120ms ease;
}
.tap-tip.visible { opacity: 1; transform: translateY(0); }
@media (hover: none) {
    /* Subtle hint that the element is tappable for info. Only on
       labels/notes/chips — not on buttons (they already look tappable). */
    [title]:not(button):not(a) { cursor: help; }
}

/* Theme toggle button (G2). */
.theme-toggle {
    background: none; border: 1px solid var(--border); border-radius: 4px;
    color: var(--text-dim); font-size: 14px; line-height: 1;
    padding: 3px 7px; cursor: pointer;
    transition: color 120ms, border-color 120ms;
}
.theme-toggle:hover { color: var(--text); border-color: var(--text-dim); }
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- detail popup (block / contract / reorg) --------------------------
   One shared modal shell. Opened by click on block numbers / contract
   rows / reorg counter, or by ?block= / ?contract= / ?reorg= query
   params on page load. Inline style="" via innerHTML is dropped by CSP
   (style-src 'self' without 'unsafe-inline'); anything dynamic (bar
   widths, SVG polyline points) is assigned via CSSOM in JS. */

.popup { position: fixed; inset: 0; z-index: 100; }
.popup.hidden { display: none; }
.popup-backdrop {
    position: absolute; inset: 0;
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(1.5px);
    -webkit-backdrop-filter: blur(1.5px);
}
[data-theme="light"] .popup-backdrop,
body.theme-light .popup-backdrop { background: rgba(20, 25, 40, 0.35); }

.popup-panel {
    position: absolute;
    left: 50%; top: 50%; transform: translate(-50%, -50%);
    width: min(720px, calc(100vw - 24px));
    /* ``dvh`` (dynamic viewport height) matches the *currently visible*
       viewport on mobile browsers where the address bar expands/shrinks
       — unlike ``vh``, which always measures to the "large" viewport
       with the bar hidden and lets the popup extend past the visible
       area. Ship the ``vh`` line first as a fallback so older Safari
       (<15.4) still gets a reasonable cap. */
    max-height: calc(100vh - 48px);
    max-height: calc(100dvh - 48px);
    display: flex; flex-direction: column;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
    font-size: 13px;
}
.popup-head {
    display: flex; align-items: flex-start; gap: 10px;
    padding: 12px 16px;
    border-bottom: 1px solid var(--border);
}
/* Title + sub stack vertically so the full contract address no longer
   has to compete with the title for horizontal space (it used to ellipse
   on desktop and overflow on mobile). Close button stays on the right. */
.popup-head-text {
    display: flex; flex-direction: column; gap: 4px;
    flex: 1 1 auto; min-width: 0;
}
.popup-title {
    margin: 0; font-size: 14px; font-weight: 600;
    letter-spacing: 0.02em; text-transform: uppercase;
    color: var(--text); flex: 0 0 auto;
}
.popup-sub-row {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    min-width: 0;
}
.popup-sub {
    font-family: var(--mono);
    color: var(--text-faint);
    font-size: 11.5px;
    min-width: 0;
    /* ``overflow-wrap: anywhere`` breaks a token only when it can't fit
       otherwise (e.g. the 42-char hex address). Prior ``word-break:
       break-all`` was aggressive and also split plain words like "last
       24h" at character boundaries — "la" / "st 24h". */
    overflow-wrap: anywhere;
}
.popup-copy {
    background: none; border: 1px solid var(--border); border-radius: 4px;
    color: var(--text-dim);
    font-family: var(--sans); font-size: 10.5px; letter-spacing: 0.04em;
    text-transform: uppercase;
    padding: 2px 8px; cursor: pointer; flex: 0 0 auto;
    transition: color 120ms, border-color 120ms, background 120ms;
}
.popup-copy:hover { color: var(--text); border-color: var(--text-dim); }
.popup-copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.popup-copy.copied {
    color: var(--ok); border-color: rgba(107,207,118,0.45);
    background: rgba(107,207,118,0.08);
}
.popup-copy.hidden { display: none; }
.popup-close {
    background: none; border: 1px solid var(--border); border-radius: 4px;
    color: var(--text-dim); font-size: 14px; line-height: 1;
    padding: 3px 9px; cursor: pointer; flex: 0 0 auto;
    transition: color 120ms, border-color 120ms;
}
.popup-close:hover { color: var(--text); border-color: var(--text-dim); }
.popup-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.popup-body {
    padding: 14px 16px 10px;
    overflow-y: auto;
    flex: 1 1 auto;
    line-height: 1.5;
}
.popup-body.loading { color: var(--text-faint); font-style: italic; padding: 24px 16px; }

.popup-foot {
    border-top: 1px solid var(--border);
    padding: 10px 16px;
    display: flex; gap: 14px; flex-wrap: wrap;
    font-size: 12px;
}
.popup-foot a { color: var(--accent); text-decoration: none; }
.popup-foot a:hover { color: var(--accent-dim); text-decoration: underline; }
.popup-foot .foot-note { color: var(--text-faint); font-size: 11.5px; }

.pk-grid {
    display: grid; gap: 10px;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    margin: 4px 0 12px;
}
.pk-cell {
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 8px 10px;
}
.pk-label {
    font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-faint);
}
.pk-val {
    font-family: var(--mono);
    font-size: 16px; font-weight: 600;
    color: var(--text);
    margin-top: 2px;
}
.pk-sub {
    font-size: 11px; color: var(--text-faint); margin-top: 2px;
    font-family: var(--mono);
}

.pk-section {
    margin: 14px 0 6px;
    font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--text-faint);
    font-weight: 600;
}
.pk-section:first-child { margin-top: 2px; }

.pk-val.ok { color: var(--ok); }
.pk-version-list {
    list-style: none; padding: 0; margin: 0;
    display: flex; flex-wrap: wrap; gap: 6px;
}
.pk-version-list li {
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 4px 8px;
    font-size: 12px;
}
.pk-version-list code { font-family: var(--mono); color: var(--text); }

.pk-top-list { display: flex; flex-direction: column; gap: 6px; }
.pk-top-row {
    display: grid; grid-template-columns: 1fr 88px 70px;
    align-items: center; gap: 10px;
    padding: 6px 10px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 5px;
    font-size: 12px;
    cursor: pointer;
    transition: border-color 120ms;
}
.pk-top-row:hover { border-color: var(--accent-dim); }
.pk-top-row .name { font-weight: 600; color: var(--text); }
.pk-top-row .addr { font-family: var(--mono); color: var(--text-faint); font-size: 11px; }
.pk-top-row .cat {
    display: inline-block; margin-left: 6px;
    padding: 1px 6px; border-radius: 3px;
    background: var(--bg);
    border: 1px solid var(--border);
    font-size: 10px; text-transform: uppercase; color: var(--text-dim);
    letter-spacing: 0.05em;
}
.pk-top-row .metric { text-align: right; font-family: var(--mono); color: var(--text-dim); }
.pk-top-row .share-bar {
    height: 4px; background: var(--border); border-radius: 2px; overflow: hidden;
    margin-top: 4px;
}
.pk-top-row .share-bar-fill { height: 100%; background: var(--accent); }
.pk-top-row .share-pct { text-align: right; font-family: var(--mono); font-size: 11px; color: var(--text-dim); }

.pk-dom-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.pk-dom-table th, .pk-dom-table td {
    text-align: right; padding: 6px 8px;
    border-bottom: 1px solid var(--border);
    font-family: var(--mono);
}
.pk-dom-table th:first-child, .pk-dom-table td:first-child { text-align: left; }
.pk-dom-table th {
    color: var(--text-faint); font-weight: 600;
    text-transform: uppercase; font-size: 10.5px;
    letter-spacing: 0.05em;
}

.pk-spark { width: 100%; height: 40px; display: block; }
.pk-spark polyline { fill: none; stroke-width: 1.5; }
/* Event marker in a sparkline (used by the reorg popup). A ▼ at the
   top points at the block, a dot anchors to the data line — no
   through-the-chart vertical which visually competes with the series. */
.pk-spark-marker-triangle { fill: var(--crit); }
.pk-spark-marker-dot {
    fill: var(--crit); stroke: var(--bg-card); stroke-width: 1.5;
}
.pk-spark-caption {
    font-family: var(--mono); font-size: 10.5px;
    color: var(--text-faint); margin-top: 2px;
}

.pk-interpret {
    padding: 8px 10px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: 4px;
    font-size: 12px;
    color: var(--text);
    margin-top: 8px;
}
.pk-interpret strong { color: var(--accent); }

.pk-peak-card {
    display: flex; align-items: center; gap: 12px;
    padding: 10px 12px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 6px;
    cursor: pointer;
    transition: border-color 120ms;
}
.pk-peak-card:hover { border-color: var(--accent-dim); }
.pk-peak-card .block-num {
    font-family: var(--mono); font-size: 16px; color: var(--accent);
    font-weight: 600;
}
.pk-peak-card .detail { flex: 1 1 auto; color: var(--text-dim); font-size: 11.5px; }

.pk-reorg-banner {
    padding: 8px 12px;
    background: rgba(255, 107, 107, 0.08);
    border: 1px solid var(--crit);
    border-radius: 4px;
    font-size: 12px;
    margin-bottom: 10px;
    display: flex; justify-content: space-between; align-items: center;
    gap: 10px; flex-wrap: wrap;
}
.pk-reorg-banner a { color: var(--crit); font-weight: 600; text-decoration: none; }
.pk-reorg-banner a:hover { text-decoration: underline; }

.pk-ids { display: flex; flex-direction: column; gap: 4px; margin: 6px 0 10px; }
.pk-id-row {
    display: flex; gap: 8px; align-items: baseline;
    font-family: var(--mono); font-size: 12px;
}
.pk-id-row .lbl { color: var(--text-faint); width: 56px; flex: 0 0 auto; font-size: 10.5px; text-transform: uppercase; }
.pk-id-row .val { color: var(--text); word-break: break-all; }
.pk-id-row.before .val::before { content: "\2212"; color: var(--text-faint); margin-right: 4px; }
.pk-id-row.after .val::before { content: "+"; color: var(--ok); margin-right: 4px; }

.pk-copy-btn {
    display: inline-block; padding: 0 6px; margin-left: 4px;
    font-size: 10.5px; background: none;
    border: 1px solid var(--border); border-radius: 3px;
    color: var(--text-faint); cursor: pointer;
    font-family: var(--mono);
}
.pk-copy-btn:hover { color: var(--text); border-color: var(--text-dim); }

/* Clickable block number / address cells on the main dashboard */
.bn-click {
    cursor: pointer; border-bottom: 1px dotted var(--accent-dim);
    transition: color 120ms;
}
.bn-click:hover { color: var(--accent); }
.bn-click:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

@media (max-width: 560px) {
    .popup-panel {
        width: calc(100vw - 12px);
        max-height: calc(100vh - 24px);
        max-height: calc(100dvh - 24px);
    }
    .popup-body { padding: 12px; }
    .pk-grid { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 8px; }
    .pk-top-row { grid-template-columns: 1fr 70px; }
    .pk-top-row .share-cell { display: none; }
}

/* Body-scroll lock when a popup is open. Keeps the dashboard beneath
   from scrolling underneath the modal on touch devices (otherwise a
   momentum swipe can drift the page behind the backdrop). Toggled by
   ``openPopup`` / ``closePopup`` in app.js. */
body.popup-open { overflow: hidden; touch-action: none; }

/* Pattern tag shown in the contract popup header row. "uniform" flags
   the 1-tx-per-block signature — common bot pattern. Kept muted so it
   informs without hijacking the layout. */
.pk-pattern-tag {
    display: inline-block;
    margin-top: 4px;
    padding: 2px 8px;
    font-size: 11px;
    font-family: var(--mono);
    color: var(--warn);
    background: rgba(255, 180, 84, 0.08);
    border: 1px solid var(--warn);
    border-radius: 3px;
    letter-spacing: 0.02em;
}
.pk-pattern-tag .pk-pattern-label {
    font-weight: 600; text-transform: uppercase; margin-right: 6px;
    letter-spacing: 0.05em; font-size: 10px;
}
