/* ═══════════════════════════════════════════════════════════════════════════
   SafeCPM design tokens — CPM-124
   ═══════════════════════════════════════════════════════════════════════════

   THE ONE PLACE TOKENS ARE DEFINED. app.css and users-roles.css consume; they
   must not declare. Loaded before both in index.html.

   ── Why this file exists ───────────────────────────────────────────────────

   app.css opened with "design tokens copied from SafePaaS DevOps app.css. Per
   M4-A directive: use DevOps tokens directly ... so SafeCPM looks like a
   sibling of devops.safepaas.com."

   DevOps has TWO :root blocks — a raw palette, then a semantic alias layer.
   CPM copied the first and stopped. Measured 2026-10-05: DevOps defines 79
   tokens, CPM defined 36, and 32 bare var() references across the app
   resolved to nothing:

     --bg-card           17 bare refs (app.css)   .card .stat-card .btn-secondary
                                                  .btn-danger .filter-input
                                                  .in-flight-panel .toc ...
     --text-muted         7 bare refs (usage.js)
     --font-family-mono   5 bare refs (usage.js)
     --text-dim           3 bare refs (usage.js)

   An undefined custom property makes its whole declaration invalid at
   computed-value time, so `background: var(--bg-card)` computed to
   TRANSPARENT. A full-surface sweep measured 148 elements rendering with no
   background across 16 screens. The single dropped line is
   `--bg-card: var(--surface)` at devops app.css:1938 — which also explains why
   --surface was defined here and used exactly zero times. Two halves of one
   line.

   A previous session met this at the wizard modal and hard-coded var(--white)
   there, fixing one of eighteen (see the CPM-55 note in app.css).

   ── The four layers, and the one rule that keeps them honest ───────────────

     1 PRIMITIVES   raw values. Hex literals live HERE AND NOWHERE ELSE.
     2 SCALES       spacing, type, radius, elevation, motion.
     3 SEMANTICS    roles: surface, primary, success, danger, text-*.
     4 THEMES       dark overrides, re-pointing layer 3 only.   (CPM-124 ph.4)

   **A component must not bind to a token whose meaning is theme-INVARIANT.**

   An earlier draft of this file said "components may consume layers 2 and 3
   only", which was too absolute and measurably wrong: CPM had 261 primitive
   colour references and zero semantic ones, so that rule would have required
   261 edits to enable a feature it was not actually blocking. `--green` means
   "the success green for the current theme" and themes perfectly well; what
   does NOT theme is `--white`, which means white — and 15 rules were using it
   to mean "card surface". Those moved to --bg-card / --bg-input; the 4 that
   mean white text on a coloured button stayed.

   So layer 4 re-points BOTH the semantic aliases AND the theme-substrate
   primitives (surfaces, borders, ink, status hues). It leaves alone the tokens
   that name a fixed colour: --white, and the extended chart palette, where a
   series colour must stay recognisable across themes.

   ── Portability ────────────────────────────────────────────────────────────

   Written so DevOps can adopt this file UNCHANGED: no CPM-only names, no
   CPM-only values. Layers 1 and 3 mirror DevOps exactly, including --purple at
   #8b5cf6 (CPM had forked it to #7c3aed). Layer 2 is net-new — DevOps has no
   spacing or type scale either — and is the part DevOps would gain by taking
   this file.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ───────────────────────────────────────────────────────────────────────────
   LAYER 1 — PRIMITIVES.  Mirrors DevOps app.css :root. Hex lives only here.
   ─────────────────────────────────────────────────────────────────────────── */
