/* ═══════════════════════════════════════════════════════════════════════════
   OCEANVIEW DESIGN SYSTEM  —  v1
   One stylesheet, shipped as two identical copies:
     JusticeLink/static/css/oceanview.css
     StateCivic/oceanview.css
   The two files are byte-identical on purpose. Edit one, copy it over the
   other. They are separate because the apps are separately served (Flask
   static/ vs. the Node static root) and cannot share a file on disk.

   AUTHORITY: the palette is the in-game loadscreen palette, already shipping.
   From FiveM-Main/server.cfg lines 62-65:
       loadscreen:primaryColor   "rgb(47, 111, 237)"
       loadscreen:secondaryColor "rgba(255, 255, 255, 0.12)"
       loadscreen:textColor      "rgb(236, 238, 243)"
       loadscreen:shadowColor    "rgba(4, 5, 7, 0.6)"
   Those four values appear below verbatim. Everything else is derived from
   them. If the server.cfg palette changes, change it here and nowhere else.

   STRUCTURE
     1. Tokens          — :root, dark (canonical) + light variant
     2. Base            — resets and primitives, scoped to opt-in only
     3. Components      — .ov-* classes for what these apps actually are
     4. Utilities       — the handful worth having
     5. Bridges         — remap Bootstrap 5 / StateCivic legacy var names onto
                          the tokens. Attribute-gated, inert until switched on.

   NAMING: every token and class is `--ov-` / `.ov-` prefixed. Nothing here can
   collide with Bootstrap or with StateCivic's existing law/treasury/shell CSS,
   so this file is safe to load alongside either without touching them.

   NO EXTERNAL RESOURCES. No @import, no CDN, no webfont. System stacks only —
   these are law-enforcement tools that must render identically on a locked-down
   workstation with no outbound internet.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ═══════════════════════════════════════════════════════════════════════════
   1. TOKENS
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  color-scheme: dark;

  /* ── the four authoritative values, verbatim from server.cfg ───────────── */
  --ov-brand-primary:   rgb(47, 111, 237);
  --ov-brand-secondary: rgba(255, 255, 255, 0.12);
  --ov-brand-text:      rgb(236, 238, 243);
  --ov-brand-shadow:    rgba(4, 5, 7, 0.6);

  /* rgb triplets for alpha compositing without re-typing the colour */
  --ov-brand-primary-rgb: 47, 111, 237;
  --ov-brand-shadow-rgb:  4, 5, 7;


  /* ── SURFACE RAMP ──────────────────────────────────────────────────────
     Four steps, black-first. Anything that needs a fifth step is over-nested
     and should be flattened instead. `sunk` is for wells (table bodies,
     inputs, code blocks) — the only step that goes *below* the page.        */
  --ov-surface-sunk:    #06080B;
  --ov-surface-page:    #0A0C10;
  --ov-surface-raised:  #111419;   /* cards, panels, table headers          */
  --ov-surface-overlay: #171B22;   /* modals, dropdowns, popovers           */
  --ov-surface-hover:   rgba(255, 255, 255, 0.045);
  --ov-surface-active:  rgba(255, 255, 255, 0.075);

  /* Borders. `--ov-border` IS the authoritative secondary colour.           */
  --ov-border:          rgba(255, 255, 255, 0.12);
  --ov-border-soft:     rgba(255, 255, 255, 0.07);
  --ov-border-strong:   rgba(255, 255, 255, 0.20);


  /* ── TEXT RAMP ─────────────────────────────────────────────────────────
     Measured against --ov-surface-raised (#111419), the worst realistic case
     for body copy. Every step clears 4.5:1, so any of the three is legal for
     body text in a dense table — `muted` is a de-emphasis choice, never an
     accessibility compromise.
       primary   #ECEEF3  15.9:1
       secondary #A7B0BF   8.4:1
       muted     #7B8494   4.9:1                                            */
  --ov-text-primary:    rgb(236, 238, 243);
  --ov-text-secondary:  #A7B0BF;
  --ov-text-muted:      #7B8494;
  --ov-text-on-accent:  #FFFFFF;   /* 4.55:1 on --ov-accent — do not tint   */
  --ov-text-inverse:    #0A0C10;


  /* ── ACCENT ────────────────────────────────────────────────────────────
     IMPORTANT: the brand blue #2F6FED is only 4.06:1 against a raised dark
     surface. It is a FILL colour, not a text colour. For blue *text* and
     links on dark, use --ov-accent-text (#6C9BF5, 6.7:1). This split is the
     single most common mistake when adopting this system.                  */
  --ov-accent:          rgb(47, 111, 237);
  --ov-accent-hover:    #3F7CF0;
  --ov-accent-active:   #2358C9;
  --ov-accent-text:     #6C9BF5;   /* links, blue text, focus ring          */
  --ov-accent-subtle:   rgba(47, 111, 237, 0.14);   /* tinted fills         */
  --ov-accent-line:     rgba(47, 111, 237, 0.42);   /* tinted borders       */


  /* ── STATUS ────────────────────────────────────────────────────────────
     Six buckets. Six is the ceiling — past that, hue stops being scannable
     in a 200-row table and starts being decoration.

     These are law-enforcement and government records. The failure mode to
     avoid is "everything adverse is red", which flattens a *wanted* subject
     and a *dismissed* charge into the same visual weight.

       critical     active warrant · wanted · revoked · escaped · BOLO
       caution      in custody · pending · delinquent · expiring · flagged
       positive     released · served · exonerated · cleared · paid
       info         scheduled · open · in progress · transferred · filed
       adjudicated  forfeited · convicted · sentenced — final and adverse,
                    but not an active threat. Violet because it is the fourth
                    hue that survives being read at 12px next to the others.
       neutral      dismissed · expired · closed · archived · inactive

     All six clear 6.6:1 on --ov-surface-raised. All six clear 4.5:1 again
     when used as text on their own -bg tint (worst case 5.3:1, critical).

     RULE: colour is never the only signal. A pill carries its label; a row
     highlight carries a pill; an icon-only control carries a title. Red and
     green are indistinguishable to ~8% of men — the text is what does the
     actual work, the colour only makes it findable faster.                 */
  --ov-status-critical:    #F87171;
  --ov-status-caution:     #F5A524;
  --ov-status-positive:    #34D399;
  --ov-status-info:        #6C9BF5;
  --ov-status-adjudicated: #B08CF8;
  --ov-status-neutral:     #A7B0BF;

  /* Tinted fills. The rgba line is the fallback; the color-mix line wins
     wherever color-mix is supported (Chrome 111+/Safari 16.2+/Firefox 113+ —
     StateCivic already depends on it) and keeps the tint locked to whatever
     surface the theme is currently on. */
  --ov-status-critical-bg:    rgba(248, 113, 113, 0.14);
  --ov-status-caution-bg:     rgba(245, 165,  36, 0.14);
  --ov-status-positive-bg:    rgba( 52, 211, 153, 0.14);
  --ov-status-info-bg:        rgba(108, 155, 245, 0.14);
  --ov-status-adjudicated-bg: rgba(176, 140, 248, 0.14);
  --ov-status-neutral-bg:     rgba(167, 176, 191, 0.12);
  --ov-status-critical-bg:    color-mix(in srgb, var(--ov-status-critical)    16%, var(--ov-surface-raised));
  --ov-status-caution-bg:     color-mix(in srgb, var(--ov-status-caution)     16%, var(--ov-surface-raised));
  --ov-status-positive-bg:    color-mix(in srgb, var(--ov-status-positive)    16%, var(--ov-surface-raised));
  --ov-status-info-bg:        color-mix(in srgb, var(--ov-status-info)        16%, var(--ov-surface-raised));
  --ov-status-adjudicated-bg: color-mix(in srgb, var(--ov-status-adjudicated) 16%, var(--ov-surface-raised));
  --ov-status-neutral-bg:     color-mix(in srgb, var(--ov-status-neutral)     14%, var(--ov-surface-raised));

  /* Halo for the one alarm animation. Tokenised rather than hardcoded so the
     light theme's darker red does not glow the wrong colour. */
  --ov-alarm-halo: rgba(248, 113, 113, 0.35);


  /* ── TYPE ──────────────────────────────────────────────────────────────
     System stack only. `ui-sans-serif` and `system-ui` get the native UI face
     on every modern OS; Segoe UI Variable is named explicitly because these
     apps are used almost entirely on Windows 11 workstations and it is a
     visibly better face than the Segoe UI fallback.                        */
  --ov-font-sans: ui-sans-serif, system-ui, "Segoe UI Variable Text", "Segoe UI",
                  -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue",
                  Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
                  "Segoe UI Emoji";
  --ov-font-mono: ui-monospace, "Cascadia Mono", "Cascadia Code", Consolas,
                  "SF Mono", "Roboto Mono", "DejaVu Sans Mono", Menlo, monospace;

  /* Scale is dense-data-first: `md` (14px) is the body size, not 16px.
     A booking sheet with 40 fields and a 200-row docket table is unusable at
     16px, and 14px system-ui at 15.9:1 contrast is comfortably readable. */
  --ov-text-2xs: 0.6875rem;  /* 11px — table micro-labels, timestamps       */
  --ov-text-xs:  0.75rem;    /* 12px — pills, captions, help text           */
  --ov-text-sm:  0.8125rem;  /* 13px — table cells, secondary UI            */
  --ov-text-md:  0.875rem;   /* 14px — BODY DEFAULT                         */
  --ov-text-lg:  1rem;       /* 16px — lead paragraphs, card titles         */
  --ov-text-xl:  1.25rem;    /* 20px — section headings                     */
  --ov-text-2xl: 1.625rem;   /* 26px — page titles                          */
  --ov-text-3xl: 2.25rem;    /* 36px — hero figures (treasury balance)      */

  --ov-leading-tight:   1.2;
  --ov-leading-snug:    1.4;
  --ov-leading-normal:  1.55;
  --ov-leading-relaxed: 1.7;

  --ov-weight-normal:   400;
  --ov-weight-medium:   500;
  --ov-weight-semibold: 600;
  --ov-weight-bold:     700;

  --ov-tracking-caps:  0.08em;   /* the ONLY place uppercase is allowed     */
  --ov-tracking-tight: -0.01em;  /* 2xl and up, optical correction          */


  /* ── SPACING ───────────────────────────────────────────────────────────
     4px base. Named by step, not by px, so a density change is one edit.   */
  --ov-space-1:  0.25rem;   /*  4px */
  --ov-space-2:  0.5rem;    /*  8px */
  --ov-space-3:  0.75rem;   /* 12px */
  --ov-space-4:  1rem;      /* 16px */
  --ov-space-5:  1.25rem;   /* 20px */
  --ov-space-6:  1.5rem;    /* 24px */
  --ov-space-8:  2rem;      /* 32px */
  --ov-space-10: 2.5rem;    /* 40px */
  --ov-space-12: 3rem;      /* 48px */
  --ov-space-16: 4rem;      /* 64px */


  /* ── RADIUS ────────────────────────────────────────────────────────────
     Restrained. Nothing here is playful; a warrant record is not a widget.  */
  --ov-radius-xs:   3px;
  --ov-radius-sm:   5px;
  --ov-radius-md:   8px;
  --ov-radius-lg:   12px;
  --ov-radius-xl:   16px;
  --ov-radius-pill: 999px;


  /* ── ELEVATION ─────────────────────────────────────────────────────────
     All three derived from the authoritative shadow rgba(4,5,7,0.6): level 3
     uses it at full strength, 1 and 2 step the alpha down.                 */
  --ov-shadow-1: 0 1px 2px rgba(var(--ov-brand-shadow-rgb), 0.40);
  --ov-shadow-2: 0 4px 12px rgba(var(--ov-brand-shadow-rgb), 0.45),
                 0 1px 3px  rgba(var(--ov-brand-shadow-rgb), 0.35);
  --ov-shadow-3: 0 16px 40px var(--ov-brand-shadow),
                 0 4px 12px  rgba(var(--ov-brand-shadow-rgb), 0.40);
  --ov-shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.04);
  --ov-scrim: rgba(var(--ov-brand-shadow-rgb), 0.72);


  /* ── FOCUS ─────────────────────────────────────────────────────────────
     Two-step ring: a surface-coloured gap, then the accent. The gap is what
     makes it visible on top of a pill, a table row, or a coloured button
     without needing a per-context override.                                */
  --ov-focus-color: var(--ov-accent-text);
  --ov-focus-ring:  0 0 0 2px var(--ov-surface-page),
                    0 0 0 4px var(--ov-focus-color);
  --ov-focus-ring-inset: inset 0 0 0 1px var(--ov-focus-color),
                         0 0 0 3px var(--ov-accent-subtle);


  /* ── MOTION ────────────────────────────────────────────────────────────
     Short and non-negotiable. Nothing in these apps should ever animate for
     longer than it takes to notice it happened.                            */
  --ov-duration-fast: 110ms;
  --ov-duration:      170ms;
  --ov-ease:          cubic-bezier(0.4, 0, 0.2, 1);


  /* ── LAYERS ────────────────────────────────────────────────────────────  */
  --ov-z-sticky:   100;
  --ov-z-dropdown: 200;
  --ov-z-scrim:    300;
  --ov-z-modal:    310;
  --ov-z-toast:    400;
}


