/* ============================================================================
   blotter.css — the design system for fairfieldscanner.com

   Serves six routes from one vocabulary:
     /blotter          the record feed        (3-col)
     /incident/:id     one CAD record         (2-col)
     /map              the whole county, pinned (full bleed, §24)
     /digest           the weekly written digest (2-col)
     /settings         personal control matrix (responsive cards)
     /plus             pricing + entitlements (2-col)

   /digest is served but is not in the primary navigation.

   Written against the approved prototype export. The prototype is a
   design-tool artifact: inline styles on a fixed 1280x836 canvas, with
   Handlebars placeholders and <sc-if> wrappers that are tooling, not
   markup. The inline styles are the visual spec; the canvas is not.
   Nothing here ships the 836px height, the 10px frame radius or the
   frame drop shadow — in production the frame is the viewport.

   ── TOKENS ────────────────────────────────────────────────────────────
   blotter-tokens.css is GENERATED from packages/tokens/src/index.ts in
   the Blotter repo and is the source of truth. This file only consumes
   it. Load order matters: tokens first, then this.

   LIGHT MODE IS NOT DEFINED HERE, ON PURPOSE. tools/build-tokens.mjs
   emits a full light ramp under BOTH :root[data-theme="light"] and a
   @media (prefers-color-scheme: light) guard that already excludes an
   explicit data-theme="dark". The committed blotter-tokens.css carries
   both blocks. There is therefore nothing for this file to do, and
   duplicating the ramp here would create a second source of truth that
   silently drifts from the first. See the handoff notes for the one
   real problem: the shipped light ramp is a WEAKER ramp than the
   prototype's, and --faint currently lands at 3.97:1 on --bg, which is
   below WCAG AA for normal text. That is a fix for the token source,
   not for this file.

   A HEX LITERAL IN THIS FILE IS A BUG. Everything is var(--token). The
   only exception is rgba() used for shadows, which carry no semantic
   meaning, and alpha-only mask gradients, which carry no colour at all.

   ── COLOUR MEANING, NON-NEGOTIABLE ────────────────────────────────────
   --accent  (blue)   the dispatch/CAD side, and primary actions.
                      NEVER anything a model wrote.
   --tp/--ip/--ep     (violet) A MACHINE WROTE THIS. Nothing else, ever.
   --plus    (gold)   Blotter+ only. Never a generic highlight.
   --unit    (cyan)   unit designators. Never severity, never state.
   --s5..--s0         severity rank only. Never department colours.

   Four token conflicts in the prototype are corrected here rather than
   copied. Each is commented at the rule that fixes it:
     1. the /blotter digest card framed model prose in --accent        (§8)
     2. the watchlist "N new" badge was violet for a computed count    (§8)
     3. the "Dispatch record" chip was violet for CAD data             (§5)
     4. /plus painted its gold lockup with the cyan --radio ramp       (§13)

   ── SEVERITY IS WIDTH AS WELL AS HUE ──────────────────────────────────
   Rank is carried by two channels at once so the ordering survives
   greyscale printing and colour blindness. Every rule that sets a
   severity colour sets the matching width in the same place. That is
   enforced structurally: §3 maps data-sev="N" to five locals at once
   (--sev, --sev-tint, --sev-ink, --sev-edge, --sev-stripe) and every
   severity-bearing element on every route reads those locals and
   nothing else. There is no way to set the hue without the width.

   ── RADIO IS OUT OF SCOPE ─────────────────────────────────────────────
   No player, no transcripts, no waveform, no talkgroups, no clip chips,
   no Listen. The --radio* ramp is absent from the generated tokens and
   is referenced nowhere below. Unit designators, which the prototype
   painted with --radio-bg/--radio-t, ship on --unit-bg/--unit: identical
   pixels in both themes, correct semantics, and the radio ramp can stay
   deleted.
   ============================================================================ */


/* ============================================================================
   §1  MISSING TOKENS — derived, not invented
   ============================================================================
   --accent-soft and --plus-soft exist in the prototype and are used by four
   routes (the live card, the digest card, the Blotter+ chip, the lock card,
   the plus price card). They are NOT in the generated blotter-tokens.css:
   the generator's CSS_NAMES map does not carry them. Rather than hand-edit a
   generated file or paste two rgba literals that would then need a light
   twin, they are derived from the tokens that DO ship. color-mix resolves
   against whichever --accent/--plus won the cascade, so one declaration
   covers dark, light and system-light with no duplication and no drift.

   The bare `transparent` line before each is a fallback for engines without
   color-mix: the wash disappears, the 1px ring survives, nothing breaks.

   These belong in packages/tokens/src/index.ts. They are here because the
   generator cannot currently see them. */

:root {
  --accent-soft: transparent;
  --accent-soft: color-mix(in srgb, var(--accent) 12%, transparent);

  --plus-soft: transparent;
  --plus-soft: color-mix(in srgb, var(--plus) 12%, transparent);

  /* One card radius. The export drew cards at 8px, 9px and 10px across the
     six routes with no pattern behind it; that is export noise, not intent,
     and unifying it is exactly the dedup this file exists to do. */
  --radius-card: 9px;

  /* ---- Motion ----------------------------------------------------------
     THREE DURATIONS AND ONE CURVE, and nothing on this site may invent a
     fourth. A publication that animates is a publication that has decided
     to make a reader wait, so the ceiling is 240ms and the floor is a
     frame you can still perceive.

       --mo-1  a state a control is already showing you (press, hover)
       --mo-2  a thing arriving or leaving (a tile, a pin, a row)
       --mo-3  a thing moving across the screen (a card rising, a list
               settling under a row a poll just pushed in)

     The curve is ease-OUT and only ease-out: motion that starts fast and
     lands soft reads as physical, and motion that starts slow reads as
     the page thinking. Nothing here eases in — a reader should never be
     watching an animation begin.

     Every one of these is collapsed by §15's prefers-reduced-motion
     block, which zeroes duration globally rather than per rule. */
  --mo-1: 120ms;
  --mo-2: 180ms;
  --mo-3: 240ms;
  --mo-ease: cubic-bezier(.22, .61, .36, 1);
}

/* 2026 information-architecture refresh: dedicated county Explore surface. */

.ex-page { height: 100%; overflow: auto; padding: 26px clamp(18px, 5vw, 72px) 64px; }
.ex-page > * { max-width: 1180px; margin-left: auto; margin-right: auto; }
.ex-hero { padding: 6px 0 18px; display: flex; align-items: end; justify-content: space-between; gap: 28px; }
.ex-hero h1, .ex-page > h1 { font: 700 clamp(30px, 4vw, 48px)/1.02 var(--font-sans); letter-spacing: -.045em; max-width: 18ch; }
.ex-hero > div > p { color: var(--muted); font-size: 14px; line-height: 1.55; max-width: 66ch; margin-top: 10px; }
.ex-source-note { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 8px; color: var(--faint); font-size: 12px; margin-bottom: 4px; }
.ex-source-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
.ex-metrics { display: grid; grid-template-columns: repeat(6, 1fr); border: 1px solid var(--line); border-radius: 14px; background: linear-gradient(135deg, var(--surface), var(--surface2)); overflow: hidden; }
.ex-metric { padding: 16px 18px; border-right: 1px solid var(--line); }
.ex-metric:last-child { border-right: 0; }
.ex-metric__value { display: block; font-size: clamp(21px, 2.2vw, 28px); letter-spacing: -.04em; white-space: nowrap; }
.ex-metric__label { display: block; color: var(--muted); font-size: 12px; margin-top: 2px; }
.ex-metric small { color: var(--faint); }
.ex-notice { margin-top: 16px; color: var(--muted); }
.ex-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 16px; }
.ex-panel, .ex-directory { border: 1px solid var(--line); border-radius: 14px; background: var(--surface); padding: 24px; }
.ex-panel h2, .ex-directory h2 { font-size: 20px; letter-spacing: -.02em; }
.ex-panel--wide { margin-top: 16px; }
.ex-panelhead { display: flex; align-items: end; justify-content: space-between; gap: 20px; }
.ex-panelhead > p { color: var(--faint); font-size: 11px; }
.ex-pulse { height: 128px; display: grid; grid-template-columns: repeat(24, 1fr); align-items: end; gap: 5px; margin-top: 18px; border-bottom: 1px solid var(--line2); }
.ex-pulse__hour { height: 100%; display: flex; flex-direction: column; justify-content: end; align-items: center; position: relative; }
.ex-pulse__hour i { width: 100%; min-height: 5px; border-radius: 3px 3px 0 0; background: linear-gradient(180deg, var(--accent), var(--accent-w)); opacity: .72; transition: opacity .15s ease; }
.ex-pulse__hour:hover i { opacity: 1; }
.ex-pulse__hour span { position: absolute; top: calc(100% + 7px); color: var(--faint); font: 11px var(--font-mono); }
.ex-bars { margin-top: 16px; }
.ex-bars li { margin-top: 11px; }
.ex-barline { display: flex; justify-content: space-between; gap: 12px; color: var(--muted); font-size: 12px; }
.ex-barline strong { color: var(--text); }
.ex-bar { display: block; height: 4px; margin-top: 5px; overflow: hidden; border-radius: 4px; background: var(--line); }
.ex-bar b { display: block; height: 100%; border-radius: inherit; background: var(--accent); }
.ex-severity { margin-top: 14px; }
.ex-severity__row { display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; align-items: center; gap: 8px; min-height: 31px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px; }
.ex-severity__row b { color: var(--text); font-weight: 600; }
.ex-network { display: grid; grid-template-columns: minmax(220px, 1.5fr) repeat(4, 1fr); align-items: stretch; margin-top: 16px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); overflow: hidden; }
.ex-network > div { padding: 22px; }
.ex-network h2 { font-size: 20px; letter-spacing: -.02em; }
.ex-network .ex-metric { border-left: 1px solid var(--line); border-right: 0; }
.ex-ranking { margin-top: 18px; }
.ex-ranking li { display: flex; justify-content: space-between; gap: 16px; padding: 9px 0; border-top: 1px solid var(--line); color: var(--muted); }
.ex-ranking strong { color: var(--text); }
.ex-directory { margin-top: 16px; }
.ex-sectionhead { display: flex; align-items: end; justify-content: space-between; gap: 20px; }
.ex-search input { width: min(280px, 40vw); border: 1px solid var(--line2); border-radius: var(--radius-pill); background: var(--bg); color: var(--text); padding: 9px 14px; outline: 0; }
.ex-search input:focus { border-color: var(--accent); }
.ex-tabs { display: flex; gap: 6px; overflow-x: auto; margin-top: 22px; padding-bottom: 2px; }
.ex-tab { cursor: pointer; white-space: nowrap; color: var(--faint); padding: 7px 12px; border-radius: var(--radius-pill); border: 1px solid var(--line); }
.ex-tab.is-on { color: var(--accent-t); border-color: var(--accent-w); background: var(--accent-soft); }
.ex-resultcount { color: var(--faint); font-size: 11px; margin-top: 16px; }
.ex-directory__list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 22px; margin-top: 6px; }
.ex-directory__row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 62px; border-top: 1px solid var(--line); }
.ex-directory__row strong, .ex-directory__row span { display: block; }
.ex-directory__row div > span, .ex-directory__place { color: var(--faint); font-size: 11px; }
.ex-directory__place { text-align: right; }

@media (max-width: 860px) {
  .ex-page { padding-top: 24px; }
  .ex-hero { display: block; }
  .ex-source-note { margin-top: 16px; }
  .ex-metrics { grid-template-columns: repeat(2, 1fr); }
  .ex-metrics .ex-metric:nth-child(even) { border-right: 0; }
  .ex-metrics .ex-metric:nth-child(-n+4) { border-bottom: 1px solid var(--line); }
  .ex-grid, .ex-directory__list { grid-template-columns: 1fr; }
  .ex-network { grid-template-columns: repeat(2, 1fr); }
  .ex-network > div:first-child { grid-column: 1 / -1; }
  .ex-network .ex-metric:nth-child(even) { border-left: 0; }
  .ex-pulse { gap: 3px; height: 120px; }
  .ex-sectionhead { align-items: stretch; flex-direction: column; }
  .ex-search input { width: 100%; }
}

/* Explore prototype integration. The page keeps the shared shell above it,
   while restoring the prototype's editorial hierarchy and data density. */
.ex-page { padding: 26px clamp(18px, 5vw, 72px) 64px; }
.ex-hero { padding: 8px 0 20px; }
.ex-hero h1 { max-width: none; }
.ex-hero-copy > p:last-child { max-width: 720px; margin: 10px 0 0; color: var(--muted); font-size: 14px; line-height: 1.55; }
.ex-hero-side { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 13px; }
.ex-source-note { margin: 0; font-size: 11px; }
.ex-source-note > span:first-child { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 10%, transparent); }
.ex-window { display: flex; padding: 3px; border: 1px solid var(--line); border-radius: var(--radius-pill); }
.ex-window button { cursor: pointer; padding: 5px 10px; border-radius: var(--radius-pill); color: var(--faint); font-size: 11px; text-align: center; }
.ex-window button.is-on { background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent-t); }
.ex-metrics { grid-template-columns: 1.35fr repeat(5, 1fr); }
.ex-metric { min-width: 0; padding: 15px 16px; border-right: 0; border-left: 1px solid var(--line); }
.ex-metric:first-child { border-left: 0; }
.ex-metric small, .ex-metric span { display: block; color: var(--faint); font-size: 11px; }
.ex-metric small { font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .08em; }
.ex-metric strong { display: block; margin-top: 3px; font-size: clamp(22px, 2vw, 28px); line-height: 1.1; letter-spacing: -.04em; }
.ex-metric span { margin-top: 4px; color: var(--muted); font-size: 11px; }
.ex-metric b { display: block; margin-top: 7px; color: var(--plus-t); font: 11px var(--font-mono); }
.ex-metric--lead { position: relative; background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 15%, transparent), transparent); }
.ex-metric--lead::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--accent); }
.ex-pulse-panel, .ex-panel, .ex-directory { border: 1px solid var(--line); border-radius: 14px; background: color-mix(in srgb, var(--surface) 92%, transparent); }
.ex-pulse-panel { margin-top: 16px; padding: 22px 24px 0; overflow: hidden; }
.ex-panel-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; }
.ex-panel-head h2, .ex-panel h2, .ex-directory h2, .ex-network h2 { margin: 0; font-size: 20px; line-height: 1.2; letter-spacing: -.025em; }
.ex-signal { display: grid; grid-template-columns: auto auto; column-gap: 8px; align-items: center; }
.ex-signal i { grid-row: 1 / 3; width: 8px; height: 8px; border-radius: 50%; background: var(--plus); box-shadow: 0 0 0 5px color-mix(in srgb, var(--plus) 8%, transparent); }
.ex-signal span { color: var(--faint); font: 11px var(--font-mono); text-transform: uppercase; letter-spacing: .08em; }
.ex-signal strong { font-size: 12px; font-weight: 600; }
.ex-chart-shell { position: relative; margin-top: 18px; padding: 0 2px 21px; background: repeating-linear-gradient(to top, transparent 0, transparent 43px, color-mix(in srgb, var(--line2) 27%, transparent) 44px); }
.ex-pulse { height: 144px; margin: 0; display: grid; grid-template-columns: repeat(24, 1fr); align-items: end; gap: 5px; border-bottom: 1px solid var(--line2); }
.ex-pulse--6 { grid-template-columns: repeat(6, 1fr); gap: 10px; }
.ex-pulse--168 { grid-template-columns: repeat(7, 1fr); gap: 10px; }
.ex-pulse > i { display: block; min-height: 5px; border-radius: 3px 3px 0 0; background: linear-gradient(to top, var(--accent-w), var(--accent)); opacity: .75; transition: height .35s ease, opacity .15s ease; }
.ex-pulse > i:hover { opacity: 1; }
.ex-pulse > i.peak { background: linear-gradient(to top, color-mix(in srgb, var(--plus) 45%, var(--bg)), var(--plus-t)); opacity: 1; }
.ex-axis { position: absolute; inset: auto 0 3px; display: flex; justify-content: space-between; color: var(--faint); font: 11px var(--font-mono); }
.ex-insight { min-height: 58px; margin: 0 -24px; padding: 13px 24px; display: grid; grid-template-columns: 90px 1fr auto; gap: 18px; align-items: center; border-top: 1px solid var(--line); background: color-mix(in srgb, var(--surface2) 78%, transparent); }
.ex-insight > span { color: var(--accent-t); font: 11px var(--font-mono); text-transform: uppercase; letter-spacing: .09em; }
.ex-insight p { color: var(--muted); font-size: 11px; }
.ex-insight em { color: var(--faint); font: normal 11px var(--font-mono); white-space: nowrap; }
.ex-panel { margin-top: 16px; padding: 20px; }
.ex-panel-head--compact { align-items: flex-start; }
.ex-panel-head--compact > span { color: var(--faint); font: 11px var(--font-mono); white-space: nowrap; }
.ex-bars { margin-top: 14px; }
.ex-bars li { margin-top: 10px; }
.ex-bar-label { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; color: var(--muted); font-size: 11px; }
.ex-bar-label strong { color: var(--text); font: 600 11px var(--font-mono); text-align: right; }
.ex-bar-track { display: block; height: 3px; margin-top: 4px; border-radius: 3px; background: var(--line); }
.ex-bar-track i { display: block; height: 100%; border-radius: inherit; background: var(--accent); }
.ex-bars--categories .ex-bar-track i { background: linear-gradient(90deg, var(--accent-w), var(--accent)); }
.ex-donut-wrap { display: grid; grid-template-columns: 110px 1fr; gap: 18px; align-items: center; margin-top: 18px; }
.ex-donut { width: 104px; height: 104px; border-radius: 50%; display: grid; place-content: center; text-align: center; position: relative; }
.ex-donut::before { content: ""; position: absolute; inset: 13px; border-radius: 50%; background: var(--surface); }
.ex-donut strong, .ex-donut span { position: relative; }
.ex-donut strong { font-size: 20px; }
.ex-donut span { color: var(--faint); font-size: 11px; }
.ex-severity { display: grid; gap: 7px; margin: 0; }
.ex-severity-row { display: grid; grid-template-columns: 10px 1fr auto; gap: 7px; align-items: center; color: var(--muted); font-size: 11px; }
.ex-severity-row i { display: block; width: var(--sev-stripe, 2px); height: 10px; border-radius: 2px; background: var(--sev, var(--s0)); }
.ex-severity-row b { color: var(--text); font: 11px var(--font-mono); }
.ex-panel-note { margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--line); color: var(--faint); font-size: 11px; }
.ex-network { grid-template-columns: minmax(240px, 1.5fr) repeat(4, 1fr); }
.ex-network > div { padding: 18px; border-left: 1px solid var(--line); display: flex; flex-direction: column; justify-content: center; }
.ex-network > div:first-child { border-left: 0; }
.ex-network-title span, .ex-network > div > span { color: var(--faint); font-size: 11px; }
.ex-network > div > strong { font-size: 24px; letter-spacing: -.04em; }
.ex-network > div:not(:first-child):hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.ex-directory { padding: 22px; }
.ex-section-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; }
.ex-section-head input { width: min(280px, 40vw); padding: 8px 13px; border: 1px solid var(--line2); border-radius: var(--radius-pill); background: var(--bg); color: var(--text); outline: 0; }
.ex-section-head input:focus { border-color: var(--accent); }
.ex-tabs { margin-top: 18px; }
.ex-tabs button { cursor: pointer; white-space: nowrap; padding: 6px 11px; border: 1px solid var(--line); border-radius: var(--radius-pill); color: var(--faint); font-size: 11px; }
.ex-tabs button.is-on { color: var(--accent-t); border-color: var(--accent-w); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.ex-result-count { margin-top: 13px; color: var(--faint); font-size: 11px; }
.ex-directory-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 22px; margin-top: 4px; }
.ex-directory-row { min-width: 0; min-height: 54px; display: flex; align-items: center; justify-content: space-between; gap: 15px; border-top: 1px solid var(--line); }
.ex-directory-row strong, .ex-directory-row small { display: block; overflow-wrap: anywhere; }
.ex-directory-row strong { font-size: 11px; }
.ex-directory-row small, .ex-directory-row > span { color: var(--faint); font-size: 11px; }
.ex-directory-row > span { flex: 0 0 auto; text-align: right; }
.ex-loading { padding-top: 52px; }

@media (max-width: 860px) {
  .ex-hero-side { align-items: flex-start; margin-top: 15px; }
  .ex-metrics { grid-template-columns: repeat(2, 1fr); }
  .ex-metric { border-bottom: 1px solid var(--line); }
  .ex-metric:nth-child(odd) { border-left: 0; }
  .ex-grid, .ex-directory-list { grid-template-columns: 1fr; }
  .ex-network { grid-template-columns: repeat(2, 1fr); }
  .ex-network-title { grid-column: 1 / -1; }
  .ex-network > div:nth-child(even) { border-left: 0; }
  .ex-insight { grid-template-columns: 1fr; }
  .ex-insight em { display: none; }
  .ex-section-head { align-items: stretch; flex-direction: column; }
  .ex-section-head input { width: 100%; }
}
@media (max-width: 520px) {
  .ex-page { padding-inline: 16px; }
  .ex-pulse-panel { padding-inline: 15px; }
  .ex-insight { margin-inline: -15px; padding-inline: 15px; }
  .ex-panel-head { display: block; }
  .ex-signal { margin-top: 13px; }
  .ex-pulse { gap: 3px; }
  .ex-donut-wrap { grid-template-columns: 96px 1fr; }
  .ex-donut { width: 90px; height: 90px; }
  .ex-directory-row { align-items: flex-start; flex-direction: column; gap: 4px; padding: 10px 0; }
  .ex-directory-row > span { text-align: left; }
}


/* ============================================================================
   §2  BASE
   ============================================================================ */

*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background:
    radial-gradient(circle at 18% -12%, color-mix(in srgb, var(--accent) 15%, transparent), transparent 38%),
    var(--bg);
  color: var(--text);
  font: 15px/1.5 var(--font-sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Nothing on this site may scroll sideways. Wide content gets .bl-scrollx. */
  overflow-x: hidden;
}

img, svg { max-width: 100%; }
svg { display: block; }

h1, h2, h3, p, dl, dd, figure { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }

a { color: var(--accent-t); text-decoration: none; }

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  text-align: left;
}

input { font: inherit; }

/* Numerals in any measured quantity line up column to column. */
.bl-num, [class*="-time"], [class*="-stat-v"] { font-variant-numeric: tabular-nums; }

@keyframes fsPulse { 0%, 100% { opacity: 1 } 50% { opacity: .25 } }


/* ---- Focus -----------------------------------------------------------------
   Every interactive element gets a visible ring. Two shapes, because half the
   components on this site clip their own overflow to hold a severity stripe:
   an outside ring on ordinary controls, an inset ring on anything with
   overflow:hidden, where outline-offset:2px would be clipped away entirely. */

a:focus-visible,
button:focus-visible,
input:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.bl-card:focus-visible,
.bl-lock:focus-visible,
.bl-addr-card:focus-visible,
.bl-note--plus:focus-visible,
.bl-map:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: inherit;
}

/* Gold context keeps its own ring so focus does not import blue into a
   Blotter+ surface, where blue means "dispatch" and would misread. */
.bl-lock:focus-visible,
.bl-note--plus:focus-visible,
.bl-btn--plus:focus-visible,
.bl-btn--ghost-plus:focus-visible,
.fs-pluschip:focus-visible {
  outline-color: var(--plus);
}

/* Utility: the sanctioned wrapper for anything genuinely wider than a phone.
   Nothing else in this file is allowed to overflow the viewport.

   A REGION THAT SCROLLS MUST BE OPERABLE FROM A KEYBOARD (WCAG 2.1.1). Where
   the wrapper holds links — the feed's table does — tabbing through them
   scrolls it and there is nothing to add. Where it holds none, as on /plus's
   entitlement matrix, the wrapper itself takes tabindex="0" and this ring;
   without it the right-hand column of that table is reachable by mouse and
   touch only. The ring is inset because the box is already flush with the
   column it sits in. */
/* position:relative IS LOAD-BEARING, not decoration. overflow only clips a
   descendant this element is a containing block FOR, and .bl-vh below is
   position:absolute — so while this box was static, the matrix's dozen
   visually-hidden column labels were laid out against the initial
   containing block, sat at x≈416 inside the 460px-wide matrix, and were
   never clipped by the overflow above them. They dragged the whole PAGE
   sideways: 97px of it on /plus at 320px, 42px at 375px, while this
   element scrolled correctly and looked fine. Making it a containing
   block puts them back inside the scroller. Costs nothing visually. */
.bl-scrollx {
  position: relative;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.bl-scrollx:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: var(--radius-sm);
}