:root {
  /* Neutrals */
  --bg: #eef0f6;
  --bg-alt: #e8ebf3;
  --white: #ffffff;
  --surface: #ffffff;
  --canvas: #fafbfe;
  --border: #d0d4e3;
  --border-light: #f1f3f8;

  /* Ink */
  --text: #1a1d2e;
  --text-strong: #3d4160;
  --muted: #4a4e68;

  /* Brand */
  --accent: #4f46e5;
  --accent-hover: #4338ca;
  --accent-light: #eef2ff;
  --accent-border: #c7d2fe;

  /* Status */
  --green: #16a34a;
  --green-bg: #f0fdf4;
  --green-border: #bbf7d0;
  --orange: #d97706;
  --orange-bg: #fffbeb;
  --orange-border: #fde68a;
  --red: #dc2626;
  --red-bg: #fef2f2;
  --red-border: #fecaca;
  --blue: #2563eb;
  --blue-bg: #eff6ff;
  --blue-border: #bfdbfe;

  /* Extended palette — charts, relationship lines, asset types. DevOps values.
     CPM previously declared --purple as #7c3aed in a second :root further down
     app.css; aligned here so the two apps do not drift on a shared name. */
  --cyan: #0891b2;
  --teal: #0d9488;
  --pink: #db2777;
  --pink-bg: #fdf2f8;
  --pink-border: #fbcfe8;
  --indigo: #6366f1;
  --purple: #8b5cf6;
  --purple-bg: #f5f3ff;
  --purple-border: #ddd6fe;
  --sky: #0ea5e9;
  --sky-bright: #3b82f6;
  --amber: #f59e0b;
  --yellow: #ca8a04;
  --yellow-bg: #fefce8;

  /* Neutral badge */
  --gray-bg: #ebedf2;
  --gray-text: #6b7089;
  --gray-border: #c0c5d8;

  /* Chart theme */
  --chart-grid: #f1f5f9;
  --chart-label: #64748b;
  --chart-axis: #94a3b8;

  /* Activity-heatmap ramp, 0 -> busiest. A sequential ramp cannot be derived
     from the status palette and was three hardcoded hexes inside dashboard.js,
     so the empty cells stayed light-mode pale and dominated the dark card.
     ApexCharts renders these from JS config, not CSS, so charts/theme.js reads
     them out — which is why they are tokens rather than literals. */
  --heat-0: #f1f5f9;
  --heat-1: #e0e3f5;
  --heat-2: #a5b4fc;
  --heat-3: #818cf8;
  --heat-4: #4f46e5;

  /* Typefaces. --font-family-mono is net-new: usage.js makes 5 bare references
     to it and nothing defined it, so every figure meant to be tabular rendered
     in the body face. */
  --f: 'Inter', sans-serif;
  --font-family-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
}


/* ───────────────────────────────────────────────────────────────────────────
   LAYER 2 — SCALES.  Net-new; neither app had these.

   Fitted to the values the code ALREADY uses, not to an abstract ideal.
   Snapping 299 spacing literals to a clean scale moves layout on every screen,
   so the steps were chosen from a census of app.css + users-roles.css:

     spacing in use : 1 2 3 4 5 6 8 9 10 11 12 14 16 18 20 22 24 28 32 38 40 60 64
     type in use    : 9 10 11 11.5 12 13 14 15 16 17 18 22 24
     radius in use  : 4 6 8 10 999

   The half-steps (1.5 / 2.5 / 3.5 / 7) each exist to make a real in-use value
   an EXACT hit rather than a snap — they are not symmetry for its own sake.
   Only 3, 5, 9, 11, 18, 22, 38 and 60 move at all, and radius needs no
   snapping whatsoever.
   ─────────────────────────────────────────────────────────────────────────── */
:root {
  /* Spacing — 4px base (ui-ux-pro-max §5 spacing-scale: 4pt/8dp system) */
  --space-0:    0;
  --space-px:   1px;
  --space-0-5:  2px;
  --space-1:    4px;
  --space-1-5:  6px;
  --space-2:    8px;
  --space-2-5: 10px;
  --space-3:   12px;
  --space-3-5: 14px;
  --space-4:   16px;
  --space-5:   20px;
  --space-6:   24px;
  --space-7:   28px;
  --space-8:   32px;
  --space-10:  40px;
  --space-12:  48px;
  --space-14:  56px;
  --space-16:  64px;

  /* Type. --text-base is 13px: CPM's actual body size, not a web default. */
  --text-2xs:  10px;
  --text-xs:   11px;
  --text-sm:   12px;
  --text-base: 13px;
  --text-md:   14px;
  --text-lg:   16px;
  --text-xl:   18px;
  --text-2xl:  22px;
  --text-3xl:  24px;

  --leading-tight:  1.2;
  --leading-normal: 1.5;
  --leading-loose:  1.65;

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

  /* Radius — covers every value in use exactly, so nothing snaps. */
  --radius-xs:    4px;
  --radius-sm:    6px;
  --radius-md:    8px;
  --radius-lg:   10px;
  --radius-xl:   12px;
  --radius-full: 9999px;

  /* Elevation — one scale, no ad-hoc shadow values (§4 elevation-consistent) */
  --shadow:    0 1px 4px rgba(0,0,0,.1), 0 1px 2px rgba(0,0,0,.06);
  --shadow-lg: 0 4px 16px rgba(0,0,0,.12);

  /* Motion. Inside the 150-300ms micro-interaction band (§7 duration-timing);
     nothing here approaches the 500ms ceiling for a single UI animation. */
  --duration-fast: 120ms;
  --duration-base: 150ms;
  --duration-slow: 250ms;
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
  --transition: all 0.15s ease;
}