/* ── LIGHT VARIANT ────────────────────────────────────────────────────────
   Dark is canonical — it is what the game looks like. Light exists because
   StateCivic already ships a working theme toggle (`html[data-theme]`) and
   removing it would be a regression, and because printed/daylight review of a
   docket is a real thing people do.

   Only the ramps invert. The accent stays the same blue and the six status
   buckets keep the same hues at darker values, so a pill means the same thing
   in both themes. Every value below clears 4.5:1 on white.

   Both selector forms are honoured: `data-theme` is what StateCivic's toggle
   already writes, `data-ov-theme` is for any app that adopts this system
   without inheriting that convention. Specificity (0,2,0) beats the plain
   :root block above regardless of source order.                            */
:root[data-theme="light"],
:root[data-ov-theme="light"] {
  color-scheme: light;

  --ov-surface-sunk:    #EDEFF3;
  --ov-surface-page:    #F5F6F8;
  --ov-surface-raised:  #FFFFFF;
  --ov-surface-overlay: #FFFFFF;
  --ov-surface-hover:   rgba(9, 12, 18, 0.04);
  --ov-surface-active:  rgba(9, 12, 18, 0.07);

  --ov-border:          rgba(9, 12, 18, 0.13);
  --ov-border-soft:     rgba(9, 12, 18, 0.08);
  --ov-border-strong:   rgba(9, 12, 18, 0.22);

  --ov-text-primary:    #0D1117;   /* 18.6:1 on white */
  --ov-text-secondary:  #414A59;   /*  9.4:1 on white */
  --ov-text-muted:      #5C6675;   /*  5.8:1 on white */
  --ov-text-inverse:    #FFFFFF;

  /* Brand blue clears 4.55:1 on white, so it is legal as light-mode text —
     but accent-text still steps darker for headroom at 11-13px. */
  --ov-accent-text:     #1D5AD4;   /*  6.1:1 on white */
  --ov-accent-subtle:   rgba(47, 111, 237, 0.10);
  --ov-accent-line:     rgba(47, 111, 237, 0.38);

  --ov-status-critical:    #C0322F;   /* 5.6:1 */
  /* Darker than it looks like it needs to be: at #9A6510 the pill's own tinted
     background dragged this to 4.29:1, under the floor. Judge the status
     colours against their -bg tint, not against the page. */
  --ov-status-caution:     #8A5A0D;   /* 5.9:1 on white, 5.1:1 on own tint */
  --ov-status-positive:    #0F7B4F;   /* 5.3:1 */
  --ov-status-info:        #1D5AD4;   /* 6.1:1 */
  --ov-status-adjudicated: #6A3CC0;   /* 7.0:1 */
  --ov-status-neutral:     #4A5566;   /* 7.6:1 */

  --ov-status-critical-bg:    rgba(192,  50,  47, 0.10);
  --ov-status-caution-bg:     rgba(138,  90,  13, 0.10);
  --ov-status-positive-bg:    rgba( 15, 123,  79, 0.10);
  --ov-status-info-bg:        rgba( 29,  90, 212, 0.10);
  --ov-status-adjudicated-bg: rgba(106,  60, 192, 0.10);
  --ov-status-neutral-bg:     rgba( 74,  85, 102, 0.09);
  --ov-status-critical-bg:    color-mix(in srgb, var(--ov-status-critical)    11%, var(--ov-surface-raised));
  --ov-status-caution-bg:     color-mix(in srgb, var(--ov-status-caution)     11%, var(--ov-surface-raised));
  --ov-status-positive-bg:    color-mix(in srgb, var(--ov-status-positive)    11%, var(--ov-surface-raised));
  --ov-status-info-bg:        color-mix(in srgb, var(--ov-status-info)        11%, var(--ov-surface-raised));
  --ov-status-adjudicated-bg: color-mix(in srgb, var(--ov-status-adjudicated) 11%, var(--ov-surface-raised));
  --ov-status-neutral-bg:     color-mix(in srgb, var(--ov-status-neutral)     10%, var(--ov-surface-raised));

  --ov-alarm-halo: rgba(192, 50, 47, 0.35);

  --ov-shadow-1: 0 1px 2px rgba(var(--ov-brand-shadow-rgb), 0.08);
  --ov-shadow-2: 0 4px 12px rgba(var(--ov-brand-shadow-rgb), 0.10),
                 0 1px 3px  rgba(var(--ov-brand-shadow-rgb), 0.07);
  --ov-shadow-3: 0 16px 40px rgba(var(--ov-brand-shadow-rgb), 0.16),
                 0 4px 12px  rgba(var(--ov-brand-shadow-rgb), 0.10);
  --ov-shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.6);
  --ov-scrim: rgba(var(--ov-brand-shadow-rgb), 0.45);
}