.bl-vh {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* ============================================================================
   §3  SEVERITY — the one place rank becomes paint
   ============================================================================
   The API returns a COLOUR WORD, not a number: {"severity":"RED"}. Routes map
   it to a rank and stamp data-sev on the element (or on an ancestor):

     DARKRED -> 5 "Critical"      3.7% of records
     RED     -> 4 "High"         27.3%
     ORANGE  -> 3 "Medium"       22.8%
     GREEN   -> 2 "Low-medium"   25.8%
     BLUE    -> 1 "Low"           9.5%
     GREY    -> 0 "Unspecified"  10.9%   (also the fallback when absent)

   A "critical" count means rank 5 ALONE. Rank 4 lands on better than a
   quarter of the feed, and a number that is always high is a number people
   stop reading.

   Setting data-sev sets hue and width together — that is the whole point of
   doing it here instead of in each component. Consumers read the locals with
   a rank-0 fallback, so an element with no data-sev degrades to grey at the
   narrowest stripe, which is what "Unspecified" should look like. */

[data-sev="5"] { --sev: var(--s5); --sev-tint: var(--t5); --sev-ink: var(--i5); --sev-edge: var(--e5); --sev-stripe: var(--stripe-5); }
[data-sev="4"] { --sev: var(--s4); --sev-tint: var(--t4); --sev-ink: var(--i4); --sev-edge: var(--e4); --sev-stripe: var(--stripe-4); }
[data-sev="3"] { --sev: var(--s3); --sev-tint: var(--t3); --sev-ink: var(--i3); --sev-edge: var(--e3); --sev-stripe: var(--stripe-3); }
[data-sev="2"] { --sev: var(--s2); --sev-tint: var(--t2); --sev-ink: var(--i2); --sev-edge: var(--e2); --sev-stripe: var(--stripe-2); }
[data-sev="1"] { --sev: var(--s1); --sev-tint: var(--t1); --sev-ink: var(--i1); --sev-edge: var(--e1); --sev-stripe: var(--stripe-1); }
[data-sev="0"] { --sev: var(--s0); --sev-tint: var(--t0); --sev-ink: var(--i0); --sev-edge: var(--e0); --sev-stripe: var(--stripe-0); }

/* The card stripe. One rule, every route: hue and width arrive together and
   cannot be separated. Absolute against a position:relative,overflow:hidden
   card so it runs the full height whatever the content does. */
.bl-stripe {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  display: block;
  width: var(--sev-stripe, var(--stripe-0));
  background: var(--sev, var(--s0));
}

/* The /blotter feed draws its stripes exactly one pixel wider than the token
   at every rank (7/6/5/4 against 6/5/4/3). Expressed as calc so the ordering
   stays token-driven: change a --stripe-N and the feed follows. */
.bl-card--feed .bl-stripe { width: calc(var(--sev-stripe, var(--stripe-0)) + 1px); }

/* Severity-filter swatch: a deliberately narrower ramp than the card stripe,
   because it sits in a 200px rail next to 12.5px text and the card widths
   would read as bars.

   SIX RANKS, SIX SIZES. This claimed "the rank ordering holds" while ranks 3
   and 2 shared 3.5px/14px and ranks 1 and 0 had no rule at all, so both fell
   to the 3px/13px base. Three of the six ranks were therefore hue-only —
   invisible to a colour-blind reader and to a greyscale printer, in the one
   control whose entire job is picking a rank. The ramp is monotonic now: no
   two ranks are the same size, so the ordering is readable with the colour
   thrown away. Widths are half-pixels on purpose; the rail is dense and a
   whole-pixel ramp over six steps would be twice as wide as the text. */
.bl-sev-sw {
  display: block;
  border-radius: 2px;
  flex: 0 0 auto;
  background: var(--sev, var(--s0));
  width: 2.5px; height: 11px;
}
[data-sev="5"].bl-sev-sw { width: 5px;   height: 16px; }
[data-sev="4"].bl-sev-sw { width: 4.5px; height: 15px; }
[data-sev="3"].bl-sev-sw { width: 4px;   height: 14px; }
[data-sev="2"].bl-sev-sw { width: 3.5px; height: 13px; }
[data-sev="1"].bl-sev-sw { width: 3px;   height: 12px; }
[data-sev="0"].bl-sev-sw { width: 2.5px; height: 11px; }

/* Inline rank marker on a grouped/related record row. */
.bl-sev-bar {
  display: block;
  flex: 0 0 auto;
  border-radius: 2px;
  height: 15px;
  width: var(--sev-stripe, var(--stripe-0));
  background: var(--sev, var(--s0));
}


/* ============================================================================
   §4  APP SHELL AND CHROME  (shared by every route)
   ============================================================================ */

.fs-shell {
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  background:
    radial-gradient(circle at 15% -10%, color-mix(in srgb, var(--accent) 13%, transparent), transparent 34%),
    radial-gradient(circle at 92% 8%, color-mix(in srgb, var(--ip) 7%, transparent), transparent 30%),
    var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 15px;
}
/* A DEFINITE HEIGHT ON THE DESKTOP FRAME. min-height alone is not a height,
   so `height: 100%` on a route inside .fs-route had nothing to resolve
   against and the map fell back to its 420px floor with the viewport empty
   beneath it. Scoped above 860px on purpose: below that §14 deliberately
   hands scrolling back to the document, and pinning the shell there would
   trap the page inside one screen. */
@media (min-width: 861px) {
  .fs-shell { height: 100svh; }
}


.fs-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 22px;
  padding: 0 26px;
  height: 60px;
  border-bottom: 1px solid var(--line);
}

/* ---- Masthead --------------------------------------------------------------
   The site's identity, back in the header. The risk with a wordmark that sits
   in the same row as the nav is that it reads as a fifth destination, so it is
   separated on three axes at once: weight (600 against the nav's 500), colour
   (--text against --faint, so the nav recedes and the name does not), and a
   hairline rule with real space either side. It also never takes the accent
   underline, because it carries no data-route for markNav() to find. */
.fs-brand {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: 7px;
  flex: 0 0 auto;
  padding-right: 21px;
  margin-right: -1px;      /* the rule sits in the header's own 22px gap */
  white-space: nowrap;
  text-decoration: none;
}
/* An inset hairline rather than a border: a full-height rule would read as a
   second header divider, and this only has to separate two words from three
   links. */
.fs-brand::after {
  content: "";
  position: absolute;
  right: 0;
  top: 50%;
  width: 1px;
  height: 22px;
  margin-top: -11px;
  background: var(--line2);
}
/* The tower mark. 22px in a 60px bar: large enough that its 1.8-unit strokes
   land at 1.65 device pixels, small enough that it sits under the cap height
   of the 15.5px wordmark rather than towering over it.

   align-self:center because .fs-brand is align-items:baseline — correct for
   two runs of text, wrong for a glyph, which would otherwise hang its whole
   box off the wordmark's baseline and drop below the rule. It inherits the
   wordmark's --text through currentColor, so it is one object with the word
   in both themes and there is no hex here to keep in step with the ramp. */
.fs-brand-logo {
  width: 22px;
  height: 22px;
  flex: 0 0 auto;
  align-self: center;
  color: var(--text);
}
/* The mark stays when the header starts scrolling sideways: it is 22px, it is
   the identity, and it is the last thing that should go. */

.fs-brand-mark {
  font: 600 15.5px var(--font-sans);
  letter-spacing: -0.01em;
  color: var(--text);
}
.fs-brand-plus {
  align-self: center;
  margin-left: -3px;
  color: var(--plus-t);
  font: 800 17px/1 var(--font-sans);
}
.fs-brand-plus[hidden] { display: none !important; }
/* The navigation landmark. It exists for semantics, not for layout: the
   same 22px the header itself uses, so wrapping the three links changed
   no pixel. The header's own gap then applies between this group and the
   masthead on one side and the spacer on the other, exactly as before. */
.fs-navgroup {
  display: flex;
  align-items: center;
  gap: 22px;
  flex: 0 0 auto;
}

.fs-nav {
  font: 500 13.5px var(--font-sans);
  height: 60px;
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
  flex: 0 0 auto;
  color: var(--faint);
  text-decoration: none;
}
.fs-nav[hidden] { display: none !important; }
/* The app's tab-bar glyphs, at the size the 13.5px label wants. They inherit
   currentColor, so the icon dims, brightens on hover and goes to --text with
   its label — the accent stays on the underline alone, which is the one thing
   that says "you are here". */
.fs-nav-ico { width: 16px; height: 16px; flex: 0 0 auto; }
.fs-nav:hover { color: var(--muted); }
/* Accent underline = you are here. Accent is the dispatch/primary colour and
   navigation is a primary action, so this is a correct use of it. */
.fs-nav.is-on,
.fs-nav[aria-current="page"] { color: var(--text); box-shadow: inset 0 -2px 0 var(--accent); }

.fs-viewas {
  min-height: 38px;
  margin-left: 5px;
  padding: 0 2px 0 12px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: 0;
  border-left: 1px solid var(--line2);
  border-radius: 0;
  background: transparent;
  color: var(--faint);
  font-family: var(--font-sans);
  white-space: nowrap;
  cursor: pointer;
}
.fs-viewas[hidden] { display: none !important; }
.fs-viewas svg { width: 15px; height: 15px; flex: 0 0 auto; }
.fs-viewas-copy { display: grid; gap: 1px; text-align: left; }
.fs-viewas-label {
  color: var(--faint);
  font: 700 11px/1 var(--font-sans);
  letter-spacing: .08em;
  text-transform: uppercase;
}
.fs-viewas strong { color: var(--text); font: 700 12px/1.15 var(--font-sans); }
.fs-viewas-short { display: none; }
.fs-viewas:hover { color: var(--text); }
.fs-viewas:hover .fs-viewas-full { text-decoration: underline; text-underline-offset: 3px; }
.fs-viewas:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.fs-viewas[data-role="blotter_plus"] .fs-viewas-full { color: var(--plus-t); }

.fs-head-spacer { flex: 1; }

/* .fs-pills is gone: the nav links ARE .fs-nav, not children of a pill
   container, and nothing in the shell ever emitted the wrapper. */

/* 230px WIDE WHEN THERE IS ROOM, AND THE ONLY THING IN THE ROW THAT GIVES
   WHEN THERE IS NOT. This was min-width:230px, which is a floor, not a
   size — and a floor nothing could push through. Every other item in
   .fs-head is flex:0 0 auto and the header does not wrap, so between
   861px (where §14's phone rules stop hiding this box and stop letting
   the header scroll itself) and about 952px (where the full row finally
   fits) the row was wider than the viewport. The Blotter+ chip hung 91px
   past the right edge at 861px and took THE WHOLE PAGE sideways with it,
   on every route.
   flex-basis carries the 230px instead, so the box still measures 230
   wherever the row fits — the spacer absorbs the slack, not this — and
   min-width:0 lets it, and only it, absorb the shortfall below that.
   The search shrinks; the page never scrolls sideways. */
/* Blotter+ is a product destination, not account metadata. Give it the
   weight of a real CTA: the product name is the accessible link text, while
   the generated second line makes the destination explicit without adding
   decorative markup to the shell. Gold still means Blotter+ and nothing
   else. */
.fs-adminchip {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 13px;
  border-radius: var(--radius-pill);
  color: var(--accent-t);
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent-w);
  font: 600 11px var(--font-sans);
  white-space: nowrap;
  text-decoration: none;
  flex: 0 0 auto;
}
.fs-adminchip:hover { color: var(--text); box-shadow: inset 0 0 0 1px var(--accent); }
.fs-adminchip[hidden] { display: none !important; }
.fs-pluschip {
  display: inline-flex;
  position: relative;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  padding: 0 11px 0 35px;
  border-radius: 9px;
  font: 700 11.5px/1.1 var(--font-sans);
  letter-spacing: -.01em;
  white-space: nowrap;
  text-decoration: none;
  flex: 0 0 auto;
  box-shadow: inset 0 0 0 1px var(--plus-w);
  color: var(--plus-t);
  background: linear-gradient(145deg, var(--plus-soft), var(--surface));
  transition: transform var(--mo-1) var(--mo-ease),
              box-shadow var(--mo-1) var(--mo-ease),
              background-color var(--mo-1) var(--mo-ease);
}
.fs-pluschip[hidden] { display: none !important; }
.fs-pluschip::before {
  content: "+";
  position: absolute;
  left: 7px;
  top: 50%;
  width: 22px;
  height: 22px;
  margin-top: -11px;
  display: grid;
  place-items: center;
  border-radius: 7px;
  background: var(--plus);
  color: var(--bg);
  font: 800 15px/1 var(--font-sans);
}
.fs-pluschip::after {
  content: "View plans";
  display: block;
  color: var(--muted);
  font: 600 11px/1 var(--font-sans);
  letter-spacing: .01em;
}
.fs-pluschip:hover {
  transform: translateY(-1px);
  box-shadow: inset 0 0 0 1px var(--plus), 0 6px 18px var(--line);
}
.fs-pluschip:active { transform: translateY(0); }

/* The content well. Desktop: the shell never scrolls and each route decides
   which of its columns do. Narrow: see §14 — the document scrolls instead,
   because nested scroll regions on a phone are a trap. */
.fs-route { flex: 1; min-height: 0; overflow: hidden; }
/* The well is the skip link's target and is focused programmatically. It is
   not a control, so it takes no ring — the reader's next Tab lands on the
   first link inside the record, which is the whole point. */
.fs-route:focus { outline: none; }

/* ---- The bare well ---------------------------------------------------------
   TWO THINGS LAND IN #bl-app WITH NO ROUTE CONTAINER AROUND THEM, and until
   now neither had a box.

   1. The server-rendered body for /incident/:id. server.js
      substitutes between the BODY fences in app.html, so what arrives is a
      bare <h1> and one or two <p>. Every one of those elements is margin:0
      under §2's reset, and .fs-route carries no padding and overflow:hidden.
      The result was the site's entire organic acquisition channel rendered
      hard against the viewport edge, headline welded to body copy, and
      clipped at the fold with no scroll able to reach the rest. Those pages
      are what a crawler and a no-script reader see, so they are the ones
      that could least afford it.

   2. The single paragraph BL.render drops there when a route throws, and the
      same from routes/plus.js when the plan objects are missing.

   .fs-fallback scrolls on its own rather than handing scroll to .fs-route,
   which stays overflow:hidden for the routes that manage their own columns. */
.fs-fallback {
  height: 100%;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  padding: 30px 26px 34px;
}
.fs-fallback h1 {
  font: 700 34px/1.14 var(--font-sans);
  letter-spacing: -.025em;
  color: var(--text);
  max-width: 22ch;
  text-wrap: balance;
}
.fs-fallback p {
  font: 400 15px/1.65 var(--font-sans);
  color: var(--muted);
  max-width: 68ch;
  margin-top: 14px;
}
.fs-fallback .bl-lede { margin: 14px 0 0; }
.fs-fallback .bl-empty { max-width: 68ch; margin-top: 0; }

/* The router's own failure paragraph, appended straight to the well. */
.fs-route > .bl-empty { margin: 26px; max-width: 68ch; }

@media (max-width: 860px) {
  /* §14 hands scrolling to the document at this width, so the fallback must
     not keep a scroller of its own inside it — that is the nested-scroller
     trap, and on the one page a search engine sent someone to. */
  .fs-fallback { height: auto; overflow: visible; padding: 22px 16px 28px; }
  .fs-fallback h1 { font-size: 27px; }
  .fs-route > .bl-empty { margin: 18px 16px; }
}


/* ============================================================================
   §5  PRIMITIVES  (deduplicated across every route)
   ============================================================================ */

/* ---- Eyebrow ---------------------------------------------------------------
   The all-caps hairline heading. Six routes described this separately as
   .bl-eyebrow, .bl-section-head, .bl-town-seclabel, .dg-card-label,
   .set-eyebrow and .plus-matrix__col. They are one thing. */
.bl-eyebrow {
  font: 500 11px var(--font-sans);
  text-transform: uppercase;
  letter-spacing: .085em;
  color: var(--faint);
  margin: 0 0 10px;
}
.bl-eyebrow--flush { margin-bottom: 0; }
.bl-eyebrow--gap   { margin-bottom: 14px; }
/* Settings' six section headings sit in a bordered list: the 2px bottom puts
   the section's first hairline rule 2px under the label, as drawn. */
.bl-eyebrow--section { padding: 22px 0 2px; margin: 0; }
/* In-table group label. One notch smaller and one notch looser than a card
   eyebrow, deliberately — it is what tells a group label from a card title. */
.bl-eyebrow--group {
  font-size: 11px;
  letter-spacing: .09em;
  padding: 14px 4px 8px;
  margin: 0;
}
.bl-eyebrow--group-later { padding-top: 18px; }

/* ---- Display type ---------------------------------------------------------- */
.bl-headline {
  font: 700 42px/1.1 var(--font-sans);
  letter-spacing: -.03em;
  color: var(--text);
  margin: 0;
  text-wrap: pretty;
}
.bl-headline--xl { font-size: 46px; line-height: 1.08; max-width: 17ch; }
.bl-headline--tight { max-width: 20ch; }

.bl-title {
  font: 700 36px/1.12 var(--font-sans);
  letter-spacing: -.028em;
  color: var(--text);
  margin: 0;
  text-wrap: balance;
}

.bl-deck {
  font: 400 15px/1.6 var(--font-sans);
  color: var(--muted);
  margin: 12px 0 0;
  max-width: 66ch;
  text-wrap: pretty;
}
.bl-deck--lg { font-size: 16px; line-height: 1.62; max-width: 60ch; }

/* Long-form model-written prose. --text, not --muted: this is the primary
   content of the page it appears on, and --muted is a .62 alpha in dark. */
.bl-prose {
  font: 400 15px/1.7 var(--font-sans);
  color: var(--text);
  max-width: 70ch;
  text-wrap: pretty;
}
.bl-prose p { margin: 0 0 16px; }
.bl-prose p:last-child { margin: 0; }

/* One paragraph of orientation above a body of records. */
.bl-lede {
  font: 400 13px/1.6 var(--font-sans);
  color: var(--muted);
  margin: 0 0 20px;
  max-width: 68ch;
  text-wrap: pretty;
}
.bl-lede b { font-weight: 600; color: var(--text); }

.bl-crumb { font: 400 12px var(--font-sans); color: var(--faint); }
.bl-crumb a { color: inherit; text-decoration: none; }
.bl-crumb a:hover { color: var(--muted); }

/* ---- Hairline --------------------------------------------------------------
   Every separator that eats remaining width. Real borders are reserved for
   structural edges (column dividers, the header rule, row rules). */
.bl-rule { flex: 1; height: 1px; background: var(--line); }

/* ---- Section header with rule ---------------------------------------------- */
.bl-sechead { display: flex; align-items: center; gap: 9px; margin-bottom: 14px; }
.bl-secnote { font: 400 11.5px var(--font-sans); color: var(--faint); white-space: nowrap; }

/* ---- Badges ----------------------------------------------------------------
   One pill, six meanings. Severity reads the §3 locals so it can never be
   set without its rank width also being set on the card it belongs to. */
.bl-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 9px;
  border-radius: 11px;
  font: 600 11px/1.5 var(--font-sans);
  text-transform: uppercase;
  letter-spacing: .045em;
  white-space: nowrap;
}
.bl-badge--lg { padding: 3px 10px; }

.bl-badge--sev { background: var(--sev-tint, var(--t0)); color: var(--sev-ink, var(--i0)); box-shadow: inset 0 0 0 1px var(--sev-edge, var(--e0)); }

/* Status is not severity. A call is open when closed, cancelled AND filed are
   all null. Open borrows the rank-1 triple and closed the rank-0 triple
   because those read as "running" and "done" — it never touches --s4/--s5,
   which would say "this call is serious" when it means "this call is live". */
.bl-badge--open   { background: var(--t1); color: var(--i1); box-shadow: inset 0 0 0 1px var(--e1); }
.bl-badge--closed { background: var(--t0); color: var(--i0); box-shadow: inset 0 0 0 1px var(--e0); }

/* A MACHINE WROTE THIS. The only sanctioned use of the violet ramp anywhere
   in this file. Put it on model-written prose, model-written titles, and
   model-inferred groupings — never on a count, a control, or CAD data. */
.bl-badge--ai {
  background: var(--tp);
  color: var(--ip);
  box-shadow: inset 0 0 0 1px var(--ep);
  font-size: 11px;
  letter-spacing: .055em;
  border-radius: 10px;
  padding: 2px 8px;
}
.bl-badge--ai-lg { font-size: 11px; letter-spacing: .06em; padding: 3px 10px; border-radius: 11px; }

.bl-badge--plus {
  background: var(--plus-soft);
  color: var(--plus-t);
  box-shadow: inset 0 0 0 1px var(--plus-w);
  font-size: 11px;
  letter-spacing: .055em;
  border-radius: 10px;
  padding: 2px 7px;
}

/* Coverage capability, e.g. "Dispatch record". The prototype painted this
   violet — wrong: a coverage fact is asserted by us from the CAD feed, not
   written by a model. It moves to the accent family, which is the CAD side. */
.bl-badge--cap {
  background: var(--accent-soft);
  color: var(--accent-t);
  box-shadow: inset 0 0 0 1px var(--accent-w);
  font: 600 11px var(--font-sans);
  letter-spacing: .06em;
  border-radius: var(--radius-pill);
  padding: 4px 10px;
}

/* Three-bar rank meter. Lead card only — it is what makes one card on the
   page louder than the rest, and it stops working the moment it is everywhere.
   currentColor, so it is always the badge's own ink. */
.bl-meter { display: inline-flex; align-items: flex-end; gap: 1.5px; height: 8px; }
.bl-meter i { display: block; width: 2.5px; border-radius: 1px; background: currentColor; }
.bl-meter i:nth-child(1) { height: 4px; }
.bl-meter i:nth-child(2) { height: 6px; }
.bl-meter i:nth-child(3) { height: 8px; }

/* ---- Chips (selectable pills) ----------------------------------------------
   The theme toggle and the export-scope picker are the same control. */
.bl-chip {
  display: inline-flex;
  align-items: center;
  padding: 0 12px;
  min-height: 30px;
  border-radius: var(--radius-pill);
  font: 500 12px var(--font-sans);
  white-space: nowrap;
  cursor: pointer;
  color: var(--muted);
  box-shadow: inset 0 0 0 1px var(--line2);
}
.bl-chip:hover { color: var(--text); }
.bl-chip.is-on,
.bl-chip[aria-pressed="true"] { color: var(--accent-t); box-shadow: inset 0 0 0 1px var(--accent); }
/* .bl-chip--sm, the header-bar size, is gone: the header's only chip pair was
   the theme switch, which is now .fs-theme, and nothing else emitted it. */

.bl-chips { display: flex; gap: 6px; flex-wrap: wrap; }

/* ---- Segmented control (Cards | Table) -------------------------------------- */
.bl-seg {
  display: inline-flex;
  border-radius: 7px;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--line2);
  flex: 0 0 auto;
}
.bl-seg-b {
  display: inline-flex;
  align-items: center;
  gap: 5px;               /* only ever seen when a padlock is in front of the label */
  padding: 5px 11px;
  font: 500 11.5px var(--font-sans);
  color: var(--muted);
  cursor: pointer;
}
.bl-seg-b:hover { color: var(--muted); }
.bl-seg-b.is-on { color: var(--accent-t); box-shadow: inset 0 0 0 1px var(--accent); }
.bl-seg-b:focus-visible { outline-offset: -2px; }

/* ---- The Blotter+ gate, as a control wears it -------------------------------
   GOLD IS THE WHOLE MECHANISM. --plus means Blotter+ and means nothing else on
   this site, so a control painted with it is saying which tier it belongs to
   before anybody touches it — which is the one thing the gate has to do, given
   that the server cannot enforce any of it (one shared key, no accounts).

   A locked control is drawn at FULL SIZE and full opacity. It is not dimmed,
   not blurred and not removed: a reader who cannot see what the subscription
   is for has no reason to want it, and the last version of this gate hid the
   product and then handed back a smear. The padlock and the gold say "this
   one costs"; the control still answers when it is pressed, with a line
   naming the price. Nothing here is disabled, so all of it stays in the tab
   order and a keyboard reader meets the same offer.

   These come after .is-on deliberately. The two cannot co-occur in practice —
   a clamped preference lights the tier the reader actually has, not the one
   they stored — but if they ever did, the gate is the more important fact. */
.bl-seg-b.is-plus,
.bl-chip.is-plus {
  color: var(--plus-t);
  background: var(--plus-soft);
  box-shadow: inset 0 0 0 1px var(--plus-w);
}
.bl-seg-b.is-plus:hover,
.bl-chip.is-plus:hover { color: var(--plus-t); box-shadow: inset 0 0 0 1px var(--plus); }
.bl-chip.is-plus { gap: 5px; }

/* The padlock itself, in front of a label. flex:0 0 auto so a long department
   name cannot squeeze it into an ellipse. */
.bl-lock-glyph { flex: 0 0 auto; color: var(--plus-t); }

