/* RoadIntel — shared front-end tokens and components.
   Served at /static/ri.css. Derived from mocks/ri-mock.css with the
   mock-only chrome removed (banner, phone frames, gallery cards,
   annotation blocks). When a mock and this file disagree, the mock is
   the spec — port the change here rather than diverging.
   Three token vocabularies used to exist across the three pages; this is
   the one that replaces them. Do not add colours to a page. */

/* ============================================================================
   RoadIntel — mock design system
   The single token + component vocabulary every mock in this folder uses.
   Mirrors UIUX-FOUNDATIONS.md §5.1 (tokens) and §5.2 (38 components).
   Ships to /static/ri.css. Mocks must NOT define their own colours or
   components here — add to this file instead.
   ========================================================================= */

/* ---------- 1 · TOKENS ---------------------------------------------------- */
:root {
  color-scheme: light;

  /* brand — unchanged from what ships; do not invent a new visual language */
  --ri-navy:#1E2761;  --ri-navy-2:#2b3576;  --ri-cyan:#17B9C7;

  /* surface + ink */
  --ri-page:#f9f9f7;  --ri-surface:#fcfcfb;  --ri-surface-2:#f2f1ec;
  --ri-ink-1:#0b0b0b; --ri-ink-2:#52514e;    --ri-ink-3:#6f6d67;
  --ri-grid:#e1e0d9;  --ri-border:rgba(11,11,11,.10);

  /* age / freshness — ordinal, glyph-paired (never colour alone) */
  --ri-age-fresh:#0f7a6c; --ri-age-ageing:#a67c00;
  --ri-age-stale:#c2410c; --ri-age-never:#6b7280;

  /* status */
  --ri-ok:#0f7a6c; --ri-warn:#a67c00; --ri-crit:#c0392b; --ri-info:#1f6feb;

  /* risk — diverging, neutral at the 0.5 decision threshold (shipped scale) */
  --ri-risk-0:#2a78d6; --ri-risk-1:#6da7ec; --ri-risk-2:#b7d3f6;
  /* THE 0.50 BUCKET — the decision threshold, and it was #f0efec: contrast
     1.00 against the positron basemap, i.e. exactly the map. A segment sitting
     on the boundary that decides whether a crew is dispatched drew as nothing.
     The dark-theme override below had the same bug mirrored (#3a3a38 was 1.33
     against the dark basemap), and could never have fixed it: this token is
     scoped to the UI THEME while the surface it lands on is the BASEMAP, and
     the two are chosen separately (setBasemap reads its own localStorage key).
     Light UI + light basemap and dark UI + dark basemap were both invisible.
     So: one basemap-agnostic mid-tone, measured to clear 3:1 on positron
     (3.65), dark_all (3.61) and all four UI surfaces (3.76–4.19). It makes the
     threshold the most prominent point on a diverging ramp, which is unusual
     and, for a decision boundary, correct. */
  --ri-risk-3:#7c7c78; --ri-risk-4:#f0b3ad; --ri-risk-5:#e34948;
  --ri-risk-6:#a82c2c;

  /* streams — Okabe–Ito subset, deliberately off the risk axis */
  --ri-stream-das:#56B4E9;  --ri-stream-eiri:#E69F00; --ri-stream-nsv:#009E73;
  --ri-stream-fwd:#CC79A7;  --ri-stream-dams:#3d3d3d;
  /* TSD is the SIXTH stream (contracts/spatial.py Stream.TSD) and the design
     system had no token for it — a page built faithfully to mocks/home.html,
     which draws five, would have hidden a whole instrument. It measures
     deflection like FWD, and was previously being relabelled AS FWD until T19
     separated them, so it must be unmistakably its own mark rather than a
     near-miss of FWD's. Okabe-Ito #0072B2: a darker VALUE than DAS's light
     blue, so the two stay apart for a deuteranope even before the shape. */
  --ri-stream-tsd:#0072B2;

  /* type */
  --ri-fs-eyebrow:11px; --ri-fs-sm:12.5px; --ri-fs-base:14px;
  --ri-fs-md:16px; --ri-fs-lg:20px; --ri-fs-xl:26px;
  --ri-font:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  --ri-mono:ui-monospace,SFMono-Regular,"Cascadia Mono",Consolas,monospace;

  /* space · radii · elevation · motion */
  --ri-sp-1:4px; --ri-sp-2:8px; --ri-sp-3:12px; --ri-sp-4:16px;
  --ri-sp-5:24px; --ri-sp-6:32px; --ri-sp-7:48px;
  --ri-r-sm:4px; --ri-r-md:7px; --ri-r-lg:10px; --ri-r-pill:999px;
  --ri-e-1:0 1px 2px rgba(0,0,0,.06);
  --ri-e-2:0 6px 18px rgba(0,0,0,.18);
  --ri-e-3:0 12px 40px rgba(0,0,0,.28);
  --ri-t-fast:120ms cubic-bezier(.2,0,.2,1);
  --ri-t-base:180ms cubic-bezier(.2,0,.2,1);
  --ri-focus:0 0 0 2px var(--ri-page),0 0 0 4px var(--ri-cyan);

  --ri-header-h:46px;
  --ri-accent-strong:var(--ri-navy);
  /* Ink for text sitting ON --ri-navy. Not --ri-page or --ri-surface: those
     invert with the theme, and the navy does not — a pressed control would
     turn near-black-on-navy in dark mode. It is a constant because its
     background is. */
  --ri-on-navy:#ffffff;
  /* Ink for text on --ri-warn. Amber is light in both themes, so this stays
     near-black in both — the same reason --ri-on-navy stays white. It already
     existed as a literal inside .ri-pill.solid.warn; naming it is what lets a
     page use it without tripping test_no_colour_literals. */
  --ri-on-warn:#150f00;
}

@media (prefers-color-scheme:dark){ :root:where(:not([data-theme="light"])) { color-scheme:dark;
  --ri-page:#0d0d0d; --ri-surface:#1a1a19; --ri-surface-2:#232321;
  --ri-ink-1:#ffffff; --ri-ink-2:#c3c2b7; --ri-ink-3:#9a988f;
  --ri-grid:#2c2c2a; --ri-border:rgba(255,255,255,.10);
  --ri-age-fresh:#3fbfa8; --ri-age-ageing:#d6a520; --ri-age-stale:#f0764a;
  --ri-ok:#3fbfa8; --ri-warn:#d6a520; --ri-crit:#e8695c; --ri-info:#589bff;
  --ri-accent-strong:var(--ri-cyan);
}}
:root[data-theme="dark"]{ color-scheme:dark;
  --ri-page:#0d0d0d; --ri-surface:#1a1a19; --ri-surface-2:#232321;
  --ri-ink-1:#ffffff; --ri-ink-2:#c3c2b7; --ri-ink-3:#9a988f;
  --ri-grid:#2c2c2a; --ri-border:rgba(255,255,255,.10);
  --ri-age-fresh:#3fbfa8; --ri-age-ageing:#d6a520; --ri-age-stale:#f0764a;
  --ri-ok:#3fbfa8; --ri-warn:#d6a520; --ri-crit:#e8695c; --ri-info:#589bff;
  --ri-accent-strong:var(--ri-cyan);
}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}
}

/* ---------- 2 · RESET + BASE --------------------------------------------- */
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{font-family:var(--ri-font);font-size:var(--ri-fs-base);line-height:1.45;
  background:var(--ri-page);color:var(--ri-ink-1);-webkit-text-size-adjust:100%}
h1,h2,h3,h4{margin:0;font-weight:700;line-height:1.25}
h1{font-size:var(--ri-fs-lg)}
h2{font-size:var(--ri-fs-md)}
h3{font-size:var(--ri-fs-eyebrow);text-transform:uppercase;letter-spacing:.06em;
  color:var(--ri-ink-2);font-weight:700}