/* ═══════════════════════════════════════════════════════════════════════════
   2. BASE
   Deliberately narrow. This file is loaded on top of two apps that already
   have their own base styles (Bootstrap in JusticeLink, law.styles.css in
   StateCivic), so unscoped element selectors here would fight them.

   Everything below is either (a) scoped to `.ov-root`, which an app opts into
   by putting the class on <body>, or (b) a :focus-visible rule, which is a
   pure accessibility improvement worth applying everywhere.
   ═══════════════════════════════════════════════════════════════════════════ */

.ov-root {
  background: var(--ov-surface-page);
  color: var(--ov-text-primary);
  font-family: var(--ov-font-sans);
  font-size: var(--ov-text-md);
  line-height: var(--ov-leading-normal);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Every figure in these apps is a case number, a dollar amount, an SSN or a
     date. Tabular figures stop columns from shimmering as data updates. */
  font-variant-numeric: tabular-nums;
}

.ov-root *,
.ov-root *::before,
.ov-root *::after { box-sizing: border-box; }

.ov-root a { color: var(--ov-accent-text); text-decoration: none; }
.ov-root a:hover { text-decoration: underline; text-underline-offset: 0.2em; }

.ov-root ::selection {
  background: var(--ov-accent-subtle);
  color: var(--ov-text-primary);
}

.ov-root ::placeholder { color: var(--ov-text-muted); opacity: 1; }

/* Applied globally, not scoped: a missing focus ring is a defect in any app,
   and :focus-visible only fires for keyboard/AT users so it costs mouse users
   nothing. Kept at low specificity so a component can still override it. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: none;
  box-shadow: var(--ov-focus-ring);
  border-radius: var(--ov-radius-sm);
}

@media (prefers-reduced-motion: reduce) {
  .ov-root *,
  .ov-root *::before,
  .ov-root *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   3. COMPONENTS
   Scoped to what these apps demonstrably are, from reading the templates:
   dense tables, status pills, stat cards, long forms, modals, empty states.
   Nothing speculative.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ───────────────────────── PAGE SCAFFOLD ──────────────────────────────── */

.ov-page {
  max-width: 1440px;
  margin: 0 auto;
  padding: var(--ov-space-6) var(--ov-space-6) var(--ov-space-16);
}

.ov-page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--ov-space-4);
  flex-wrap: wrap;
  padding-bottom: var(--ov-space-4);
  margin-bottom: var(--ov-space-6);
  border-bottom: 1px solid var(--ov-border);
}

.ov-eyebrow {
  font-size: var(--ov-text-2xs);
  font-weight: var(--ov-weight-bold);
  letter-spacing: var(--ov-tracking-caps);
  text-transform: uppercase;
  color: var(--ov-text-muted);
  margin-bottom: var(--ov-space-1);
}

.ov-title {
  font-size: var(--ov-text-2xl);
  font-weight: var(--ov-weight-semibold);
  line-height: var(--ov-leading-tight);
  letter-spacing: var(--ov-tracking-tight);
  color: var(--ov-text-primary);
  margin: 0;
}

.ov-subtitle {
  font-size: var(--ov-text-md);
  color: var(--ov-text-secondary);
  margin: var(--ov-space-2) 0 0;
  max-width: 68ch;
}

.ov-section-title {
  font-size: var(--ov-text-2xs);
  font-weight: var(--ov-weight-bold);
  letter-spacing: var(--ov-tracking-caps);
  text-transform: uppercase;
  color: var(--ov-text-muted);
  margin: var(--ov-space-8) 0 var(--ov-space-3);
}
.ov-section-title:first-child { margin-top: 0; }

.ov-toolbar {
  display: flex;
  align-items: center;
  gap: var(--ov-space-2);
  flex-wrap: wrap;
  margin-bottom: var(--ov-space-4);
}
.ov-toolbar-spacer { margin-left: auto; }


/* ───────────────────────── NAV ────────────────────────────────────────── */

.ov-nav {
  position: sticky;
  top: 0;
  z-index: var(--ov-z-sticky);
  display: flex;
  align-items: center;
  gap: var(--ov-space-2);
  height: 56px;
  padding: 0 var(--ov-space-5);
  background: var(--ov-surface-sunk);
  border-bottom: 1px solid var(--ov-border);
}

.ov-nav-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--ov-space-2);
  font-size: var(--ov-text-md);
  font-weight: var(--ov-weight-semibold);
  letter-spacing: var(--ov-tracking-tight);
  color: var(--ov-text-primary);
  margin-right: var(--ov-space-4);
  white-space: nowrap;
}
.ov-nav-brand:hover { text-decoration: none; color: var(--ov-text-primary); }
/* The one place the raw brand blue is allowed as a foreground: a mark/seal is
   a graphic, not text, so the 4.5:1 body-text floor does not apply. */
.ov-nav-brand .ov-nav-mark { color: var(--ov-accent); display: inline-flex; }

.ov-nav-links { display: flex; align-items: center; gap: var(--ov-space-1); }

.ov-nav-link {
  position: relative;
  padding: var(--ov-space-2) var(--ov-space-3);
  border-radius: var(--ov-radius-sm);
  font-size: var(--ov-text-sm);
  font-weight: var(--ov-weight-medium);
  color: var(--ov-text-secondary);
  white-space: nowrap;
  transition: background var(--ov-duration-fast) var(--ov-ease),
              color var(--ov-duration-fast) var(--ov-ease);
}
.ov-nav-link:hover {
  background: var(--ov-surface-hover);
  color: var(--ov-text-primary);
  text-decoration: none;
}
.ov-nav-link.is-active {
  color: var(--ov-text-primary);
  background: var(--ov-accent-subtle);
}
/* Active state is carried by BOTH a tint and an underline rule — a tint alone
   is too quiet on a black nav at a glance. */
.ov-nav-link.is-active::after {
  content: "";
  position: absolute;
  left: var(--ov-space-3);
  right: var(--ov-space-3);
  bottom: -1px;
  height: 2px;
  background: var(--ov-accent);
  border-radius: 2px 2px 0 0;
}
.ov-nav-link.is-disabled {
  color: var(--ov-text-muted);
  opacity: 0.55;
  pointer-events: none;
}


/* ───────────────────────── CARD ───────────────────────────────────────── */

.ov-card {
  background: var(--ov-surface-raised);
  border: 1px solid var(--ov-border);
  border-radius: var(--ov-radius-lg);
  box-shadow: var(--ov-shadow-1);
  overflow: hidden;
}

/* Deliberately NOT a hover-lift. Cards in these apps are records, not links;
   a card that lifts implies it is clickable. Add .ov-card--link only where it
   genuinely navigates. */
.ov-card--link {
  display: block;
  transition: border-color var(--ov-duration) var(--ov-ease),
              box-shadow var(--ov-duration) var(--ov-ease);
}
.ov-card--link:hover {
  border-color: var(--ov-border-strong);
  box-shadow: var(--ov-shadow-2);
  text-decoration: none;
}

.ov-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ov-space-3);
  padding: var(--ov-space-3) var(--ov-space-4);
  border-bottom: 1px solid var(--ov-border-soft);
  background: var(--ov-surface-hover);
}
.ov-card-title {
  font-size: var(--ov-text-sm);
  font-weight: var(--ov-weight-semibold);
  color: var(--ov-text-primary);
  margin: 0;
}
.ov-card-body { padding: var(--ov-space-4); }
.ov-card-body > :first-child { margin-top: 0; }
.ov-card-body > :last-child { margin-bottom: 0; }
.ov-card-foot {
  padding: var(--ov-space-3) var(--ov-space-4);
  border-top: 1px solid var(--ov-border-soft);
  background: var(--ov-surface-sunk);
  font-size: var(--ov-text-xs);
  color: var(--ov-text-muted);
}