/* The way out of a gate, inside the sub-line the gate just wrote. It is the
   only link that appears in a .bl-row-sub, which is why it is styled here
   rather than as a rule on that class — a sub-line is otherwise plain text
   and should not start underlining things. */
.bl-gate-link { color: var(--plus-t); text-decoration: none; white-space: nowrap; }
.bl-gate-link:hover { text-decoration: underline; }

/* One line of the gate list in the /settings rail: padlock, then the name of
   a control the reader can scroll up and look at. */
.bl-gate-item {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 7px;
  font: 400 12.5px var(--font-sans);
  color: var(--muted);
}

/* ---- Tabs ------------------------------------------------------------------- */
.bl-tabs { display: flex; gap: 5px; margin-top: 20px; padding-bottom: 6px; border-bottom: 1px solid var(--line); }
.bl-tab {
  padding: 0 0 12px;
  font: 500 14px var(--font-sans);
  white-space: nowrap;
  cursor: pointer;
  color: var(--faint);
}
.bl-tab:hover { color: var(--muted); }
.bl-tab { padding: 8px 11px; border-radius: 8px; }
.bl-tab.is-on,
.bl-tab[aria-selected="true"] { color: var(--text); background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent-w); }
.bl-tab:focus-visible { outline-offset: 2px; }

/* ---- Buttons ----------------------------------------------------------------- */
.bl-btn {
  flex: 1;
  min-height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 16px;
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px var(--line2);
  color: var(--muted);
  font: 500 12.5px var(--font-sans);
  white-space: nowrap;
  cursor: pointer;
  text-decoration: none;
}
.bl-btn:hover { color: var(--text); }
.bl-btn--primary { box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent-t); }
/* Buying a subscription is a Blotter+ action, not a dispatch action, so the
   one primary control on /plus is still not painted --accent. */
.bl-btn--plus { box-shadow: inset 0 0 0 1px var(--plus-w); background: var(--plus-soft); color: var(--plus-t); font-size: 13px; }
.bl-btn--ghost-plus { box-shadow: inset 0 0 0 1px var(--plus); background: none; color: var(--plus-t); }
.bl-btn--lg { min-height: 48px; padding: 0 22px; font-size: 15px; }
.bl-btn--auto { flex: 0 0 auto; }

.bl-actions { display: flex; gap: 8px; }

/* Quiet inline action — a text button that must not compete with a card. */
.bl-linkbtn { font: 500 11px var(--font-sans); color: var(--accent-t); cursor: pointer; }
.bl-linkbtn:hover { text-decoration: underline; }

/* ---- Notes / editorial cards -------------------------------------------------
   Six routes described this box separately (privacy notes, rail cards, the
   "Delivered" card, the plus rail, the town empty state). It is one component:
   transparent, a 1px inset --line ring over the page ground, no fill. */
.bl-note {
  padding: 16px 18px;
  border-radius: var(--radius-card);
  box-shadow: inset 0 0 0 1px var(--line);
  background: none;
}
.bl-note--lg { padding: 18px 20px; }
.bl-note--plus {
  box-shadow: inset 0 0 0 1px var(--plus-w);
  background: var(--plus-soft);
  display: block;
  text-decoration: none;
}
.bl-note-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.bl-note-head svg { color: var(--faint); flex: 0 0 auto; }
.bl-note-title { font: 500 12.5px var(--font-sans); color: var(--text); margin: 0; }
.bl-note-title--plus { font: 600 15px var(--font-sans); color: var(--plus-t); }
.bl-note-body { font: 400 12.5px/1.65 var(--font-sans); color: var(--muted); margin: 0; }
.bl-note-body + .bl-note-body { margin-top: 8px; }
.bl-note-cta { display: block; font: 500 12.5px var(--font-sans); color: var(--plus-t); margin-top: 11px; }

/* Absence, not a statement — same box, one step quieter than body copy.
   IT IS NOT ON --faint. This is the class every loading, empty, error and
   offline message on the site is built from, and --faint measures 3.74:1 in
   dark and 3.97:1 in light against --bg, below AA for normal text at any
   size. A reader who cannot read the failure message has been told nothing.
   --muted is 6.2:1 dark and 7.0:1 light and is the quietest ink a sentence
   may take here. When the token ramp is repaired (see the note at the top of
   this file) --faint becomes legal again — but a state message still wants
   the weight, so this stays. */
.bl-empty {
  padding: 16px 18px;
  border-radius: var(--radius-card);
  box-shadow: inset 0 0 0 1px var(--line);
  font: 400 12.5px/1.65 var(--font-sans);
  color: var(--muted);
  margin: 0;
}

/* ---- Unit chips --------------------------------------------------------------
   From responses_all_depts[].unit. CAD data, not radio. --unit is the token
   that means "unit designator" and it means nothing else. */
.bl-units { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.bl-unit {
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  font: 500 11px var(--font-mono);
  background: var(--unit-bg);
  color: var(--unit);
}
.bl-unit--lg { padding: 5px 10px; border-radius: 6px; font-size: 12px; }
.bl-unit-more { font: 400 11px var(--font-sans); color: var(--faint); }
.bl-unit-sp { flex: 1; }
.bl-incid { font: 400 11px var(--font-mono); color: var(--faint); }

/* ---- Load-more divider --------------------------------------------------------
   A rule-label-rule, not a button, so the end of the window does not compete
   with the records above it. */
.bl-more { display: flex; align-items: center; gap: 12px; padding: 8px 2px; width: 100%; cursor: pointer; }
.bl-more-label { font: 400 12px var(--font-sans); color: var(--faint); white-space: nowrap; }
.bl-more:hover .bl-more-label { color: var(--muted); }

/* ---- Stats -------------------------------------------------------------------- */
.bl-stats { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 9px; }
.bl-stat-l {
  display: block;
  font: 500 11px var(--font-sans);
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--faint);
  white-space: nowrap;
}
.bl-stat-v { display: block; font: 600 17px/1.2 var(--font-mono); color: var(--text); margin-top: 4px; }

/* Left-packed, capped at 150px, count-agnostic: grid-auto-flow:column means
   three cells or five cells both work with no template change, and
   justify-content:start keeps the strip from stretching to fill. */
.bl-statgrid {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 150px);
  justify-content: start;
  gap: 18px;
  margin: 24px 0 0;
}
.bl-statgrid .bl-stat-l { font-size: 11px; letter-spacing: .085em; }
.bl-statgrid .bl-stat-v { font-size: 26px; line-height: 1.15; margin-top: 7px; }

/* Key/value row form — a rule above every row including the first, so a
   hairline sits under the card's label. */
.bl-statrow {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 9px 0;
  border-top: 1px solid var(--line);
}
.bl-statrow-k { font: 400 13px var(--font-sans); color: var(--muted); flex: 1; }
.bl-statrow-v { font: 500 14px var(--font-mono); color: var(--text); font-variant-numeric: tabular-nums; }
/* A value that is a name rather than a number leaves the mono face and drops
   a pixel — mono is reserved for measured quantities. */
.bl-statrow-v--text { font: 500 13px var(--font-sans); }

/* ---- Map ----------------------------------------------------------------------
   A CSS drawing in the prototype. A real tile layer must keep the height, the
   radius and the --map/--grid ground; pins come from latitude/longitude, and
   render nothing at all when either is null. */
.bl-map {
  position: relative;
  overflow: hidden;
  height: 96px;
  background: var(--map);
}
.bl-map--lg { height: 180px; border-radius: var(--radius-md); box-shadow: 0 0 0 1px var(--line2); }
/* The town's real municipal border, drawn as SVG from the same polygons
   reference.js uses to decide which town a call is in.

   What was here was .bl-map-grid — a repeating-linear-gradient street
   grid — and .bl-map-road, a single 74deg gradient standing in for a
   road. Both are gone. The pin over them was placed from the record's
   true coordinates, and that is precisely what made the fake street
   underneath dishonest rather than decorative. */
.bl-map-ground { position: absolute; inset: 0; overflow: hidden; }
.bl-map-tile {
  position: absolute;
  width: 256px; height: 256px;
  /* Full strength. The pins carry their own halo and the border is a
     stroke, so neither needs the ground dimmed to stay legible — and a
     knocked-back basemap on a small panel just reads as fog. */
  opacity: 1;
  transition: opacity var(--mo-2) var(--mo-ease);
}
/* THE GROUND ASSEMBLES, IT DOES NOT FLASH. An <img> that has no bytes yet
   paints nothing, so a tile layer built without this arrives as a grid of
   rectangles snapping on one at a time in whatever order the network
   answered. BL.tileIn holds each tile here and clears the class a frame
   after its own load — or its own error, because a 404 tile stuck at zero
   would be a permanent hole that looks exactly like a bug. */
.bl-map-tile.is-loading { opacity: 0; }
.bl-map-over { position: absolute; inset: 0; width: 100%; height: 100%; }

/* The town's real municipal border, over the tiles. Same polygons
   reference.js uses to decide which town a call is in, so the outline
   and the record cannot disagree. Fill is a wash, not a block: the
   streets underneath are the reason the tiles are there. */
.bl-map-area {
  fill: var(--accent);
  fill-opacity: .07;
  stroke: var(--accent);
  stroke-opacity: .55;
  stroke-width: 1.25;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

/* Severity by fill AND radius, the same rule the stripes follow, so the
   ordering survives greyscale. The halo is the page background, so a pin
   stays findable over a busy tile. */
.bl-map-pin {
  fill: var(--s0);
  stroke: var(--bg);
  stroke-width: 2;
  paint-order: stroke;
}
.bl-map-pin[data-sev="5"] { fill: var(--s5); r: 6.5; }
.bl-map-pin[data-sev="4"] { fill: var(--s4); r: 6; }
.bl-map-pin[data-sev="3"] { fill: var(--s3); r: 5.5; }
.bl-map-pin[data-sev="2"] { fill: var(--s2); r: 5; }
.bl-map-pin[data-sev="1"] { fill: var(--s1); r: 4.5; }
.bl-map-pin[data-sev="0"] { fill: var(--s0); r: 4; }

.bl-map-credit {
  position: absolute;
  right: 4px; bottom: 3px;
  padding: 1px 5px;
  border-radius: var(--radius-sm, 4px);
  background: var(--bg);
  color: var(--faint);
  font: 400 11px var(--font-sans);
  opacity: .8;
  pointer-events: none;
}

/* The empty state of a map panel, so it follows .bl-empty off --faint. */
.bl-map-none {
  margin: 0;
  padding: 0 12px;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  color: var(--muted);
  font: 400 11.5px var(--font-sans);
}

/* The absolutely-positioned .bl-pin system is gone. Three pin
   implementations existed at once; the two that survive are .bl-map-pin
   (the rail panels) and .bl-mappin (the map screen), both SVG circles
   that carry rank as radius as well as hue. */

/* ---- The mini map -------------------------------------------------------------
   BL.miniMap, for a rail that is showing ONE call. .bl-map above answers
   "where in the county" — it fits an extent and traces a municipal border, so a
   single call comes out as a dot inside an outline of a town. This answers the
   question a reader of one incident is actually asking, which is WHICH ROAD.

   It is a different object and it has to look like one, or a reader will try to
   pan it. Four tells, none of them decoration:

     · a taller box with a radius and its own ring, so it reads as a plate
       rather than as the flush band .bl-map draws;
     · corner brackets — the viewfinder convention for "this is a crop";
     · a reticle through the marker, so the marker is being POINTED AT rather
       than merely present;
     · a scale bar, which is the only element on the panel that states how
       tight the zoom is. That is the whole difference between this and the
       county map, and it is stated rather than implied.

   THERE IS NO STREET VIEW HERE and there is not going to be one. See the header
   over BL.miniMap in blotter.js: a Google panorama needs a billed Maps key and
   sends the incident's address to Google on page load for every reader, on a
   site whose disclaimer says nothing leaves the machine but a map tile. The road
   labels on the voyager ground are the honest version of what it was for. */
.bl-minimap {
  height: 128px;
  /* A stated height in a flex-column rail has to refuse to be squeezed: an
     empty box has a min-content height of zero, so without this the panel
     collapses the moment the rail is taller than its column. */
  flex: 0 0 auto;
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px var(--line2);
}
.bl-minimap--tall { height: 168px; }

/* The viewfinder brackets. Two pseudo-elements, two corners — four would be a
   frame, and a frame reads as a border rather than as a crop.

   BOTH ARE AT THE TOP, which is not an aesthetic choice: the bottom edge of
   this panel is already carrying the scale bar on the left and the © credit on
   the right, and a bracket ruled through either of them is a hairline crossing
   text. The top edge is the only clear one. */
.bl-minimap::before,
.bl-minimap::after {
  content: "";
  position: absolute;
  top: 6px;
  z-index: 2;
  width: 11px; height: 11px;
  border-top: 1px solid var(--line2);
  pointer-events: none;
  opacity: .75;
}
.bl-minimap::before { left: 6px; border-left: 1px solid var(--line2); }
.bl-minimap::after { right: 6px; border-right: 1px solid var(--line2); }
/* The empty state fills the plate, so the brackets are not left framing
   nothing. */
.bl-minimap .bl-map-none { position: relative; z-index: 1; }

/* The reticle. Hairline, low contrast, and BROKEN around the marker — lines
   crossing underneath a dot read as a defect in the tile, a gap reads as an
   instrument pointing at something. */
.bl-minimap-cross {
  stroke: var(--text);
  stroke-opacity: .18;
  stroke-width: 1;
  stroke-dasharray: 2 4;
}

/* Halo, ring, dot: the page ground knocked back under the marker, the severity
   hue as an outline, and the severity hue solid. Rank is size AND colour here
   exactly as it is on a card stripe, so the marker survives greyscale. --sev
   comes from data-sev on .bl-minimap itself (§3). */
.bl-minimap-halo {
  fill: var(--bg);
  fill-opacity: .55;
  stroke: var(--sev, var(--s0));
  stroke-opacity: .25;
  stroke-width: 1;
}
.bl-minimap-ring {
  fill: none;
  stroke: var(--sev, var(--s0));
  stroke-opacity: .7;
  stroke-width: 1.5;
}
.bl-minimap-dot {
  fill: var(--sev, var(--s0));
  stroke: var(--bg);
  stroke-width: 2;
  paint-order: stroke;
  r: 4.5;
}
[data-sev="5"] .bl-minimap-dot { r: 6; }
[data-sev="4"] .bl-minimap-dot { r: 5.5; }
[data-sev="3"] .bl-minimap-dot { r: 5; }
[data-sev="2"] .bl-minimap-dot { r: 4.5; }
[data-sev="1"] .bl-minimap-dot { r: 4; }
[data-sev="0"] .bl-minimap-dot { r: 3.5; }

/* Other calls that happen to be in frame. Deliberately plain — they are
   context, and a second marker with the same treatment as the first would make
   the panel ambiguous about which call it is a picture of. */
.bl-minimap-near {
  fill: var(--sev, var(--s0));
  fill-opacity: .8;
  stroke: var(--bg);
  stroke-width: 1.5;
  paint-order: stroke;
}

/* The scale bar. Real distance, computed from the latitude and the zoom, in
   feet because this is a Connecticut street. It sits opposite the © credit so
   the two never collide. */
.bl-minimap-scale {
  position: absolute;
  left: 5px; bottom: 4px;
  z-index: 2;
  display: flex;
  align-items: flex-end;
  gap: 5px;
  padding: 2px 5px 1px;
  border-radius: var(--radius-sm, 4px);
  background: var(--bg);
  color: var(--faint);
  font: 400 11px var(--font-sans);
  opacity: .86;
  pointer-events: none;
}
.bl-minimap-scale i {
  display: block;
  height: 4px;
  border: 1px solid var(--faint);
  border-top: 0;
  opacity: .8;
}

/* ---- Locked prose: DELETED, and not to be written again -------------------------
   There were two rules here, .bl-blur and .bl-blur--sm, which put a 5px
   smear over model-written paragraphs a free reader had not paid for.
   Nothing referenced them: routes/digest.js removed the last use and its
   header says why — the blur "is defeated by the element inspector, so it
   claimed an enforcement that did not exist, and it handed a reader who
   had asked for the week a 4px smear instead of an answer."

   They are deleted rather than left dead because dead CSS in a design
   system is an invitation. This is the failure this project shipped twice,
   and a ready-made class named for it is how it gets shipped a third time.

   WHAT TO USE INSTEAD, in every case: .bl-lock / BL.lockCard below. A gate
   that cannot be enforced has to be honest and legible — say what is
   behind it, say what it costs, and leave the text out of the DOM. Never
   render the thing and obscure it. */

/* ---- Blotter+ lock row / card ---------------------------------------------------
   A quiet outlined offer, never an error and never a denial. A reader who
   cannot have the third saved view is not doing anything wrong. */
.bl-lock {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 18px;
  border-radius: var(--radius-card);
  box-shadow: inset 0 0 0 1px var(--plus-w);
  background: var(--plus-soft);
  cursor: pointer;
  text-decoration: none;
  color: inherit;
  max-width: 640px;
}
.bl-lock-icon { color: var(--plus-t); flex: 0 0 auto; }
.bl-lock-text { flex: 1; display: block; min-width: 0; }
.bl-lock-title { display: block; font: 500 13.5px var(--font-sans); color: var(--text); }
.bl-lock-sub { display: block; font: 400 11.5px/1.5 var(--font-sans); color: var(--muted); margin-top: 3px; }
.bl-lock-cta { font: 500 12.5px var(--font-sans); color: var(--plus-t); white-space: nowrap; }


/* ============================================================================
   §6  INCIDENT CARD  (the workhorse — /blotter and /town)
   ============================================================================
   One shell, three densities and four states. The /blotter feed card and the
   /town dispatch card were specified separately; they are the same component
   at two type scales, so they are one component here.

   States: open+ranked, ranked with no status badge, closed (dimmed), and a
   single-line variant with no right-hand meta. */

.bl-card {
  position: relative;
  overflow: hidden;
  display: block;
  /* DO NOT LET A CARD SHRINK. .bl-feed is a flex column, and the moment
     the shell got a definite height every card started shrinking to fit
     the box instead of the box scrolling — with overflow:hidden above,
     that clipped every title top and bottom and turned the feed into
     twenty squashed bars. flex-shrink defaults to 1; a card is content,
     not a spacer. */
  flex: 0 0 auto;
  background: var(--surface2);
  border-radius: var(--radius-md);
  box-shadow: 0 0 0 1px var(--line);
  cursor: pointer;
  text-decoration: none;
  color: inherit;
  padding: 13px 16px 13px 24px;
}
.bl-card:hover { box-shadow: 0 0 0 1px var(--line2); }

/* Closed records keep their text contrast: the severity the CAD assigned is
   still true after the call closes, so the stripe keeps its real rank. Rank 0
   means "Unspecified", not "finished", and must never be used to mean closed. */
.bl-card.is-closed { opacity: 1; }

/* The ordinary feed is a scan list. Its old 21px headline, 13px vertical
   padding and agency-reference footer made each row needlessly tall. */
.bl-card--feed { padding: 9px 13px 9px 19px; }
.bl-card--feed .bl-titlerow { gap: 6px; }
.bl-card--feed .bl-card-title {
  font: 600 16px/1.2 var(--font-sans);
  letter-spacing: 0;
}
.bl-card--feed .bl-time { font-size: 11px; }
.bl-card--feed .bl-badge { padding: 1px 7px; font-size: 11px; }
.bl-card--feed .bl-meta { font-size: 12px; }
.bl-card--feed .bl-meta--solo { margin-top: 4px; }

/* Rail density (/town's promoted list). Flat 21px left padding at every rank,
   NOT stripe-width + gap: titles must align down the column across severities
   or the list reads as ragged. The stripe still varies; the text does not. */
.bl-card--compact {
  padding: 12px 14px 12px 21px;
  box-shadow: inset 0 0 0 1px var(--line);
  background: none;
  margin-bottom: 7px;
}
.bl-card--compact:last-child { margin-bottom: 0; }
/* "Featured" is carried by the surface and the accent ring. The prototype also
   widened its stripe to 5px at rank 3, which would lie about rank — the width
   channel belongs to severity alone, so it stays token-driven here. */
.bl-card--compact.is-featured { background: var(--surface); box-shadow: 0 0 0 1px var(--accent-w); }

.bl-titlerow { display: flex; align-items: baseline; gap: 9px; }

/* inc_category arrives SHOUTED and verbose when the model-written title is
   null ("PSYCH/ABN. BEHAVIOR/SUIC ATTEMPT -COLD"), which is the common case.
   The single-line ellipsis is load-bearing, and the fallback needs
   case-normalising before it reaches a 21px bold line. */
.bl-card-title {
  font: 700 21px/1.15 var(--font-sans);
  letter-spacing: -.015em;
  color: var(--text);
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin: 0;
}
.bl-card--compact .bl-card-title { font: 600 16px/1.2 var(--font-sans); letter-spacing: 0; }

.bl-time { font: 500 12px var(--font-mono); color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.bl-card--compact .bl-time { font-size: 11.5px; }
/* Tinted elapsed is a THRESHOLD STATE — a call that has been open long enough
   to be worth noticing — not decoration. Routes must pick that threshold
   explicitly and only then add this class. */
.bl-elapsed { color: var(--i3); }

.bl-metarow { display: flex; align-items: baseline; gap: 16px; margin-top: 8px; }
.bl-meta {
  font: 400 13.5px var(--font-sans);
  color: var(--muted);
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin: 0;
}
.bl-meta--solo { margin: 8px 0 0; flex: none; display: block; }
.bl-card--compact .bl-meta { font-size: 12.5px; color: var(--faint); margin-top: 6px; }
/* Reserved slot for a short right-hand qualifier. Kept because the layout
   depends on .bl-metarow having a flex:1 child plus an optional fixed one. */
.bl-meta-quiet { font: 400 11.5px var(--font-sans); color: var(--faint); white-space: nowrap; flex: 0 0 auto; }

.bl-card .bl-units { margin-top: 10px; }


/* ============================================================================
   §7  GROUPED / RELATED RECORDS  (/blotter lead card footer, /incident foot)
   ============================================================================
   The other CAD records the dispatch centre opened for the same event — the
   second alarm, the cancelled smoke call next door.

   The violet pill is CORRECT here and is the only violet on either route: the
   grouping is inferred by a model, not asserted by the CAD. Members are held
   in secondary type so they read as subordinate without reducing contrast. */

.bl-group {
  padding: 11px 16px 13px 24px;
  border-top: 1px solid var(--line2);
  background: var(--surface2);
}
/* Standalone (on /incident it is a card at the foot of the scroll body, not a
   footer welded to another card). */
.bl-group--card {
  margin-top: 26px;
  padding: 11px 16px 13px;
  border-top: 0;
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--line2);
}
.bl-group-top { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; }
.bl-group-cap { font: 400 11px var(--font-sans); color: var(--faint); flex: 1; }
.bl-group-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
  opacity: 1;
  text-decoration: none;
  color: inherit;
}
.bl-group-row:hover { opacity: 1; }
.bl-group-txt {
  font: 400 12.5px var(--font-sans);
  color: var(--muted);
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bl-group-time { font: 500 11px var(--font-mono); color: var(--faint); white-space: nowrap; }

/* A group welded beneath a feed card is supporting context, not a second
   card. Keep the standalone /incident treatment roomy, but compress the feed
   footer so one related CAD record does not nearly double the row height. */
.bl-card--feed .bl-group { padding: 6px 13px 7px 19px; }
.bl-card--feed .bl-group-top { margin-bottom: 2px; }
.bl-card--feed .bl-group-row { gap: 8px; padding: 3px 0; }
.bl-card--feed .bl-sev-bar { height: 12px; }


/* ============================================================================
   §8  ROUTE — /blotter   three columns
   ============================================================================
   236px filter rail | fluid feed | 408px context rail. Each column scrolls
   independently; the grid itself never scrolls.

   The context rail is the only --surface2 field on the route. The feed sits
   directly on --bg and its standard cards are --surface2, so they read as the
   same value as the rail. That is deliberate: the lead card is the only thing
   on the page lifted to --surface. */

.bl-blotter {
  height: 100%;
  display: grid;
  grid-template-columns: 236px minmax(0, 1fr) 408px;
  overflow: hidden;
}

/* The loading, empty and failed states have no context rail: it held the
   standing privacy panel and nothing else. Two tracks rather than 408px of
   empty column beside a one-line message. */
.bl-blotter--noctx { grid-template-columns: 236px minmax(0, 1fr); }

.bl-rail {
  border-right: 1px solid var(--line);
  padding: 18px 18px 20px;
  display: flex;
  flex-direction: column;
  gap: 20px;
  overflow: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line2) transparent;
}

/* The browser default can become a heavy black gutter in a themed rail.
   Keep the affordance, but make it a narrow surface-coloured thumb with no
   contrasting track so it belongs to both the light and dark palettes. */
