/* OBEINSON FREQUENCY X — the design tokens.
 *
 * WHAT THIS REPLACES
 *
 * An audit of the 24 patch stylesheets in this folder found 206 distinct hex
 * values, five competing accent families, 53 box-shadows and 26 different
 * border-radius values. That is not a bold aesthetic; it is the same unowned
 * patch-layer problem the scripts have, showing up as pixels. It is also why
 * the light theme went unexercised for weeks — 206 values cannot be themed, so
 * nobody tried, and every "light mode" screenshot was quietly the dark one.
 *
 * THE RULE THIS FILE EXISTS TO ENFORCE
 *
 * No colour enters the app without a stated meaning. Four accents, one job each.
 * If a new element needs a fifth, the element is wrong, not the palette.
 *
 *     --fx-structure   cyan    headings, section labels, the frame
 *     --fx-value       amber   figures, counts, bitrates — anything measured
 *     --fx-live        green   ON AIR state, and nothing else
 *     --fx-control     violet  sliders and interactive affordances
 *
 * Amber absorbs the old orange (#ff7f00) and yellow (#ffd400), which were three
 * hues doing one job.
 *
 * LIGHT AND DARK ARE AUTHORED HERE TOGETHER, IN THE SAME FILE, ON PURPOSE.
 * The previous theme rotted because it was a retrofit: a light value was only
 * ever written when someone noticed a dark one looked wrong. Every token below
 * has both values on adjacent lines, so omitting one is visible while writing
 * it rather than months later on a stranger's phone.
 *
 * CONTRAST IS CHECKED, NOT ASSUMED. Every ink/surface pair here is verified to
 * WCAG AA by test/contract/contrast.mjs, which fails the build. The ratios in
 * the comments are measured output from that script, not estimates.
 *
 * HOW TO USE IT
 *
 * New components reference tokens only — no hex literals, no !important. The
 * existing patch files are NOT converted by this commit; they keep working
 * untouched. This is the vocabulary the rebuild is written in, introduced
 * alongside what is already there rather than in place of it.
 */

:root {
  /* ---- surfaces: three depths, darkest first ------------------------------
     The cockpit reads as an instrument panel because it is mostly dark and
     mostly flat. Depth comes from these three steps, not from shadow. */
  --fx-surface-0: #0b1220;   /* the card and the page behind everything */
  --fx-surface-1: #121c2b;   /* panels: the STREAMING bar, the counts strip */
  --fx-surface-2: #1b2838;   /* raised: rows, pressed states, the dial well */

  /* ---- ink: three weights -------------------------------------------------
     Named for the job, not the shade, so a theme flip cannot invert meaning. */
  /* Ratios are the MEASURED WORST CASE across all three surfaces, from
     `npm run contrast`. Quoting the best case (against surface-0) would flatter
     every token by about three points and hide the one that actually matters. */
  --fx-ink-strong: #eaf2fb;  /* station names, what you read first     — 13.2:1 */
  --fx-ink:        #b9c8da;  /* body, metadata                         —  8.8:1 */
  --fx-ink-muted:  #8ba0bd;  /* labels, inactive presets               —  5.6:1 */

  /* ---- accents: four, one meaning each ------------------------------------
     Each is given twice: a readable ink value and a brighter emissive value
     used only where light means something (see --fx-glow below). Separating
     them is what lets the palette pass contrast without going dull, which was
     the Disruptor's objection to a compliance-led palette. */
  --fx-structure:      #5fd8f0;   /* cyan  — headings and frame     —  8.9:1 */
  --fx-structure-emit: #00e1ff;   /*         the bright original, for glow    */
  --fx-value:          #ffb039;   /* amber — measured figures       —  8.2:1 */
  --fx-value-emit:     #ffd400;
  --fx-live:           #45e3a0;   /* green — ON AIR only            —  9.1:1 */
  --fx-live-emit:      #00ff22;
  --fx-control:        #c3a0ff;   /* violet— sliders and controls   —  7.0:1 */
  --fx-control-emit:   #af79f6;

  /* ---- hairlines ---------------------------------------------------------- */
  --fx-line:        rgba(130, 170, 215, .18);
  --fx-line-strong: rgba(130, 170, 215, .38);

  /* ---- corners: three, down from 26 ---------------------------------------
     Pill for badges, panel for anything with content in it, circle for
     transport. The cut-corner HUD polygon stays as the panel signature; it is
     distinctive, costs nothing, and is the one piece of the old visual language
     worth protecting. */
  --fx-round-pill:   999px;
  --fx-round-panel:  12px;
  --fx-round-circle: 50%;
  --fx-cut: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 0 100%);

  /* ---- rhythm -------------------------------------------------------------
     --fx-sec is the equal-section unit solved on 2026-10-24: every stacked
     block below the dial is one of these. --fx-gap is the single inter-section
     gap; equal blocks separated by unequal gaps still read as irregular. */
  --fx-sec: 50px;
  --fx-gap: 5px;
  --fx-unit: 4px;            /* the baseline grid everything else lands on */

  /* ---- type: two families, split by job ------------------------------------
     Monospace is instrumentation — frequencies, bitrates, counters, timers,
     section labels. It is the wrong face for a station NAME: uppercase mono at
     wide tracking turns "Classical KING 98.1 FM" into a serial number, on the
     screen people scan fastest. Names and prose get the sans. */
  --fx-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  --fx-sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;

  --fx-label:  700 10.5px/1.35 var(--fx-mono);   /* uppercase, tracked */
  --fx-figure: 800 13px/1.2   var(--fx-mono);    /* counts and readings */
  --fx-name:   700 16px/1.25  var(--fx-sans);    /* station names */
  --fx-body:   400 13.5px/1.45 var(--fx-sans);
  --fx-track:  .07em;

  /* ---- light means ON AIR, and nothing else --------------------------------
     This is the whole lighting rule in one token. Before it, SCAN glowed as
     brightly as the live indicator — the interface telling a lie every frame.
     Now a thing emits only if it is on air: the dial sweep, the live dot, the
     station currently playing.

     STATIC box-shadow ONLY. Never an animated opacity, and never on a
     pseudo-element: measured on 2026-10-23, an opacity pulse on ::before cost
     about ten points of main thread, because Chromium cannot composite
     pseudo-element animation and repaints it every frame. The same effect on a
     real element was free. */
  --fx-glow-live:      0 0 18px rgba(0, 255, 34, .20);
  --fx-glow-structure: 0 0 16px rgba(0, 225, 255, .18);
}