/* Accent rail — the cheapest way to type a card (agency, severity, outcome)
   without spending a fill. Set --ov-rail on the element. */
.ov-card--rail { border-left: 3px solid var(--ov-rail, var(--ov-accent)); }


/* ───────────────────────── STAT CARD ──────────────────────────────────── */

.ov-stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--ov-space-3);
  margin-bottom: var(--ov-space-6);
}

.ov-stat {
  background: var(--ov-surface-raised);
  border: 1px solid var(--ov-border);
  border-radius: var(--ov-radius-lg);
  padding: var(--ov-space-4);
  box-shadow: var(--ov-shadow-1);
}
.ov-stat-label {
  font-size: var(--ov-text-2xs);
  font-weight: var(--ov-weight-bold);
  letter-spacing: var(--ov-tracking-caps);
  text-transform: uppercase;
  color: var(--ov-text-muted);
}
.ov-stat-value {
  font-size: var(--ov-text-2xl);
  font-weight: var(--ov-weight-semibold);
  line-height: var(--ov-leading-tight);
  letter-spacing: var(--ov-tracking-tight);
  color: var(--ov-text-primary);
  margin-top: var(--ov-space-2);
  font-variant-numeric: tabular-nums;
}
.ov-stat-meta {
  font-size: var(--ov-text-xs);
  color: var(--ov-text-muted);
  margin-top: var(--ov-space-1);
}
/* A stat is typed by rail + label text, never by colouring the number itself —
   a red figure reads as "error" rather than "this is the delinquent count". */
.ov-stat--critical    { border-left: 3px solid var(--ov-status-critical); }
.ov-stat--caution     { border-left: 3px solid var(--ov-status-caution); }
.ov-stat--positive    { border-left: 3px solid var(--ov-status-positive); }
.ov-stat--info        { border-left: 3px solid var(--ov-status-info); }
.ov-stat--adjudicated { border-left: 3px solid var(--ov-status-adjudicated); }

/* Hero figure — the treasury balance, the inmate headcount. One per page. */
.ov-hero {
  background:
    radial-gradient(120% 180% at 0% 0%, var(--ov-accent-subtle) 0%, transparent 62%),
    var(--ov-surface-raised);
  border: 1px solid var(--ov-border);
  border-radius: var(--ov-radius-xl);
  padding: var(--ov-space-6);
  box-shadow: var(--ov-shadow-2);
  margin-bottom: var(--ov-space-6);
}
.ov-hero-label {
  font-size: var(--ov-text-2xs);
  font-weight: var(--ov-weight-bold);
  letter-spacing: var(--ov-tracking-caps);
  text-transform: uppercase;
  color: var(--ov-accent-text);
}
.ov-hero-value {
  font-size: var(--ov-text-3xl);
  font-weight: var(--ov-weight-bold);
  line-height: var(--ov-leading-tight);
  letter-spacing: var(--ov-tracking-tight);
  color: var(--ov-text-primary);
  margin: var(--ov-space-2) 0 var(--ov-space-1);
  font-variant-numeric: tabular-nums;
}
.ov-hero-meta {
  font-family: var(--ov-font-mono);
  font-size: var(--ov-text-xs);
  color: var(--ov-text-muted);
}


/* ───────────────────────── STATUS PILL ────────────────────────────────── */

/* A pill ALWAYS contains its label. The dot is a redundant second channel for
   fast scanning, not a replacement for the word. Never ship a bare dot. */
.ov-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--ov-space-2);
  padding: 2px var(--ov-space-2);
  border-radius: var(--ov-radius-pill);
  border: 1px solid transparent;
  font-size: var(--ov-text-xs);
  font-weight: var(--ov-weight-semibold);
  line-height: 1.5;
  white-space: nowrap;
  vertical-align: baseline;
  background: var(--ov-status-neutral-bg);
  color: var(--ov-status-neutral);
  border-color: var(--ov-status-neutral);
}
.ov-pill::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}
.ov-pill--nodot::before { display: none; }

.ov-pill--critical {
  background: var(--ov-status-critical-bg);
  color: var(--ov-status-critical);
  border-color: var(--ov-status-critical);
}
.ov-pill--caution {
  background: var(--ov-status-caution-bg);
  color: var(--ov-status-caution);
  border-color: var(--ov-status-caution);
}
.ov-pill--positive {
  background: var(--ov-status-positive-bg);
  color: var(--ov-status-positive);
  border-color: var(--ov-status-positive);
}
.ov-pill--info {
  background: var(--ov-status-info-bg);
  color: var(--ov-status-info);
  border-color: var(--ov-status-info);
}
.ov-pill--adjudicated {
  background: var(--ov-status-adjudicated-bg);
  color: var(--ov-status-adjudicated);
  border-color: var(--ov-status-adjudicated);
}
.ov-pill--neutral {
  background: var(--ov-status-neutral-bg);
  color: var(--ov-status-neutral);
  border-color: var(--ov-status-neutral);
}

/* Borders sit at full hue, which is loud in a 200-row table. --ghost drops the
   border and keeps the tint; use it inside table cells. */
.ov-pill--ghost { border-color: transparent; }

/* Active warrant is the one state in either app that must interrupt a scan.
   It gets the only animation in this system, and it is disabled entirely under
   prefers-reduced-motion by the base rule above. */
.ov-pill--alarm {
  background: var(--ov-status-critical-bg);
  color: var(--ov-status-critical);
  border-color: var(--ov-status-critical);
  animation: ov-pulse 2.4s var(--ov-ease) infinite;
}
@keyframes ov-pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--ov-alarm-halo); }
  50%      { box-shadow: 0 0 0 4px transparent; }
}

/* Mono chip for identifiers: case numbers, SSNs, booking numbers, statute
   codes. These are the join keys across all three systems and deserve to be
   visually distinct from prose. */
.ov-code {
  display: inline-block;
  font-family: var(--ov-font-mono);
  font-size: var(--ov-text-xs);
  font-weight: var(--ov-weight-medium);
  letter-spacing: 0.01em;
  padding: 1px var(--ov-space-2);
  border-radius: var(--ov-radius-xs);
  background: var(--ov-surface-sunk);
  border: 1px solid var(--ov-border-soft);
  color: var(--ov-text-secondary);
  white-space: nowrap;
}
.ov-code--accent { color: var(--ov-accent-text); border-color: var(--ov-accent-line); }


/* ───────────────────────── DATA TABLE ─────────────────────────────────── */

/* Wrapper is required, not optional: it owns the border, the radius and the
   horizontal scroll. A wide docket table must scroll inside its own box —
   the page body must never scroll sideways. */
.ov-table-wrap {
  border: 1px solid var(--ov-border);
  border-radius: var(--ov-radius-lg);
  background: var(--ov-surface-raised);
  overflow-x: auto;
  overflow-y: visible;
  box-shadow: var(--ov-shadow-1);
  -webkit-overflow-scrolling: touch;
}
/* Vertical scroll only when a max-height is set on the wrapper; sticky headers
   need a scroll container to stick to. */
.ov-table-wrap--scroll { overflow-y: auto; max-height: var(--ov-table-height, 70vh); }

.ov-table {
  width: 100%;
  border-collapse: separate;   /* separate, not collapse — collapse drops the
                                  header's bottom border when it goes sticky */
  border-spacing: 0;
  font-size: var(--ov-text-sm);
  font-variant-numeric: tabular-nums;
}

.ov-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  text-align: left;
  padding: var(--ov-space-2) var(--ov-space-3);
  background: var(--ov-surface-sunk);
  border-bottom: 1px solid var(--ov-border);
  font-size: var(--ov-text-2xs);
  font-weight: var(--ov-weight-bold);
  letter-spacing: var(--ov-tracking-caps);
  text-transform: uppercase;
  color: var(--ov-text-muted);
  white-space: nowrap;
}

.ov-table tbody td {
  padding: var(--ov-space-2) var(--ov-space-3);
  border-bottom: 1px solid var(--ov-border-soft);
  color: var(--ov-text-secondary);
  vertical-align: middle;
}
.ov-table tbody tr:last-child td { border-bottom: none; }
.ov-table tbody tr:hover td { background: var(--ov-surface-hover); }

/* Zebra striping is opt-in. On a black surface it is usually noise; it earns
   its place only past ~8 columns, where the eye needs help tracking a row. */
.ov-table--zebra tbody tr:nth-child(even) td { background: rgba(255, 255, 255, 0.022); }
.ov-table--zebra tbody tr:hover td { background: var(--ov-surface-hover); }