.bl-rail::-webkit-scrollbar { width: 8px; }
.bl-rail::-webkit-scrollbar-track { background: transparent; }
.bl-rail::-webkit-scrollbar-thumb {
  border: 2px solid transparent;
  border-radius: var(--radius-pill);
  background: var(--line2);
  background-clip: padding-box;
}
.bl-rail::-webkit-scrollbar-thumb:hover { background-color: var(--muted); }

.bl-feed {
  min-width: 0;
  overflow: auto;
  padding: 20px 24px 26px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.bl-ctx {
  border-left: 1px solid var(--line);
  background: var(--surface2);
  padding: 20px 22px 26px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  overflow: auto;
}
.bl-ctx .bl-eyebrow { margin-bottom: 11px; }

/* ---- Live status card (filter rail) ---------------------------------------- */
.bl-live {
  padding: 12px 13px;
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px var(--accent-w);
  background: var(--accent-soft);
}
.bl-live-top { display: flex; align-items: center; gap: 7px; }
/* The prototype painted this dot --s1/--s0, which are SEVERITY RANK tokens. A
   poll indicator has no rank — it is status. It takes the same rank-1/rank-0
   ink the open/closed status badge uses, so "green means running" stays one
   idea across the site without claiming a severity the call does not have. */
.bl-live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--i0); display: block; flex: 0 0 auto; }
.bl-live-dot.is-live { background: var(--i1); animation: fsPulse 2s ease-in-out infinite; }
.bl-live-label { font: 500 11.5px var(--font-sans); flex: 1; }
.bl-live-foot { display: flex; align-items: center; gap: 7px; margin-top: 9px; }
/* Both clocks must reflect the real poll cadence (45s plus, 120s free) or the
   card is a lie about a product entitlement. */
.bl-live-next { font: 400 11px var(--font-sans); color: var(--muted); flex: 1; }

/* ---- Compact feed filters ---------------------------------------------------- */
.bl-filterbox { border: 1px solid var(--line); border-radius: var(--radius-md); padding: 11px; background: var(--surface2); }
.bl-filterhead { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
.bl-filterhead .bl-eyebrow { margin: 0; }
.bl-filterall { cursor: pointer; border: 1px solid var(--line2); border-radius: var(--radius-pill); padding: 3px 8px; color: var(--faint); font: 600 11px var(--font-sans); }
.bl-filterall.is-on { color: var(--accent-t); border-color: var(--accent-w); background: var(--accent-soft); }
.bl-sev-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 5px; }
.bl-sev-row { display: flex; align-items: center; gap: 7px; cursor: pointer; width: 100%; min-width: 0; }
.bl-feedchip { min-height: 31px; padding: 0 7px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); }
.bl-feedchip.is-on { border-color: var(--accent-w); background: var(--accent-soft); }
.bl-sev-name { min-width: 0; font: 400 12.5px var(--font-sans); color: var(--muted); flex: 1; }
.bl-sev-row.is-on .bl-sev-name { color: var(--text); }
.bl-sev-n { font: 400 11.5px var(--font-mono); color: var(--faint); font-variant-numeric: tabular-nums; }

/* ---- Detailed feed filters -------------------------------------------------- */
.bl-filtercount {
  display: inline-grid;
  place-items: center;
  min-width: 42px;
  min-height: 22px;
  padding: 0 7px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--muted);
  font: 600 11px var(--font-mono);
  box-shadow: inset 0 0 0 1px var(--line);
}
.bl-facet-form { display: grid; gap: 8px; }
.bl-facet-field { display: grid; gap: 4px; min-width: 0; }
.bl-facet-label { color: var(--faint); font: 600 11px/1.2 var(--font-sans); letter-spacing: .04em; text-transform: uppercase; }
.bl-facet-input {
  width: 100%;
  min-width: 0;
  min-height: 34px;
  padding: 0 9px;
  border: 1px solid var(--line2);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: 500 11.5px var(--font-sans);
}
.bl-facet-input::placeholder { color: var(--faint); }
.bl-facet-input:focus { border-color: var(--accent); outline: 2px solid var(--accent-w); outline-offset: 1px; }
.bl-facet-field .bl-menu { min-width: 0; width: 100%; }
.bl-facet-field .bl-menu-button { min-height: 34px; padding-left: 9px; border-radius: 8px; background: var(--surface); font-size: 11.5px; }
.bl-facet-actions { display: flex; align-items: center; gap: 9px; margin-top: 2px; }
.bl-facet-apply {
  min-height: 32px;
  padding: 0 12px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--accent);
  color: var(--bg);
  font: 700 11px var(--font-sans);
  cursor: pointer;
}
.bl-facet-apply:hover { filter: brightness(1.08); }
.bl-facet-result { color: var(--faint); font: 400 11px/1.4 var(--font-sans); }

/* ---- Watchlist --------------------------------------------------------------- */
.bl-watchbox { border: 1px solid var(--line); border-radius: var(--radius-md); padding: 11px; background: var(--surface2); }
.bl-watchhead { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.bl-watchhead .bl-eyebrow { margin: 0; }
.bl-watchcount { display: inline-grid; place-items: center; min-width: 22px; min-height: 22px; padding: 0 6px; border-radius: var(--radius-pill); background: var(--surface); color: var(--muted); font: 600 11px var(--font-mono); box-shadow: inset 0 0 0 1px var(--line); }
.bl-watchbox-body { margin: 0; }
.bl-watch-save { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px; padding-top: 8px; border-top: 1px solid var(--line); }
.bl-watch-input { min-width: 0; min-height: 31px; padding: 0 8px; border: 1px solid var(--line2); border-radius: 8px; background: var(--surface); color: var(--text); font: 500 11px var(--font-sans); }
.bl-watch-input:focus { border-color: var(--plus-t); outline: 2px solid var(--plus-w); outline-offset: 1px; }
.bl-watch-savebtn { min-height: 31px; padding: 0 10px; border: 1px solid var(--plus-t); border-radius: 8px; background: var(--plus-t); color: var(--surface); font: 700 11px var(--font-sans); cursor: pointer; }
.bl-watch-note { grid-column: 1 / -1; color: var(--faint); font: 400 11px/1.35 var(--font-sans); }
.bl-watch-note:empty { display: none; }
.bl-watch-upgrade { display: block; padding-top: 9px; border-top: 1px solid var(--line); color: var(--plus-t); font: 650 11px/1.35 var(--font-sans); text-decoration: none; }
.bl-wl-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 28px; gap: 5px; margin-bottom: 6px; }
.bl-wl-wrap .bl-wl { margin-bottom: 0; }
.bl-wl-remove { border: 1px solid var(--line); border-radius: 8px; background: var(--surface); color: var(--faint); cursor: pointer; font: 500 16px/1 var(--font-sans); }
.bl-wl-remove:hover { border-color: var(--line2); color: var(--text); }
.bl-wl {
  display: block;
  padding: 10px 12px;
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px var(--line);
  cursor: pointer;
  margin-bottom: 6px;
  text-decoration: none;
  color: inherit;
}
.bl-wl.is-on { box-shadow: inset 0 0 0 1px var(--accent-w); }
.bl-wl-top { display: flex; align-items: center; gap: 8px; }
.bl-wl-name {
  font: 500 12.5px var(--font-sans);
  color: var(--muted);
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bl-wl.is-on .bl-wl-name { color: var(--text); }
/* CORRECTED. The prototype painted this violet. Violet means a machine WROTE
   something; a count of records newly matching a saved view is computed, not
   written. It ships as an unread affordance on the accent family. */
.bl-wl-new {
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  font: 600 11px var(--font-sans);
  text-transform: uppercase;
  letter-spacing: .05em;
  background: var(--accent-soft);
  color: var(--accent-t);
  box-shadow: inset 0 0 0 1px var(--accent-w);
  white-space: nowrap;
}
.bl-wl-quiet { font: 400 11px var(--font-sans); color: var(--muted); white-space: nowrap; }
.bl-wl-sub { display: block; font: 400 11px var(--font-sans); color: var(--muted); margin-top: 4px; }

/* ---- Feed header --------------------------------------------------------------- */
.bl-feedhead { display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; margin-bottom: 10px; }
.bl-feedhead-copy { display: flex; align-items: center; gap: 11px; min-width: 0; }
/* Brand mark. Ships as /img/mark-feed.svg — it carries four fixed brand hexes
   that belong to no token ramp, and inlining them here would be the exact bug
   the "no hex literals" rule is for. Sizing only. */
.bl-mark { flex: 0 0 auto; width: 24px; height: 24px; }
.bl-feedtitle { font: 600 23px/1.15 var(--font-sans); letter-spacing: -.02em; margin: 0; }
.bl-feedsub { font: 400 12.5px var(--font-sans); color: var(--muted); margin: 3px 0 0; }
.bl-feedtools { display: flex; align-items: center; gap: 12px; min-width: 0; }
.bl-feedsearch {
  display: flex;
  align-items: center;
  min-width: 210px;
  border: 1px solid var(--line2);
  border-radius: var(--radius-pill);
  background: var(--surface);
  overflow: hidden;
}
.bl-feedsearch:focus-within { border-color: var(--accent); }
.bl-feedsearch-in {
  width: 100%;
  min-width: 0;
  padding: 8px 4px 8px 12px;
  border: 0;
  outline: 0;
  background: none;
  color: var(--text);
  font: 400 12.5px var(--font-sans);
}
.bl-feedsearch-in::placeholder { color: var(--faint); }
.bl-feedsearch-go {
  min-height: 34px;
  padding: 0 12px;
  border: 0;
  border-left: 1px solid var(--line);
  background: var(--surface2);
  color: var(--muted);
  font: 600 11px var(--font-sans);
  cursor: pointer;
}
.bl-feedsearch-go:hover { color: var(--text); background: var(--accent-soft); }
.bl-viewpicker { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.bl-viewlabel { font: 600 11px var(--font-sans); text-transform: uppercase; letter-spacing: .1em; color: var(--faint); }
.bl-feedhead .bl-seg { border-radius: 9px; background: var(--surface2); }
.bl-feedhead .bl-seg-b { min-height: 34px; padding: 0 14px; font-size: 12px; }
.bl-feedhead .bl-seg-b.is-on { background: var(--accent-soft); }

/* ---- Digest card (context rail) --------------------------------------------------
   CORRECTED. The prototype framed this model-written paragraph in
   --accent-w / --accent-soft / --accent-t. Accent is the dispatch side and is
   forbidden for anything a model wrote, so the frame, the field, the icon and
   the footnote rule all move to the violet ramp — which is the ramp that
   means exactly this. The "Unlock" CTA stays on accent, because it is a
   primary action rather than model output. */
.bl-digestcard {
  border-radius: var(--radius-card);
  padding: 15px 16px;
  box-shadow: inset 0 0 0 1px var(--ep);
  background: var(--tp);
}
.bl-digestcard-top { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; }
.bl-digestcard-top svg { color: var(--ip); flex: 0 0 auto; }
.bl-digestcard-title { font: 600 11.5px var(--font-sans); flex: 1; margin: 0; }
.bl-digestcard-body { font: 400 12.5px/1.6 var(--font-sans); color: var(--muted); margin: 0; }
.bl-digestcard-cta { display: block; font: 500 11.5px var(--font-sans); color: var(--accent-t); margin-top: 10px; text-decoration: none; }
.bl-digestcard-foot {
  font: 400 11px/1.5 var(--font-sans);
  color: var(--faint);
  margin: 10px 0 0;
  padding-top: 9px;
  border-top: 1px solid var(--ep);
}

/* ---- Address card (context rail) -------------------------------------------- */
.bl-addr-card {
  display: block;
  border-radius: var(--radius-card);
  box-shadow: inset 0 0 0 1px var(--line);
  overflow: hidden;
  cursor: pointer;
  text-decoration: none;
  color: inherit;
}
.bl-addr-body { padding: 12px 14px; }
.bl-addr-name { font: 600 15px var(--font-sans); margin: 0; }
/* .bl-addr-note is gone with the copy it was for. It styled the export's
   "Six of the nine accidents sit between Judd Rd and Route 136" line, which
   cannot be derived from this feed and is deliberately not shipped —
   routes/feed.js addressCard(). If that derivation ever exists, the note
   comes back with it. */

/* ---- Department list ------------------------------------------------------------
   Every row carries a border-top, first included, so the list separates itself
   from the eyebrow above it. */
.bl-dept {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 0;
  border-top: 1px solid var(--line);
  cursor: pointer;
  text-decoration: none;
  color: inherit;
}
.bl-dept-n {
  font: 400 13px var(--font-sans);
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bl-dept-aid { font: 400 11px var(--font-sans); color: var(--faint); white-space: nowrap; }
.bl-dept-c { font: 500 12px var(--font-mono); color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }


/* ============================================================================
   §9  ROUTE — /incident/:inc_id   timeline + fact rail
   ============================================================================ */

.bl-inc {
  height: 100%;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 430px;
  overflow: hidden;
}
.bl-inc-main { min-width: 0; display: flex; flex-direction: column; overflow: hidden; }

/* The masthead wash is the SEVERITY TINT and must track rank. It is not a
   fixed brand gradient — a rank-0 record gets a rank-0 wash. */
.bl-inc-head {
  flex: 0 0 auto;
  padding: 26px 36px 0;
  background: linear-gradient(180deg, var(--sev-tint, var(--t0)), transparent);
}
.bl-inc-scroll { flex: 1; min-height: 0; overflow: auto; padding: 22px 36px 30px; }

.bl-inc-rail {
  border-left: 1px solid var(--line);
  background: var(--surface2);
  padding: 26px 26px 30px;
  display: flex;
  flex-direction: column;
  gap: 20px;
  overflow: auto;
}

.bl-inc-badges { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.bl-inc-id { font: 400 12px var(--font-mono); color: var(--faint); }

/* ---- Provenance chips ------------------------------------------------------------
   CORRECTED. The prototype painted the "Dispatch record · N entries" chip
   violet. A count of CAD entries is the opposite of machine-written — it is
   the most CAD thing on the page — so it takes the accent family. The violet
   trio is reserved for the model-attribution chip beside it, which is the only
   thing on this route that earns it. */
.bl-inc-src { display: flex; align-items: center; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
.bl-srcchip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 7px;
  font: 500 11.5px var(--font-sans);
  white-space: nowrap;
}
.bl-srcchip i { width: 7px; height: 7px; border-radius: 2px; display: block; flex: 0 0 auto; }
.bl-srcchip--cad { background: var(--accent-soft); color: var(--accent-t); box-shadow: inset 0 0 0 1px var(--accent-w); }
.bl-srcchip--cad i { background: var(--accent); }
.bl-srcchip--ai { background: var(--tp); color: var(--ip); box-shadow: inset 0 0 0 1px var(--ep); }
.bl-srcchip--ai i { background: var(--ip); }

/* ---- Timeline ---------------------------------------------------------------------
   One grid per event. The continuity line is drawn inside each row's rail cell
   as a flex:1 bar, so rows butt against each other with no wrapper element and
   the line never outruns the last node. */
.bl-tl-item { display: grid; grid-template-columns: 58px 20px 1fr; gap: 0 14px; }
.bl-tl-time {
  font: 500 12px/1.5 var(--font-mono);
  color: var(--muted);
  padding-top: 12px;
  font-variant-numeric: tabular-nums;
}
.bl-tl-rail { display: flex; flex-direction: column; align-items: center; padding-top: 16px; }
/* Square node = the CAD wrote this. It is the only node shape shipping this
   pass; the circular node was the radio transcript row. */
.bl-tl-node { width: 10px; height: 10px; border-radius: 2px; background: var(--accent); display: block; flex: 0 0 auto; }
.bl-tl-line { flex: 1; width: 1px; background: var(--line2); display: block; }
.bl-tl-body { padding: 10px 0 14px; }
.bl-tl-what { font: 500 14.5px/1.4 var(--font-sans); color: var(--text); }
.bl-tl-meta { font: 400 12.5px/1.55 var(--font-sans); color: var(--faint); margin-top: 4px; }

/* Terminal row: hollow ring, no trailing line, everything faint. This is where
   `status` is read — open means closed, cancelled AND filed are all null. */
.bl-tl-item.is-now .bl-tl-time { color: var(--faint); }
.bl-tl-item.is-now .bl-tl-node { border-radius: 50%; background: none; box-shadow: inset 0 0 0 1.5px var(--line2); }
.bl-tl-item.is-now .bl-tl-line { display: none; }
.bl-tl-item.is-now .bl-tl-body { padding-bottom: 0; }
.bl-tl-item.is-now .bl-tl-what { color: var(--faint); }

/* detail === "skeleton": the dispatch centre published the call but not the
   times. Most records are this. It ends the timeline on an honest row instead
   of a page that looks broken. */
.bl-tl-item.is-thin .bl-tl-meta { max-width: 52ch; text-wrap: pretty; }

/* Units is an operational response board, not a second timeline. */
.bl-response-board { padding-bottom: 26px; }
.bl-response-boardhead { display: flex; align-items: end; justify-content: space-between; gap: 22px; }
.bl-response-boardhead .bl-eyebrow { color: var(--unit); }
.bl-response-boardhead h2 { margin-top: 5px; font: 700 24px/1.15 var(--font-sans); letter-spacing: -.025em; }
.bl-response-boardhead .bl-lede { max-width: 66ch; margin-top: 7px; }
.bl-response-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-top: 20px; }
.bl-response-stats .bl-response-fact { border-color: color-mix(in srgb, var(--unit) 24%, var(--line)); background: color-mix(in srgb, var(--unit-bg) 46%, var(--surface2)); }
.bl-response-list { display: grid; gap: 10px; margin-top: 12px; }
.bl-response-card { overflow: hidden; border: 1px solid var(--line); border-radius: 13px; background: linear-gradient(135deg, var(--surface), var(--surface2)); transition: border-color var(--mo-1) var(--mo-ease), box-shadow var(--mo-1) var(--mo-ease); }
.bl-response-card:hover { border-color: var(--line2); }
.bl-response-card.is-open { border-color: color-mix(in srgb, var(--unit) 50%, var(--line)); box-shadow: 0 14px 32px rgba(3, 8, 18, .14), inset 0 1px 0 color-mix(in srgb, var(--unit) 10%, transparent); }
.bl-response-summary { width: 100%; min-height: 72px; display: grid; grid-template-columns: 12px minmax(170px, 1fr) auto auto 18px; align-items: center; gap: 13px; padding: 13px 17px; cursor: pointer; }
.bl-response-summary:hover { background: color-mix(in srgb, var(--unit) 5%, transparent); }
.bl-response-mark { width: 9px; height: 9px; transform: rotate(45deg); border: 2px solid var(--unit); background: transparent; box-shadow: none; }
.bl-response-identity { min-width: 0; }
.bl-response-identity strong { display: block; overflow: hidden; color: var(--text); font: 700 16px/1.25 var(--font-mono); text-overflow: ellipsis; white-space: nowrap; }
.bl-response-identity small { display: block; overflow: hidden; margin-top: 3px; color: var(--muted); font: 400 12px/1.3 var(--font-sans); text-overflow: ellipsis; white-space: nowrap; }
.bl-response-time { min-width: 90px; color: var(--unit); font: 600 11.5px var(--font-mono); text-align: right; white-space: nowrap; }
.bl-response-time.is-empty { visibility: hidden; }
.bl-response-phase { min-width: 84px; padding: 5px 9px; border: 1px solid var(--line2); border-radius: var(--radius-pill); color: var(--muted); font: 650 11px var(--font-sans); text-align: center; white-space: nowrap; }
.bl-response-phase[data-phase="scene"] { border-color: var(--unit); color: var(--unit); background: color-mix(in srgb, var(--unit-bg) 65%, transparent); }
.bl-response-phase[data-phase="clear"] { border-color: color-mix(in srgb, var(--ok) 55%, var(--line)); color: var(--ok-ink); background: color-mix(in srgb, var(--ok) 9%, transparent); }
.bl-response-chevron { width: 8px; height: 8px; border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted); transform: rotate(45deg) translateY(-2px); transition: transform var(--mo-1) var(--mo-ease); }
.bl-response-summary[aria-expanded="true"] .bl-response-chevron { transform: rotate(225deg) translate(-1px, -1px); }
.bl-response-body { padding: 0 17px 16px 42px; }
.bl-response-progress { position: relative; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; padding-top: 12px; border-top: 1px solid var(--line); }
.bl-response-progress::before { content: ""; position: absolute; left: 12.5%; right: 12.5%; top: 23px; height: 1px; background: var(--line2); }
.bl-response-stage { position: relative; z-index: 1; min-width: 0; text-align: center; }
.bl-response-stage i { display: block; width: 9px; height: 9px; margin: 7px auto 9px; border: 2px solid var(--line2); border-radius: 50%; background: var(--surface2); }
.bl-response-stage.is-done i { border-color: var(--unit); background: var(--unit); box-shadow: 0 0 10px color-mix(in srgb, var(--unit) 42%, transparent); }
.bl-response-stage span { display: block; color: var(--faint); font: 600 11px var(--font-sans); letter-spacing: .07em; text-transform: uppercase; }
.bl-response-stage strong { display: block; margin-top: 3px; color: var(--muted); font: 600 11px var(--font-mono); }
.bl-response-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 15px; }
.bl-response-fact { min-width: 0; padding: 10px 11px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface2); }
.bl-response-fact span { display: block; color: var(--faint); font: 600 11px var(--font-sans); letter-spacing: .08em; text-transform: uppercase; }
.bl-response-fact strong { display: block; overflow: hidden; margin-top: 4px; color: var(--text); font: 600 12px/1.35 var(--font-sans); text-overflow: ellipsis; }
.bl-response-note { margin-top: 10px; color: var(--faint); font: 400 11px/1.45 var(--font-sans); }

@media (max-width: 720px) {
  .bl-response-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .bl-response-summary { grid-template-columns: 12px minmax(0, 1fr) auto 16px; }
  .bl-response-time { display: none; }
  .bl-response-body { padding-left: 17px; }
  .bl-response-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 460px) {
  .bl-response-boardhead { align-items: flex-start; }
  .bl-response-summary { gap: 10px; padding-left: 13px; padding-right: 13px; }
  .bl-response-phase { min-width: 0; padding-left: 7px; padding-right: 7px; }
  .bl-response-body { padding-left: 13px; padding-right: 13px; }
  .bl-response-progress { gap: 3px; }
  .bl-response-stage strong { font-size: 11px; }
  .bl-response-facts { grid-template-columns: minmax(0, 1fr); }
}

/* ---- Provenance table --------------------------------------------------------------
   Fact, value, source dot. The page's honesty device. Every dot is a square
   accent dot this pass; the 46px dot column stays anyway, because it is what
   makes the claim legible the moment a second source exists.

   The key type here is NOT .bl-eyebrow: .08em tracking and a 1.6 line-height
   against the eyebrow's .085em and normal. Both values are drawn and both ship. */
.bl-prov-grid {
  display: grid;
  grid-template-columns: 82px 1fr 46px;
  gap: 9px 12px;
  align-items: baseline;
  font: 400 13px/1.5 var(--font-sans);
  margin: 0;
}
.bl-prov-grid dt {
  font: 500 11px/1.6 var(--font-sans);
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--faint);
}
.bl-prov-grid dd { margin: 0; }
.bl-prov-src { text-align: right; }
.bl-prov { display: inline-block; width: 8px; height: 8px; border-radius: 2px; background: var(--accent); }


/* ============================================================================
   §11  ROUTE — /digest   the weekly written digest
   ============================================================================
   The whole route is model output in a container. The route ships the
   container; it never ships the sentences.

   No severity badge, count or stripe appears anywhere here, so no --s* and no
   --stripe-* rule belongs in this section. This route also paints no panel
   fill at all: every card is a transparent box with a 1px inset --line ring
   over the page --bg. The lock card is the only thing with a background. */

.bl-digest {
  height: 100%;
  overflow: auto;
  padding: 40px 56px 44px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 36px;
  align-content: start;
}
.bl-digest-main { min-width: 0; display: flex; flex-direction: column; gap: 26px; }
.bl-digest-side { min-width: 0; display: flex; flex-direction: column; gap: 18px; }

/* Masthead has no flex gap — the badge/dateline row is its own flex row and
   the headline is spaced by its own margin. */
