/* ============================================================
   Seagoat — Design system core
   The single source of truth for tokens + shared component
   classes, carried over verbatim from the design system's
   styles.css. Admin-specific rules live in admin.css and build
   on top of these. Never redefine colors/fonts/radii with raw
   values — always reference the tokens below.
   ============================================================ */

/* ---------- Typefaces ----------
   Self-hosted, not loaded from the Google Fonts CDN — the same
   vendoring rule idiomorph.min.js and leaflet.js already follow (see
   CLAUDE.md). Both files are the latin subset of the variable font,
   so one file covers every weight the app uses: ~24KB and ~22KB.
   Serving them from our own origin also means no third-party
   request on every page load, and no silent fallback to system-ui
   when that origin is unreachable.

   Both are SIL Open Font License 1.1. The CDN's subsetting strips the
   name table, so the .woff2 files carry no licence string of their own —
   the licence therefore lives beside them in VENDOR-LICENSES.txt, which
   OFL clause 2 requires be distributed with the fonts. That file covers
   every vendored asset, not just the fonts — Leaflet's BSD 2-Clause notice
   has the same "must travel with the distribution" requirement. */
@font-face {
  font-family: "Manrope";
  src: url("/static/manrope.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Space Grotesk";
  src: url("/static/space-grotesk.woff2") format("woff2");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

/* ---------- Tokens ----------
   Every themed color below is one light-dark() pair, not two palettes:
   the first value is the original light-theme value — unchanged,
   byte-for-byte, so the 2026-08-16 a11y audit's own measured ratios all
   still hold — and the second is its dark-theme counterpart. Which half
   resolves is decided by the used `color-scheme` and nothing else, so
   the entire theme switch is a single property on <html> rather than a
   duplicated block of overrides that can drift out of sync with the
   light one. (See internal/web/static/theme.js for the reader-facing
   control, and CLAUDE.md's Theming decision for why it's built this way.)

   The one thing light-dark() can't carry is a value that isn't a bare
   <color> — admin.css's --select-chevron (a data: URI with its stroke
   baked in) is the single token in this family that needs a real
   per-theme override instead. */
:root {
  /* The whole switch. `light dark` = follow the device; the two
     [data-theme] rules below this block pin it once a reader has
     actually chosen. It also re-themes what CSS can't otherwise reach:
     native form controls, the <select> popup list, scrollbars, and the
     canvas painted behind the page before our own background lands. */
  color-scheme: light dark;

  /* brand (driven by tweak: --brand-h hue) — a fixed ladder of FILLS,
     identical in both themes: a --brand-600 button is the same blue with
     the same white label whichever theme is on, so none of these four
     flip. Only the two tints (a background behind text) and the two ink
     roles below them do. */
  --brand-h: 250;
  --brand:      oklch(0.62 0.135 var(--brand-h));
  --brand-600:  oklch(0.56 0.142 var(--brand-h));
  --brand-700:  oklch(0.49 0.135 var(--brand-h));
  --brand-300:  oklch(0.74 0.105 var(--brand-h));
  --brand-tint: light-dark(oklch(0.96 0.025 var(--brand-h)), oklch(0.33 0.07 var(--brand-h)));
  --brand-tint-2: light-dark(oklch(0.93 0.045 var(--brand-h)), oklch(0.40 0.09 var(--brand-h)));

  /* Because that ladder doesn't flip, it can't double as brand-colored
     TEXT the way the light theme used it to: --brand-700 lettering on a
     dark surface is unreadable. These are the two text/glyph roles,
     split exactly the way the light theme already split them, so every
     call site is a mechanical rename with no light-theme change at all:
       --brand-ink   was `color: var(--brand-700)`  — a brand glyph
                     sitting on a --brand-tint fill (active nav item,
                     .status.s-transit, an on-state filter chip).
       --brand-link  was `color: var(--brand-600)`  — a brand-colored
                     link or icon sitting straight on --bg/--bg-soft
                     (.card-link, .cell-id, .dispatch-pin-link).
     Measured on the dark surfaces: --brand-ink 8.0:1 on --brand-tint and
     6.0:1 on --brand-tint-2; --brand-link 8.3:1 on --bg. */
  --brand-ink:  light-dark(var(--brand-700), oklch(0.86 0.08 var(--brand-h)));
  --brand-link: light-dark(var(--brand-600), oklch(0.78 0.105 var(--brand-h)));

  /* The keyboard focus ring, previously written as a literal
     --brand-700 outline at each of its ~10 call sites. --brand-700 is
     nearly the darkest thing in the dark palette, so a focus ring drawn
     in it would be the one indicator a keyboard user can't see.
     Measured 9.4:1 on --bg, 10.6:1 on --bg-soft. */
  --focus-ring: light-dark(var(--brand-700), oklch(0.82 0.10 var(--brand-h)));

  --accent: oklch(0.74 0.12 195);     /* cyan — live / AI */

  /* Dark surface/ink ramp. Reserved at scaffold time and, until the
     theme switcher, genuinely unused — this is what it was held back
     for, so the values are the design system's own rather than a second
     dark palette invented beside them. --dark-muted-2 is the one
     addition: the light theme has three text tones (--ink/-2/-3) and
     this ramp only shipped two. */
  --dark:    oklch(0.19 0.035 257);
  --dark-2:  oklch(0.24 0.04 257);
  --dark-3:  oklch(0.30 0.045 257);
  --dark-ink:   oklch(0.97 0.01 255);
  --dark-muted: oklch(0.78 0.022 255);
  --dark-muted-2: oklch(0.70 0.022 255);
  --dark-line:  oklch(1 0 0 / 0.10);
  --dark-line-2: oklch(1 0 0 / 0.16);

  /* Surfaces. The dark half deliberately INVERTS the light half's
     ordering instead of mirroring it: on light, --bg (#fff) is the
     raised card and --bg-soft the recessed page behind it; on dark the
     page has to be the darkest thing on screen and a card sits above
     it, so --bg resolves LIGHTER than --bg-soft. Nothing downstream
     changes — `.card { background: var(--bg) }` and
     `body { background: var(--bg-soft) }` both keep meaning "card" and
     "page" — which is the whole reason to encode elevation in the token
     rather than at each call site.
     Measured on dark: --ink 15.1:1, --ink-2 8.3:1, --ink-3 6.2:1 on
     --bg; --ink-3 5.1:1 on --bg-soft-2, its worst case. */
  --bg:      light-dark(#ffffff, var(--dark-2));
  --bg-soft: light-dark(oklch(0.975 0.006 250), var(--dark));
  --bg-soft-2: light-dark(oklch(0.96 0.009 250), var(--dark-3));
  --ink:     light-dark(oklch(0.22 0.03 255), var(--dark-ink));
  --ink-2:   light-dark(oklch(0.42 0.025 255), var(--dark-muted));
  --ink-3:   light-dark(oklch(0.52 0.02 255), var(--dark-muted-2));
  --line:    light-dark(oklch(0.92 0.012 255), var(--dark-line));
  --line-2:  light-dark(oklch(0.88 0.015 255), var(--dark-line-2));

  --pos: oklch(0.66 0.14 155);  /* positive green for deltas */

  /* type */
  --font-display: "Space Grotesk", system-ui, sans-serif;
  --font-body: "Manrope", system-ui, sans-serif;

  /* shape */
  --r-xs: 8px;
  --r-sm: 12px;
  --r:    16px;
  --r-lg: 22px;
  --r-xl: 28px;

  /* Shadows. light-dark() only substitutes colors, never a whole
     box-shadow, so the shadow INK is tokenised on its own and the
     geometry below stays written exactly once. Navy-tinted and very
     faint on light; plain black and much deeper on dark, because a navy
     shadow against a navy surface is invisible — on dark, depth has to
     come from opacity rather than from hue. */
  --sh-1: light-dark(oklch(0.4 0.05 255 / 0.06), oklch(0 0 0 / 0.36));
  --sh-2: light-dark(oklch(0.4 0.05 255 / 0.05), oklch(0 0 0 / 0.28));
  --sh-3: light-dark(oklch(0.4 0.05 255 / 0.07), oklch(0 0 0 / 0.42));
  --sh-4: light-dark(oklch(0.35 0.06 255 / 0.13), oklch(0 0 0 / 0.55));
  --sh-5: light-dark(oklch(0.35 0.06 255 / 0.08), oklch(0 0 0 / 0.40));
  --sh-sm: 0 1px 2px var(--sh-1), 0 1px 3px var(--sh-2);
  --sh-md: 0 4px 12px var(--sh-3), 0 2px 6px var(--sh-2);
  --sh-lg: 0 18px 44px var(--sh-4), 0 6px 16px var(--sh-5);
  --sh-brand: 0 12px 30px oklch(0.55 0.14 var(--brand-h) / 0.32);

  --container: 1200px;
  --maxw: 1280px;
}

/* An explicit choice pins color-scheme — which is the only thing every
   light-dark() pair above reads — so one attribute on <html> re-resolves
   the whole palette with no second copy of it to maintain. theme.js sets
   the attribute, and only when the reader has actually picked a side;
   with no attribute (and with no JS at all) :root's own `light dark`
   follows the device. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

/* Print always comes out light, whatever the screen is showing. Two
   pages here are documents meant for paper (billing/invoice.html,
   orders/receipt.html), and printing them dark puts near-white body text
   on a background printers drop by default — i.e. blank. Matching
   [data-theme]'s own specificity, and sitting after it, is what lets
   this beat an explicit dark choice rather than lose to it. */
@media print {
  :root, :root[data-theme] { color-scheme: light; }
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  font-family: var(--font-body);
  color: var(--ink);
  background: var(--bg);
  font-size: 18px;
  line-height: 1.6;
  letter-spacing: -0.006em;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.06;
  margin: 0;
  text-wrap: balance;
}
p { margin: 0; text-wrap: pretty; }
a { color: inherit; text-decoration: none; }
img { max-width: 100%; display: block; }
button { font-family: inherit; cursor: pointer; }
::selection { background: var(--brand-tint-2); }

/* ---------- Eyebrow / kicker ---------- */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--brand-link);
}
.eyebrow::before {
  content: "";
  width: 22px; height: 1.5px;
  background: var(--brand);
  border-radius: 2px;
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: -0.01em;
  padding: 7px 12px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: transparent;
  transition: transform .15s ease, box-shadow .2s ease, background .2s ease, border-color .2s ease, color .2s ease;
  white-space: nowrap;
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }
.btn svg { width: 17px; height: 17px; }
.btn-primary {
  background: var(--brand-600);
  color: #fff;
  box-shadow: var(--sh-brand);
}
.btn-primary:hover { background: var(--brand-700); transform: translateY(-1px); }
.btn-primary:active { transform: translateY(0); }
.btn-ghost {
  background: var(--bg);
  color: var(--ink);
  /* --line-2 measures 1.44:1 on white — under 3:1 SC 1.4.11 needs for a
     UI component's only boundary (a11y audit, 2026-08-16). --field-line
     is admin.css's own pre-verified 3.11:1 border tone, reused here. */
  border-color: var(--field-line);
  box-shadow: var(--sh-sm);
}
.btn-ghost:hover { border-color: var(--ink-3); transform: translateY(-1px); }
/* Always a white fill, so its label pins the LIGHT --ink literal rather
   than the token — var(--ink) resolves near-white under a dark theme and
   would render white-on-white. (Unused by any template today; fixed here
   so it cannot ship broken the first time a page reaches for it.) */
.btn-white { background: #fff; color: oklch(0.22 0.03 255); }
.btn-white:hover { transform: translateY(-1px); box-shadow: var(--sh-lg); }
.btn-lg { padding: 16px 28px; font-size: 17.5px; }
.btn-block { width: 100%; }

.link-arrow {
  display: inline-flex; align-items: center; gap: 7px;
  font-weight: 600; color: var(--brand-link);
}
.link-arrow svg { width: 16px; height: 16px; transition: transform .2s ease; }
.link-arrow:hover svg { transform: translateX(3px); }

/* ---------- Pills / chips ---------- */
.pill {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 4px 5px 4px 7px;
  border-radius: 999px;
  font-size: 14.5px; font-weight: 600;
  /* --line-2 → --field-line: same 1.44:1-as-sole-boundary fix as
     .btn-ghost above (a11y audit, 2026-08-16). */
  border: 1px solid var(--field-line);
  background: var(--bg);
  box-shadow: var(--sh-sm);
}
.pill .tag {
  display: inline-flex; align-items: center;
  padding: 3px 9px; border-radius: 999px;
  background: var(--brand-tint); color: var(--brand-ink);
  font-size: 13px; font-weight: 700; letter-spacing: 0.01em;
}

/* ---------- Icon chips (feature-card icon themes) ---------- */
.feat-ic {
  width: 46px; height: 46px; border-radius: 13px;
  display: grid; place-items: center; margin-bottom: 18px;
}
.feat-ic svg { width: 23px; height: 23px; }
/* Each chip is a tint + a matching ink at the same hue, so the dark half
   is the same pair rotated: the tint drops to ~0.33 lightness and the ink
   rises to ~0.86, keeping every one of them above 7:1 against its own
   fill rather than merely inverting the two values. */
.ic-blue  { background: var(--brand-tint); color: var(--brand-link); }
.ic-cyan  { background: light-dark(oklch(0.95 0.03 195), oklch(0.33 0.075 195)); color: light-dark(oklch(0.5 0.1 195), oklch(0.86 0.09 195)); }
.ic-violet{ background: light-dark(oklch(0.95 0.035 285), oklch(0.33 0.08 285)); color: light-dark(oklch(0.52 0.13 285), oklch(0.86 0.10 285)); }
.ic-green { background: light-dark(oklch(0.95 0.04 155), oklch(0.33 0.075 155)); color: light-dark(oklch(0.5 0.12 155), oklch(0.86 0.13 155)); }
.ic-amber { background: light-dark(oklch(0.95 0.05 75), oklch(0.33 0.075 75));   color: light-dark(oklch(0.55 0.12 65), oklch(0.86 0.12 75)); }
.ic-rose  { background: light-dark(oklch(0.95 0.03 15), oklch(0.33 0.075 15));   color: light-dark(oklch(0.55 0.13 18), oklch(0.86 0.11 15)); }
.ic-indigo{ background: light-dark(oklch(0.95 0.035 265), oklch(0.33 0.08 265)); color: light-dark(oklch(0.5 0.13 265), oklch(0.86 0.09 265)); }
.ic-teal  { background: light-dark(oklch(0.95 0.035 180), oklch(0.33 0.07 180)); color: light-dark(oklch(0.5 0.1 180), oklch(0.86 0.09 180)); }

/* ---------- Live pulse dot ---------- */
.badge-live {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 700; letter-spacing: 0.04em;
  /* was var(--pos), 2.92:1 (a11y audit, 2026-08-16) — same darkened tone
     admin.css's .status.s-live already uses for the same color on text.
     Now --pos-ink (admin.css, beside --pos-tint), which carries that exact
     light value plus the lightened dark-theme counterpart. */
  color: var(--pos-ink);
}
.badge-live .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--pos); box-shadow: 0 0 0 0 oklch(0.66 0.14 155 / 0.6); animation: pulse 2s infinite; }
@keyframes pulse { 0%{ box-shadow:0 0 0 0 oklch(0.66 0.14 155 / .55);} 70%{ box-shadow:0 0 0 7px oklch(0.66 0.14 155 / 0);} 100%{ box-shadow:0 0 0 0 transparent;} }

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