/* Comfortable / compact density. Default sits between them. */
.ov-table--compact thead th,
.ov-table--compact tbody td { padding: var(--ov-space-1) var(--ov-space-2); }
.ov-table--roomy thead th,
.ov-table--roomy tbody td { padding: var(--ov-space-3) var(--ov-space-4); }

/* The primary identifying cell in a row — name, case number, statute. */
.ov-table .ov-cell-key {
  color: var(--ov-text-primary);
  font-weight: var(--ov-weight-medium);
}
/* Money, counts, days. Right-aligned and mono so digits line up on the decimal. */
.ov-table .ov-cell-num,
.ov-table th.ov-cell-num {
  text-align: right;
  font-family: var(--ov-font-mono);
  white-space: nowrap;
}
.ov-table .ov-cell-meta {
  color: var(--ov-text-muted);
  font-size: var(--ov-text-xs);
  white-space: nowrap;
}
.ov-table .ov-cell-actions { text-align: right; white-space: nowrap; width: 1%; }

/* Row-level state. Carried by a left rule, never a full-row fill: a filled row
   destroys the contrast the text ramp was tuned for. */
.ov-table tbody tr.ov-row--critical    td:first-child { box-shadow: inset 3px 0 0 var(--ov-status-critical); }
.ov-table tbody tr.ov-row--caution     td:first-child { box-shadow: inset 3px 0 0 var(--ov-status-caution); }
.ov-table tbody tr.ov-row--positive    td:first-child { box-shadow: inset 3px 0 0 var(--ov-status-positive); }
.ov-table tbody tr.ov-row--info        td:first-child { box-shadow: inset 3px 0 0 var(--ov-status-info); }
.ov-table tbody tr.ov-row--adjudicated td:first-child { box-shadow: inset 3px 0 0 var(--ov-status-adjudicated); }
/* Closed/expired/dismissed records: still legible (muted clears 4.5:1), just
   demoted. Opacity is NOT used here — it would push text below the floor. */
.ov-table tbody tr.ov-row--inactive td { color: var(--ov-text-muted); }


/* ───────────────────────── KEY / VALUE ────────────────────────────────── */

/* The record-detail pattern: view_warrant, view_bond_details, view_docket all
   render long lists of label→value pairs. */
.ov-kv {
  display: grid;
  grid-template-columns: minmax(120px, max-content) 1fr;
  gap: var(--ov-space-2) var(--ov-space-4);
  margin: 0;
  font-size: var(--ov-text-sm);
}
.ov-kv dt {
  font-size: var(--ov-text-2xs);
  font-weight: var(--ov-weight-bold);
  letter-spacing: var(--ov-tracking-caps);
  text-transform: uppercase;
  color: var(--ov-text-muted);
  padding-top: 2px;
}
.ov-kv dd { margin: 0; color: var(--ov-text-primary); min-width: 0; word-break: break-word; }
/* A value the source system did not return. Say so — never render an empty
   cell that reads as "zero" or "none". */
.ov-kv dd.ov-unavailable,
.ov-unavailable {
  color: var(--ov-text-muted);
  font-style: italic;
}


/* ───────────────────────── FORM CONTROLS ──────────────────────────────── */

.ov-form { display: flex; flex-direction: column; gap: var(--ov-space-4); }

.ov-field { display: flex; flex-direction: column; gap: var(--ov-space-2); min-width: 0; }

.ov-field-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--ov-space-4);
}

.ov-label {
  font-size: var(--ov-text-xs);
  font-weight: var(--ov-weight-semibold);
  color: var(--ov-text-secondary);
}
/* Required is marked with a glyph AND the field is `required` for AT —
   colour on the asterisk is decoration on top of both. */
.ov-label .ov-req { color: var(--ov-status-critical); margin-left: 2px; }
.ov-label .ov-optional {
  color: var(--ov-text-muted);
  font-weight: var(--ov-weight-normal);
  margin-left: var(--ov-space-1);
}

.ov-input,
.ov-select,
.ov-textarea {
  width: 100%;
  font-family: inherit;
  font-size: var(--ov-text-md);
  line-height: var(--ov-leading-snug);
  color: var(--ov-text-primary);
  background: var(--ov-surface-sunk);
  border: 1px solid var(--ov-border);
  border-radius: var(--ov-radius-md);
  padding: var(--ov-space-2) var(--ov-space-3);
  transition: border-color var(--ov-duration-fast) var(--ov-ease),
              box-shadow var(--ov-duration-fast) var(--ov-ease);
  appearance: none;
}
.ov-textarea { resize: vertical; min-height: 88px; line-height: var(--ov-leading-normal); }

/* Native select needs its arrow back once appearance is stripped. Inline SVG
   data URI — no external asset, no icon font. Colour is baked to
   --ov-text-muted's dark value; the light-theme override follows. */
.ov-select {
  padding-right: var(--ov-space-8);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%237B8494' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--ov-space-3) center;
  background-size: 16px 16px;
}
:root[data-theme="light"] .ov-select,
:root[data-ov-theme="light"] .ov-select {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%235C6675' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
}

.ov-input:hover,
.ov-select:hover,
.ov-textarea:hover { border-color: var(--ov-border-strong); }

.ov-input:focus,
.ov-select:focus,
.ov-textarea:focus {
  outline: none;
  border-color: var(--ov-accent);
  box-shadow: 0 0 0 3px var(--ov-accent-subtle);
}

.ov-input:disabled,
.ov-select:disabled,
.ov-textarea:disabled {
  background: var(--ov-surface-raised);
  color: var(--ov-text-muted);
  cursor: not-allowed;
}

.ov-input[readonly] { background: var(--ov-surface-raised); color: var(--ov-text-secondary); }

.ov-input--sm, .ov-select--sm {
  font-size: var(--ov-text-sm);
  padding: var(--ov-space-1) var(--ov-space-2);
  border-radius: var(--ov-radius-sm);
}
.ov-input--mono { font-family: var(--ov-font-mono); }

.ov-hint { font-size: var(--ov-text-xs); color: var(--ov-text-muted); }

/* Invalid state is border + icon-free text message, both required. A red
   border with no message is not an error state, it is a puzzle. */
.ov-input.is-invalid,
.ov-select.is-invalid,
.ov-textarea.is-invalid { border-color: var(--ov-status-critical); }
.ov-input.is-invalid:focus,
.ov-select.is-invalid:focus,
.ov-textarea.is-invalid:focus {
  box-shadow: 0 0 0 3px var(--ov-status-critical-bg);
}
.ov-error {
  font-size: var(--ov-text-xs);
  font-weight: var(--ov-weight-medium);
  color: var(--ov-status-critical);
}

.ov-check {
  display: inline-flex;
  align-items: center;
  gap: var(--ov-space-2);
  font-size: var(--ov-text-sm);
  color: var(--ov-text-secondary);
  cursor: pointer;
}
.ov-check input[type="checkbox"],
.ov-check input[type="radio"] {
  width: 16px;
  height: 16px;
  accent-color: var(--ov-accent);
  cursor: pointer;
  flex: 0 0 auto;
}

/* Segmented control — StateCivic's time-range selector, JusticeLink's list
   filters. Cheaper than a select for 2-5 mutually exclusive options. */
.ov-segment {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--ov-surface-sunk);
  border: 1px solid var(--ov-border);
  border-radius: var(--ov-radius-md);
}
.ov-segment-item {
  font-family: inherit;
  font-size: var(--ov-text-sm);
  font-weight: var(--ov-weight-medium);
  color: var(--ov-text-muted);
  background: none;
  border: none;
  border-radius: var(--ov-radius-sm);
  padding: var(--ov-space-1) var(--ov-space-3);
  cursor: pointer;
  transition: background var(--ov-duration-fast) var(--ov-ease),
              color var(--ov-duration-fast) var(--ov-ease);
}
.ov-segment-item:hover { color: var(--ov-text-primary); background: var(--ov-surface-hover); }
.ov-segment-item.is-active {
  background: var(--ov-surface-raised);
  color: var(--ov-text-primary);
  box-shadow: var(--ov-shadow-1);
}


/* ───────────────────────── BUTTONS ────────────────────────────────────── */