.bl-masthead-meta { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.bl-dateline { font: 400 12px var(--font-sans); color: var(--faint); }
.bl-digest .bl-headline { margin-top: 16px; }

/* blur() bleeds about twice its radius past the box. The 70ch measure inside a
   716px column leaves ~86px of slack, so nothing reaches the 52px gutter. Do
   not widen .bl-prose on this route. */

/* The provenance sentence — "a model wrote this and nobody read it". It is
   the most important paragraph on the route and it does not get to be the
   least legible one, so it sits on --muted rather than --faint. */
.bl-disclaimer {
  font: 400 11.5px/1.6 var(--font-sans);
  color: var(--muted);
  max-width: 70ch;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  margin: 0;
}

.bl-crosslink {
  display: block;
  padding: 16px 18px;
  border-radius: var(--radius-card);
  box-shadow: inset 0 0 0 1px var(--line);
  cursor: pointer;
  font: 400 13px/1.6 var(--font-sans);
  color: var(--muted);
  text-decoration: none;
}
.bl-crosslink:hover { color: var(--text); }


/* ============================================================================
   §12  ROUTE — /settings   this browser's settings, every row a live control
   ============================================================================ */

.bl-settings {
  height: 100%;
  overflow: auto;
  padding: 34px clamp(20px, 5vw, 72px) 72px;
}
.bl-set-main { width: min(1240px, 100%); min-width: 0; margin: 0 auto; }

.bl-set-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 0; }
.bl-set-card {
  min-width: 0;
  padding: 24px 26px 10px;
  border: 1px solid var(--line);
  border-radius: 17px;
  background: linear-gradient(145deg, color-mix(in srgb, var(--surface) 96%, var(--accent)), var(--surface2));
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 5%, transparent), 0 14px 34px rgba(4, 8, 18, .14);
}
.bl-set-card:first-child { grid-column: 1 / -1; }
.bl-set-card > .bl-eyebrow { margin: 0 0 14px; color: var(--accent-t); font-size: 11px; letter-spacing: .13em; }
.bl-set-card:has(.bl-account-settings) { grid-column: 1 / -1; scroll-margin-top: 82px; }
.bl-set-card#map { scroll-margin-top: 82px; }
.bl-account-settings { min-height: 72px; }
.bl-account-settings.is-error { color: var(--s4); font-size: 12px; }
.bl-account-identity { display: grid; grid-template-columns: 46px minmax(0, 1fr) auto; align-items: center; gap: 13px; padding: 4px 0 18px; }
.bl-account-avatar { width: 46px; height: 46px; display: grid; place-items: center; overflow: hidden; border: 1px solid var(--accent-w); border-radius: 11px; background: var(--accent-soft); color: var(--accent-t); font-weight: 760; }
.bl-account-avatar img, .bl-profile-photo-preview img { width: 100%; height: 100%; display: block; object-fit: cover; }
.bl-account-identity strong, .bl-account-identity span { display: block; }
.bl-account-identity strong { font-size: 16px; letter-spacing: -.02em; }
.bl-account-identity div > span { margin-top: 2px; color: var(--faint); font-size: 11px; }
.bl-account-identity em { padding: 5px 9px; border: 1px solid var(--plus-w); border-radius: 999px; background: var(--plus-soft); color: var(--plus-t); font: 700 11px var(--font-sans); letter-spacing: .09em; text-transform: uppercase; }
.bl-billing-card { margin-bottom: 12px; padding: 14px 16px; border: 1px solid var(--line2); border-radius: 12px; background: color-mix(in srgb, var(--bg) 88%, var(--accent-soft)); }
.bl-billing-card.is-plus { border-color: var(--plus-w); background: color-mix(in srgb, var(--bg) 90%, var(--plus-soft)); }
.bl-billing-head { margin-bottom: 11px; }
.bl-billing-head strong { font-size: 13px; }
.bl-billing-summary { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto auto; align-items: center; gap: 11px; }
.bl-billing-mark { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 9px; background: var(--accent-soft); color: var(--accent-t); font: 800 15px var(--font-sans); }
.bl-billing-card.is-plus .bl-billing-mark { background: var(--plus); color: var(--bg); }
.bl-billing-plan strong, .bl-billing-plan span, .bl-billing-price strong, .bl-billing-price span { display: block; }
.bl-billing-plan strong { font-size: 13px; }
.bl-billing-plan span { margin-top: 2px; color: var(--faint); font-size: 11px; line-height: 1.35; }
.bl-billing-price { min-width: 76px; text-align: right; }
.bl-billing-price strong { font-size: 13px; }
.bl-billing-price span { margin-top: 2px; color: var(--faint); font-size: 11px; }
.bl-billing-action { text-decoration: none; }
.bl-account-forms { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.bl-account-form { padding: 18px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface2); }
.bl-account-formhead { min-height: 38px; display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.bl-account-formhead strong { font-size: 14px; }
.bl-account-formhead span { color: var(--faint); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
.bl-avatar-editor { display: grid; grid-template-columns: 72px minmax(0, 1fr); align-items: center; gap: 15px; margin: 10px 0 3px; padding: 14px; border: 1px solid var(--line2); border-radius: 12px; background: linear-gradient(145deg, var(--bg), color-mix(in srgb, var(--bg) 94%, var(--accent))); transition: opacity var(--mo-1) var(--mo-ease), border-color var(--mo-1) var(--mo-ease); }
.bl-avatar-editor:focus-within { border-color: var(--accent-w); box-shadow: 0 0 0 3px var(--accent-soft); }
.bl-avatar-editor.is-uploading { opacity: .62; }
.bl-profile-photo-preview { width: 72px; height: 72px; display: grid; place-items: center; overflow: hidden; border: 1px solid var(--accent-w); border-radius: 17px; background: var(--accent-soft); color: var(--accent-t); font-weight: 760; box-shadow: 0 8px 18px rgba(0,0,0,.12); }
.bl-avatar-editor-copy strong, .bl-avatar-editor-copy > span { display: block; }
.bl-avatar-editor-copy strong { font-size: 12px; letter-spacing: -.01em; }
.bl-avatar-editor-copy > span { margin-top: 4px; color: var(--muted); font-size: 11px; line-height: 1.45; }
.bl-avatar-editor-copy small { display: block; margin-top: 7px; color: var(--faint); font-size: 11px; line-height: 1.3; }
.bl-avatar-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.bl-avatar-action { min-height: 31px; padding: 0 11px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--line2); border-radius: 8px; background: var(--surface2); color: var(--text); font: 680 11px var(--font-sans); cursor: pointer; transition: border-color var(--mo-1) var(--mo-ease), background var(--mo-1) var(--mo-ease), color var(--mo-1) var(--mo-ease); }
.bl-avatar-action:not(.bl-avatar-remove) { border-color: var(--accent-w); background: var(--accent-soft); color: var(--accent-t); }
.bl-avatar-remove { color: var(--faint); }
.bl-avatar-action:not(.bl-avatar-remove):hover { border-color: var(--accent); }
.bl-avatar-remove:hover { border-color: color-mix(in srgb, var(--s4) 48%, var(--line2)); color: var(--s4); }
.bl-avatar-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.bl-account-field { display: block; margin-top: 11px; color: var(--muted); font-size: 11px; font-weight: 650; }
.bl-account-input { width: 100%; min-height: 39px; margin-top: 6px; padding: 8px 11px; border: 1px solid var(--line2); border-radius: 8px; outline: 0; background: var(--bg); color: var(--text); font: 13px var(--font-sans); }
.bl-account-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.bl-account-form p { min-height: 31px; margin: 10px 0 12px; color: var(--faint); font-size: 11px; line-height: 1.45; }
.bl-account-message { min-height: 18px; margin-top: 10px; color: var(--accent-t); font-size: 11px; }
.bl-account-message.is-error { color: var(--s4); }
.bl-account-signout { margin-top: 12px; }
.bl-account-danger { margin-top: 16px; padding: 14px 16px; border: 1px solid color-mix(in srgb, var(--s4) 28%, var(--line)); border-radius: 12px; background: color-mix(in srgb, var(--s4) 5%, var(--surface2)); }
.bl-account-dangerhead { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.bl-account-dangerhead strong { font-size: 14px; color: var(--i4); }
.bl-account-dangerhead span { color: var(--faint); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
.bl-account-danger p { margin: 8px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
.bl-account-danger .bl-account-field { margin-top: 12px; }
.bl-account-delete { margin-top: 12px; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--s4) 55%, var(--line2)); color: var(--s4); }
.bl-account-delete:hover:not(:disabled) { box-shadow: inset 0 0 0 1px var(--s4); background: color-mix(in srgb, var(--s4) 9%, transparent); }
.bl-account-empty { display: flex; align-items: center; justify-content: space-between; gap: 16px; color: var(--muted); font-size: 12px; }

/* Every row has a border-top and no border-bottom, so the list ends without a
   closing rule and the first rule of each section lands under its eyebrow. */
.bl-row { display: flex; align-items: center; gap: 16px; margin: 0 -10px; padding: 15px 10px; border-top: 1px solid var(--line); border-radius: 9px; transition: background-color var(--mo-1) var(--mo-ease), border-color var(--mo-1) var(--mo-ease); }
.bl-row:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); border-top-color: var(--line2); }
.bl-row-body { flex: 1; min-width: 0; }
.bl-row-label { font: 600 14px var(--font-sans); color: var(--text); }
.bl-row-sub { font: 400 12px/1.5 var(--font-sans); color: var(--faint); margin-top: 4px; }
.bl-row-value { font: 400 13px var(--font-sans); color: var(--muted); white-space: nowrap; flex: 0 0 auto; }
/* --i5 is the rank-5 severity INK used as the destructive colour. It is not a
   severity indicator: no stripe, no width, and never --s5. */
.bl-row-danger { color: var(--i5); }

/* Toggle. A <button role="switch"> with a real handler, and there are only two
   states left. The prototype drew these as read-only indicators and a later
   pass added an `is-locked` third state for the rows a plan decided; both are
   gone. There is one shared key and no accounts, so the server cannot tell a
   paying reader from any other — a switch that refuses to move is not a
   paywall, it is a control that appears broken, which is exactly how readers
   reported it. Every switch on /settings moves.

   The ON colour is --accent like every other control: --tp/--ip/--ep marks
   machine-written CONTENT, not controls, so a violet switch on the
   "Written by a model" rows would break the rule as surely as a blue one on
   model output would. */
.bl-toggle {
  width: 46px; height: 28px; border-radius: var(--radius-pill);
  flex: 0 0 auto; position: relative; display: block;
  padding: 0; border: 0; cursor: pointer;
  transition: background-color .15s ease;
}
.bl-toggle:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent); }
.bl-toggle i { transition: transform .15s ease; }
@media (prefers-reduced-motion: reduce) {
  .bl-toggle, .bl-toggle i { transition: none; }
}
.bl-toggle i { position: absolute; top: 3px; width: 22px; height: 22px; border-radius: 50%; display: block; }
.bl-toggle.is-on { background: var(--accent); box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 38%, transparent); }
.bl-toggle.is-on i { right: 3px; background: var(--bg); }
.bl-toggle.is-off { box-shadow: inset 0 0 0 1px var(--line2); }
.bl-toggle.is-off i { left: 3px; background: var(--line2); }
button.bl-toggle { cursor: pointer; }

/* Export block. flex-start top-aligns the button with the label, not with the
   block centre. Its padding and note leading differ from a normal row. */
.bl-export { display: flex; align-items: flex-start; gap: 16px; padding: 16px 0; border-top: 1px solid var(--line); flex-wrap: wrap; }
.bl-export-body { flex: 1; min-width: 0; }
.bl-export-note { font: 400 11.5px/1.5 var(--font-sans); color: var(--faint); margin-top: 4px; max-width: 52ch; }
.bl-export .bl-chips { margin-top: 11px; }


/* ============================================================================
   §13  ROUTE — /plus   pricing and the entitlement matrix
   ============================================================================
   CORRECTED THROUGHOUT. The prototype painted the wordmark "+", the Blotter+
   price card ring and wash, and the CTA ring and label with the cyan --radio
   ramp. Cyan is --unit and means "unit designator"; gold is --plus and means
   Blotter+. With radio out of scope, cyan here would be a colour with no
   referent, and this route would be the only place it meant neither. All four
   move to the --plus ramp. Geometry is unchanged. */

.bl-plus {
  height: 100%;
  overflow: auto;
  padding: 44px clamp(24px, 5vw, 72px) 58px;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 34px;
  align-content: start;
}
.bl-plus-main { width: min(100%, 1180px); min-width: 0; margin: 0 auto; display: flex; flex-direction: column; gap: 26px; }
.bl-plus-rail {
  width: min(100%, 1180px);
  min-width: 0;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
}
.bl-plus-hero {
  position: relative;
  overflow: hidden;
  padding: clamp(24px, 4vw, 46px);
  border: 1px solid var(--plus-w);
  border-radius: 18px;
  background:
    radial-gradient(circle at 88% 12%, var(--plus-soft), transparent 34%),
    linear-gradient(145deg, var(--surface2), var(--surface));
}
.bl-plus-hero::after {
  content: "+";
  position: absolute;
  right: clamp(18px, 5vw, 64px);
  bottom: -42px;
  color: var(--plus-soft);
  font: 700 clamp(150px, 20vw, 280px)/1 var(--font-sans);
  pointer-events: none;
}
.bl-plus-hero > * { position: relative; z-index: 1; }

.bl-brand { display: flex; align-items: center; gap: 11px; }
/* Fixed brand asset — its own paper and ink in both themes, which is correct
   for a logo. Ships as /img/blotter-plus.svg; sizing only here. */
.bl-brand-mark { flex: 0 0 auto; width: 30px; height: 30px; }
.bl-brand-word { font: 600 16px/1 var(--font-sans); letter-spacing: .1em; color: var(--text); }
.bl-brand-plus { color: var(--plus-t); }

.bl-plus .bl-headline { margin-top: 20px; }
.bl-plus .bl-deck { max-width: 82ch; margin-top: 16px; }

/* ---- Prices --------------------------------------------------------------------- */
.bl-prices { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; width: 100%; }
.bl-price { padding: 20px 22px; border-radius: 10px; box-shadow: inset 0 0 0 1px var(--line2); }
.bl-price--plus { box-shadow: inset 0 0 0 1px var(--plus); background: linear-gradient(150deg, var(--plus-soft), transparent 72%); }
.bl-price--plus .bl-eyebrow { color: var(--plus-t); }
.bl-price-amount { font: 600 34px/1.1 var(--font-sans); margin-top: 10px; color: var(--text); }
.bl-price-per { font: 400 15px var(--font-sans); color: var(--muted); }
.bl-price-note { font: 400 12px var(--font-sans); color: var(--faint); margin-top: 6px; }

/* ---- Entitlement matrix -----------------------------------------------------------
   Every value must render from the one shared entitlements object. If a number
   changes there and this table does not follow, the page is lying about what
   someone is buying. */
.bl-matrix { width: 100%; min-width: 560px; }
.bl-matrix-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 132px 132px;
  gap: 0 14px;
  padding: 0 4px 10px;
  border-bottom: 1px solid var(--line2);
}
.bl-matrix-col { text-align: center; }
.bl-matrix-col--plus { color: var(--plus-t); }

.bl-mrow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 132px 132px;
  gap: 0 14px;
  padding: 11px 4px;
  border-bottom: 1px solid var(--line);
  /* baseline, not centre: a two-line sub must not drag the values down. */
  align-items: baseline;
}
.bl-mrow-label { min-width: 0; }
.bl-mrow-name { font: 400 13.5px var(--font-sans); color: var(--text); }
/* The sub-line is what the row actually means — "how far back a place page
   reaches" — and someone deciding whether to pay has to be able to read it. */
.bl-mrow-sub { font: 400 11px/1.45 var(--font-sans); color: var(--muted); margin-top: 2px; }
/* The 400/500 weight step is load-bearing: it is what separates the two
   columns when hue is gone. Never make them the same weight. */
.bl-mrow-free { text-align: center; font: 400 12.5px var(--font-sans); color: var(--faint); }
.bl-mrow-plus { text-align: center; font: 500 12.5px var(--font-sans); color: var(--plus-t); }