/* ───────────────────────────────────────────────────────────────────────────
   LAYER 3 — SEMANTICS.  Mirrors DevOps app.css's second :root.

   THIS is the layer dark mode re-points, which is why it has to exist before
   dark mode is possible at all. A component bound to a ROLE inherits a theme
   for free; a component bound to a primitive cannot.
   (§6 color-semantic: semantic tokens, not raw hex in components.)
   ─────────────────────────────────────────────────────────────────────────── */
:root {
  /* Brand role */
  --primary:        var(--accent);
  --primary-bg:     var(--accent-light);
  --primary-border: var(--accent-border);
  --primary-hover:  var(--accent-hover);

  /* Text ON a filled primary surface. Needed because the accent INVERTS in
     brightness between themes: #4f46e5 in light takes white at 6.5:1, but the
     dark theme's #818cf8 takes white at only 2.98:1 — measured, and below the
     4.5:1 floor. A fixed `color: #fff` on .btn-primary cannot satisfy both, so
     the pairing becomes a token and layer 4 flips it to near-black. */
  --on-primary:     var(--white);

  /* Status roles */
  --success:        var(--green);
  --success-bg:     var(--green-bg);
  --success-border: var(--green-border);
  --danger:         var(--red);
  --danger-bg:      var(--red-bg);
  --danger-border:  var(--red-border);
  --warning:        var(--orange);
  --warning-bg:     var(--orange-bg);
  --warning-border: var(--orange-border);
  --info:           var(--blue);
  --info-bg:        var(--blue-bg);
  --info-border:    var(--blue-border);

  /* Status TEXT roles.
     A status colour chosen to be a legible FILL is not automatically legible
     as TEXT on its own tint. Measured across 13 screens: --green on --green-bg
     is 3.15:1, --orange on --orange-bg 3.07:1, --red on --red-bg 4.41:1,
     --gray-text on --gray-bg 4.17:1 — every pip, pill, chip and badge in CPM
     has shipped below the 4.5:1 floor since M4-A, 120 occurrences in total.
     These are the 700-level tones, which clear it while staying the same hue.
     The fill/icon tokens keep DevOps's values, so parity is unaffected. */
  --success-text: #15803d;
  --warning-text: #b45309;
  --danger-text:  #b91c1c;
  --info-text:    #1d4ed8;
  --neutral-text: #585d75;

  /* Ink roles */
  --text-secondary: var(--text-strong);
  --text-muted:     var(--muted);
  --text-dim:       var(--muted);

  /* Surface roles.  --bg-card is THE line CPM never copied: 17 bare references
     across app.css, every one computing to transparent until now. */
  --bg-card:       var(--surface);
  --bg-card-hover: #f3f5fb;
  --bg-input:      var(--surface);
  --bg-code:       var(--canvas);
  --bg-overlay:    rgba(26, 29, 46, 0.45);

  /* Back-compat aliases. ~40 existing rules say var(--r); keeping it pointed at
     the scale lets the migration proceed file by file instead of in one
     irreversible sweep. Not deprecated — just no longer the only name. */
  --r:      var(--radius-xl);
  --radius: var(--radius-xl);
}


/* ───────────────────────────────────────────────────────────────────────────
   LAYER 4 — THEMES.

   Resolution order, and why it is this shape:

     :root                           light values (above)
     @media (prefers-color-scheme: dark) :root:not([data-theme="light"])
                                     follow the OS, unless the user pinned light
     :root[data-theme="dark"]        user pinned dark, regardless of OS

   "System" is stored as the ABSENCE of data-theme, so the media query governs.
   That is why the :not([data-theme="light"]) guard exists — without it, a user
   who pinned light on a dark-mode OS would still get dark.

   SURFACES LIFT, THEY DO NOT INVERT. Inverting #eef0f6 to near-black destroys
   the elevation cue --shadow carries, because a shadow is invisible on black.
   Dark surfaces step UP from the page (#15171f -> #1c1f2b -> #242837) so depth
   reads as lightness instead.

   STATUS HUES SHIFT LIGHTER. --green #16a34a on a #1c1f2b card is about 3.0:1,
   below the 4.5:1 body-text floor. The dark variants are 400-level tones, which
   clear it. (ui-ux-pro-max §6 color-dark-mode: "dark mode uses desaturated /
   lighter tonal variants, not inverted colors; test contrast separately" —
   phase 5 measures every pair in BOTH themes rather than assuming.)

   THE SERIES COLOURS IN LAYER 1 DO NOT MOVE. A chart series must stay
   recognisable when a reader switches theme; only the chart FURNITURE (grid,
   axis, labels) is re-pointed.
   ─────────────────────────────────────────────────────────────────────────── */

:root                      { color-scheme: light; }
:root[data-theme="dark"]   { color-scheme: dark; }
:root[data-theme="light"]  { color-scheme: light; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { color-scheme: dark; }
}