.ov-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ov-space-2);
  font-family: inherit;
  font-size: var(--ov-text-sm);
  font-weight: var(--ov-weight-semibold);
  line-height: 1.4;
  padding: var(--ov-space-2) var(--ov-space-4);
  border-radius: var(--ov-radius-md);
  border: 1px solid var(--ov-border);
  background: var(--ov-surface-raised);
  color: var(--ov-text-primary);
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: background var(--ov-duration-fast) var(--ov-ease),
              border-color var(--ov-duration-fast) var(--ov-ease),
              color var(--ov-duration-fast) var(--ov-ease);
}
.ov-btn:hover { background: var(--ov-surface-overlay); border-color: var(--ov-border-strong); text-decoration: none; }
.ov-btn:active { background: var(--ov-surface-active); }
.ov-btn:disabled,
.ov-btn.is-disabled {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* One primary action per view. If a screen has two, one of them is secondary. */
.ov-btn--primary {
  background: var(--ov-accent);
  border-color: var(--ov-accent);
  color: var(--ov-text-on-accent);
}
.ov-btn--primary:hover { background: var(--ov-accent-hover); border-color: var(--ov-accent-hover); }
.ov-btn--primary:active { background: var(--ov-accent-active); border-color: var(--ov-accent-active); }

.ov-btn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--ov-text-secondary);
}
.ov-btn--ghost:hover { background: var(--ov-surface-hover); border-color: transparent; color: var(--ov-text-primary); }

.ov-btn--subtle {
  background: var(--ov-accent-subtle);
  border-color: var(--ov-accent-line);
  color: var(--ov-accent-text);
}
.ov-btn--subtle:hover { background: var(--ov-accent-subtle); border-color: var(--ov-accent); }

/* Destructive: outlined by default. A solid red button is too easy to hit by
   muscle memory, and these actions release inmates and void bonds. */
.ov-btn--danger {
  background: transparent;
  border-color: var(--ov-status-critical);
  color: var(--ov-status-critical);
}
.ov-btn--danger:hover { background: var(--ov-status-critical-bg); }
/* Solid destructive — confirm-in-a-modal only, never a list-row action.
   Foreground is the page surface, not a hardcoded near-black: that keeps the
   label legible against BOTH the light red of the dark theme (6.9:1) and the
   dark red of the light theme (5.1:1). */
.ov-btn--danger-solid {
  background: var(--ov-status-critical);
  border-color: var(--ov-status-critical);
  color: var(--ov-surface-page);
}

.ov-btn--sm {
  font-size: var(--ov-text-xs);
  padding: var(--ov-space-1) var(--ov-space-2);
  border-radius: var(--ov-radius-sm);
  gap: var(--ov-space-1);
}
.ov-btn--lg { font-size: var(--ov-text-md); padding: var(--ov-space-3) var(--ov-space-5); }
.ov-btn--block { width: 100%; }
/* Icon-only buttons MUST carry aria-label + title. The square is sized to the
   44px-ish touch floor at the default step. */
.ov-btn--icon { padding: var(--ov-space-2); aspect-ratio: 1; }

.ov-btn-group { display: inline-flex; gap: var(--ov-space-2); flex-wrap: wrap; }


/* ───────────────────────── ALERTS ─────────────────────────────────────── */

/* Tinted, not solid. base.html's current solid fills (#198754, #ffc107…) are
   the loudest thing on any page they appear on; a flash message should not
   outrank the record beneath it. */
.ov-alert {
  display: flex;
  align-items: flex-start;
  gap: var(--ov-space-3);
  padding: var(--ov-space-3) var(--ov-space-4);
  border: 1px solid var(--ov-border);
  border-left-width: 3px;
  border-radius: var(--ov-radius-md);
  background: var(--ov-surface-raised);
  font-size: var(--ov-text-sm);
  color: var(--ov-text-primary);
}
.ov-alert-title { font-weight: var(--ov-weight-semibold); margin-bottom: 2px; }
.ov-alert-body { color: var(--ov-text-secondary); min-width: 0; }
.ov-alert-close { margin-left: auto; }

.ov-alert--critical { border-left-color: var(--ov-status-critical); background: var(--ov-status-critical-bg); }
.ov-alert--caution  { border-left-color: var(--ov-status-caution);  background: var(--ov-status-caution-bg); }
.ov-alert--positive { border-left-color: var(--ov-status-positive); background: var(--ov-status-positive-bg); }
.ov-alert--info     { border-left-color: var(--ov-status-info);     background: var(--ov-status-info-bg); }


/* ───────────────────────── MODAL ──────────────────────────────────────── */

.ov-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--ov-z-scrim);
  background: var(--ov-scrim);
  backdrop-filter: blur(2px);
}

.ov-modal {
  position: fixed;
  inset: 0;
  z-index: var(--ov-z-modal);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: var(--ov-space-8) var(--ov-space-4);
  overflow-y: auto;
}

.ov-modal-panel {
  width: 100%;
  max-width: var(--ov-modal-width, 560px);
  background: var(--ov-surface-overlay);
  border: 1px solid var(--ov-border);
  border-radius: var(--ov-radius-lg);
  box-shadow: var(--ov-shadow-3);
  overflow: hidden;
}
.ov-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ov-space-3);
  padding: var(--ov-space-4);
  border-bottom: 1px solid var(--ov-border-soft);
}
.ov-modal-title {
  font-size: var(--ov-text-lg);
  font-weight: var(--ov-weight-semibold);
  color: var(--ov-text-primary);
  margin: 0;
}
.ov-modal-body { padding: var(--ov-space-4); font-size: var(--ov-text-md); color: var(--ov-text-secondary); }
.ov-modal-foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--ov-space-2);
  padding: var(--ov-space-3) var(--ov-space-4);
  border-top: 1px solid var(--ov-border-soft);
  background: var(--ov-surface-sunk);
}


/* ───────────────────────── EMPTY / LOADING / ERROR ────────────────────── */

/* An empty state must say WHY it is empty and what to do next. JusticeLink's
   database was intentionally cleared — "No results" alone would read as a bug
   to every user who opens it for the first time. */
.ov-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--ov-space-2);
  padding: var(--ov-space-12) var(--ov-space-6);
  border: 1px dashed var(--ov-border);
  border-radius: var(--ov-radius-lg);
  background: var(--ov-surface-raised);
}
.ov-empty-icon { color: var(--ov-text-muted); opacity: 0.7; margin-bottom: var(--ov-space-1); }
.ov-empty-title {
  font-size: var(--ov-text-lg);
  font-weight: var(--ov-weight-semibold);
  color: var(--ov-text-primary);
}
.ov-empty-body { font-size: var(--ov-text-sm); color: var(--ov-text-muted); max-width: 46ch; }
.ov-empty-action { margin-top: var(--ov-space-3); }

/* Same shell, different meaning: nothing is there vs. something broke. Never
   render a failed fetch as an empty state. */
.ov-empty--error { border-style: solid; border-color: var(--ov-status-critical); background: var(--ov-status-critical-bg); }
.ov-empty--error .ov-empty-title { color: var(--ov-status-critical); }

.ov-loading {
  padding: var(--ov-space-12) var(--ov-space-6);
  text-align: center;
  font-size: var(--ov-text-sm);
  color: var(--ov-text-muted);
}

.ov-skeleton {
  background: linear-gradient(90deg,
    var(--ov-surface-raised) 0%,
    var(--ov-surface-overlay) 50%,
    var(--ov-surface-raised) 100%);
  background-size: 200% 100%;
  border-radius: var(--ov-radius-sm);
  animation: ov-shimmer 1.4s linear infinite;
  min-height: 1em;
}
@keyframes ov-shimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}


/* ───────────────────────── PAGINATION ─────────────────────────────────── */

.ov-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--ov-space-4);
  margin-top: var(--ov-space-5);
}
.ov-pager-status {
  font-family: var(--ov-font-mono);
  font-size: var(--ov-text-xs);
  color: var(--ov-text-muted);
}


/* ───────────────────────── RESPONSIVE ─────────────────────────────────── */

@media (max-width: 880px) {
  .ov-page { padding: var(--ov-space-4) var(--ov-space-4) var(--ov-space-12); }
  .ov-nav { padding: 0 var(--ov-space-3); }
  .ov-kv { grid-template-columns: 1fr; gap: var(--ov-space-1) 0; }
  .ov-kv dd { margin-bottom: var(--ov-space-2); }
  .ov-modal { padding: var(--ov-space-4) var(--ov-space-2); }
  .ov-stat-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
}


/* ═══════════════════════════════════════════════════════════════════════════
   4. UTILITIES
   Only the ones that stop people from writing inline styles. Not a framework.
   ═══════════════════════════════════════════════════════════════════════════ */