.bl-cta { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
/* Price and cancellation terms. Small print is still print. */
.bl-cta-fine { font: 400 12px/1.55 var(--font-sans); color: var(--muted); max-width: 36ch; margin: 0; }


/* ============================================================================
   §14  RESPONSIVE
   ============================================================================
   The prototype is a fixed 1280 canvas; real readers are on phones. At and
   above 1240px every route reproduces the export exactly. Below that the
   columns collapse in order of how much they carry, and the nested scroll
   regions hand scrolling back to the document — stacked independent scrollers
   on a touch screen are a trap.

   Nothing may scroll horizontally at any width. */

/* ---- /blotter: drop the context rail under the feed first, then the filter
        rail, because the feed is the route. ------------------------------- */
@media (max-width: 1180px) {
  .bl-blotter { grid-template-columns: 236px minmax(0, 1fr); }
  .bl-ctx {
    grid-column: 1 / -1;
    border-left: 0;
    border-top: 1px solid var(--line);
  }
}

/* ---- /incident, /digest, /settings, /plus: two columns become one. -------- */
@media (max-width: 1080px) {
  .bl-inc { grid-template-columns: minmax(0, 1fr); overflow: auto; }
  .bl-inc-main { overflow: visible; }
  .bl-inc-scroll { overflow: visible; }
  .bl-inc-rail { border-left: 0; border-top: 1px solid var(--line); overflow: visible; }
  .bl-inc-head, .bl-inc-scroll { padding-left: 20px; padding-right: 20px; }
  .bl-tl-item { grid-template-columns: 50px 20px 1fr; }

  .bl-digest { grid-template-columns: minmax(0, 1fr); gap: 32px; padding: 28px 20px 36px; }
  .bl-plus { grid-template-columns: minmax(0, 1fr); gap: 32px; padding: 32px 20px; }
}

@media (max-width: 700px) {
  .bl-plus-rail, .bl-prices { grid-template-columns: minmax(0, 1fr); }
  .bl-plus-hero { padding: 24px 20px; border-radius: 14px; }
  .bl-plus-hero::after { opacity: .58; }
}

@media (max-width: 1180px) {
  .bl-settings { grid-template-columns: minmax(0, 1fr); gap: 32px; }
}

/* ---- Phones ---------------------------------------------------------------- */
@media (max-width: 860px) {
  /* Hand scrolling to the document. */
  .fs-route { overflow: auto; -webkit-overflow-scrolling: touch; }
  .bl-blotter,
  .bl-inc,
  .bl-digest,
  .bl-settings,
  .bl-plus { height: auto; overflow: visible; }

  .bl-blotter { grid-template-columns: minmax(0, 1fr); }
  .bl-rail, .bl-feed, .bl-ctx { overflow: visible; }
  .bl-rail { border-right: 0; border-bottom: 1px solid var(--line); }
  /* The filter rail is a control surface, not reading: on a phone it belongs
     under the feed, not above it. */
  .bl-rail { order: 2; }
  .bl-feed { order: 1; }
  .bl-ctx { order: 3; }

  .fs-head { gap: 14px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; }
  .fs-head::-webkit-scrollbar { display: none; }
  /* The landmark group tracks the header's own gap so the row still reads as
     one evenly spaced strip once it starts scrolling sideways. */
  .fs-navgroup { gap: 14px; }

  .bl-feed { padding: 16px 16px 22px; }
  .bl-settings { padding: 24px 20px 32px; }

  .bl-account-forms { grid-template-columns: minmax(0, 1fr); }
  .bl-account-identity { grid-template-columns: 42px minmax(0, 1fr); }
  .bl-account-identity em { grid-column: 2; justify-self: start; }
  .bl-billing-summary { grid-template-columns: 34px minmax(0, 1fr) auto; }
  .bl-billing-action { grid-column: 2 / -1; justify-self: start; }
  .bl-account-formhead { align-items: flex-start; flex-direction: column; gap: 3px; }
  .bl-account-formhead span { text-align: left; }

  .bl-headline { font-size: 32px; }
  .bl-headline--xl { font-size: 34px; max-width: none; }
  .bl-title { font-size: 28px; }
  .bl-card-title { font-size: 18px; }
  .bl-feedtitle { font-size: 20px; }
  .bl-statgrid .bl-stat-v { font-size: 22px; }

  .bl-prices { grid-template-columns: minmax(0, 1fr); }
  .bl-cta, .bl-actions { flex-direction: column; align-items: stretch; }
  .bl-cta-fine { max-width: none; }

  /* The matrix is the one genuinely wide table on the site. It keeps its
     tracks and scrolls inside its own container rather than pushing the
     page sideways. */
  .bl-matrix { min-width: 460px; }

  .bl-statgrid { grid-auto-flow: row; grid-auto-columns: auto; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
}

@media (max-width: 560px) {
  .bl-row { flex-wrap: wrap; }
  .bl-export { flex-direction: column; align-items: stretch; }
  .bl-export .bl-btn { flex: 0 0 auto; }
  .bl-titlerow { flex-wrap: wrap; }
  /* At this width the ellipsis has nothing left to give; let the title wrap
     rather than truncating a SHOUTED inc_category to three words. */
  .bl-card-title { white-space: normal; }
  .bl-meta { white-space: normal; }
  /* Chronological cards are scan rows, so a long CAD category stays one line
     and ellipsizes instead of turning a compact phone row into four lines. */
  .bl-card--feed .bl-card-title,
  .bl-card--feed .bl-meta { white-space: nowrap; }
  .bl-prov-grid { grid-template-columns: 74px 1fr 32px; }
  .bl-avatar-editor { grid-template-columns: 62px minmax(0, 1fr); padding: 12px; }
  .bl-profile-photo-preview { width: 62px; height: 62px; border-radius: 15px; }
}


/* ============================================================================
   §15  MOTION
   ============================================================================
   THIS SECTION USED TO BE FOUR LINES, and the four lines were right about the
   thing they were for: the live-poll dot is the only animation that LOOPS on
   this site, and it exists to say "this page is still listening". Nothing below
   loops. Everything below is a transition between two states the reader has
   just caused, and it exists because the alternative — the map cutting, the
   record appearing whole, a poll shuffling the feed under a reader's eye — is
   not calm, it is jarring. A page that never moves does not read as still; it
   reads as replacing itself.

   THE BUDGET IS §1's THREE DURATIONS AND ONE CURVE, and there is no fourth of
   either. Nothing here delays a fact: an entrance is a fade over 180–240ms
   that begins at once, never a wait followed by a reveal. If a duration below
   ever wants to be 400ms, the answer is that the thing should not be animated.

   ONE VOCABULARY, SHARED. .bl-enter / .bl-exit / .bl-settle / .bl-fresh are
   not route-local: the map, the incident timeline and the feed all had the
   same problem and three routes inventing three timings is three timings that
   drift. They are stamped by BL.enter / BL.stagger / BL.exit in blotter.js.

   AND ALL OF IT IS DROPPED AT THE BOTTOM. The reduced-motion block zeroes
   duration globally, so every rule in this section, and every rule anywhere
   else in this file, is off for a reader who asked for less — without any of
   them having to remember to ask. */

/* ---- The shared entrance ------------------------------------------------------
   A keyframe rather than a transition, on purpose: a caller stamps this on a
   node it is about to append and is then finished with it. There is no "from"
   state to arrange and nothing to clean up, which is what makes it safe to use
   from a list builder that runs on every poll.

   6px, not 16px. A row that travels far enough to be noticed as travelling is a
   row the reader watched instead of read. */
@keyframes blEnter {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.bl-enter {
  animation: blEnter var(--mo-3) var(--mo-ease) both;
}

/* The other half. The caller owns the removal — BL.exit says when — because
   only the caller knows what "gone" means for the node. */
@keyframes blExit {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(-4px); }
}
.bl-exit {
  animation: blExit var(--mo-2) var(--mo-ease) both;
  pointer-events: none;
}

/* ---- The list that a poll rewrote ---------------------------------------------
   FOR THE FEED, and stated here rather than in §8 because the map's callout list
   and the incident page's related records want exactly the same behaviour.

   A poll that lands two new calls pushes every row below them down. Done by
   replacing the DOM, that is a jump: the sentence a reader was halfway through
   is suddenly 180px further up the page. .bl-settle is the FLIP half — measure,
   rewrite, set a transform equal to the delta, then clear it on the next frame
   and the rows slide from where they were to where they are.

   It transitions TRANSFORM ONLY. Animating height or top on a list of cards is
   a layout pass per frame, which on a fifty-row feed is exactly the stutter this
   is meant to remove. */
.bl-settle {
  transition: transform var(--mo-3) var(--mo-ease);
  will-change: transform;
}

/* The row that is genuinely new in this window, marked for as long as it takes
   to notice and no longer. --accent because a call arriving is dispatch data,
   which is what accent is for; violet here would be a lie about who wrote it.

   It fades ITSELF out — a state that needs a timer to clear is a state that gets
   left on the page when the timer is cancelled by a re-render. */
@keyframes blFresh {
  from { background-color: var(--accent-soft); }
  to   { background-color: transparent; }
}
.bl-fresh {
  animation: blFresh 1100ms var(--mo-ease) both;
}

/* ---- The incident record ------------------------------------------------------
   /incident is the page most readers arrive on, from a shared link, cold. It used
   to appear whole and all at once, which for a page of thirty timestamps reads as
   a screenshot rather than as a record.

   The masthead comes in as one block; the timeline comes in as a sequence,
   because a timeline IS a sequence and the entrance is the only chance the page
   has to say so. BL.stagger caps the step at eight rows — see its header — so a
   long record still finishes arriving in a quarter of a second.

   THERE IS NO RULE HERE THAT SAYS SO, and that is the point. The entrance is
   stamped by routes/incident.js with BL.enter, because whether a record is
   ARRIVING or merely being REPAINTED is a fact only the route knows. Written as
   a selector on .bl-inc-head it would fire on every refreshEverywhere poll too,
   and a masthead that fades itself in every two minutes under a reader who has
   not moved is the poll taking the page away from them — which is the exact
   thing that route keeps its tab state in a module variable to prevent. */

/* Switching Timeline/Units repaints the panel. Without this the two tabs swap
   instantly, which makes the tab look like a page load rather than a view. */
.bl-tab { transition: color var(--mo-1) var(--mo-ease), box-shadow var(--mo-1) var(--mo-ease); }

/* ---- Reduced motion -----------------------------------------------------------
   THE READER'S ANSWER OVERRIDES EVERY RULE ABOVE. Zeroing duration globally is
   deliberate: a per-rule opt-out is a rule somebody forgets to write, and the
   one they forget is always the one that makes somebody ill.

   Zeroed, not removed — a `both`-filled keyframe still applies its end state, so
   an element that fades in still ends up visible. The three that are explicitly
   turned off below are the ones where "instant" is not the same as "off": a
   looping dot, a fill that would flash, and a tile whose opacity is driven from
   JavaScript rather than by a transition it can skip. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  .bl-live-dot.is-live { animation: none; opacity: 1; }
  /* No wash at all rather than a wash that appears and vanishes in one frame.
     A single-frame flash is the exact stimulus this preference exists to stop. */
  .bl-fresh { animation: none; }
  /* The class is cleared by a load handler, not by a transition, so zeroing
     duration is not enough — the tile has to be visible from the start. */
  .bl-map-tile.is-loading { opacity: 1; }
  .bl-settle { will-change: auto; }
}


/* ============================================================================
   §16  PRINT
   ============================================================================
   A dispatch record is a public record and people print them. Severity must
   survive the trip, which it does: the stripe width carries the rank on paper
   even when the hue is greyscale. */

@media print {

  /* ---- The palette --------------------------------------------------------
     The theme is forced to light for the duration of the print job by the
     small listener in app.html, which flips the same data-theme attribute
     the tokens key off and puts it straight back afterwards. That is where
     the fix lives, because it is the only place a hex-free file can reach a
     hex ramp. WITHOUT IT the default dark ramp prints #e9e9ed text onto
     white paper — browsers drop backgrounds by default — and the record is
     invisible. If scripting is off that listener never runs, which is the
     one case still outstanding: the real repair is a @media print ramp in
     the generated blotter-tokens.css. See the report. */

  /* ---- What is not part of the record ------------------------------------
     Controls that cannot be operated on paper, and the one gold surface,
     which is a sales pitch and has no business on a printed public record.

     .bl-chip is NOT in this list, deliberately: on /map the severity chips
     are the legend, not a control, and a map printed without its key is a
     page of coloured dots. Nor is .bl-toggle, which reads as a state
     indicator once it can no longer be pressed. */
  .fs-skip, .fs-head, .fs-foot-links,
  .bl-rail, .bl-more, .bl-actions, .bl-seg, .bl-tabs,
  .bl-btn, .bl-linkbtn,
  .bl-lock, .bl-crosslink,
  .bl-mapbar, .bl-mapctl, .bl-mapbtn, .bl-mapcard-x, .bl-mapscreen-hit {
    display: none !important;
  }

  /* ---- Let it all flow onto paper ---------------------------------------- */
  .fs-shell { min-height: 0; }
  .fs-route, .fs-fallback,
  .bl-blotter, .bl-inc, .bl-digest, .bl-settings, .bl-plus,
  .bl-feed, .bl-ctx, .bl-inc-rail, .bl-inc-scroll, .bl-inc-main,
  .bl-maproute, .bl-maprail, .bl-scrollx {
    height: auto;
    overflow: visible;
  }
  .bl-blotter, .bl-inc, .bl-digest, .bl-settings, .bl-plus,
  .bl-maproute {
    display: block;
  }
  /* The matrix keeps a min-width for the screen's scroll box; on paper the
     page is the box. */
  .bl-matrix, .bl-ftab { min-width: 0; }

  /* ---- Ink -----------------------------------------------------------------
     Panel fills and the severity wash across the top of an incident are
     surface, not content. Most browsers drop them anyway; saying so keeps
     the printed page identical whether or not the reader has "print
     backgrounds" switched on. The rail keeps a rule so it still reads as a
     separate block once its fill is gone. */
  .bl-inc-head, .bl-inc-rail, .bl-ctx, .fs-foot, .bl-mapscreen,
  .bl-card, .bl-group, .bl-group--card {
    background: none;
  }
  .bl-inc-rail { border-left: 0; border-top: 1px solid var(--line2); padding-left: 0; padding-right: 0; }
  .bl-inc-head, .bl-inc-scroll { padding-left: 0; padding-right: 0; }
  .fs-foot { padding-left: 0; padding-right: 0; }

  /* ---- Severity has to survive the trip ------------------------------------
     THIS IS WHAT MAKES §3'S CLAIM TRUE ON PAPER. Rank is carried by a
     coloured element with no text in it, and the default print-color-adjust
     is "economy", which drops exactly that — so the stripe, the bar, the
     swatch and the pins all disappeared and the printed record lost its
     ordering entirely, hue AND width. Forced to exact, the widths print even
     on a greyscale printer, which is the channel that was supposed to be
     doing the work all along. */
  .bl-stripe, .bl-sev-bar, .bl-sev-sw, .bl-sevdot,
  .bl-map-pin, .bl-mappin, .bl-tl-node, .bl-prov, .bl-srcchip i,
  /* The mini map's marker is a rank-coloured circle with no text in it —
     the same case, and it is the only thing on that panel saying how bad
     the call was. */
  .bl-minimap-dot, .bl-minimap-ring, .bl-minimap-near {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  /* A tile whose opacity is being driven from JavaScript must not go to the
     printer at zero. The class is cleared on load, and a print dialog opened
     before the ground finished arriving would otherwise print a blank map. */
  .bl-map-tile.is-loading { opacity: 1; }
  /* Likewise every entrance: paper has no frames to animate over, and a
     `both`-filled keyframe caught at its from-state prints as an empty box. */
  .bl-enter, .bl-exit, .bl-fresh, .bl-mapcard {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  /* The rule that un-blurred .bl-blur for print went with .bl-blur itself
     — see the note where those two rules used to be, above. Nothing on
     this site obscures text it has already rendered, so print has nothing
     to undo. */

  /* ---- Where the page may break -------------------------------------------
     A dispatch record is read as rows. A row split across a page break is a
     row somebody misreads. */
  .bl-card, .bl-note, .bl-tl-item, .bl-group-row, .bl-statrow,
  .bl-row, .bl-mrow, .bl-dept, .bl-price, .bl-ftab tr {
    break-inside: avoid;
  }
  h1, h2, h3, .bl-eyebrow, .bl-headline, .bl-title { break-after: avoid; }
  p, .bl-prose p { orphans: 3; widows: 3; }
}


/* ============================================================================
   §17  ROUTE-LOCAL — /plus, and nothing else
   ============================================================================
   Added by routes/plus.js. One class, because the entitlement matrix needed a
   footnote and no existing class fits: .bl-secnote is nowrap (it is a label,
   not a sentence), .bl-cta-fine is capped at 36ch under a button, and
   .bl-export-note belongs to /settings.

   WHAT IT CARRIES: the single derived number on the route — how many calls the
   record actually holds — so that "7 days" versus "everything" is measured
   against something real instead of being an abstraction. It is a footnote to
   a table: no box, no rule, sub-line size so it never competes with the rows it
   explains, and the 4px side margin lines it up with .bl-mrow's own padding.
   Vertical spacing comes from .bl-plus-main's 26px gap, so the element carries
   no top margin of its own.

   It sits OUTSIDE the .bl-scrollx wrapper on purpose: the matrix is allowed to
   scroll sideways on a phone, but a paragraph never is. */
.bl-matrix-note {
  font: 400 11.5px/1.5 var(--font-sans);
  /* The one measured number on the page. --faint is below AA in both themes
     and this is the sentence that says what someone is buying. */
  color: var(--muted);
  max-width: 62ch;
  margin: 0 4px;
}


/* ============================================================================
   §18  ROUTE-LOCAL — /settings, and nothing else
   ============================================================================
   Controls and responsive details emitted only by routes/settings.js.

   1. .bl-row-sub--tight. The prototype gives every row sub-line 11.5px/1.45
      except one: "Clear local data", which is 11.5px with a normal line-height.
      It is a real one-off in the export, not noise — the destructive row sits
      tighter than the rows above it — and it only becomes visible when the
      sub-line wraps, which it does below about 480px.

   2. .bl-row-action. The prototype puts cursor:pointer on exactly three things
      and the settings rows are not among them: the list is a read-only mirror,
      and the toggles ship as indicators. "Clear local data" is the one
      exception, because it is the one row that needs no account and no server
      — it clears this browser. A row that acts must say so with the pointer,
      and being reachable from the keyboard, it needs the ring the base rules
      already give [tabindex]. Nothing else on the route uses either class.

*/
.bl-row-sub--tight { line-height: normal; }
.bl-row-action { cursor: pointer; }

/* .bl-select is here rather than in §7 because the site has exactly
      one <select> — the centre-town picker, which is a list of every town in
      the coverage and is therefore the one control on the site that a
      segmented row or a strip of chips genuinely cannot carry.

      appearance:none because a UA-styled select is painted from the operating
      system's own palette, and on a page with no `color-scheme` declared that
      lands as black text on a white slab in the middle of a dark list. The
      geometry is .bl-seg-b's, so the picker and the segmented rows above and
      below it are the same object at the same size.

      The <option> list is drawn by the browser and cannot be styled beyond its
      two colours; those are set so the popup is not the one light rectangle on
      a dark page. */
.bl-select {
  flex: 0 0 auto;
  max-width: 220px;
  padding: 5px 11px;
  border: 0;
  border-radius: 7px;
  box-shadow: inset 0 0 0 1px var(--line2);
  background: var(--surface);
  color: var(--text);
  font: 500 11.5px var(--font-sans);
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
}
.bl-select:hover { box-shadow: inset 0 0 0 1px var(--accent); }
.bl-select option { background: var(--surface); color: var(--text); }

/* 5. The departments picker is a multi-select over however many departments
      the loaded window mentions, so it wraps — and .bl-row-value is nowrap by
      design, because every other value on the page is two words. A wrapping
      control therefore hangs under the sub-line inside .bl-row-body instead,
      and this is the only spacing it needs. */
.bl-row-body .bl-chips { margin-top: 9px; }
.bl-deptpicker { position: relative; margin-top: 9px; }
.bl-deptpicker .bl-chips { margin-top: 0; max-height: 68px; overflow: hidden; }
.bl-deptpicker:not(.is-open)::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 34px; height: 36px;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent, var(--bg));
}
.bl-deptpicker.is-open .bl-chips { max-height: none; }
.bl-depttoggle {
  position: relative;
  z-index: 1;
  display: inline-flex;
  margin-top: 8px;
  min-height: 30px;
  align-items: center;
  color: var(--accent-t);
  font: 600 11.5px var(--font-sans);
  cursor: pointer;
}
.bl-depttoggle:hover { text-decoration: underline; }

/* 6. A row's segmented control has up to five positions on a phone, which is
      wider than the value column. Wrapping is the alternative to a control
      that runs off the side of a document that is not allowed to scroll
      sideways. */
.bl-row .bl-seg { flex-wrap: wrap; }

/* Settings presents the shared controls as illuminated console hardware. */
.bl-set-card .bl-seg { padding: 3px; gap: 2px; border: 1px solid var(--line2); border-radius: 10px; background: var(--surface2); box-shadow: none; }
.bl-set-card .bl-seg-b { min-height: 30px; padding: 6px 10px; border-radius: 7px; }
.bl-set-card .bl-seg-b:hover { color: var(--text); background: color-mix(in srgb, var(--accent) 7%, transparent); }
.bl-set-card .bl-seg-b.is-on { color: var(--text); background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent), 0 0 14px color-mix(in srgb, var(--accent) 18%, transparent); }
.bl-set-card .bl-select { min-height: 36px; padding: 7px 32px 7px 12px; border: 1px solid var(--line2); border-radius: 9px; background: var(--surface2); box-shadow: none; }
.bl-set-card .bl-select:hover { border-color: var(--accent); box-shadow: 0 0 14px color-mix(in srgb, var(--accent) 16%, transparent); }
.bl-set-card .bl-export { margin: 0 -10px; padding: 16px 10px; }
.bl-theme-seg .bl-seg-b:first-child::before,
.bl-theme-seg .bl-seg-b:last-child::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-t); box-shadow: 0 0 9px var(--accent); }
.bl-theme-seg .bl-seg-b:last-child::before { background: var(--plus-t); box-shadow: 0 0 9px var(--plus); }

@media (max-width: 980px) {
  .bl-set-grid { grid-template-columns: minmax(0, 1fr); }
  .bl-set-card:first-child { grid-column: auto; }
}

@media (max-width: 600px) {
  .bl-settings { padding: 18px 14px 48px; }
  .bl-set-card { padding: 21px 18px 8px; border-radius: 15px; }
  .bl-row { align-items: flex-start; }
  .bl-row > .bl-row-value,
  .bl-row > .bl-seg,
  .bl-row > .bl-select { margin-left: auto; }
}


/* ============================================================================
   §19  ROUTE-LOCAL — /blotter, and nothing else
   ============================================================================
   Added by routes/feed.js, in two parts: the Table half of the Cards | Table
   control, and the preview panel the third column became. §8 has no table
   and no preview, and nothing already in this file was edited or reordered
   to make room for either.

   WHY THE TABLE EXISTS. The segmented control is in the approved design. A
   control whose second position does nothing claims a capability the page
   does not have, so the alternative is real: the same records, one line
   each, sortable, for a reader scanning a shift rather than reading a story.

   SEVERITY SURVIVES THE FORMAT. The rank column reuses .bl-sev-bar, which
   reads --sev and --sev-stripe from the [data-sev] on the row, so hue and
   width still arrive together — the ordering holds in greyscale here exactly
   as it does on the cards. Nothing below sets a severity colour of its own.

   It ships inside .bl-scrollx and carries a min-width, so on a phone the
   table scrolls within its own box and the document still never scrolls
   sideways. */

.bl-ftab {
  width: 100%;
  /* Eight columns now — time, rank, call, place, town, department, units and
     duration. The floor is what those need before words start breaking mid
     syllable; past it the .bl-scrollx wrapper scrolls, and the page does
     not. */
  min-width: 790px;
  border-collapse: collapse;
}
/* Column headings are the same eyebrow as every section label on the route,
   so the table reads as part of the page and not as pasted-in data. */
.bl-ftab th {
  font: 500 11px var(--font-sans);
  text-transform: uppercase;
  letter-spacing: .085em;
  color: var(--muted);
  text-align: left;
  padding: 0 10px 8px;
  border-bottom: 1px solid var(--line);
}
.bl-ftab td {
  padding: 9px 10px;
  border-top: 1px solid var(--line);
  vertical-align: baseline;
}
/* The first and last cells sit flush with the feed column's own padding, so
   the table lines up with the cards it replaces. */
.bl-ftab th:first-child, .bl-ftab td:first-child { padding-left: 0; }
.bl-ftab th:last-child,  .bl-ftab td:last-child  { padding-right: 0; }
/* Closed records dim by the same amount as .bl-card.is-closed — the severity
   the CAD assigned is still true after the call closes. */
.bl-ftab tbody tr.is-closed { opacity: 1; }
.bl-ftab tbody tr:hover { background: var(--surface2); }
/* The whole row is a click target ONLY where the preview rail is beside the
   feed — routes/feed.js leaves the click alone below that width, so the hand
   cursor is scoped to the same breakpoint rather than promising a selection
   the page has decided not to make. The call and the town are links at every
   width and carry their own cursor. */
@media (min-width: 1181px) {
  .bl-ftab tbody tr { cursor: pointer; }
}

/* ---- Sortable headings -------------------------------------------------------
   The heading is a button in a <th aria-sort>, which is the pair that makes a
   sortable column real rather than clickable: the button is what a keyboard
   reaches and what a screen reader is told can be pressed, and the attribute
   is what tells it which way the table is currently ordered. The button
   inherits the eyebrow above rather than restating it, so a sortable heading
   and a plain one are the same piece of type. */
.bl-ftab th button.bl-ftab-sort {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font: inherit;
  color: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  cursor: pointer;
  white-space: nowrap;
}
.bl-ftab-sort:hover { color: var(--muted); }
.bl-ftab-sort.is-on { color: var(--accent-t); }
.bl-ftab-sort:focus-visible { outline-offset: 2px; }

/* The arrow. A CSS triangle rather than a glyph, so it cannot arrive in a
   different font from the label, and aria-hidden in the markup because the
   direction is already on the th where a screen reader reads it. The unsorted
   state is a dash of the same width, so the heading row does not shift by two
   pixels every time a reader changes column. */
.bl-ftab-arrow {
  display: block;
  width: 7px; height: 7px;
  flex: 0 0 auto;
  background: currentColor;
  opacity: .45;
}
.bl-ftab-arrow[data-dir="none"] { opacity: 0; }
.bl-ftab-arrow[data-dir="down"] { clip-path: polygon(50% 100%, 0 25%, 100% 25%); }
.bl-ftab-arrow[data-dir="up"]   { clip-path: polygon(50% 0, 0 75%, 100% 75%); }
.bl-ftab-sort.is-on .bl-ftab-arrow { opacity: 1; }

/* The rank column is the bar and its heading. */
.bl-ftab-rank { width: 1%; padding-left: 0; padding-right: 10px; }

/* Clock: the same mono face and colour as .bl-time on a card. Also the
   duration column, which is the same kind of measured quantity. */
.bl-ftab-when { font: 500 12px var(--font-mono); color: var(--muted); white-space: nowrap; }

/* The call itself is the one link in the row and the only thing at --text;
   it carries the card meta size because it is doing the card title's job in
   a line of type rather than a headline. */
.bl-ftab-what { font: 400 13.5px var(--font-sans); color: var(--text); }
.bl-ftab-what:hover { text-decoration: underline; }

/* Place, department and duration are context, one notch quieter, and each is
   allowed to wrap: a truncated address is worse than a two-line row. */
.bl-ftab-where { font: 400 12.5px var(--font-sans); color: var(--muted); }

/* At the app's common laptop width the department and elapsed columns turn
   every useful value into a vertical word stack. Both remain on the incident
   page and return in the table on wide screens; the scan view keeps the six
   fields a dispatcher can compare at a glance. */
@media (max-width: 1300px) {
  .bl-ftab { min-width: 650px; }
  .bl-ftab th:nth-child(6), .bl-ftab td:nth-child(6),
  .bl-ftab th:nth-child(8), .bl-ftab td:nth-child(8) { display: none; }
}

@media (max-width: 640px) {
  .bl-feedhead { align-items: flex-start; flex-direction: column; gap: 10px; }
  .bl-feedtools { width: 100%; flex-direction: column; align-items: stretch; }
  .bl-feedsearch { width: 100%; }
  .bl-viewpicker { width: 100%; justify-content: space-between; }
}
/* The town is a link out of the table; it keeps the quiet size and takes the
   accent ink every other link on the site takes. */
.bl-ftab-town { font: 400 12.5px var(--font-sans); color: var(--accent-t); white-space: nowrap; }
.bl-ftab-town:hover { text-decoration: underline; }
/* Unit designators are CAD data and ship on the unit ramp, in mono, as they
   do everywhere else — the chips would be too loud at one line a call. */
.bl-ftab-units { font: 500 11.5px var(--font-mono); color: var(--unit); white-space: nowrap; }
/* The label on a gap, in the cell where the gap is: the street number was
   stripped before this record left the server. */
.bl-ftab-note { display: block; font: 400 11px var(--font-sans); color: var(--faint); margin-top: 2px; }

/* ---- The selected record -----------------------------------------------------
   One ring, two shapes. Selection is not severity and not status, so it takes
   the accent — the same ink every primary action on the site takes — and it
   never touches a --sN, which would say the call got worse because somebody
   clicked it. The lead card already wears a 1px accent ring; selected is the
   same colour at twice the weight, so the two read as one idea at two
   strengths rather than as two different states. */
.bl-card--feed.is-selected { background: var(--surface); box-shadow: 0 0 0 2px var(--accent); }
.bl-card--feed.is-selected:hover { box-shadow: 0 0 0 2px var(--accent); }
.bl-ftab tbody tr.is-selected { background: var(--accent-soft); }
.bl-ftab tbody tr.is-selected td { border-top-color: var(--accent-w); }
.bl-ftab tbody tr.is-selected .bl-ftab-what { color: var(--accent-t); }


/* ============================================================================
   §19b  THE PREVIEW RAIL  (routes/feed.js, third column)
   ============================================================================
   The third column used to be a standing panel about whichever record the
   page had picked as its lead — furniture a reader could not change and did
   not choose. It is a preview now: clicking a card or a row fills it with
   that call, and a "Full details" link is the only thing that leaves for
   /incident/:id.

   THE PANEL IS NOT A CARD IN THE FEED'S SENSE. It is not a link, nothing in
   it navigates by accident, and it is deliberately quieter than the cards it
   describes: the loud thing on this route is the record column. What it
   borrows from §6 is the stripe, so the rank of the call being previewed is
   stated in the same width and the same hue it has in the list.

   The address block under it is the §8 .bl-addr-card, unchanged, and it is
   the same component it always was — only what it is ABOUT changed, from a
   record the page chose to the record the reader did. */

.bl-prev { display: flex; flex-direction: column; gap: 14px; }
.bl-prev-top { display: flex; align-items: baseline; gap: 10px; }
.bl-prev-top .bl-eyebrow { flex: 1; }

.bl-prev-card {
  position: relative;
  overflow: hidden;
  background: var(--surface);
  border-radius: var(--radius-card);
  box-shadow: inset 0 0 0 1px var(--line2);
  padding: 14px 16px 15px 24px;
}
/* A closed call dims exactly as far as its card in the feed does. */
.bl-prev-card.is-closed { opacity: 1; }

/* WRAPS, where the card's title does not. A card is one line in a list and
   an ellipsis is the price of the list staying scannable; the preview is the
   place the whole SHOUTED inc_category finally gets to be read. */
.bl-prev-title {
  font: 700 19px/1.25 var(--font-sans);
  letter-spacing: -.015em;
  color: var(--text);
  margin: 0;
  text-wrap: pretty;
}
.bl-prev-badges { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 10px; }

/* The facts, as §5 key/value rows — the same component /town's coverage box
   and /incident's fact rail use, so three rails on the site do not each have
   their own idea of what a labelled fact looks like. */
.bl-prev-facts { margin-top: 12px; }
.bl-prev-card .bl-units { margin-top: 12px; }

/* A note about the record rather than a fact in it: the gap the server left,
   or a unit log we have not read yet. */
.bl-prev-note { font: 400 11.5px/1.55 var(--font-sans); color: var(--faint); margin: 10px 0 0; }

/* A MACHINE WROTE THIS. The violet is carried by the .bl-badge--ai chip
   inline at the head of the paragraph — the site's one mark for the claim —
   and the prose sits at --muted like every other body paragraph, because
   tinting the words as well would make the model's account the loudest thing
   in a rail about a dispatch record. */
.bl-prev-ai { font: 400 12.5px/1.65 var(--font-sans); color: var(--muted); margin: 12px 0 0; }
.bl-prev-ai .bl-badge--ai { margin-right: 4px; vertical-align: 1px; }

/* The way out of the preview and into the record. Accent, because it is the
   primary action of this panel and the only navigation in it. */
.bl-prev-full {
  display: inline-block;
  font: 500 12px var(--font-sans);
  color: var(--accent-t);
  margin-top: 14px;
}
.bl-prev-full:hover { text-decoration: underline; }

@media print {
  /* Neither the sort direction nor a link to a page that is not this one
     means anything on paper. */
  .bl-ftab-arrow, .bl-prev-full { display: none; }
  .bl-prev-card { background: none; }
}


/* ============================================================================
   §23  SHELL EDGES THE PROTOTYPE DOES NOT HAVE
   ============================================================================
   Two things the design artifact could not describe, because it is a fixed
   1280x836 canvas rather than a website: a skip link, and a footer.

   Neither invents a new visual language — both are built from the chrome
   tokens already in §4 so they read as part of the same shell. */

/* ---- Skip link -------------------------------------------------------------
   The header carries four nav links, a theme control and a search box before
   the record starts. On a keyboard that is six stops before the first thing
   anyone came here to read. Hidden until focused, then it sits over the
   header rather than shifting it, so nothing reflows when it appears. */