/* ===========================================================================
   LIGHT THEME
   Set by the app on <html data-theme="light">, read from the `bs-theme`
   localStorage key. THE KEY MATTERS: this repo spent weeks writing to
   `obn-theme`, which nothing reads, so every light-theme screenshot taken
   through the test harness was really the dark theme and none of this was ever
   exercised. If a light value looks wrong, check the key before the colour.
   =========================================================================== */
:root[data-theme="light"] {
  --fx-surface-0: #f4f8fc;
  --fx-surface-1: #ffffff;
  --fx-surface-2: #e8eff7;

  --fx-ink-strong: #0f1b29;   /* 15.0:1 worst case */
  --fx-ink:        #33455a;   /*  8.5:1 */
  --fx-ink-muted:  #55687e;   /*  4.9:1 — the tightest token in the system */

  /* The accents darken rather than change hue: the app must stay recognisably
     itself in light mode, so the meanings hold and only the luminance moves. */
  --fx-structure:      #06677d;   /*  5.6:1 */
  --fx-structure-emit: #0892ad;
  --fx-value:          #8a4b00;   /*  5.9:1 */
  --fx-value-emit:     #b26100;
  --fx-live:           #0a6b45;   /*  5.7:1 */
  --fx-live-emit:      #0e8a58;
  --fx-control:        #5b3aa8;   /*  6.9:1 */
  --fx-control-emit:   #7349d4;

  --fx-line:        rgba(30, 60, 95, .16);
  --fx-line-strong: rgba(30, 60, 95, .32);

  /* Glow is a dark-room effect. In daylight it reads as a smudge, so it becomes
     a crisp ring instead — the meaning survives, the technique changes. */
  --fx-glow-live:      0 0 0 2px rgba(14, 138, 88, .35);
  --fx-glow-structure: 0 0 0 2px rgba(8, 146, 173, .30);
}

/* Someone who has asked their phone for less motion is also asking for less
   decoration. Emission drops to a flat ring; nothing else changes. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --fx-glow-live:      0 0 0 2px rgba(0, 255, 34, .30);
    --fx-glow-structure: 0 0 0 2px rgba(0, 225, 255, .28);
  }
}