.ov-mono { font-family: var(--ov-font-mono); font-variant-numeric: tabular-nums; }
.ov-num  { font-variant-numeric: tabular-nums; text-align: right; }
.ov-t-primary   { color: var(--ov-text-primary); }
.ov-t-secondary { color: var(--ov-text-secondary); }
.ov-t-muted     { color: var(--ov-text-muted); }
.ov-t-accent    { color: var(--ov-accent-text); }
.ov-t-critical    { color: var(--ov-status-critical); }
.ov-t-caution     { color: var(--ov-status-caution); }
.ov-t-positive    { color: var(--ov-status-positive); }
.ov-t-adjudicated { color: var(--ov-status-adjudicated); }
.ov-caps {
  font-size: var(--ov-text-2xs);
  font-weight: var(--ov-weight-bold);
  letter-spacing: var(--ov-tracking-caps);
  text-transform: uppercase;
}
.ov-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.ov-divider { height: 1px; background: var(--ov-border); border: none; margin: var(--ov-space-5) 0; }
.ov-stack   { display: flex; flex-direction: column; gap: var(--ov-space-3); }
.ov-row     { display: flex; align-items: center; gap: var(--ov-space-2); flex-wrap: wrap; }

/* Visually hidden but read by screen readers. Required for icon-only controls
   and for the text half of any colour-coded signal that has no visible label. */
.ov-sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}


/* ═══════════════════════════════════════════════════════════════════════════
   5. BRIDGES
   Both apps already have thousands of lines of markup written against another
   vocabulary. Rewriting that markup is a separate job owned by other people.
   These blocks remap the OTHER system's variable names onto Oceanview tokens,
   so an app adopts the palette by adding one <link> and one attribute — with
   zero edits to its existing rules.

   Both are gated on `data-ov-bridge` on <html> and are completely inert until
   you switch them on. Specificity is (0,2,0), which beats both `:root` and
   `html[data-theme="dark"]`, so the remap wins regardless of load order.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── BOOTSTRAP 5.3 (JusticeLink) ──────────────────────────────────────────
   <html data-ov-bridge="bootstrap" data-bs-theme="dark">
   Repoints Bootstrap's own theme variables. Every .card, .table, .btn-primary,
   .badge, .form-control and .modal already in the templates picks these up
   with no class changes at all.                                             */
:root[data-ov-bridge~="bootstrap"] {
  --bs-body-font-family: var(--ov-font-sans);
  --bs-body-font-size:   var(--ov-text-md);
  --bs-body-line-height: var(--ov-leading-normal);

  --bs-body-bg:          var(--ov-surface-page);
  --bs-body-bg-rgb:      10, 12, 16;
  --bs-body-color:       var(--ov-text-primary);
  --bs-body-color-rgb:   236, 238, 243;
  --bs-emphasis-color:   var(--ov-text-primary);
  --bs-secondary-color:  var(--ov-text-secondary);
  --bs-tertiary-color:   var(--ov-text-muted);

  --bs-secondary-bg:     var(--ov-surface-raised);
  --bs-tertiary-bg:      var(--ov-surface-overlay);
  --bs-border-color:     var(--ov-border);
  --bs-border-color-translucent: var(--ov-border-soft);

  --bs-primary:          var(--ov-accent);
  --bs-primary-rgb:      var(--ov-brand-primary-rgb);
  --bs-primary-text-emphasis: var(--ov-accent-text);
  --bs-primary-bg-subtle:     var(--ov-accent-subtle);
  --bs-primary-border-subtle: var(--ov-accent-line);

  --bs-link-color:       var(--ov-accent-text);
  --bs-link-color-rgb:   108, 155, 245;
  --bs-link-hover-color: var(--ov-text-primary);

  /* Bootstrap's four semantic slots map onto four of the six Oceanview
     buckets. `adjudicated` and `neutral` have no Bootstrap equivalent —
     use .ov-pill--adjudicated / --neutral directly for those. */
  --bs-success: var(--ov-status-positive);
  --bs-danger:  var(--ov-status-critical);
  --bs-warning: var(--ov-status-caution);
  --bs-info:    var(--ov-status-info);
  --bs-success-rgb:  52, 211, 153;
  --bs-danger-rgb:  248, 113, 113;
  --bs-warning-rgb: 245, 165,  36;
  --bs-info-rgb:    108, 155, 245;
  --bs-success-text-emphasis: var(--ov-status-positive);
  --bs-danger-text-emphasis:  var(--ov-status-critical);
  --bs-warning-text-emphasis: var(--ov-status-caution);
  --bs-info-text-emphasis:    var(--ov-status-info);
  --bs-success-bg-subtle: var(--ov-status-positive-bg);
  --bs-danger-bg-subtle:  var(--ov-status-critical-bg);
  --bs-warning-bg-subtle: var(--ov-status-caution-bg);
  --bs-info-bg-subtle:    var(--ov-status-info-bg);
  --bs-secondary-bg-subtle: var(--ov-status-neutral-bg);

  --bs-border-radius-sm: var(--ov-radius-sm);
  --bs-border-radius:    var(--ov-radius-md);
  --bs-border-radius-lg: var(--ov-radius-lg);
  --bs-border-radius-xl: var(--ov-radius-xl);
  --bs-border-radius-pill: var(--ov-radius-pill);

  --bs-box-shadow-sm: var(--ov-shadow-1);
  --bs-box-shadow:    var(--ov-shadow-2);
  --bs-box-shadow-lg: var(--ov-shadow-3);

  --bs-focus-ring-color: var(--ov-accent-subtle);
  --bs-focus-ring-width: 3px;

  --bs-heading-color: var(--ov-text-primary);
  --bs-code-color:    var(--ov-accent-text);
  --bs-highlight-bg:  var(--ov-accent-subtle);
}

/* The -rgb triplets above cannot be written as var() — Bootstrap feeds them to
   rgba(), which needs literal channels. They therefore have to be restated for
   the light theme or every `rgba(var(--bs-danger-rgb), .1)` in Bootstrap keeps
   painting the dark theme's colour. JusticeLink is dark-only today
   (base.html pins data-bs-theme="dark"), so this block is insurance. */
:root[data-ov-bridge~="bootstrap"][data-theme="light"],
:root[data-ov-bridge~="bootstrap"][data-ov-theme="light"] {
  --bs-body-bg-rgb:     245, 246, 248;
  --bs-body-color-rgb:   13,  17,  23;
  --bs-link-color-rgb:   29,  90, 212;
  --bs-success-rgb:      15, 123,  79;
  --bs-danger-rgb:      192,  50,  47;
  --bs-warning-rgb:     138,  90,  13;
  --bs-info-rgb:         29,  90, 212;
}

/* Bootstrap component vars that live on the component, not on :root, so they
   have to be reached individually. Scoped under the bridge attribute so this
   file still touches nothing when the bridge is off. */