.fs-skip {
  position: absolute;
  left: 12px;
  top: -60px;
  z-index: 50;
  padding: 9px 14px;
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  font: 500 13px var(--font-sans);
  text-decoration: none;
  box-shadow: inset 0 0 0 1px var(--accent);
  transition: top .12s ease;
}
.fs-skip:focus { top: 12px; }

/* ---- Footer ----------------------------------------------------------------
   The prototype has no footer: it is an app frame, and .fs-route owns the
   scroll. This is a website, so two things have to be reachable from every
   page regardless — the RSS address and what the site will not print.

   It is flex:0 0 auto inside .fs-shell, so on desktop it pins under the
   scrolling route rather than being scrolled past and never seen. */
/* NOT ON --faint. Everything the site promises about what it will not print,
   and the one disclosure that tiles leave for CARTO, is in here. On
   --surface2 --faint measures 3.71:1 in dark and 3.73:1 in light — the worst
   contrast anywhere on the site, on the paragraph that can least afford it.
   --muted is 5.99:1 and 6.59:1 on the same field. */
.fs-foot {
  flex: 0 0 auto;
  padding: 14px 26px 16px;
  border-top: 1px solid var(--line);
  background: var(--surface2);
  color: var(--muted);
  font: 400 11.5px/1.55 var(--font-sans);
}
.fs-foot p { margin: 0 0 5px; max-width: 96ch; }
.fs-foot p:last-child { margin-bottom: 0; }
.fs-foot strong { color: var(--text); font-weight: 600; }
.fs-foot-links { display: flex; flex-wrap: wrap; gap: 6px; }
.fs-foot-links a { color: var(--muted); text-decoration: none; }
.fs-foot-links a:hover { color: var(--text); text-decoration: underline; }

@media (max-width: 860px) {
  /* The route hands scrolling to the document at this width (§14), so a
     pinned footer would fight it. Let it flow to the end instead. */
  .fs-foot { padding: 14px 16px 20px; }
}


/* ---- Theme switch ----------------------------------------------------------
   Two pills became one object. The prototype showed Dark and Light as a
   pair so both states are visible, and that is worth keeping — a single
   icon button makes you read the icon to work out which way it will go.
   This keeps both, and adds a thumb so the current state is legible from
   shape and position rather than from a border colour alone.

   THE THUMB IS DRIVEN BY :root[data-theme], not by JavaScript. It is the
   same attribute the tokens key off, so the thumb and the palette cannot
   disagree — including on the pre-paint pass, before any script runs. */
.fs-theme {
  position: relative;
  display: flex;
  flex: 0 0 auto;
  padding: 2px;
  border-radius: var(--radius-pill);
  box-shadow: inset 0 0 0 1px var(--line2);
}

.fs-theme-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 30px;
  height: 24px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--line2);
  transition: transform .18s cubic-bezier(.4, 0, .2, 1);
}
:root[data-theme="light"] .fs-theme-thumb { transform: translateX(30px); }

.fs-theme-opt {
  position: relative;      /* over the thumb */
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 24px;
  padding: 0;
  border: 0;
  background: none;
  border-radius: var(--radius-pill);
  color: var(--faint);
  cursor: pointer;
  transition: color .18s ease;
}
.fs-theme-opt svg { width: 13px; height: 13px; }
.fs-theme-opt:hover { color: var(--muted); }
.fs-theme-opt[aria-pressed="true"] { color: var(--text); }
.fs-theme-opt:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  .fs-theme-thumb { transition: none; }
}

@media (max-width: 860px) {
  /* The header scrolls sideways at this width; the switch keeps its size
     but loses the gap it does not need. */
  .fs-theme { flex: 0 0 auto; }
}


/* ============================================================================
   §24  THE MAP SCREEN  (/map)
   ============================================================================
   The map itself is full-bleed, the same shape as the app's Nearby tab: a bar
   floating over the top, a card rising over the bottom. Beside it, a rail that
   says what the pins mean and holds the filters. The route well is
   overflow:hidden on desktop (§4), which is exactly what this wants — the map
   pans, the rail scrolls, the page does neither. */

/* Advanced unit controls receive their complete stylesheet when the responder
   layer starts. Hide their raw markup until that layer is ready. */
.bl-unitops { display: none; }

.bl-maproute {
  height: 100%;
  /* Belt and braces, and DELIBERATELY NOT calc(100svh - 60px), which is what
     the map itself used to carry. The well is the viewport less the header
     AND the footer, so a 100svh-60px floor overflows it by the height of the
     footer — invisible while this route was one full-bleed map, fatal now
     that a scrolling rail lives in the same grid, because the bottom of the
     rail ends up below the clip and no scroll can reach it. A flat floor
     keeps the map usable if the percentage ever fails to resolve without
     ever making the grid taller than the box it is clipped to. */
  min-height: 420px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 276px;
  grid-template-rows: minmax(0, 1fr);
  overflow: hidden;
}

.bl-mapscreen {
  position: relative;
  height: 100%;
  min-height: 0;
  overflow: hidden;
  background: var(--map);
  /* layout+paint, NOT strict. `contain: strict` adds SIZE containment,
     which makes the box size as if it had no contents — so height:100%
     had nothing to resolve against and the map collapsed to a third of
     the window. */
  contain: layout paint;
}

/* ---- The rail --------------------------------------------------------------
   --surface2, the same field /blotter's context rail sits on, so the map
   reads as the subject and the words as the apparatus around it. */
.bl-maprail {
  border-left: 1px solid var(--line);
  background: var(--surface2);
  padding: 14px 14px 20px;
  overflow: auto;
}
.bl-maprail-h {
  font: 700 21px/1.22 var(--font-sans);
  letter-spacing: -.02em;
  color: var(--text);
  margin: 2px 0 0;
  text-wrap: balance;
}
.bl-maprail-fine {
  font: 400 11px/1.55 var(--font-sans);
  color: var(--faint);
  margin: 9px 0 0;
}
/* The one line that reports a filter actively hiding records. It is the same
   ink as the rest of the fine print — it is a fact about the view, not a
   warning — but it earns a rule so it is not read as more small print. */
.bl-maprail-fine--warn {
  padding-top: 8px;
  border-top: 1px solid var(--line2);
  color: var(--muted);
}
/* Section headings inside the rail: the shared eyebrow, pushed down so the
   sections read as separate rather than as one long column. */
.bl-maprail-head { display: flex; align-items: start; justify-content: space-between; gap: 12px; }
.bl-maprail-live { margin-top: 2px; border: 1px solid var(--accent-w); border-radius: var(--radius-pill); padding: 4px 8px; color: var(--accent-t); background: var(--accent-soft); font: 700 11px/1 var(--font-sans); letter-spacing: .08em; text-transform: uppercase; }
.bl-eyebrow.bl-maprail-sec { margin-top: 15px; }
.bl-mapfilter-head { display: flex; align-items: end; justify-content: space-between; gap: 10px; }
.bl-mapfilter-hint { color: var(--faint); font: 500 11px/1 var(--font-sans); letter-spacing: .03em; }
.bl-maplimit-row { margin-top: 7px; }
.bl-maplimit-row .bl-menu { width: 100%; min-width: 0; }
.bl-maplimit-row .bl-menu-button { min-height: 34px; background: var(--surface); font-weight: 600; }
.bl-maplimit-row .bl-menu-pop { width: 100%; }
@media (max-width: 700px) {
  /* The shared select becomes a viewport-fixed sheet on phones. The map rail
     is itself the mobile scrolling surface, so that sheet is clipped below
     the rail; keep this compact five-choice menu anchored to its field. */
  .bl-maplimit-row .bl-menu-pop {
    position: absolute;
    inset: auto auto auto 0;
    top: calc(100% + 6px);
    width: 100%;
    max-width: none;
    max-height: 270px;
    padding: 6px;
    border-radius: 11px;
  }
}

.bl-mapchips { margin-bottom: 2px; }
.bl-maprankgrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px; margin-top: 7px; }
.bl-maprankgrid .bl-mapchip { width: 100%; justify-content: flex-start; padding: 0 7px; }
.bl-maprankgrid .bl-mapchip-n { margin-left: auto; }
.bl-mapquick { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
.bl-mapbasegrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 5px; margin-top: 7px; }
.bl-mapbasegrid .bl-seg-b { min-width: 0; border: 1px solid var(--line2); border-radius: 8px; padding: 7px 8px; background: var(--surface); }
.bl-mapbasegrid .bl-seg-b.is-on { color: var(--accent-t); border-color: var(--accent-w); background: var(--accent-soft); }
.bl-mapunitbtn { width: 100%; justify-content: space-between; margin-top: 7px; min-height: 34px; }
.bl-maptools { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px; margin-top: 7px; }
.bl-maptools .bl-seg-b { min-width: 0; border: 1px solid var(--line2); border-radius: 8px; padding: 8px; background: var(--surface); color: var(--muted); }
.bl-maptools .bl-seg-b:hover { border-color: var(--accent-w); color: var(--text); }
.bl-maptools .bl-maptools-wide { grid-column: 1 / -1; text-align: center; text-decoration: none; }
.bl-mapcoverage { margin-top: 16px; padding: 12px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }
.bl-mapcoverage-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.bl-mapcoverage-head .bl-eyebrow { margin: 0 0 3px; }
.bl-mapcoverage-h { margin: 0; color: var(--text); font: 650 14px/1.25 var(--font-sans); }
.bl-mapcoverage-state { flex: 0 0 auto; padding: 3px 6px; border: 1px solid var(--accent-w); border-radius: var(--radius-pill); color: var(--accent-t); background: var(--accent-soft); font: 650 11px/1 var(--font-sans); letter-spacing: .06em; text-transform: uppercase; }
.bl-mapcoverage-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 5px; margin-top: 11px; }
.bl-mapcoverage-stat { min-width: 0; padding: 7px 3px 6px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface2); text-align: center; }
.bl-mapcoverage-n { display: block; color: var(--text); font: 650 15px/1 var(--font-mono); }
.bl-mapcoverage-stat span { display: block; overflow: hidden; margin-top: 4px; color: var(--faint); font: 500 11px/1 var(--font-sans); text-overflow: ellipsis; }
.bl-mapmeters { display: grid; gap: 7px; margin-top: 11px; }
.bl-mapmeter-row { display: grid; grid-template-columns: 78px minmax(0, 1fr); align-items: center; gap: 8px; color: var(--faint); font: 500 11px/1 var(--font-sans); }
.bl-mapmeter { position: relative; height: 4px; overflow: hidden; border-radius: var(--radius-pill); background: var(--line2); }
.bl-mapmeter-fill { display: block; width: 0; height: 100%; border-radius: inherit; background: var(--accent); transition: width var(--mo-2) var(--mo-ease); }
.bl-mapmeter-fill--budget { background: var(--muted); }
.bl-mapcoverage-scope { margin: 10px 0 0; padding-top: 9px; border-top: 1px solid var(--line); color: var(--faint); font: 500 11px/1.4 var(--font-sans); }
.bl-mapcoverage .bl-maprail-fine { margin-top: 7px; }
.bl-mapcoverage .bl-gate-link { display: block; margin-top: 3px; }
.bl-mapmore { margin-top: 15px; border-top: 1px solid var(--line); padding-top: 10px; }
.bl-mapmore summary { cursor: pointer; color: var(--muted); font: 600 11px/1.4 var(--font-sans); list-style-position: outside; }
.bl-mapmore[open] summary { color: var(--text); }
.bl-mapmore .bl-maprail-fine { margin-left: 14px; }
/* The severity filter is the legend. One control, so a reader can never be
   looking at a key that disagrees with the map. Rank arrives as the shared
   §3 swatch — width AND hue — rather than as a dot, so the ramp is ordered
   here exactly as it is on the card stripes. */
.bl-mapchip { gap: 7px; padding: 0 10px; min-height: 28px; }
.bl-mapchip-name { font: 500 12px var(--font-sans); }
.bl-mapchip-n { font: 400 11px var(--font-sans); color: var(--faint); }
.bl-mapchip.is-on .bl-mapchip-n { color: inherit; opacity: .8; }
/* A rank that is switched off keeps its swatch at full strength — the swatch
   is the legend, and a greyed-out legend is a legend you cannot read. The
   chip's own border carries the on/off state, per §5. */
.bl-mapchip[aria-pressed="false"] .bl-mapchip-name { color: var(--faint); }
.bl-mapchip--reset { color: var(--accent-t); box-shadow: inset 0 0 0 1px var(--accent-w); }

.bl-mapscreen-ground,
.bl-mapscreen-stale {
  position: absolute;
  inset: 0;
  /* THE ZOOM FRACTION IS THIS TRANSFORM. Tiles are fetched for an integer
     base zoom and positioned in that zoom's pixels; the display zoom's
     fractional part is one scale here. transform-origin MUST be the top-left
     corner, because the route computes the translate as
     "screen = translate + scale * local" — the default 50% 50% would add an
     origin-dependent offset the arithmetic knows nothing about and the map
     would slide sideways as it zoomed. */
  transform-origin: 0 0;
  will-change: transform;
}
/* The outgoing tile layer, kept under the incoming one until the incoming one
   has actually loaded. Emptying the ground first is what made a zoom blink:
   the reader got a hole where the map was, and there is nothing smooth about
   a hole. Tiles in here are the wrong resolution by one step, never the wrong
   place — they are re-laid into the new zoom's pixel space before they are
   parked. */
.bl-mapscreen-stale { z-index: 0; }
.bl-mapscreen-ground { z-index: 1; }
.bl-mapscreen-over { position: absolute; inset: 0; pointer-events: none; z-index: 2; }

/* THE HANDOVER, NOT A CUT. The stale layer used to be emptied the instant the
   last incoming tile reported complete — which is one frame after the last tile
   became AVAILABLE and some number of frames before the reader's eye had
   finished reading the new ground. Together with the per-tile fade in §5 this
   makes a zoom or a basemap swap a dissolve: the new tiles come up as they
   decode, and only once they are all up does the old ground go down.

   Opacity on the LAYER, not on its tiles. The stale layer holds up to forty
   images; fading them individually is forty composited layers for the same
   effect one gives. */
.bl-mapscreen-stale {
  transition: opacity var(--mo-2) var(--mo-ease);
}
.bl-mapscreen-stale.is-out { opacity: 0; }

/* The drag surface sits OVER the pins on purpose: a drag that starts on a
   pin should still pan. Clicks are resolved with elementFromPoint instead,
   so a pin is still selectable.

   touch-action:none, because every gesture over the map is the map's: one
   finger pans, two pinch, and a browser that starts scrolling the page
   before the first touchmove arrives makes both impossible. The rail below
   it on a narrow screen is where a reader scrolls the page. */
.bl-mapscreen-hit { position: absolute; inset: 0; cursor: grab; z-index: 3; touch-action: none; }
.bl-mapscreen.is-dragging .bl-mapscreen-hit { cursor: grabbing; }
.bl-mapscreen-hit:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }

/* Severity by fill AND radius, the same rule the stripes follow, so the
   ordering survives greyscale. The halo is the page ground, so a pin stays
   findable over a busy tile. */
.bl-mappin {
  fill: var(--s0);
  stroke: var(--bg);
  stroke-width: 2;
  paint-order: stroke;
  pointer-events: auto;
  cursor: pointer;
}
.bl-mappin[data-sev="5"] { fill: var(--s5); }
.bl-mappin[data-sev="4"] { fill: var(--s4); }
.bl-mappin[data-sev="3"] { fill: var(--s3); }
.bl-mappin[data-sev="2"] { fill: var(--s2); }
.bl-mappin[data-sev="1"] { fill: var(--s1); }
.bl-mappin[data-sev="0"] { fill: var(--s0); }

/* ---- Pin motion ---------------------------------------------------------------
   SELECTION IS A SCALE, NOT A NEW RADIUS. The route used to redraw the whole pin
   layer to select one call, stamping a radius 3.5 units larger on the chosen
   circle — so the pin did not grow, it was replaced by a bigger pin, and forty
   nine other circles were rebuilt to do it. Now the node survives and CSS grows
   it, which is both the animation and one fewer full rebuild per click.

   transform, NOT r. `r` is a CSS property in every engine this site supports and
   the ramp above uses it — but the route writes cx/cy on these nodes sixty times
   a second while panning, and a transition on a geometry property is a
   transition the layout tree has to reconcile against those writes. A transform
   is composited and touches neither.

   transform-box: fill-box pins the origin to the circle's own centre, which is
   wherever cx/cy currently are — so a selected pin stays selected, and stays
   centred, straight through a pan. */
.bl-mappin {
  transform-box: fill-box;
  transform-origin: center;
  transition:
    transform var(--mo-2) var(--mo-ease),
    stroke-width var(--mo-1) var(--mo-ease),
    opacity var(--mo-2) var(--mo-ease);
}
.bl-mappin.is-on { stroke: var(--text); stroke-width: 2.5; transform: scale(1.42); }

/* A rank chip turning on or off. The pins for that rank used to blink out of
   existence between two frames, which on a screen showing four hundred of them
   reads as the map breaking rather than as a filter working.

   .is-new is stamped at append and cleared on the next frame — that is the
   zero-state a transition needs something to move FROM. .is-off is stamped on
   the way out and the route removes the node when it has finished. Both are
   listed after .is-on so a selected pin still leaves properly. */
.bl-mappin.is-new,
.bl-mappin.is-off {
  opacity: 0;
  transform: scale(.45);
}
.bl-mappin.is-off { pointer-events: none; }

/* The chrome floating over the map. Everything from here down sits above the
   hit layer (z-index 3) — a control the drag surface covers is a control
   nobody can press. */
.bl-mapbar {
  position: absolute; top: 12px; left: 12px; z-index: 4;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  max-width: calc(100% - 96px);
  padding: 7px 12px;
  border-radius: var(--radius-pill);
  background: var(--bg);
  box-shadow: inset 0 0 0 1px var(--line2);
}
.bl-mapbar-count { font: 600 12.5px var(--font-sans); color: var(--text); white-space: nowrap; }
.bl-mapbar-note { font: 400 11.5px var(--font-sans); color: var(--faint); white-space: nowrap; }

.bl-mapctl { position: absolute; top: 12px; right: 12px; z-index: 4; display: flex; flex-direction: column; gap: 5px; }
.bl-mapbtn {
  width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--radius-md);
  background: var(--bg);
  box-shadow: inset 0 0 0 1px var(--line2);
  color: var(--text);
  font: 500 16px/1 var(--font-sans);
  cursor: pointer;
}
.bl-mapbtn:hover { background: var(--surface); }
.bl-mapbtn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent); }
/* The press. A zoom button whose only feedback is the map moving 200ms later
   feels unresponsive on the frame it was actually pressed. */
.bl-mapbtn { transition: background-color var(--mo-1) var(--mo-ease), transform var(--mo-1) var(--mo-ease); }
.bl-mapbtn:active { transform: scale(.9); }
/* The filter chips and the basemap segment: the map's answer takes a frame or
   two to arrive, so the control must not. */
.bl-mapchip, .bl-seg-b {
  transition:
    background-color var(--mo-1) var(--mo-ease),
    box-shadow var(--mo-1) var(--mo-ease),
    color var(--mo-1) var(--mo-ease);
}
.bl-mapchip-n { transition: color var(--mo-1) var(--mo-ease), opacity var(--mo-1) var(--mo-ease); }

.bl-mapcard-host { position: absolute; left: 12px; right: 12px; bottom: 12px; z-index: 4; display: flex; justify-content: center; }
.bl-mapcard {
  position: relative;
  width: 100%; max-width: 520px;
  border-radius: var(--radius-lg);
  background: var(--bg);
  box-shadow: inset 0 0 0 1px var(--line2), 0 12px 32px rgba(0, 0, 0, .35);
}

/* THE CARD RISES, it does not appear. It is the answer to a click on a pin
   twenty pixels away, and an answer that materialises fully formed over the map
   is an answer the reader has to re-find. Rising from the bottom edge says where
   it came from and where it will go back to.

   It leaves the same way, which is why the route holds the node for --mo-2
   after the close rather than removing it on the click: a card that vanishes
   instantly after rising in makes the close read as a mis-click. */
@keyframes blCardIn {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}
@keyframes blCardOut {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(8px); }
}
.bl-mapcard { animation: blCardIn var(--mo-3) var(--mo-ease) both; }
.bl-mapcard.is-out { animation: blCardOut var(--mo-2) var(--mo-ease) both; pointer-events: none; }
/* A card that is being REBUILT rather than opened. The route re-selects after
   every re-read so an open callout survives a poll, and without this the reader
   watches it rise again every two minutes for a record they never closed. */
.bl-mapcard.is-held { animation: none; }
/* The card reuses BL.card, so a call reads the same here as on the feed —
   one component, not a second one that drifts. */