/* The dark palette is written TWICE — once for the OS-driven case and once for
   the pinned case — because CSS cannot apply one declaration block from two
   selectors and a preprocessor is not worth adding for a single block.
   Two hand-maintained colour lists is precisely how --bg-card went missing, so
   the pairing is asserted by test rather than trusted: css-tokens.test.js
   checks the two blocks declare the same tokens with the same values. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #15171f;
    --bg-alt: #1a1d26;
    --surface: #1c1f2b;
    --canvas: #12141b;
    --border: #2e3344;
    --border-light: #232735;

    --text: #e8eaf2;
    --text-strong: #c3c8d8;
    --muted: #a9afc4;

    --accent: #818cf8;
    --accent-hover: #a5b4fc;
    --accent-light: #262a44;
    --accent-border: #3b4168;
  --on-primary: #10131c;
    /* Near-black on the lighter indigo: 8.9:1, where white was 2.98:1. */
    --on-primary: #10131c;

    --green: #4ade80;
    --green-bg: #15241c;
    --green-border: #27452f;
    --orange: #fbbf24;
    --orange-bg: #2a2114;
    --orange-border: #4a3a1c;
    --red: #f87171;
    --red-bg: #2a1718;
    --red-border: #4d2626;
    --blue: #60a5fa;
    --blue-bg: #15213a;
    --blue-border: #22365c;

    --gray-bg: #242837;
    --gray-text: #9aa1b8;
    --gray-border: #343a4d;

  --success-text: var(--green);
  --warning-text: var(--orange);
  --danger-text:  var(--red);
  --info-text:    var(--blue);
  --neutral-text: var(--gray-text);

    /* In dark the 400-level hues already clear AA on the dark tints
       (e.g. #4ade80 on #15241c is 8.9:1), so the text roles follow the fill. */
    --success-text: var(--green);
    --warning-text: var(--orange);
    --danger-text:  var(--red);
    --info-text:    var(--blue);
    --neutral-text: var(--gray-text);

    --chart-grid: #262b38;
    --chart-label: #8b93a9;
    --chart-axis: #5d667e;

    /* Dark heatmap ramp: the empty cell must RECEDE into the card, not sit on
       top of it. The light ramp's #f1f5f9 did the opposite. */
    --heat-0: #1e2230;
    --heat-1: #2b3152;
    --heat-2: #434d93;
    --heat-3: #5a66c4;
    --heat-4: #818cf8;

    --shadow: 0 1px 4px rgba(0,0,0,.4), 0 1px 2px rgba(0,0,0,.3);
    --shadow-lg: 0 4px 16px rgba(0,0,0,.5);

    --bg-card-hover: #242837;
    --bg-overlay: rgba(8, 9, 13, 0.6);
  }
}

:root[data-theme="dark"] {
  --bg: #15171f;
  --bg-alt: #1a1d26;
  --surface: #1c1f2b;
  --canvas: #12141b;
  --border: #2e3344;
  --border-light: #232735;

  --text: #e8eaf2;
  --text-strong: #c3c8d8;
  --muted: #a9afc4;

  --accent: #818cf8;
  --accent-hover: #a5b4fc;
  --accent-light: #262a44;
  --accent-border: #3b4168;
  /* Near-black on the lighter indigo: 8.9:1, where white was 2.98:1. */
  --on-primary: #10131c;

  /* In dark the 400-level hues already clear AA on the dark tints
     (e.g. #4ade80 on #15241c is 8.9:1), so the text roles follow the fill. */
  --success-text: var(--green);
  --warning-text: var(--orange);
  --danger-text:  var(--red);
  --info-text:    var(--blue);
  --neutral-text: var(--gray-text);

  --green: #4ade80;
  --green-bg: #15241c;
  --green-border: #27452f;
  --orange: #fbbf24;
  --orange-bg: #2a2114;
  --orange-border: #4a3a1c;
  --red: #f87171;
  --red-bg: #2a1718;
  --red-border: #4d2626;
  --blue: #60a5fa;
  --blue-bg: #15213a;
  --blue-border: #22365c;

  --gray-bg: #242837;
  --gray-text: #9aa1b8;
  --gray-border: #343a4d;

  --chart-grid: #262b38;
  --chart-label: #8b93a9;
  --chart-axis: #5d667e;

  --heat-0: #1e2230;
  --heat-1: #2b3152;
  --heat-2: #434d93;
  --heat-3: #5a66c4;
  --heat-4: #818cf8;

  --shadow: 0 1px 4px rgba(0,0,0,.4), 0 1px 2px rgba(0,0,0,.3);
  --shadow-lg: 0 4px 16px rgba(0,0,0,.5);

  --bg-card-hover: #242837;
  --bg-overlay: rgba(8, 9, 13, 0.6);
}