p{margin:0 0 var(--ri-sp-3)}
a{color:var(--ri-info);text-decoration:none}
a:hover{text-decoration:underline}
:focus-visible{outline:none;box-shadow:var(--ri-focus);border-radius:var(--ri-r-sm)}
code,.ri-mono{font-family:var(--ri-mono);font-size:.92em}
.num,td.num,.ri-num{font-variant-numeric:tabular-nums}
table{border-collapse:collapse;width:100%}
img{max-width:100%;display:block}
[hidden]{display:none!important}

/* ---------- 3 · APP SHELL ------------------------------------------------- */
.ri-header{background:var(--ri-navy);color:#fff;display:flex;align-items:center;
  gap:var(--ri-sp-1);padding:0 var(--ri-sp-4);min-height:var(--ri-header-h);
  position:sticky;top:0;z-index:1600}
/* 1600, above every layer that can sit under it. The header is sticky WITH a
   z-index, so it forms a stacking context: its children — the nav panel and
   the account menu — can never rise above it, no matter what z-index they
   carry. At 900 that put the whole shell BELOW Leaflet's map controls (1000)
   and below console.html's search results (1200) and context menu (1500), so
   the open nav drew behind a zoom button. Raising the children was the
   obvious fix and could not work; the header itself had to move. */
.ri-header .brand{display:flex;align-items:center;gap:var(--ri-sp-2);
  font-weight:800;color:var(--ri-cyan);font-size:15px;margin-right:var(--ri-sp-4);
  text-decoration:none;white-space:nowrap}
.ri-nav{display:flex;align-self:stretch;gap:2px}
.ri-nav a{display:flex;align-items:center;padding:0 var(--ri-sp-3);
  color:rgba(255,255,255,.72);font-size:13px;font-weight:600;text-decoration:none;
  border-bottom:3px solid transparent;border-top:3px solid transparent}