.bl-mapcard-link { display: block; padding: 13px 34px 14px 15px; color: inherit; text-decoration: none; }
.bl-mapcard-link:hover { text-decoration: none; }
.bl-mapcard-link:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent); border-radius: var(--radius-lg); }
.bl-mapcard-meta { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
.bl-mapcard-rank { font: 600 11px var(--font-sans); letter-spacing: .05em; text-transform: uppercase; color: var(--muted); }
.bl-mapcard-when { margin-left: auto; font: 400 11.5px var(--font-sans); color: var(--faint); white-space: nowrap; }
.bl-mapcard-title { margin: 0 0 3px; font: 600 15.5px/1.3 var(--font-sans); color: var(--text); }
.bl-mapcard-where { margin: 0; font: 400 12.5px/1.45 var(--font-sans); color: var(--muted); }

/* The rank dot. Size AND hue, same rule as the stripes and the pins. */
.bl-sevdot { flex: 0 0 auto; border-radius: 50%; width: 8px; height: 8px; background: var(--s0); }
.bl-sevdot[data-sev="5"] { background: var(--s5); width: 11px; height: 11px; }
.bl-sevdot[data-sev="4"] { background: var(--s4); width: 10px; height: 10px; }
.bl-sevdot[data-sev="3"] { background: var(--s3); width: 9px; height: 9px; }
.bl-sevdot[data-sev="2"] { background: var(--s2); width: 8px; height: 8px; }
.bl-sevdot[data-sev="1"] { background: var(--s1); width: 7px; height: 7px; }
.bl-sevdot[data-sev="0"] { background: var(--s0); width: 6px; height: 6px; }
.bl-mapcard-x {
  position: absolute; top: 6px; right: 8px; z-index: 1;
  width: 24px; height: 24px;
  border: 0; border-radius: var(--radius-pill);
  background: none; color: var(--faint);
  font: 400 17px/1 var(--font-sans); cursor: pointer;
}
.bl-mapcard-x:hover { color: var(--text); background: var(--surface); }

.bl-mapscreen .bl-map-credit { z-index: 4; }

/* ---- Responsive -------------------------------------------------------------
   Two columns become one, and the map stops being "whatever is left" and takes
   a stated height — a grid row sized by content would collapse it, because a
   position:absolute interior has no content height to give. */
@media (max-width: 1080px) {
  .bl-maproute {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto;
    overflow: auto;
  }
  .bl-mapscreen { height: 62svh; min-height: 340px; }
  .bl-maprail {
    border-left: 0;
    border-top: 1px solid var(--line);
    overflow: visible;
  }
}

@media (max-width: 860px) {
  /* The route hands scrolling to the document at this width (§14), so the
     grid must not keep a scroller of its own inside it. */
  .bl-maproute { height: auto; overflow: visible; }
  .bl-mapscreen { height: 58svh; min-height: 320px; }
  .bl-maprail { padding: 14px 16px 22px; }
  .bl-mapbar { max-width: calc(100% - 70px); }
}


/* ============================================================================
   §25  READER PREFERENCES
   ============================================================================
   Stamped on the root by BL.applyPrefs, so a preference changes the page
   without any route re-rendering. Only the two that CSS can act on alone
   live here; the rest are read by the routes as they build. */

/* Severity without the width channel. Off by default and it should stay
   off — hue alone fails in greyscale and for a colour-blind reader, which
   is the entire reason the stripe widens. It exists because a reader who
   finds the varying widths noisy is better served by an explicit choice
   than by no severity at all. */
:root[data-stripes="hue"] .bl-card::before,
:root[data-stripes="hue"] .bl-stripe { width: var(--stripe-0); }
:root[data-stripes="hue"] .bl-sev-sw { width: var(--stripe-3); }

/* Larger type.
   THIS BLOCK DID NOTHING, AND WHAT IT DID DO WENT THE WRONG WAY. It read:

     :root[data-type="large"] { font-size: 112.5%; }
     :root[data-type="large"] body { font-size: 16.5px; }
     :root[data-type="large"] .bl-card-title { font-size: 17.5px; }
     :root[data-type="large"] .bl-card-where,
     :root[data-type="large"] .bl-card-account { font-size: 14px; }
     :root[data-type="large"] .bl-setrow .bl-label { font-size: 15px; }

   Four faults, in order. The root percentage moved nothing, because every
   size in this file is stated in px and only the ch/em measures are
   relative. The body rule was overridden by .fs-shell's own font-size:15px,
   which every route sits inside. .bl-card-where, .bl-card-account,
   .bl-setrow and .bl-label are emitted by no route and no server template —
   the real classes are .bl-meta, .bl-row and .bl-row-label. And the one
   selector that did match made card titles SMALLER: 21px down to 17.5px on
   a desktop feed, so a reader who asked for larger type got smaller type.

   Replaced with the reading surfaces, named by the classes the routes
   actually emit, each moved up roughly a tenth. It stays a list because a
   px design cannot be scaled from the root, and a list that is right beats
   a one-liner that is not. Anything not named here is a label or a control
   and is deliberately left alone: growing the chrome to match would take
   the extra measure straight back off the words. */
:root[data-type="large"] .fs-shell { font-size: 16.5px; }

:root[data-type="large"] .bl-card-title { font-size: 23px; }
:root[data-type="large"] .bl-card--compact .bl-card-title { font-size: 17.5px; }
:root[data-type="large"] .bl-meta { font-size: 15px; }
:root[data-type="large"] .bl-card--compact .bl-meta { font-size: 13.5px; }
:root[data-type="large"] .bl-time { font-size: 13px; }

:root[data-type="large"] .bl-prose { font-size: 16.5px; }
:root[data-type="large"] .bl-deck { font-size: 16.5px; }
:root[data-type="large"] .bl-lede { font-size: 14px; }
:root[data-type="large"] .bl-empty,
:root[data-type="large"] .bl-note-body { font-size: 13.5px; }
:root[data-type="large"] .bl-disclaimer { font-size: 12.5px; }

:root[data-type="large"] .bl-tl-what { font-size: 16px; }
:root[data-type="large"] .bl-tl-meta,
:root[data-type="large"] .bl-tl-time { font-size: 13.5px; }

:root[data-type="large"] .bl-row-label { font-size: 16px; }
:root[data-type="large"] .bl-row-sub { font-size: 12.5px; }
:root[data-type="large"] .bl-row-value { font-size: 14px; }

:root[data-type="large"] .bl-ftab-what { font-size: 15px; }
:root[data-type="large"] .bl-ftab-where { font-size: 13.5px; }
:root[data-type="large"] .bl-mrow-name { font-size: 15px; }
:root[data-type="large"] .bl-mrow-sub { font-size: 12px; }
:root[data-type="large"] .bl-mrow-free,
:root[data-type="large"] .bl-mrow-plus { font-size: 13.5px; }
:root[data-type="large"] .fs-foot { font-size: 12.5px; }

/* The feed card title already steps down on a phone; the large-type reader
   gets that step back. Ordered after §14 by source position, which is what
   makes it win without a specificity fight. */
@media (max-width: 860px) {
  :root[data-type="large"] .bl-card-title { font-size: 20px; }
}


/* ============================================================================
   §26  THE PREFERENCE FILTER NOTICE
   ============================================================================
   This is the sentence
   BL.filterSummary writes — what the reader's own saved preferences removed
   from the list they are looking at — and the link that undoes it.

   WHY IT IS NOT ONE OF THE CLASSES THAT ALREADY EXIST. .bl-secnote is
   white-space: nowrap, because it is a label beside a rule rather than a
   sentence, and this wraps to two lines on a narrow feed column.
   .bl-maprail-fine belongs to the map screen and .bl-matrix-note to /plus.
   So: one rule, no box, no rule above it, sized
   between the feed subtitle and the card meta so it reads as a footnote to the
   list rather than as an alert about it.

   IT IS DELIBERATELY QUIET AND DELIBERATELY NOT A WARNING. Nothing has gone
   wrong — the page is doing exactly what the reader asked it to. --faint is
   the token for "true, and not the point", which is what this is. The inline
   .bl-linkbtn beside it carries --accent-t, because the way back to a full
   list is a primary action and accent is what actions are.

   Vertical rhythm comes from .bl-feed's own 9px gap, so it carries no margin
   of its own. max-width is in ch against the text, not the column: a sentence
   that runs the full width of a wide feed column is a sentence nobody
   finishes. */
.bl-filternote {
  font: 400 11.5px/1.55 var(--font-sans);
  color: var(--faint);
  max-width: 68ch;
  margin: 0;
}
.bl-filternote .bl-linkbtn { white-space: nowrap; }

:root[data-type="large"] .bl-filternote { font-size: 13px; }

/* ════════════════════════════════════════════════════════
   §26 DISPATCH NOTES

   The dispatch centre's own working notes, served now behind
   V1_PUBLIC_REMARKS and rendered on /incident/:id.

   THEY ARE SET APART FROM THE PROSE ON PURPOSE. The account above them
   is this project's sentence about the call; these are somebody else's
   shorthand, unedited. A monospaced timestamp and a hanging indent make
   that difference visible before a word is read — the reader should
   never be in doubt about which text this site wrote and which it only
   passed on.

   Not .bl-note: that class is the transparent notice card used by the
   plus rail and the town empty state, and reusing it would put a 1px
   ring around every line of a log. */
.bl-notes {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.bl-dnote {
  display: grid;
  grid-template-columns: 4.5rem 1fr;
  gap: 12px;
  align-items: baseline;
  padding: 7px 0;
  border-top: 1px solid var(--line);
}
.bl-dnote:first-child { border-top: 0; }

.bl-dnote-at {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The notes are terse and often abbreviated; a slightly tighter measure
   and the body colour keep them readable without inviting them to be
   mistaken for the written account. */
.bl-dnote-text {
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--text);
  overflow-wrap: anywhere;
}

/* One column on a phone: 4.5rem of timestamp against a narrow measure
   leaves the text in a gutter. */
@media (max-width: 560px) {
  .bl-dnote { grid-template-columns: 1fr; gap: 2px; }
  .bl-dnote-at { font-size: 11.5px; }
}

/* PATIENT ROWS ARE NOT NOTES AND MUST NOT BORROW THEIR GRID. They were
   given .bl-dnote at first, which is two columns — a 4.5rem timestamp
   and the text. A patient row has no timestamp, so its single span
   landed in the TIMESTAMP column and wrapped at about ten characters
   down the left edge of a full-width card. Their own class, one column,
   no grid. */
.bl-pstat {
  padding: 7px 0;
  border-top: 1px solid var(--line);
}
.bl-pstat:first-child { border-top: 0; }

.bl-pstat-text {
  font-size: 14px;
  line-height: 1.5;
  color: var(--text);
}

/* Admin patient data is operational information, not prose. Keep the
   patient's identity separate from the two clinical checks so an operator
   can scan the answer without parsing a dot-separated sentence. */
.bl-patients--admin { border-color: var(--accent-w); }
.bl-patient-title-row,
.bl-patient-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.bl-patient-title-row { margin-bottom: 14px; }
.bl-patient-title-row .bl-panel-title { margin: 0; }
.bl-patient {
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-raised, var(--surface));
}
.bl-patient + .bl-patient { margin-top: 10px; }
.bl-patient-identity { font-size: 16px; line-height: 1.35; color: var(--text); }
.bl-patient-number {
  color: var(--faint);
  font: 600 11px/1 var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  letter-spacing: .06em;
  text-transform: uppercase;
}
.bl-patient-facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  margin: 12px 0 0;
}
.bl-patient-fact {
  min-width: 0;
  padding: 10px 11px;
  border-radius: 8px;
  background: var(--bg);
}
.bl-patient-fact dt {
  margin: 0 0 4px;
  color: var(--faint);
  font-size: 11px;
  font-weight: 650;
  letter-spacing: .05em;
  text-transform: uppercase;
}
.bl-patient-fact dd { margin: 0; color: var(--text); font-size: 14px; font-weight: 650; line-height: 1.3; }
.bl-patient-fact--urgent { background: var(--t5); }
.bl-patient-fact--urgent dd { color: var(--i5); }
@media (max-width: 560px) {
  .bl-patient-facts { grid-template-columns: 1fr; }
}
.bl-admincall { border-color: var(--accent-w); }
.bl-admincall-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.bl-admincall-name { margin-top: 13px; font: 650 18px/1.3 var(--font-sans); color: var(--text); }
.bl-admincall-meta { margin-top: 4px; font-size: 12px; color: var(--accent-t); }
.bl-badge--admin { color: var(--accent-t); border: 1px solid var(--accent-w); background: var(--accent-soft); }

:root[data-type="large"] .bl-dnote-text { font-size: 15px; }

/* The Details tab's two panels. They were given .bl-card--panel, a class
   that existed nowhere in this stylesheet, so they inherited .bl-card's
   ring and stacked flush against each other with the notes' heading
   pressed onto the patient card's bottom edge. */
.bl-card--panel {
  padding: 18px 20px 20px;
  margin-bottom: 16px;
}
.bl-card--panel:last-child { margin-bottom: 0; }

.bl-panel-title {
  font-size: 17px;
  font-weight: 650;
  letter-spacing: -.01em;
  margin: 0 0 4px;
}
/* The lede under a panel title is a caption, not body copy — it sat at
   full body size and read as the first line of the notes. */
.bl-card--panel .bl-lede {
  margin: 0 0 12px;
  font-size: 13px;
  color: var(--muted);
}
.bl-card--panel .bl-notes { margin-top: 2px; }

/* Keep the refresh rules last in the cascade. The original stylesheet has
   route-specific chrome later in the file; these are the intended final
   values for the simplified shell. */
.fs-theme--secondary { display: none; }
.fs-head { gap: 18px; height: 64px; padding-inline: clamp(12px, 1.3vw, 20px); background: var(--surface); }
.fs-brand { padding-right: 18px; }
.fs-navgroup { gap: 6px; }
.fs-nav { height: 38px; padding: 0 12px; border-radius: var(--radius-pill); }
.fs-nav.is-on,
.fs-nav[aria-current="page"] { color: var(--text); background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent-w); }
.fs-settingschip {
  min-height: 32px;
  padding: 0 8px;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border-radius: 8px;
  color: var(--faint);
  font-size: 11.5px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  flex: 0 0 auto;
}
.fs-settingschip svg { width: 15px; height: 15px; }
.fs-settingschip:hover,
.fs-settingschip[aria-current="page"] { color: var(--text); background: var(--surface2); }
.bl-blotter--noctx {
  width: 100%;
  margin: 0;
  grid-template-columns: 240px minmax(0, 1fr);
  background: var(--bg);
}
.bl-blotter--noctx .bl-rail { background: var(--surface2); }
.bl-blotter--noctx .bl-feed { padding: 14px clamp(18px, 3vw, 38px) 48px; }
.bl-feedhead { position: sticky; top: -14px; z-index: 4; padding: 10px 0 12px; margin-bottom: 5px; background: var(--bg); }

@media (min-width: 861px) {
  .fs-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
    padding: 8px clamp(18px, 3vw, 38px);
    line-height: 1.35;
  }
  .fs-foot p { margin: 0; }
  .fs-foot-links { flex: 0 0 auto; }
}

@media (max-width: 860px) {
  .fs-head { overflow: visible; gap: 8px; padding-inline: 12px; }
  .fs-brand { padding-right: 4px; }
  .fs-brand::after, .fs-nav span { display: none; }
  .fs-navgroup { gap: 2px; }
  .fs-nav { width: 42px; justify-content: center; padding: 0; }
  .fs-nav-ico { width: 19px; height: 19px; }
  .fs-viewas {
    width: 38px;
    min-height: 34px;
    margin-left: 2px;
    padding: 0;
    justify-content: center;
    border: 1px solid var(--line2);
    border-radius: 4px;
  }
  .fs-viewas svg, .fs-viewas-copy { display: none; }
  .fs-viewas-short { display: inline; }
  .fs-head-spacer { display: block; }
  .fs-settingschip { width: 34px; padding: 0; justify-content: center; }
  .fs-settingschip span { display: none; }
  /* The phone header is already full with identity, four section icons and
     Settings. Keep Blotter+ one tap away without making that row scroll or
     hiding another destination. The shell padding reserves the button's
     footprint so it never covers the footer or the final record row. */
  .fs-shell { padding-bottom: calc(66px + env(safe-area-inset-bottom)); }
  html[data-blotter-plus="true"] .fs-shell { padding-bottom: 0; }
  .fs-pluschip {
    position: fixed;
    right: 14px;
    bottom: calc(12px + env(safe-area-inset-bottom));
    z-index: 30;
    min-height: 46px;
    padding: 0 17px 0 45px;
    border-radius: 14px;
    background: var(--surface);
    box-shadow: inset 0 0 0 1px var(--plus), 0 8px 24px var(--line2);
  }
  .fs-pluschip::before { left: 11px; width: 26px; height: 26px; margin-top: -13px; }
  .fs-pluschip::after { display: none; }
  .fs-adminchip { width: 34px; padding: 0; justify-content: center; overflow: hidden; color: transparent; }
  .fs-adminchip::before { content: "A"; color: var(--accent-t); }
  .bl-blotter--noctx { display: flex; flex-direction: column; }
  .bl-blotter--noctx .bl-rail { border: 0; border-bottom: 1px solid var(--line); padding: 12px 16px; gap: 12px; }
  .bl-blotter--noctx .bl-feed { padding-top: 10px; }
  .bl-feedhead { top: -10px; }
}


/* ============================================================================
   §25  INTERACTION POLISH — the operational-intelligence layer
   ============================================================================
   The shared motion primitives above keep live data stable. This layer makes
   the controls and route changes around that data feel responsive: navigation
   gets a directional trace, pressable surfaces answer on the press frame, and
   analytical graphics assemble from their axes. None of it delays content and
   §15's global reduced-motion rule still wins over every animation here. */

@keyframes blRouteIn {
  from { opacity: 0; transform: translateY(9px) scale(.997); }
  to { opacity: 1; transform: none; }
}

@keyframes blDataRise {
  from { opacity: .18; transform: scaleY(.12); }
  to { opacity: 1; transform: scaleY(1); }
}

@keyframes blDataRun {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/* blotter.js stamps this only for a real pathname change. Live polling and
   preference repaints therefore stay still under the reader. */
.fs-route.is-route-entering > :first-child {
  animation: blRouteIn var(--mo-3) var(--mo-ease) both;
}

/* Identity and navigation. The trace grows from the direction of travel and
   the icon moves only one pixel: enough feedback to feel, not enough to chase. */
.fs-brand-logo {
  transition: transform var(--mo-2) var(--mo-ease), filter var(--mo-2) var(--mo-ease);
}
.fs-brand:hover .fs-brand-logo,
.fs-brand:focus-visible .fs-brand-logo {
  transform: rotate(-4deg) scale(1.07);
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent) 55%, transparent));
}
.fs-nav {
  position: relative;
  overflow: hidden;
  transition: color var(--mo-1) var(--mo-ease), background-color var(--mo-1) var(--mo-ease), box-shadow var(--mo-1) var(--mo-ease), transform var(--mo-1) var(--mo-ease);
}
.fs-nav::after {
  content: "";
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 5px;
  height: 1px;
  border-radius: 1px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--mo-2) var(--mo-ease);
}
.fs-nav.is-on::after,
.fs-nav.is-current::after,
.fs-nav[aria-current="page"]::after { transform: scaleX(1); }
.fs-nav-ico {
  transition: transform var(--mo-1) var(--mo-ease), color var(--mo-1) var(--mo-ease), filter var(--mo-1) var(--mo-ease);
}
.fs-nav:hover .fs-nav-ico,
.fs-nav:focus-visible .fs-nav-ico {
  color: var(--accent-t);
  transform: translateY(-1px);
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--accent) 45%, transparent));
}
.fs-nav:active { transform: scale(.97); }
.fs-settingschip,
.fs-adminchip,
.fs-viewas {
  transition: color var(--mo-1) var(--mo-ease), background-color var(--mo-1) var(--mo-ease), box-shadow var(--mo-1) var(--mo-ease), transform var(--mo-1) var(--mo-ease);
}
.fs-settingschip:hover svg,
.fs-settingschip:focus-visible svg { color: var(--accent-t); transform: rotate(-3deg); }
.fs-settingschip svg { transition: color var(--mo-1) var(--mo-ease), transform var(--mo-1) var(--mo-ease); }
.fs-settingschip:active,
.fs-adminchip:active,
.fs-viewas:active { transform: scale(.96); }

/* Tactile controls. State colour still carries the meaning; transform only
   answers the finger or pointer and returns immediately. */
.bl-btn,
.bl-chip,
.bl-seg-b,
.bl-tab,
.bl-more,
.bl-row-action,
.bl-depttoggle,
.bl-facet-apply,
.bl-watch-savebtn,
.bl-wl-remove,
.bl-feedsearch-go {
  transition: transform var(--mo-1) var(--mo-ease), color var(--mo-1) var(--mo-ease), background-color var(--mo-1) var(--mo-ease), border-color var(--mo-1) var(--mo-ease), box-shadow var(--mo-1) var(--mo-ease);
}
.bl-btn:hover,
.bl-chip:hover,
.bl-seg-b:hover,
.bl-tab:hover {
  transform: translateY(-1px);
}
.bl-btn:active,
.bl-chip:active,
.bl-seg-b:active,
.bl-tab:active,
.bl-more:active,
.bl-row-action:active,
.bl-depttoggle:active,
.bl-facet-apply:active,
.bl-watch-savebtn:active,
.bl-wl-remove:active,
.bl-feedsearch-go:active { transform: scale(.97); }

/* Record cards are navigation, so depth on hover is an honest affordance.
   Panels that are merely informational do not lift. */
.bl-card,
.bl-lock,
.bl-note--plus,
.bl-crosslink,
.bl-mapcard {
  transition: transform var(--mo-2) var(--mo-ease), box-shadow var(--mo-2) var(--mo-ease), border-color var(--mo-1) var(--mo-ease), background-color var(--mo-1) var(--mo-ease);
}
.bl-card:hover,
.bl-card:focus-visible,
.bl-lock:hover,
.bl-lock:focus-visible,
.bl-note--plus:hover,
.bl-note--plus:focus-visible,
.bl-crosslink:hover,
.bl-crosslink:focus-visible {
  transform: translateY(-2px);
  box-shadow: 0 0 0 1px var(--accent-w), 0 10px 26px color-mix(in srgb, var(--text) 9%, transparent);
}
.bl-card:active,
.bl-lock:active,
.bl-note--plus:active,
.bl-crosslink:active { transform: translateY(0) scale(.995); }

/* Settings cards become active-looking only while one of their real controls
   is focused. This keeps static cards from pretending to be clickable. */
.bl-set-card,
.bl-billing-card,
.bl-response-card,
.ex-panel,
.ex-pulse-panel,
.ex-directory,
.ex-metrics {
  transition: border-color var(--mo-2) var(--mo-ease), box-shadow var(--mo-2) var(--mo-ease), background-color var(--mo-2) var(--mo-ease);
}
.bl-set-card:focus-within,
.bl-billing-card:focus-within,
.bl-response-card:focus-within,
.ex-directory:focus-within {
  border-color: var(--accent-w);
  box-shadow: 0 0 0 3px var(--accent-soft), 0 14px 34px color-mix(in srgb, var(--text) 7%, transparent);
}

/* Explore is the analytical surface. Bars grow from their baseline and
   tracks run from the label toward the value, which makes a changed time
   window visibly recompute without turning the page into a loader. */
.fs-route.is-route-entering .ex-pulse > i {
  transform-origin: center bottom;
  animation: blDataRise var(--mo-3) var(--mo-ease) both;
}
.fs-route.is-route-entering .ex-pulse > i:nth-child(3n+2) { animation-delay: 24ms; }
.fs-route.is-route-entering .ex-pulse > i:nth-child(3n) { animation-delay: 48ms; }
.fs-route.is-route-entering .ex-bar-track i {
  transform-origin: left center;
  animation: blDataRun var(--mo-3) var(--mo-ease) both;
}
.ex-metric strong,
.ex-network strong,
.ex-directory-row {
  transition: color var(--mo-1) var(--mo-ease), transform var(--mo-1) var(--mo-ease), background-color var(--mo-1) var(--mo-ease);
}
.ex-metric:hover strong,
.ex-network > div:hover strong { color: var(--accent-t); transform: translateY(-1px); }
.ex-directory-row:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }

/* The map is already the most spatial surface. Keep additions to its console
   chrome: status and filters react while the ground continues to pan freely. */
.bl-mapbar,
.bl-mapctl,
.bl-maprail-live,
.bl-mapcoverage-state {
  transition: box-shadow var(--mo-2) var(--mo-ease), border-color var(--mo-1) var(--mo-ease), background-color var(--mo-1) var(--mo-ease);
}
.bl-mapbar:hover { box-shadow: inset 0 0 0 1px var(--accent-w), 0 10px 26px color-mix(in srgb, var(--text) 8%, transparent); }
.bl-maprail-live { box-shadow: 0 0 16px color-mix(in srgb, var(--accent) 12%, transparent); }

@media (max-width: 860px) {
  .fs-nav::after { left: 10px; right: 10px; }
  .bl-card:hover,
  .bl-lock:hover,
  .bl-note--plus:hover,
  .bl-crosslink:hover { transform: none; }
}

@media print {
  .fs-route.is-route-entering > :first-child,
  .fs-route.is-route-entering .ex-pulse > i,
  .fs-route.is-route-entering .ex-bar-track i {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}

/* Shared browser surfaces follow the same palette as the authored controls. */
:root { color-scheme: dark; accent-color: var(--accent); --selection-ink: var(--bg); }
:root[data-theme="light"] { color-scheme: light; --selection-ink: var(--surface); }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) { color-scheme: light; --selection-ink: var(--surface); }
}
::selection { background: var(--accent); color: var(--selection-ink); }
input, textarea { caret-color: var(--accent); }
input::placeholder, textarea::placeholder { color: var(--faint); opacity: 1; }
input[type="search"] { appearance: none; -webkit-appearance: none; }
input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
  width: 16px; height: 16px; cursor: pointer;
  background: var(--muted);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m6 6 12 12M6 18 18 6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m6 6 12 12M6 18 18 6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
* { scrollbar-width: thin; scrollbar-color: var(--line2) var(--surface2); }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--surface2); }
::-webkit-scrollbar-thumb { background: var(--line2); border: 2px solid var(--surface2); border-radius: 8px; }
a { text-underline-offset: .18em; }
button:disabled { cursor: not-allowed; }
select:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ex-directory-list > .ex-panel-note { grid-column: 1 / -1; }
.bl-account-identity strong, .bl-account-field, .bl-wl-name, .ex-directory-row strong { overflow-wrap: anywhere; }
.bl-facet-apply, .bl-watch-savebtn, .bl-filterall, .bl-linkbtn, .bl-wl-remove { min-height: 24px; }
.bl-account-input[aria-invalid="true"] { border-color: var(--s4); }

@media (max-width: 700px) {
  .fs-head { overflow-x: auto; }
  .fs-nav, .fs-settingschip, .fs-viewas { min-width: 44px; min-height: 44px; flex-shrink: 0; }
  .bl-facet-input, .bl-watch-input, .bl-account-input, .bl-feedsearch-in, .ex-search input, .ex-section-head input { min-height: 44px; font-size: 16px; }
  .bl-facet-apply, .bl-watch-savebtn, .bl-filterall, .bl-linkbtn, .bl-avatar-action,
  .bl-feedsearch-go, .bl-seg-b, .bl-tab, .ex-tabs button, .ex-tab, .ex-window button,
  .bl-unitaction, .bl-unitfilters button, .bl-mapchip, .bl-maptools .bl-seg-b, .bl-feedchip, .bl-btn { min-height: 44px; }
  .bl-facet-apply, .bl-watch-savebtn, .bl-wl-remove { min-width: 44px; }
  .bl-wl-remove { min-height: 44px; }
  .bl-toggle::before { content: ""; position: absolute; inset: -8px -2px; }
  .bl-facet-label, .bl-watch-note, .bl-maprail-fine, .bl-account-field, .bl-account-form p { font-size: 12px; }
  .bl-account-formhead, .bl-avatar-action { font-size: 12px; }
  .bl-card--feed .bl-titlerow { flex-wrap: wrap; }
  .bl-card--feed .bl-card-title { flex-basis: 100%; white-space: normal; overflow-wrap: anywhere; }
  .bl-feedhead { position: static; }
  .fs-foot-links { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
  .fs-foot-links a { display: inline-flex; align-items: center; min-height: 44px; padding-inline: 4px; }
}

@media (max-width: 380px) {
  .fs-head { gap: 8px; padding-inline: 8px; }
  .fs-brand { gap: 0; padding-right: 0; }
  .fs-brand-mark, .fs-brand-plus, .fs-head-spacer { display: none; }
  .fs-navgroup { gap: 0; }
  .fs-nav, .fs-settingschip { width: 44px; padding-inline: 0; justify-content: center; }
}