:root[data-ov-bridge~="bootstrap"] .table {
  --bs-table-bg: transparent;
  --bs-table-color: var(--ov-text-secondary);
  --bs-table-border-color: var(--ov-border-soft);
  --bs-table-striped-bg: rgba(255, 255, 255, 0.022);
  --bs-table-striped-color: var(--ov-text-secondary);
  --bs-table-hover-bg: var(--ov-surface-hover);
  --bs-table-hover-color: var(--ov-text-primary);
  --bs-table-active-bg: var(--ov-surface-active);
}
:root[data-ov-bridge~="bootstrap"] .card {
  --bs-card-bg: var(--ov-surface-raised);
  --bs-card-color: var(--ov-text-secondary);
  --bs-card-border-color: var(--ov-border);
  --bs-card-border-radius: var(--ov-radius-lg);
  --bs-card-cap-bg: var(--ov-surface-hover);
  --bs-card-cap-color: var(--ov-text-primary);
  --bs-card-title-color: var(--ov-text-primary);
}
:root[data-ov-bridge~="bootstrap"] .dropdown-menu {
  --bs-dropdown-bg: var(--ov-surface-overlay);
  --bs-dropdown-border-color: var(--ov-border);
  --bs-dropdown-border-radius: var(--ov-radius-md);
  --bs-dropdown-color: var(--ov-text-secondary);
  --bs-dropdown-link-color: var(--ov-text-secondary);
  --bs-dropdown-link-hover-color: var(--ov-text-primary);
  --bs-dropdown-link-hover-bg: var(--ov-surface-hover);
  --bs-dropdown-link-active-bg: var(--ov-accent-subtle);
  --bs-dropdown-link-active-color: var(--ov-text-primary);
  --bs-dropdown-box-shadow: var(--ov-shadow-3);
}
:root[data-ov-bridge~="bootstrap"] .modal {
  --bs-modal-bg: var(--ov-surface-overlay);
  --bs-modal-border-color: var(--ov-border);
  --bs-modal-border-radius: var(--ov-radius-lg);
  --bs-modal-color: var(--ov-text-secondary);
  --bs-modal-header-border-color: var(--ov-border-soft);
  --bs-modal-footer-border-color: var(--ov-border-soft);
  --bs-modal-box-shadow: var(--ov-shadow-3);
}
:root[data-ov-bridge~="bootstrap"] .navbar {
  --bs-navbar-color: var(--ov-text-secondary);
  --bs-navbar-hover-color: var(--ov-text-primary);
  --bs-navbar-active-color: var(--ov-text-primary);
  --bs-navbar-brand-color: var(--ov-text-primary);
  --bs-navbar-brand-hover-color: var(--ov-text-primary);
}
:root[data-ov-bridge~="bootstrap"] .list-group {
  --bs-list-group-bg: var(--ov-surface-raised);
  --bs-list-group-color: var(--ov-text-secondary);
  --bs-list-group-border-color: var(--ov-border-soft);
  --bs-list-group-action-hover-bg: var(--ov-surface-hover);
  --bs-list-group-action-hover-color: var(--ov-text-primary);
  --bs-list-group-active-bg: var(--ov-accent-subtle);
  --bs-list-group-active-border-color: var(--ov-accent-line);
  --bs-list-group-active-color: var(--ov-text-primary);
}
:root[data-ov-bridge~="bootstrap"] .alert {
  --bs-alert-border-color: var(--ov-border);
  --bs-alert-border-radius: var(--ov-radius-md);
}
:root[data-ov-bridge~="bootstrap"] .form-control,
:root[data-ov-bridge~="bootstrap"] .form-select {
  background-color: var(--ov-surface-sunk);
  border-color: var(--ov-border);
  color: var(--ov-text-primary);
}
:root[data-ov-bridge~="bootstrap"] .form-control:focus,
:root[data-ov-bridge~="bootstrap"] .form-select:focus {
  background-color: var(--ov-surface-sunk);
  border-color: var(--ov-accent);
  color: var(--ov-text-primary);
  box-shadow: 0 0 0 3px var(--ov-accent-subtle);
}
/* Bootstrap's .bg-* utilities set a raw hex fill with no matching foreground.
   On badges that produces dark-on-dark. Repoint them at the tinted status
   pairs so `<span class="badge bg-danger">` stays legible. */
:root[data-ov-bridge~="bootstrap"] .badge.bg-danger    { background-color: var(--ov-status-critical-bg) !important; color: var(--ov-status-critical); }
:root[data-ov-bridge~="bootstrap"] .badge.bg-success   { background-color: var(--ov-status-positive-bg) !important; color: var(--ov-status-positive); }
:root[data-ov-bridge~="bootstrap"] .badge.bg-warning   { background-color: var(--ov-status-caution-bg)  !important; color: var(--ov-status-caution); }
:root[data-ov-bridge~="bootstrap"] .badge.bg-info      { background-color: var(--ov-status-info-bg)     !important; color: var(--ov-status-info); }
:root[data-ov-bridge~="bootstrap"] .badge.bg-primary   { background-color: var(--ov-accent-subtle)      !important; color: var(--ov-accent-text); }
:root[data-ov-bridge~="bootstrap"] .badge.bg-secondary { background-color: var(--ov-status-neutral-bg)  !important; color: var(--ov-status-neutral); }
/* `.text-gray-800` is a SB-Admin leftover that appears 17x in the templates
   and resolves to nothing under Bootstrap 5 — it renders as inherited body
   text today. Pin it so those headings are deliberate rather than accidental. */
:root[data-ov-bridge~="bootstrap"] .text-gray-800 { color: var(--ov-text-primary) !important; }


/* ── STATECIVIC LEGACY NAMES (Vellum / Chambers) ──────────────────────────
   <html data-ov-bridge="statecivic">
   law.styles.css / treasury.styles.css / shell.styles.css are written against
   --bg / --surface / --ink / --gold / --t-* / --c-*. Remapping those names is
   what converts the entire existing State Treasury + statute browser UI from
   the gold-on-parchment palette to Oceanview, without editing a single one of
   their ~1000 rules.

   The gold accent becomes the Oceanview blue. `--gold` is used as both a fill
   (sidebar active, hero bar, chart series) and as text, so it maps to the
   accent while `--gold-text` maps to the lifted accent-text — same fill/text
   split as the rest of this system.                                         */
:root[data-ov-bridge~="statecivic"] {
  --sans:  var(--ov-font-sans);
  --serif: var(--ov-font-sans);   /* the serif is the loudest thing separating
                                     StateCivic from the game; it goes */
  --mono:  var(--ov-font-mono);

  --bg:             var(--ov-surface-page);
  --bg-sunk:        var(--ov-surface-sunk);
  --surface:        var(--ov-surface-raised);
  --surface-raised: var(--ov-surface-overlay);
  --surface-wash:   var(--ov-surface-hover);
  --border:         var(--ov-border);
  --border-soft:    var(--ov-border-soft);

  --ink:    var(--ov-text-primary);
  --ink-2:  var(--ov-text-primary);
  --body:   var(--ov-text-secondary);
  --mute:   var(--ov-text-secondary);
  --faint:  var(--ov-text-muted);

  --gold:      var(--ov-accent);
  --gold-text: var(--ov-accent-text);

  --header-bg:   var(--ov-surface-sunk);
  --header-bg-2: var(--ov-surface-raised);
  --header-fg:   var(--ov-text-primary);
  --header-line: var(--ov-border);

  --scrim:      var(--ov-scrim);
  --row-stripe: rgba(255, 255, 255, 0.022);

  --shadow-1: var(--ov-shadow-1);
  --shadow-2: var(--ov-shadow-2);
  --shadow-3: var(--ov-shadow-3);
  --plate-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05),
                  inset 0 0 0 1px rgba(var(--ov-brand-primary-rgb), 0.10);

  --callout-bg: var(--ov-accent-subtle);
  --callout-fg: var(--ov-accent-text);
  --mark-bg:    var(--ov-accent-subtle);
  --mark-fg:    var(--ov-text-primary);

  --severity-wash-alpha: 0.10;

  /* Statute severity accents. Eight steps that must stay ordered AND
     distinguishable — this is a legal severity ladder, so it reads as a ramp
     from critical red through caution amber to the neutral end, rather than
     eight arbitrary hues. All sit in the same lightness band as the status
     tokens, so they clear 4.5:1 on --ov-surface-raised. */
  --t-capital: #FF8A8A;
  --t-f1:      var(--ov-status-critical);
  --t-f2:      #F79552;
  --t-f3:      var(--ov-status-caution);
  --t-sjf:     #D9C255;
  --t-ma:      var(--ov-status-info);
  --t-mb:      var(--ov-status-positive);
  --t-mc:      #6FD1A6;
  --t-traf:    var(--ov-status-neutral);
  --t-reg:     var(--ov-text-muted);
  --t-def:     var(--ov-accent-text);

  /* Code-family spine colours. Eight identifiers, no order — pure
     categorical, so these are spread around the wheel and every one is used
     as a small swatch or a 3px rule, never as body text. */
  --c-sapc:  #F87171;
  --c-satc:  #6C9BF5;
  --c-sahsc: #34D399;
  --c-safc:  #E58FD1;
  --c-sabcc: #F5A524;
  --c-saac:  #A7B0BF;
  --c-safg:  #6FD1A6;
  --c-saec:  #B08CF8;
}

/* StateCivic's own light theme, kept working. The bridge above resolves
   through --ov-* tokens, so this only has to flip the ov theme — which
   :root[data-theme="light"] at the top of this file already does. This block
   only fixes the two places law.styles.css hardcodes a light-mode assumption
   that the variable remap cannot reach. */
:root[data-ov-bridge~="statecivic"][data-theme="light"] {
  --row-stripe: rgba(9, 12, 18, 0.025);
  --plate-shadow: inset 0 1px 2px rgba(var(--ov-brand-shadow-rgb), 0.10);
}

/* The one non-variable override the StateCivic bridge needs: .t-discord-btn
   hardcodes Discord blurple (#5865F2), which sits a few degrees off the
   Oceanview blue — close enough to read as a mistake rather than a brand.
   Repointed at the accent; it is the sign-in action, so primary weight is
   correct for it. */
:root[data-ov-bridge~="statecivic"] .t-discord-btn {
  background: var(--ov-accent);
  color: var(--ov-text-on-accent);
}
:root[data-ov-bridge~="statecivic"] .t-discord-btn:hover {
  background: var(--ov-accent-hover);
}

/* ═══════════════════════════════ END ══════════════════════════════════════ */