.ri-nav a:hover{color:#fff;text-decoration:none}
.ri-nav a[aria-current="page"]{color:#fff;border-bottom-color:var(--ri-cyan)}
.ri-header .right{margin-left:auto;display:flex;align-items:center;gap:var(--ri-sp-2)}

/* ---- shell controls: nav disclosure + identity menu ----------------------
   Built by ri.js mountShell() into whatever header a page already has, so
   there is one implementation rather than eight copies — the same rule
   deploy/ci/check_shell_vocabulary.sh enforces for the fetch helper. */
/* Always visible, not only on a phone: in direction D the destinations live
   in the palette, and this is the POINTER path to the same places. A palette
   that is the only way to navigate is a product with no menu, which is D's
   one real risk — this is the mitigation, not an afterthought. */
.ri-iconbtn{display:inline-flex;align-items:center;justify-content:center;
  width:34px;height:34px;flex:0 0 34px;border-radius:var(--ri-r-md);
  border:1px solid rgba(255,255,255,.22);background:rgba(255,255,255,.10);
  color:#fff;cursor:pointer;padding:0}
.ri-iconbtn:hover{background:rgba(255,255,255,.18)}
.ri-iconbtn svg{display:block}

/* The identity control. Replaces a bare "sign out" text link: sign-out is a
   destructive, rarely-wanted action that was the single most prominent
   control in the bar, while WHO YOU ARE — the thing that changes what every
   number on the page means — was not shown at all on six of the pages. */
.ri-user{display:flex;align-items:center;gap:7px;max-width:230px;
  border:1px solid rgba(255,255,255,.22);background:rgba(255,255,255,.10);
  border-radius:var(--ri-r-pill);padding:3px 10px 3px 3px;color:#fff;
  font:inherit;font-size:var(--ri-fs-sm);font-weight:600;cursor:pointer}
.ri-user:hover{background:rgba(255,255,255,.18)}
.ri-user .av{width:26px;height:26px;flex:0 0 26px;border-radius:50%;
  background:var(--ri-cyan);color:var(--ri-navy);display:grid;place-items:center;
  font-size:10.5px;font-weight:800;letter-spacing:.02em}
.ri-user .who{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.ri-user .caret{opacity:.7;flex:0 0 auto}

.ri-menuwrap{position:relative;display:flex;align-items:center}
.ri-menu{position:absolute;top:calc(100% + 6px);right:0;min-width:250px;
  background:var(--ri-surface);color:var(--ri-ink-1);border:1px solid var(--ri-grid);
  border-radius:var(--ri-r-lg);box-shadow:var(--ri-e-2);padding:var(--ri-sp-2);
  /* Only ever compared against its siblings inside the header's own stacking
     context, so a small number is the honest one. */
  z-index:3}
.ri-menu[hidden]{display:none}
.ri-menu .head{padding:var(--ri-sp-2);border-bottom:1px solid var(--ri-grid);
  margin-bottom:var(--ri-sp-2)}
.ri-menu .head .em{font-weight:700;font-size:var(--ri-fs-sm);word-break:break-all}
.ri-menu .head .meta{font-size:var(--ri-fs-eyebrow);color:var(--ri-ink-3);margin-top:2px}
.ri-menu .lbl{font-size:var(--ri-fs-eyebrow);text-transform:uppercase;
  letter-spacing:.06em;color:var(--ri-ink-3);font-weight:700;
  padding:var(--ri-sp-2) var(--ri-sp-2) 4px}
.ri-menu a,.ri-menu button{display:flex;align-items:center;gap:8px;width:100%;
  padding:7px var(--ri-sp-2);border:0;background:none;border-radius:var(--ri-r-md);
  color:var(--ri-ink-1);font:inherit;font-size:var(--ri-fs-sm);text-align:left;
  cursor:pointer;text-decoration:none}
.ri-menu a:hover,.ri-menu button:hover{background:var(--ri-surface-2);text-decoration:none}
.ri-menu button[aria-current="true"]::after{content:"✓";margin-left:auto;color:var(--ri-ok)}
.ri-menu .sep{height:1px;background:var(--ri-grid);margin:var(--ri-sp-2) 0}
.ri-menu .danger{color:var(--ri-crit);font-weight:600}

/* ---- narrow: the nav becomes a disclosure, not a clipped scroller --------
   It used to be overflow-x:auto with the scrollbar hidden, so at 390px the
   eighth item was sliced through the middle of a word. A clipped word reads
   as broken; nothing about it says "scroll me". */
/* The nav panel — a drop-down from the bar, at every width. */
.ri-header{position:sticky;flex-wrap:nowrap}
.ri-nav{position:absolute;top:100%;left:0;display:none;width:min(260px,100vw);
  flex-direction:column;align-self:auto;gap:1px;background:var(--ri-navy);
  border-bottom-right-radius:var(--ri-r-lg);
  border-top:1px solid rgba(255,255,255,.14);box-shadow:var(--ri-e-3);
  padding:var(--ri-sp-2);z-index:2;max-height:78vh;overflow-y:auto}
.ri-nav.open{display:flex}
.ri-nav a{padding:9px var(--ri-sp-3);border-radius:var(--ri-r-md);
  border-top:0;border-bottom:0;font-size:var(--ri-fs-sm);color:rgba(255,255,255,.8)}
.ri-nav a:hover{background:rgba(255,255,255,.10);color:#fff}
.ri-nav a[aria-current="page"]{background:rgba(255,255,255,.16);color:#fff}
.ri-nav .grp{font-size:10px;letter-spacing:.08em;text-transform:uppercase;
  color:rgba(255,255,255,.45);font-weight:800;padding:8px var(--ri-sp-3) 3px}

/* ---- the command trigger: an input that is really a button ---------------
   Visible chrome on purpose. D's stated weakness is discoverability — a
   first-time user who does not know about a keyboard shortcut sees nothing —
   so the shortcut is advertised in the bar rather than hidden behind it. */
.ri-cmd{display:flex;align-items:center;gap:8px;flex:0 1 420px;min-width:0;
  margin:0 auto;background:rgba(255,255,255,.10);color:rgba(255,255,255,.78);
  border:1px solid rgba(255,255,255,.20);border-radius:var(--ri-r-md);
  padding:5px 9px;font:inherit;font-size:var(--ri-fs-sm);cursor:pointer;
  text-align:left}
.ri-cmd:hover{background:rgba(255,255,255,.17);color:#fff}
.ri-cmd .ph{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.ri-cmd kbd{margin-left:auto;flex:0 0 auto;background:rgba(255,255,255,.16);
  border-radius:var(--ri-r-sm);padding:1px 6px;font-size:10.5px;
  font-family:var(--ri-font);font-weight:700}

/* ---- the palette --------------------------------------------------------
   Appended to <body>, not to the header: the header is a stacking context, and
   a dialog that can be trapped underneath a map control is not a dialog. */
.ri-palette{position:fixed;inset:0;z-index:5000;display:flex;
  justify-content:center;align-items:flex-start;padding:11vh var(--ri-sp-4) var(--ri-sp-4);
  background:rgba(0,0,0,.45)}
.ri-palette[hidden]{display:none}
.ri-palette .box{width:min(620px,100%);max-height:74vh;display:flex;
  flex-direction:column;background:var(--ri-surface);color:var(--ri-ink-1);
  border:1px solid var(--ri-border);border-radius:var(--ri-r-lg);
  box-shadow:var(--ri-e-3);overflow:hidden}
.ri-palette input{width:100%;border:0;border-bottom:1px solid var(--ri-grid);
  background:none;color:var(--ri-ink-1);font:inherit;font-size:var(--ri-fs-md);
  padding:14px var(--ri-sp-4);outline:none}
.ri-palette .results{overflow-y:auto;padding:6px}
.ri-palette .grp{font-size:10px;letter-spacing:.07em;text-transform:uppercase;
  color:var(--ri-ink-3);font-weight:800;padding:9px 10px 3px}
.ri-palette .row{display:flex;align-items:center;gap:10px;width:100%;
  padding:8px 10px;border:0;background:none;border-radius:var(--ri-r-md);
  color:var(--ri-ink-1);font:inherit;font-size:var(--ri-fs-sm);text-align:left;
  cursor:pointer}
.ri-palette .row .meta{margin-left:auto;flex:0 0 auto;color:var(--ri-ink-3);
  font-size:var(--ri-fs-eyebrow);white-space:nowrap}
.ri-palette .row .tag{flex:0 0 auto;font-size:10px;font-weight:800;
  border-radius:var(--ri-r-sm);padding:1.5px 5px;background:var(--ri-surface-2);
  color:var(--ri-ink-2)}
.ri-palette .row[aria-selected="true"]{background:var(--ri-navy);color:#fff}
.ri-palette .row[aria-selected="true"] .meta,
.ri-palette .row[aria-selected="true"] .tag{color:rgba(255,255,255,.8)}
.ri-palette .row[aria-selected="true"] .tag{background:rgba(255,255,255,.18)}
.ri-palette .none{padding:var(--ri-sp-5) var(--ri-sp-4);text-align:center;
  color:var(--ri-ink-3);font-size:var(--ri-fs-sm)}
.ri-palette .foot{display:flex;gap:var(--ri-sp-3);padding:7px var(--ri-sp-4);
  border-top:1px solid var(--ri-grid);font-size:var(--ri-fs-eyebrow);
  color:var(--ri-ink-3)}
.ri-palette .foot b{color:var(--ri-ink-2);font-weight:700}

@media (max-width:760px){
  /* The trigger keeps the icon and the shortcut hint goes: on a phone there
     is no keyboard to press it with, and the space is worth more. */
  .ri-cmd{flex:1 1 auto;margin:0}
  .ri-cmd kbd{display:none}
  .ri-user{max-width:44px;padding:3px}
  .ri-user .who,.ri-user .caret{display:none}
}

.ri-search{display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.10);
  border:1px solid rgba(255,255,255,.20);border-radius:var(--ri-r-md);
  padding:4px 8px;color:rgba(255,255,255,.75);font-size:var(--ri-fs-sm);min-width:210px}
.ri-search kbd{margin-left:auto;background:rgba(255,255,255,.16);border-radius:var(--ri-r-sm);
  padding:1px 5px;font-size:10.5px;font-family:var(--ri-font)}

.ri-loc{display:flex;align-items:center;gap:7px;background:rgba(255,255,255,.10);
  border:1px solid rgba(255,255,255,.22);border-radius:var(--ri-r-pill);
  padding:4px 12px 4px 9px;color:#fff;font-size:var(--ri-fs-sm);font-weight:600}
/* The corridor picker is a <select> sitting on the navy pill. Left as browser
   chrome it renders unreadable there. colour:inherit takes .ri-loc's own
   colour rather than restating one. Promoted here from console.html because
   all three pages carry this picker — three copies of it is the exact
   duplication the shared shell exists to end. */
.ri-loc select{appearance:none;-webkit-appearance:none;background:transparent;
  border:0;color:inherit;font:inherit;font-size:var(--ri-fs-sm);font-weight:600;
  max-width:230px;cursor:pointer;outline:none}
.ri-loc select option{color:var(--ri-ink-1);background:var(--ri-surface);font-weight:400}
.ri-loc .nh{font-size:10px;font-weight:800;color:var(--ri-navy);background:var(--ri-cyan);
  border-radius:var(--ri-r-sm);padding:1.5px 5px;letter-spacing:.04em}
/* Nothing selected. Without this the control is a pin and a dash — a
   rendering fault to look at, on the object every page is scoped to. */
.ri-loc.empty{border-style:dashed;color:rgba(255,255,255,.66);font-weight:600}
.ri-loc.empty .nh,.ri-loc.empty select{display:none}
.ri-locempty{white-space:nowrap}
.ri-env{font-size:10px;font-weight:800;letter-spacing:.08em;border-radius:var(--ri-r-sm);
  padding:2px 6px;background:var(--ri-warn);color:#150f00}
.ri-env.prod{background:var(--ri-ok);color:#00201b}
.ri-env.bad{background:var(--ri-crit);color:#fff}
.ri-bell{position:relative;color:rgba(255,255,255,.85);font-size:15px;text-decoration:none}
.ri-bell b{position:absolute;top:-5px;right:-8px;background:var(--ri-crit);color:#fff;
  border-radius:var(--ri-r-pill);font-size:9.5px;padding:1px 4px;font-weight:800}
.ri-avatar{width:26px;height:26px;border-radius:var(--ri-r-pill);background:var(--ri-cyan);
  color:var(--ri-navy);font-weight:800;font-size:11px;display:grid;place-items:center}

/* tenant-fallback / global warning strip */
.ri-globalwarn{background:color-mix(in srgb,var(--ri-warn) 16%,var(--ri-surface));
  border-bottom:1px solid var(--ri-grid);color:var(--ri-ink-1);
  font-size:var(--ri-fs-sm);padding:6px var(--ri-sp-4);display:flex;gap:8px;align-items:center}

/* layout: rail · content · drawer */
.ri-layout{display:grid;grid-template-columns:240px 1fr;gap:var(--ri-sp-3);
  padding:var(--ri-sp-3);align-items:start}
.ri-layout.with-drawer{grid-template-columns:240px 1fr 420px}
.ri-layout.norail{grid-template-columns:1fr}
.ri-layout.norail.with-drawer{grid-template-columns:1fr 420px}
.ri-rail{display:flex;flex-direction:column;gap:var(--ri-sp-3);position:sticky;top:calc(var(--ri-header-h) + 12px)}
.ri-main{min-width:0;display:flex;flex-direction:column;gap:var(--ri-sp-3)}
.ri-page{max-width:1320px;margin:0 auto;padding:var(--ri-sp-4)}
.ri-page.narrow{max-width:720px}

@media (max-width:1100px){
  /* must out-specify .ri-layout.norail.with-drawer (3 classes) or the
     drawer keeps its fixed 420px track and the page scrolls sideways */
  .ri-layout,.ri-layout.with-drawer,
  .ri-layout.norail,.ri-layout.norail.with-drawer{grid-template-columns:minmax(0,1fr)}
  .ri-rail{position:static;order:2}
  .ri-search{display:none}
  /* The nav is a disclosure below 900px (see the shell-controls block), so it
     no longer needs to scroll here. What still has to give is the corridor
     label — truncate that, never the page. */
  .ri-nav a{white-space:nowrap}
  .ri-header .right{min-width:0}
  .ri-loc{min-width:0;max-width:190px}
  .ri-loc > span:not(.nh),.ri-loc select{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
}
@media (max-width:640px){
  /* NOT flex-wrap:wrap any more. Wrapping put brand, nav and identity on three
     rows and spent ~130px of an 844px viewport before a single number showed.
     One row, and the nav drops over the content when asked for. */
  .ri-header{padding:0 var(--ri-sp-2)}
  .ri-loc{max-width:150px;overflow:hidden}
}

/* ---------- 4 · CARDS · SECTIONS ----------------------------------------- */
.ri-card{background:var(--ri-surface);border:1px solid var(--ri-grid);
  border-radius:var(--ri-r-lg);padding:var(--ri-sp-4);box-shadow:var(--ri-e-1)}
.ri-card.tight{padding:var(--ri-sp-3)}
.ri-card.flush{padding:0;overflow:hidden}
.ri-card > h3{margin-bottom:var(--ri-sp-3)}
.ri-cardhead{display:flex;align-items:center;gap:var(--ri-sp-2);
  padding:var(--ri-sp-3) var(--ri-sp-4);border-bottom:1px solid var(--ri-grid)}
.ri-cardhead h3{margin:0}
.ri-cardhead .sp{margin-left:auto}
.ri-cardbody{padding:var(--ri-sp-4)}
.ri-sectionhead{display:flex;align-items:baseline;gap:var(--ri-sp-3);
  margin:var(--ri-sp-5) 0 var(--ri-sp-3);padding-bottom:var(--ri-sp-2);
  border-bottom:1px solid var(--ri-grid)}
.ri-grid2{display:grid;grid-template-columns:1fr 1fr;gap:var(--ri-sp-3)}
.ri-grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--ri-sp-3)}
.ri-grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--ri-sp-3)}
@media (max-width:900px){.ri-grid2,.ri-grid3,.ri-grid4{grid-template-columns:1fr}}
.ri-row{display:flex;align-items:center;gap:var(--ri-sp-2);flex-wrap:wrap}
.ri-row.tight{gap:var(--ri-sp-1)}
.ri-sp{margin-left:auto}
.ri-muted{color:var(--ri-ink-3)}
.ri-dim{color:var(--ri-ink-2)}
.ri-eyebrow{font-size:var(--ri-fs-eyebrow);text-transform:uppercase;
  letter-spacing:.06em;color:var(--ri-ink-2);font-weight:700}
.ri-small{font-size:var(--ri-fs-sm)}
hr.ri-rule{border:0;border-top:1px solid var(--ri-grid);margin:var(--ri-sp-4) 0}

/* ---------- 5 · CONTROLS -------------------------------------------------- */
.ri-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;
  padding:8px 14px;border:1px solid transparent;border-radius:var(--ri-r-md);
  background:var(--ri-navy);color:#fff;font:inherit;font-size:var(--ri-fs-sm);
  font-weight:700;cursor:pointer;transition:background var(--ri-t-fast)}
.ri-btn:hover{background:var(--ri-navy-2)}
.ri-btn.secondary{background:var(--ri-surface);color:var(--ri-ink-1);border-color:var(--ri-grid)}
.ri-btn.secondary:hover{background:var(--ri-surface-2)}
.ri-btn.ghost{background:transparent;color:var(--ri-ink-2);border-color:transparent}
.ri-btn.ghost:hover{background:var(--ri-surface-2);color:var(--ri-ink-1)}
.ri-btn.danger{background:var(--ri-crit)}
.ri-btn.sm{padding:4px 9px;font-size:var(--ri-fs-eyebrow)}
.ri-btn.lg{padding:13px 20px;font-size:var(--ri-fs-md)}
.ri-btn.block{width:100%}
.ri-btn[disabled],.ri-btn:disabled{opacity:.45;cursor:not-allowed}
.ri-btngroup{display:inline-flex;border:1px solid var(--ri-grid);border-radius:var(--ri-r-md);overflow:hidden}
.ri-btngroup button{border:0;background:var(--ri-surface);color:var(--ri-ink-2);
  padding:5px 11px;font:inherit;font-size:var(--ri-fs-sm);font-weight:600;cursor:pointer}
.ri-btngroup button[aria-pressed="true"]{background:var(--ri-navy);color:#fff}
.ri-btngroup button + button{border-left:1px solid var(--ri-grid)}

label.ri-lbl{display:block;font-size:var(--ri-fs-sm);color:var(--ri-ink-2);
  margin:0 0 var(--ri-sp-1);font-weight:600}
/* email and password join the list because login is the first page with
   either. Without them those two fields keep the user-agent's own metrics —
   visibly shorter, differently padded — next to identical-looking text
   inputs, and a sign-in form is the one screen where that is most obvious. */
.ri-input,input[type=text].ri-input,input[type=date].ri-input,
input[type=number].ri-input,input[type=email].ri-input,
input[type=password].ri-input,select.ri-input,textarea.ri-input{
  width:100%;padding:8px 10px;border:1px solid var(--ri-grid);
  border-radius:var(--ri-r-md);background:var(--ri-surface);color:var(--ri-ink-1);
  font:inherit;font-size:var(--ri-fs-sm)}
.ri-field{margin-bottom:var(--ri-sp-3)}
.ri-field .hint{font-size:var(--ri-fs-sm);color:var(--ri-ink-3);margin-top:3px}
.ri-field.invalid .ri-input{border-color:var(--ri-crit);box-shadow:0 0 0 2px color-mix(in srgb,var(--ri-crit) 18%,transparent)}
.ri-field .err{font-size:var(--ri-fs-sm);color:var(--ri-crit);margin-top:3px;font-weight:600}
.ri-check{display:flex;align-items:center;gap:7px;font-size:var(--ri-fs-sm);cursor:pointer}
.ri-range{width:100%;accent-color:var(--ri-navy)}
.ri-switch{display:inline-flex;align-items:center;gap:7px;font-size:var(--ri-fs-sm);cursor:pointer}
.ri-switch i{width:32px;height:18px;border-radius:var(--ri-r-pill);background:var(--ri-grid);
  position:relative;transition:background var(--ri-t-fast);flex:none}
.ri-switch i::after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px;
  border-radius:50%;background:#fff;transition:transform var(--ri-t-fast);box-shadow:var(--ri-e-1)}
.ri-switch.on i{background:var(--ri-ok)}
.ri-switch.on i::after{transform:translateX(14px)}

/* ---------- 6 · PILLS · BADGES · CHIPS ----------------------------------- */
.ri-pill{display:inline-flex;align-items:center;gap:5px;border-radius:var(--ri-r-pill);
  padding:2px 9px;font-size:var(--ri-fs-eyebrow);font-weight:700;
  border:1px solid currentColor;background:transparent;white-space:nowrap}
.ri-pill::before{font-size:10px;line-height:1}
.ri-pill.fresh {color:var(--ri-age-fresh)}  .ri-pill.fresh::before {content:"●"}
.ri-pill.ageing{color:var(--ri-age-ageing)} .ri-pill.ageing::before{content:"◐"}
.ri-pill.stale {color:var(--ri-age-stale)}  .ri-pill.stale::before {content:"◔"}
.ri-pill.never {color:var(--ri-age-never)}  .ri-pill.never::before {content:"⊘"}
.ri-pill.ok    {color:var(--ri-ok)}         .ri-pill.ok::before    {content:"✓"}
.ri-pill.warn  {color:var(--ri-warn)}       .ri-pill.warn::before  {content:"▲"}
/* `crit` is an alias of `err`, not a second style. The COLOUR TOKEN is
   --ri-crit, so `crit` is the name a writer reaches for; admin.html and
   platform.html both did, in 8 places, and every one of them silently lost its
   colour and its glyph — "no corridors, can see nothing", tenant suspended,
   tenant orphaned, "reaches every tenant", "grants nothing". The highest-stakes
   pills in the product were the only ones rendering as plain neutral text.
   Aliasing beats renaming the call sites: the next writer will reach for the
   token's name too. */
.ri-pill.err,
.ri-pill.crit  {color:var(--ri-crit)}
.ri-pill.err::before,
.ri-pill.crit::before  {content:"✕"}
.ri-pill.info  {color:var(--ri-info)}       .ri-pill.info::before  {content:"ℹ"}
.ri-pill.neutral{color:var(--ri-ink-3)}     .ri-pill.neutral::before{content:"·"}
.ri-pill.solid{color:#fff;border-color:transparent}
.ri-pill.solid.ok{background:var(--ri-ok)} .ri-pill.solid.warn{background:var(--ri-warn);color:var(--ri-on-warn)}
.ri-pill.solid.err{background:var(--ri-crit)} .ri-pill.solid.info{background:var(--ri-info)}

/* GradeBadge — metrology filled, screening hollow+dashed. Never opacity alone. */
.ri-grade{display:inline-flex;align-items:center;gap:5px;font-size:var(--ri-fs-eyebrow);
  font-weight:700;border-radius:var(--ri-r-sm);padding:1px 6px;letter-spacing:.03em}
.ri-grade.metrology{background:var(--ri-navy);color:#fff;border:1px solid var(--ri-navy)}
.ri-grade.metrology::before{content:"■"}
.ri-grade.screening{background:transparent;color:var(--ri-ink-2);
  border:1px dashed var(--ri-ink-3)}
.ri-grade.screening::before{content:"□"}

/* stream marks — hue AND shape */
.ri-stream{display:inline-flex;align-items:center;gap:5px;font-size:var(--ri-fs-sm);font-weight:600}
.ri-stream::before{font-size:11px;line-height:1}
.ri-stream.das ::before,.ri-stream.das::before  {content:"●";color:var(--ri-stream-das)}
.ri-stream.eiri::before{content:"▲";color:var(--ri-stream-eiri)}
.ri-stream.nsv ::before,.ri-stream.nsv::before  {content:"■";color:var(--ri-stream-nsv)}
.ri-stream.fwd ::before,.ri-stream.fwd::before  {content:"◆";color:var(--ri-stream-fwd)}
.ri-stream.tsd ::before,.ri-stream.tsd::before  {content:"★";color:var(--ri-stream-tsd)}
.ri-stream.dams::before{content:"✚";color:var(--ri-stream-dams)}

.ri-chip{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--ri-grid);
  border-radius:var(--ri-r-pill);padding:2px 9px;font-size:var(--ri-fs-eyebrow);
  background:var(--ri-surface);color:var(--ri-ink-2);font-weight:600}
.ri-chip.on{background:var(--ri-navy);color:#fff;border-color:var(--ri-navy)}
.ri-tag{font-family:var(--ri-mono);font-size:11px;background:var(--ri-surface-2);
  border:1px solid var(--ri-grid);border-radius:var(--ri-r-sm);padding:1px 5px;color:var(--ri-ink-2)}

/* ---------- 7 · TRUST / HONESTY UI --------------------------------------- */
.ri-provenance{font-size:var(--ri-fs-sm);color:var(--ri-ink-2);display:flex;
  flex-wrap:wrap;gap:6px;align-items:center}
.ri-provenance .sep{color:var(--ri-ink-3)}

.ri-value{display:inline-flex;align-items:baseline;gap:4px;font-variant-numeric:tabular-nums}
.ri-value b{font-size:var(--ri-fs-md);font-weight:700}
.ri-value .unit{font-size:var(--ri-fs-sm);color:var(--ri-ink-2)}
.ri-value .qual{font-size:var(--ri-fs-sm);color:var(--ri-warn);font-style:italic}

.ri-conf{display:flex;align-items:center;gap:8px;font-size:var(--ri-fs-sm)}
.ri-conf .track{position:relative;flex:1;min-width:80px;height:8px;border-radius:var(--ri-r-pill);
  background:var(--ri-grid);overflow:hidden}
.ri-conf .track i{position:absolute;top:0;bottom:0;left:0;background:var(--ri-navy);border-radius:inherit}
.ri-conf .track u{position:absolute;top:-2px;bottom:-2px;background:color-mix(in srgb,var(--ri-navy) 26%,transparent);
  border-left:1px solid var(--ri-navy);border-right:1px solid var(--ri-navy)}

/* ComponentBasis — stacked weighted bar with greyed unassessed slots */
.ri-basis{display:flex;height:20px;border-radius:var(--ri-r-sm);overflow:hidden;
  border:1px solid var(--ri-grid);background:var(--ri-surface-2)}
.ri-basis span{display:block;font-size:9.5px;color:#fff;text-align:center;line-height:20px;font-weight:700}
.ri-basis .rough{background:var(--ri-stream-nsv)}
.ri-basis .rut{background:var(--ri-stream-eiri);color:#150f00}
.ri-basis .distress{background:var(--ri-stream-das);color:#00131f}
.ri-basis .struct{background:var(--ri-stream-fwd)}
.ri-basis .gap{background:repeating-linear-gradient(45deg,var(--ri-grid) 0 5px,transparent 5px 10px);
  color:var(--ri-ink-3)}
.ri-basisnote{font-size:var(--ri-fs-sm);color:var(--ri-warn);font-weight:600;margin-top:5px}

/* MissRateBadge */
.ri-missrate{border:1px solid var(--ri-navy);border-radius:var(--ri-r-lg);
  background:color-mix(in srgb,var(--ri-navy) 6%,var(--ri-surface));padding:var(--ri-sp-3)}
.ri-missrate .big{font-size:var(--ri-fs-xl);font-weight:800;font-variant-numeric:tabular-nums;
  color:var(--ri-accent-strong);line-height:1.1}
.ri-missrate .sentence{font-size:var(--ri-fs-sm);color:var(--ri-ink-2);margin-top:6px}
.ri-missrate.unknown{border-color:var(--ri-grid);background:var(--ri-surface-2)}
.ri-missrate.unknown .big{color:var(--ri-ink-3)}

/* NotAssessedPanel */
.ri-notassessed{border-left:3px solid var(--ri-warn);
  background:color-mix(in srgb,var(--ri-warn) 9%,transparent);
  border-radius:0 var(--ri-r-md) var(--ri-r-md) 0;padding:var(--ri-sp-3)}
.ri-notassessed h3{color:var(--ri-warn)}
.ri-notassessed li{margin:6px 0;font-size:var(--ri-fs-sm)}

/* generic callouts */
.ri-note{border-radius:var(--ri-r-md);padding:10px 12px;font-size:var(--ri-fs-sm);
  border:1px solid var(--ri-grid);background:var(--ri-surface-2);color:var(--ri-ink-2)}
.ri-note.warn{border-color:var(--ri-warn);background:color-mix(in srgb,var(--ri-warn) 10%,transparent);color:var(--ri-ink-1)}
.ri-note.crit{border-color:var(--ri-crit);background:color-mix(in srgb,var(--ri-crit) 10%,transparent);color:var(--ri-ink-1)}
.ri-note.ok{border-color:var(--ri-ok);background:color-mix(in srgb,var(--ri-ok) 10%,transparent);color:var(--ri-ink-1)}
.ri-note.info{border-color:var(--ri-info);background:color-mix(in srgb,var(--ri-info) 10%,transparent);color:var(--ri-ink-1)}

/* ---------- 8 · KPI · TABLES · CHART FRAMES ------------------------------ */
.ri-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:var(--ri-sp-2)}
.ri-kpi{background:var(--ri-surface);border:1px solid var(--ri-grid);
  border-radius:var(--ri-r-lg);padding:10px 12px;display:flex;flex-direction:column;gap:2px}
.ri-kpi > span{font-size:var(--ri-fs-eyebrow);text-transform:uppercase;
  letter-spacing:.05em;color:var(--ri-ink-2);font-weight:700}
.ri-kpi > b{font-size:var(--ri-fs-xl);font-variant-numeric:tabular-nums;line-height:1.1}
.ri-kpi em{font-style:normal;font-size:var(--ri-fs-sm);color:var(--ri-ink-3)}
.ri-kpi.alert{border-color:var(--ri-warn);background:color-mix(in srgb,var(--ri-warn) 8%,var(--ri-surface))}
.ri-kpi.clickable{cursor:pointer}
.ri-kpi.clickable:hover{border-color:var(--ri-navy)}

.ri-table{width:100%;font-size:var(--ri-fs-sm)}
.ri-table th{text-align:left;font-size:var(--ri-fs-eyebrow);text-transform:uppercase;
  letter-spacing:.05em;color:var(--ri-ink-2);font-weight:700;padding:7px 9px;
  border-bottom:1px solid var(--ri-grid);white-space:nowrap;background:var(--ri-surface)}
.ri-table th[aria-sort]{cursor:pointer}
.ri-table th[aria-sort="ascending"]::after{content:" ▲";font-size:8px}
.ri-table th[aria-sort="descending"]::after{content:" ▼";font-size:8px}
.ri-table td{padding:7px 9px;border-bottom:1px solid var(--ri-grid);vertical-align:top}
.ri-table tbody tr:hover{background:var(--ri-surface-2)}
.ri-table tbody tr[aria-selected="true"]{background:color-mix(in srgb,var(--ri-cyan) 14%,transparent)}
.ri-table .num{text-align:right;font-variant-numeric:tabular-nums}
.ri-tablefoot{display:flex;align-items:center;gap:var(--ri-sp-3);padding:8px 10px;
  font-size:var(--ri-fs-sm);color:var(--ri-ink-3);border-top:1px solid var(--ri-grid)}
.ri-scrollx{overflow-x:auto;max-width:100%}

.ri-chart{border:1px solid var(--ri-grid);border-radius:var(--ri-r-lg);background:var(--ri-surface)}
.ri-chart .head{display:flex;align-items:baseline;gap:var(--ri-sp-2);padding:10px 12px 4px}
.ri-chart .head h3{margin:0}
.ri-chart .meta{font-size:var(--ri-fs-sm);color:var(--ri-ink-3);margin-left:auto}
.ri-chart .body{padding:4px 12px 12px}

/* ---------- 9 · STAGE CHIPS · LADDER · COVERAGE STRIP -------------------- */
.ri-stages{display:inline-flex;gap:3px;align-items:center}
.ri-stages i{width:11px;height:11px;border-radius:50%;background:var(--ri-grid);display:block}
.ri-stages i.on{background:var(--ri-ok)}
.ri-stages i.bad{background:var(--ri-crit)}
/* Held, not failed. main's fan-in stops a session whose detections cannot be
   addressed to a road and KEEPS them; extract and detect succeeded, so a crit
   dot would send someone hunting a breakage that is not there. */
.ri-stages i.hold{background:var(--ri-warn)}
.ri-stages i.now{background:var(--ri-info);box-shadow:0 0 0 3px color-mix(in srgb,var(--ri-info) 25%,transparent)}

.ri-ladder{list-style:none;margin:0;padding:0}
.ri-ladder li{position:relative;padding:0 0 var(--ri-sp-3) 26px}
.ri-ladder li::before{content:"";position:absolute;left:5px;top:14px;bottom:0;
  width:2px;background:var(--ri-grid)}
.ri-ladder li:last-child::before{display:none}
.ri-ladder li .dot{position:absolute;left:0;top:3px;width:12px;height:12px;
  border-radius:50%;background:var(--ri-ok)}
.ri-ladder li.pend .dot{background:var(--ri-grid)}
.ri-ladder li.bad .dot{background:var(--ri-crit)}
.ri-ladder li.now .dot{background:var(--ri-info)}
.ri-ladder b{font-size:var(--ri-fs-sm)}
.ri-ladder .t{float:right;font-size:var(--ri-fs-eyebrow);color:var(--ri-ink-3);font-variant-numeric:tabular-nums}
.ri-ladder dl{margin:4px 0 0;display:grid;grid-template-columns:auto 1fr;gap:2px 10px;
  font-size:var(--ri-fs-sm)}
.ri-ladder dt{color:var(--ri-ink-3)}
.ri-ladder dd{margin:0;font-variant-numeric:tabular-nums}

.ri-strip{display:grid;gap:1px;height:20px;border:1px solid var(--ri-grid);
  border-radius:var(--ri-r-sm);overflow:hidden;background:var(--ri-grid)}
.ri-strip i{display:block;background:var(--ri-surface-2)}
.ri-strip i.fresh {background:var(--ri-age-fresh)}
.ri-strip i.ageing{background:var(--ri-age-ageing)}
.ri-strip i.stale {background:var(--ri-age-stale)}
.ri-strip i.never {background:repeating-linear-gradient(45deg,var(--ri-grid) 0 3px,var(--ri-surface) 3px 6px)}
.ri-stripaxis{display:flex;justify-content:space-between;font-size:10.5px;
  color:var(--ri-ink-3);margin-top:2px;font-variant-numeric:tabular-nums}
.ri-striprow{display:grid;grid-template-columns:88px 1fr auto;gap:var(--ri-sp-2);
  align-items:center;margin-bottom:6px}

.ri-legend{display:flex;flex-wrap:wrap;gap:var(--ri-sp-3);font-size:var(--ri-fs-sm);
  color:var(--ri-ink-2);align-items:center}
.ri-legend span{display:inline-flex;align-items:center;gap:5px}
.ri-legend i{width:12px;height:12px;border-radius:3px;display:block}
.ri-ramp{display:flex;height:12px;border-radius:3px;overflow:hidden;width:150px}
.ri-ramp i{flex:1;display:block}

/* ---------- 10 · MAP SURROGATE ------------------------------------------- */
.ri-map{position:relative;border:1px solid var(--ri-grid);border-radius:var(--ri-r-lg);
  overflow:hidden;background:
    linear-gradient(0deg,rgba(0,0,0,.03) 1px,transparent 1px) 0 0/40px 40px,
    linear-gradient(90deg,rgba(0,0,0,.03) 1px,transparent 1px) 0 0/40px 40px,
    var(--ri-surface-2);
  min-height:340px}
:root[data-theme="dark"] .ri-map{background:
    linear-gradient(0deg,rgba(255,255,255,.04) 1px,transparent 1px) 0 0/40px 40px,
    linear-gradient(90deg,rgba(255,255,255,.04) 1px,transparent 1px) 0 0/40px 40px,
    #12120f}
.ri-map .maplbl{position:absolute;left:10px;bottom:8px;font-size:10.5px;
  color:var(--ri-ink-3);background:color-mix(in srgb,var(--ri-surface) 82%,transparent);
  padding:2px 7px;border-radius:var(--ri-r-sm)}
.ri-map .overlay{position:absolute;background:color-mix(in srgb,var(--ri-surface) 92%,transparent);
  border:1px solid var(--ri-grid);border-radius:var(--ri-r-md);padding:8px 10px;
  font-size:var(--ri-fs-sm);box-shadow:var(--ri-e-1)}
.ri-map .overlay.tl{top:10px;left:10px} .ri-map .overlay.tr{top:10px;right:10px}
.ri-map .overlay.bl{bottom:10px;left:10px} .ri-map .overlay.br{bottom:10px;right:10px}

/* ---------- 11 · DRAWER · SHEET · DIALOG · TOAST ------------------------- */
.ri-drawer{background:var(--ri-surface);border:1px solid var(--ri-grid);
  border-radius:var(--ri-r-lg);box-shadow:var(--ri-e-3);display:flex;flex-direction:column;
  max-height:calc(100vh - var(--ri-header-h) - 32px);position:sticky;
  top:calc(var(--ri-header-h) + 12px);overflow:hidden}
.ri-drawerhead{display:flex;align-items:flex-start;gap:var(--ri-sp-2);
  padding:var(--ri-sp-3) var(--ri-sp-4);border-bottom:1px solid var(--ri-grid)}
.ri-drawerhead .eyebrow{font-size:var(--ri-fs-eyebrow);text-transform:uppercase;
  letter-spacing:.06em;color:var(--ri-ink-3);font-weight:700}
.ri-drawerbody{padding:var(--ri-sp-4);overflow-y:auto;display:flex;
  flex-direction:column;gap:var(--ri-sp-4)}
.ri-drawer section > h3{margin-bottom:var(--ri-sp-2)}

.ri-dialog{background:var(--ri-surface);border:1px solid var(--ri-grid);
  border-radius:var(--ri-r-lg);box-shadow:var(--ri-e-3);padding:var(--ri-sp-4);
  max-width:460px;width:100%}

.ri-toast{display:flex;align-items:center;gap:10px;background:var(--ri-ink-1);
  color:var(--ri-page);border-radius:var(--ri-r-md);padding:10px 14px;
  font-size:var(--ri-fs-sm);box-shadow:var(--ri-e-2);max-width:460px}
.ri-toast.err{background:var(--ri-crit);color:#fff}
.ri-toast .act{margin-left:auto;font-weight:700;text-decoration:underline;cursor:pointer}

/* ---------- 12 · EMPTY · ERROR · SKELETON ------------------------------- */
.ri-empty,.ri-error{display:flex;flex-direction:column;align-items:center;
  text-align:center;gap:var(--ri-sp-2);padding:var(--ri-sp-6) var(--ri-sp-4);
  border:1px dashed var(--ri-grid);border-radius:var(--ri-r-lg);color:var(--ri-ink-2)}
.ri-empty .ico,.ri-error .ico{font-size:28px;line-height:1}
.ri-empty h2,.ri-error h2{font-size:var(--ri-fs-md);color:var(--ri-ink-1)}
.ri-empty p,.ri-error p{max-width:46ch;font-size:var(--ri-fs-sm);margin:0}
/* Compact variant, for a panel BESIDE the thing the user came to look at.
   The full state is right when it IS the page — a corridor list that failed
   should stop you. It is wrong in a 300px rail next to a map: "No risk surface
   yet" was rendering taller than the data it sat beside, so the absence of a
   run was the most prominent object on a screen about a road. Same words, one
   quarter the height, left-aligned so it reads as a note rather than an event. */
.ri-empty.compact{flex-direction:row;align-items:flex-start;text-align:left;
  padding:var(--ri-sp-3);gap:var(--ri-sp-2);border-style:solid}
.ri-empty.compact .ico{font-size:15px;opacity:.7;margin-top:1px}
.ri-empty.compact h2{font-size:var(--ri-fs-base)}
.ri-empty.compact .txt{display:flex;flex-direction:column;gap:2px;min-width:0}
.ri-empty.compact p{max-width:none}

.ri-error{border-style:solid;border-color:var(--ri-crit);
  background:color-mix(in srgb,var(--ri-crit) 6%,transparent)}
.ri-error .code{font-family:var(--ri-mono);font-size:var(--ri-fs-sm);color:var(--ri-crit)}

.ri-skel{background:var(--ri-surface-2);border-radius:var(--ri-r-sm);height:12px;display:block}
.ri-skel + .ri-skel{margin-top:8px}
.ri-skel.w70{width:70%} .ri-skel.w50{width:50%} .ri-skel.w30{width:30%}
.ri-skel.tall{height:34px}

/* ---------- 13 · EVIDENCE ------------------------------------------------ */
.ri-evidence{border:1px solid var(--ri-grid);border-radius:var(--ri-r-md);overflow:hidden;
  background:var(--ri-surface-2)}
.ri-evidence .frame{position:relative;aspect-ratio:16/9;display:grid;place-items:center;
  color:var(--ri-ink-3);font-size:var(--ri-fs-sm);
  background:repeating-linear-gradient(135deg,var(--ri-surface-2) 0 8px,var(--ri-surface) 8px 16px)}
.ri-evidence .bbox{position:absolute;border:2px solid var(--ri-crit);border-radius:2px}
.ri-evidence .bbox::after{content:attr(data-label);position:absolute;top:-17px;left:-2px;
  background:var(--ri-crit);color:#fff;font-size:9.5px;padding:1px 4px;border-radius:2px;white-space:nowrap}
.ri-evidence figcaption{padding:6px 9px;font-size:var(--ri-fs-eyebrow);color:var(--ri-ink-2);
  border-top:1px solid var(--ri-grid);background:var(--ri-surface)}
.ri-evidencepair{display:grid;grid-template-columns:1fr 1fr;gap:var(--ri-sp-2)}
.ri-evidence .expired{background:var(--ri-surface-2);color:var(--ri-warn);font-weight:600}

/* ---------- 14 · FILE QUEUE --------------------------------------------- */
.ri-fq{border:1px solid var(--ri-grid);border-radius:var(--ri-r-md);overflow:hidden}
.ri-fq .f{display:grid;grid-template-columns:1fr 110px 84px auto;gap:10px;align-items:center;
  padding:8px 10px;border-bottom:1px solid var(--ri-grid);font-size:var(--ri-fs-sm)}
.ri-fq .f:last-child{border-bottom:0}
.ri-fq .n{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--ri-mono);font-size:11.5px}
.ri-bar{height:6px;border-radius:3px;background:var(--ri-grid);overflow:hidden}
.ri-bar i{display:block;height:100%;background:var(--ri-cyan)}
.ri-bar.ok i{background:var(--ri-ok)} .ri-bar.err i{background:var(--ri-crit)}
.ri-drop{border:2px dashed var(--ri-grid);border-radius:var(--ri-r-lg);
  padding:var(--ri-sp-5) var(--ri-sp-4);text-align:center;color:var(--ri-ink-2)}

/* ---------- 15 · MAP CHROME ---------------------------------------------
   Controls that float ON the map. Here rather than in console.html because
   audit #11 is right: the map is the product's primary surface and had no
   design-system representation, so every future map (plan, work order, the
   unified /map) would re-derive this. Colour literals also belong here —
   console.html is guarded against them by test_no_colour_literals. */
/* The map wrapper: positioning context for the controls below, and the thing
   .ri-mapfull re-positions. Both live HERE, at equal specificity and in the
   right source order, so the state can override the base. Keeping the base on
   the page as `#mapwrap` is what broke fullscreen: an ID beats a class, so
   position:fixed never applied. */
.ri-mapwrap{position:relative;flex:1;min-height:280px;display:flex;min-width:0}

/* Controls floating over the map. z-index 400 clears Leaflet's tile and
   overlay panes (200/400) but stays under its popups (700), so a popup is
   never hidden behind the legend. */
.ri-mapctl{position:absolute;z-index:400;background:var(--ri-surface);
  border:1px solid var(--ri-grid);border-radius:var(--ri-r-md);
  box-shadow:var(--ri-e-2);padding:var(--ri-sp-2)}
.ri-basemap{top:var(--ri-sp-2);right:var(--ri-sp-2);display:flex;gap:2px;padding:2px}
.ri-basemap button{border:0;background:transparent;color:var(--ri-ink-2);
  font:inherit;font-size:var(--ri-fs-sm);padding:3px 9px;
  border-radius:var(--ri-r-sm);cursor:pointer}
.ri-basemap button:hover{background:var(--ri-surface-2);color:var(--ri-ink-1)}
.ri-basemap button[aria-pressed="true"]{background:var(--ri-navy);color:var(--ri-on-navy);font-weight:600}
.ri-basemap button:focus-visible{outline:2px solid var(--ri-focus);outline-offset:1px}
/* The fullscreen toggle rides in the basemap group — same corner, same pill —
   because both answer "how do I want to look at this", and a second floating
   box in the same corner is clutter. Separated by a rule, not a gap. */
.ri-basemap #fsbtn{border-left:1px solid var(--ri-grid);margin-left:2px;
  padding:3px 8px;font-size:14px;line-height:1}

/* BELOW Leaflet's zoom control, not beside it. Leaflet puts +/- at top-left
   with a 10px margin and ~64px of height; sharing that corner put "Copy view
   link" straight through the zoom buttons. 82px clears it at every zoom. */
.ri-mapshare{top:82px;left:var(--ri-sp-2);display:flex;align-items:center;
  gap:6px;font:inherit;font-size:var(--ri-fs-sm);font-weight:600;
  color:var(--ri-ink-2);cursor:pointer;padding:5px 10px}
.ri-mapshare:hover{color:var(--ri-ink-1);background:var(--ri-surface-2)}
.ri-mapshare:focus-visible{outline:2px solid var(--ri-focus);outline-offset:1px}
@media (max-width:560px){ .ri-mapshare span{display:none} }

/* Fullscreen. Uses a class rather than the Fullscreen API: the API hides the
   app shell, so the corridor picker, the nav and the sign-out go with it —
   the user is left inside a map with no way back to the product. This fills
   the viewport BELOW the header instead, so the map is as large as it can be
   while the way out stays visible. Escape leaves. */
.ri-mapfull{position:fixed;inset:var(--ri-header-h) 0 0 0;z-index:1500;
  border-radius:0;margin:0;background:var(--ri-page)}
.ri-mapfull #map{border-radius:0;border-left:0;border-right:0;border-bottom:0}

/* Leaflet's own scale bar, re-painted in tokens — it ships light-only chrome,
   the same reason console.html already re-paints the popups and zoom bar. */
.leaflet-control-scale-line{background:color-mix(in srgb,var(--ri-surface) 82%,transparent);
  border:1px solid var(--ri-ink-3);border-top:0;color:var(--ri-ink-1);
  font-family:var(--ri-font);font-size:11px;line-height:1.2;padding:1px 5px;
  text-shadow:none;box-shadow:none}

/* Per-layer status, ON the map — and ONLY in fullscreen.
   The chips live in the rail (#layerstatus), which `position:fixed` covers the
   moment the map goes fullscreen. They are the only readout that separates
   "this layer is empty" from "this layer failed", so fullscreen removed the
   one thing that says whether you are looking at a healthy road or a broken
   request — exactly when the map is being studied hardest. Duplicated rather
   than moved: outside fullscreen the rail is the better home, beside the other
   corridor facts.
   Bottom-LEFT, above Leaflet's scale bar (~22px + its 10px margin). */
.ri-maplayers{display:none;bottom:44px;left:var(--ri-sp-2);max-width:min(46vw,340px);
  gap:4px;flex-wrap:wrap;padding:5px 7px}
.ri-mapfull .ri-maplayers{display:flex}
.ri-maplayers .ri-pill{font-size:11px;padding:1px 6px}
/* A failed layer keeps its retry INSIDE fullscreen too — telling someone a
   layer broke while the fix is on the far side of an Escape key is half a
   message. */
.ri-maplayers .ri-btn.sm{font-size:11px;padding:1px 7px}

/* Clear of Leaflet's attribution, which also sits bottom-right and is a
   licence notice — covering it is both ugly and wrong. 26px is its height
   plus a hair. */
.ri-maplegend{bottom:26px;right:var(--ri-sp-2);width:210px;
  font-size:var(--ri-fs-sm);line-height:1.4}
.ri-maplegend .k{margin:0 0 4px;font-size:var(--ri-fs-eyebrow);letter-spacing:.07em;
  text-transform:uppercase;color:var(--ri-ink-3)}
.ri-maplegend .ramp{height:9px;border-radius:2px;border:1px solid var(--ri-grid);
  position:relative}
/* The decision threshold is a fact about the scoring, not decoration: 0.50
   is where "watch" becomes "act", so the ramp marks it. */
.ri-maplegend .ramp::after{content:"";position:absolute;left:50%;top:-3px;bottom:-3px;
  width:1px;background:var(--ri-ink-1);opacity:.7}
.ri-maplegend .ends{display:flex;justify-content:space-between;
  color:var(--ri-ink-3);font-size:10.5px;margin-top:3px}
.ri-maplegend .hatchrow{margin:var(--ri-sp-2) 0 0;display:flex;align-items:center;gap:6px}
.ri-maplegend .sw-hatch{width:22px;height:11px;border:1px solid var(--ri-ink-3);
  border-radius:2px;background:repeating-linear-gradient(45deg,
    transparent 0 3px, var(--ri-ink-3) 3px 4px)}
.ri-maplegend .streams{display:grid;grid-template-columns:1fr 1fr;gap:1px 8px;
  margin-top:var(--ri-sp-2);padding-top:var(--ri-sp-2);
  border-top:1px solid var(--ri-grid)}
.ri-maplegend .streams span{display:flex;align-items:center;gap:5px;
  color:var(--ri-ink-2);font-size:11px}
/* Under 560px the legend would cover most of a phone map. The rail still
   carries the full one, so this hides rather than shrinks. */
@media (max-width:560px){ .ri-maplegend{display:none} }

/* ---------- 17 · OVERFLOW GUARDS ----------------------------------------
   Wide content scrolls inside its OWN container; the page body never
   scrolls horizontally. Fixes measured at 390px across all mocks.        */
code,.ri-tag,.ri-mono{overflow-wrap:anywhere;word-break:break-word}
/* corridor names are user data — "Chilakaluripet–Vijayawada" must wrap,
   not widen the document (console.html has no guard for this today) */
h1,h2,h3,h4{overflow-wrap:break-word}
/* NOT Leaflet's overlay. This guard stops a wide inline SVG widening the
   document — but Leaflet renders every vector layer into an <svg> whose
   parent, .leaflet-map-pane, is ZERO WIDTH BY DESIGN: the pane is a transform
   container, not a box. `max-width:100%` therefore resolves to 0 and beats the
   element's own width="1536" attribute, so the overlay is clipped out of
   existence while the basemap tiles — plain <img>, in a different pane — keep
   painting perfectly.

   The result was a map that has NEVER drawn a single vector: no centreline, no
   risk surface, no coverage gaps, no observations, no driven tracks. Only
   OpenStreetMap. Every "the corridor looks empty" investigation on this
   product, including several of mine, was chasing data that was present,
   correct, and clipped to zero pixels.
   Proved by toggling this one declaration: both overlay panes went 0 -> 1082px
   and the corridor appeared immediately. invalidateSize() and a zoom nudge
   both fail, because it was never a Leaflet redraw problem. */
svg:not(.leaflet-zoom-animated){max-width:100%}
.ri-range,input[type=range]{min-width:0;max-width:100%}
/* a select sizes to its widest OPTION; with width:auto that can exceed the
   viewport. max-width beats an inline width, so this holds regardless. */
.ri-input,select.ri-input,input.ri-input,textarea.ri-input{max-width:100%}
.ri-card,.ri-main,.ri-drawer,.ri-kpi{min-width:0}
.ri-provenance,.ri-legend{min-width:0}

/* every table becomes its own scroll container on narrow screens rather
   than pushing the document wider — the "table twin" stays readable */
@media (max-width:900px){
  .ri-table,table.ri-table{display:block;overflow-x:auto}
  .ri-table th,.ri-table td{white-space:nowrap}
  .ri-table caption{white-space:normal;position:sticky;left:0;max-width:calc(100vw - 64px)}
  .ri-map{min-height:240px}
  .ri-drawer{position:static;max-height:none}
  .ri-rail{position:static}
}

@media print{
  .ri-header,.ri-rail,.ri-globalwarn,.no-print{display:none!important}
  body{background:#fff}
  .ri-card{break-inside:avoid;box-shadow:none}
}
