/* ══════════════════════════════════════════════════════════════════════════
   fx-tokens.css — the FX design language (FX-1/FX-2).

   Single source of truth for every gameplay-animation color, duration and
   easing across ALL THREE animation layers:
     · arena MECHANIC_FX      (app.js + the cob-/rules- keyframes in styles.css)
     · codex event feed       (codex-anim.js primitives)
     · premium effect icons   (co-effect-icons.js)

   GOVERNANCE: animation code must reference var(--fx-*) — never a raw hex
   except as the var() FALLBACK argument (see below). The FX-5 drift guard
   enforces this. Palette values derive from the board's own design tokens
   (frontend/design/cob_v3/tokens.json): the battlefield outlines attackers
   in attackRed and blockers in blockGreen; the FX layer speaks the same
   language.

   THEME FALLBACK (FX-2): the color tokens are scoped to
   html[data-fx-theme="unified"] — NOT :root — and every retheme call site
   keeps its pre-FX-2 color as the var() fallback:
       box-shadow: 0 0 26px var(--fx-harm-a80, #e0564acc);
   With the attribute set (the default; app.js/codex.js/the playground set it
   from localStorage "fx-theme"), everything reads the unified palette.
   Remove it and every call site falls back to its EXACT original hex —
   a pixel-perfect "classic" mode with one attribute flip, no duplicated CSS.

   Alpha ladder: each family ships -a80/-a60/-a40/-a20 stops (hex8 literals,
   ~80/60/40/20% opacity) because an alpha suffix cannot be applied to a var()
   at the call site, and color-mix(var(--x)) would break the classic fallback
   (an undefined var inside color-mix invalidates the whole declaration).

   The 2026-07-04 audit that motivated this found ~30 ungoverned hexes
   (7 reds, 8 greens, 5 blues, 5 purples, 6 golds) and cross-layer
   contradictions (codex attack beam gold vs. board attacker red; poison
   green in the arena but red in codex floats).
   ══════════════════════════════════════════════════════════════════════════ */

html[data-fx-theme="unified"] {
  /* ── Semantic color tokens (8 families) ────────────────────────────────
     One meaning → one color. -bright is the highlight/flash stop of the
     same family; -deep is the shadow/smoulder stop. */

  /* harm — damage, death, destruction, attack beams, life loss */
  --fx-harm:            #d45b4f;   /* = tokens.json accents.attackRed */
  --fx-harm-bright:     #f0837a;
  --fx-harm-deep:       #7a1f16;
  --fx-harm-a80: #d45b4fcc; --fx-harm-a60: #d45b4f99;
  --fx-harm-a40: #d45b4f66; --fx-harm-a20: #d45b4f33;
  --fx-harm-deep-a40: #7a1f1666;

  /* vital — life gain, +1/+1 counters, regeneration, block beams */
  --fx-vital:           #72dc91;   /* = tokens.json accents.blockGreen */
  --fx-vital-bright:    #a5f0bd;
  --fx-vital-deep:      #1f6e3d;
  --fx-vital-a80: #72dc91cc; --fx-vital-a60: #72dc9199;
  --fx-vital-a40: #72dc9166; --fx-vital-a20: #72dc9133;

  /* toxic — poison and deathtouch ONLY (distinct from vital on purpose) */
  --fx-toxic:           #5cbf4e;
  --fx-toxic-bright:    #8fe07f;
  --fx-toxic-deep:      #1f6e16;
  --fx-toxic-a80: #5cbf4ecc; --fx-toxic-a60: #5cbf4e99;
  --fx-toxic-a40: #5cbf4e66; --fx-toxic-a20: #5cbf4e33;
  --fx-toxic-deep-a80: #1f6e16cc;

  /* arcane — casts, zone motion, mana, tokens, reveals, resolution (the
     "magic default"; matches the board's gold chrome) */
  --fx-arcane:          #d9af5d;   /* = tokens.json accents.gold */
  --fx-arcane-bright:   #f0d488;   /* = accents.goldHover */
  --fx-arcane-deep:     #8a6a2e;
  --fx-arcane-a80: #d9af5dcc; --fx-arcane-a60: #d9af5d99;
  --fx-arcane-a40: #d9af5d66; --fx-arcane-a20: #d9af5d33;
  --fx-arcane-bright-a80: #f0d488cc; --fx-arcane-bright-a60: #f0d48899;
  --fx-arcane-bright-a40: #f0d48866; --fx-arcane-bright-a20: #f0d48833;

  /* exile — exile flights and rifts (amber, deliberately apart from gold) */
  --fx-exile:           #efa94d;   /* = tokens.json accents.threatAmber */
  --fx-exile-bright:    #ffcf8a;
  --fx-exile-deep:      #8f5c1d;
  --fx-exile-a80: #efa94dcc; --fx-exile-a60: #efa94d99;
  --fx-exile-a40: #efa94d66; --fx-exile-a20: #efa94d33;

  /* aether — counterspells, bounce, blink, wards, protection, keywords */
  --fx-aether:          #7db8ff;
  --fx-aether-bright:   #bfe6ff;
  --fx-aether-deep:     #2c5a9e;
  --fx-aether-a80: #7db8ffcc; --fx-aether-a60: #7db8ff99;
  --fx-aether-a40: #7db8ff66; --fx-aether-a20: #7db8ff33;
  --fx-aether-bright-a80: #bfe6ffcc; --fx-aether-bright-a60: #bfe6ff99;

  /* dominion — copy, control theft, reanimation (mind + necromancy) */
  --fx-dominion:        #b389ff;
  --fx-dominion-bright: #d8c2ff;
  --fx-dominion-deep:   #7c5ed2;   /* = tokens.json accents.priorityPurple */
  --fx-dominion-a80: #b389ffcc; --fx-dominion-a60: #b389ff99;
  --fx-dominion-a40: #b389ff66; --fx-dominion-a20: #b389ff33;

  /* neutral — info floats, structural text, non-semantic chrome */
  --fx-neutral:         #cdd6e0;
  --fx-neutral-deep:    #7d8894;
  --fx-neutral-a80: #cdd6e0cc; --fx-neutral-a60: #cdd6e099;
}

:root {
  /* ── Duration scale ────────────────────────────────────────────────────
     Named stops, matched to the (already coherent) measured clusters.
     Pick the nearest stop; don't invent new durations. Theme-invariant:
     FX-2 changes no timing, so these live at :root. */
  --fx-t-snap:      260ms;   /* mechanical state change: tap/untap, deflate  */
  --fx-t-pop:       550ms;   /* badge/counter pop, token materialise         */
  --fx-t-core:      650ms;   /* standard one-shot FX: damage, glow, poof     */
  --fx-t-flight:    810ms;   /* zone flights (spring — see easing note)      */
  --fx-t-float:    1400ms;   /* rising text floats (readability-locked)      */
  --fx-t-banner:   1900ms;   /* turn/phase/outcome banners                   */
  --fx-t-spectacle: 1850ms;  /* dice, coin, big one-off set pieces           */

  /* ── Easing vocabulary ────────────────────────────────────────────────
     Zone flights use the locked JS spring (stiffness 63, damping 12,
     app.js _RULES_SPRING) — that curve cannot live in CSS; these cover
     everything else. */
  --fx-e-out:    cubic-bezier(.25, .6, .3, 1);     /* default decay          */
  --fx-e-pop:    cubic-bezier(.2, .8, .3, 1.4);    /* overshoot pop-in       */
  --fx-e-glide:  cubic-bezier(.3, .7, .3, 1);      /* floats, drifts         */
  --fx-e-settle: cubic-bezier(.34, 1.25, .5, 1);   /* tap rotate, snap-backs */
  --fx-e-gate:   cubic-bezier(.2, .85, .3, 1);     /* enters, reveals        */
}

/* ── Glow recipes ──────────────────────────────────────────────────────────
   The ONE ring/aura treatment, parameterized by semantic color. Use these
   classes (or copy the recipe with --fx-* stops) instead of hand-rolling
   box-shadows per animation:

     ring:  box-shadow: 0 0 0 2px C, 0 0 24px C-a60;
     aura:  box-shadow: 0 0 12px 3px C-a40, 0 0 28px 8px C-a20;

   .fx-ring-* are provided for direct class use (unified theme only).      */
.fx-ring-harm     { box-shadow: 0 0 0 2px var(--fx-harm),     0 0 24px var(--fx-harm-a60); }
.fx-ring-vital    { box-shadow: 0 0 0 2px var(--fx-vital),    0 0 24px var(--fx-vital-a60); }
.fx-ring-toxic    { box-shadow: 0 0 0 2px var(--fx-toxic),    0 0 24px var(--fx-toxic-a60); }
.fx-ring-arcane   { box-shadow: 0 0 0 2px var(--fx-arcane),   0 0 24px var(--fx-arcane-a60); }
.fx-ring-exile    { box-shadow: 0 0 0 2px var(--fx-exile),    0 0 24px var(--fx-exile-a60); }
.fx-ring-aether   { box-shadow: 0 0 0 2px var(--fx-aether),   0 0 24px var(--fx-aether-a60); }
.fx-ring-dominion { box-shadow: 0 0 0 2px var(--fx-dominion), 0 0 24px var(--fx-dominion-a60); }

/* Float-pill: the one treatment for rising text (FX-3 unifies arena + codex
   floats onto this). Color via the semantic text classes below. */
.fx-pill {
  font-weight: 700; font-size: 15px; line-height: 1;
  padding: 4px 10px; border-radius: 999px; white-space: nowrap;
  /* FX-6: solid-ish bg instead of backdrop-filter:blur (a per-frame scene
     readback) — reads identically over board art, composites for free. */
  background: rgba(14, 18, 27, .86);
  box-shadow: 0 2px 10px #0007, 0 0 0 1px #ffffff14;
  text-shadow: 0 1px 3px #000a;
}
/* FX-3: the arena's rising floats adopt the pill chrome under the unified
   theme (codex-anim floats already have it) — one float language everywhere.
   Classic theme keeps the bare-text originals: these rules simply don't match
   without the attribute. Colors still come from the elements' own classes. */
html[data-fx-theme="unified"] .rules-lifefx,
html[data-fx-theme="unified"] .cob-v3 .cob-ctr-float {
  padding: 3px 9px; border-radius: 999px;
  background: rgba(14, 18, 27, .86);   /* FX-6: was backdrop-filter:blur */
  box-shadow: 0 2px 10px #0007, 0 0 0 1px #ffffff14;
  text-shadow: 0 1px 3px #000a;
}

.fx-c-harm     { color: var(--fx-harm-bright, #ff6b6b); }
.fx-c-vital    { color: var(--fx-vital, #7ee0a0); }
.fx-c-toxic    { color: var(--fx-toxic-bright, #7fd45e); }
.fx-c-arcane   { color: var(--fx-arcane-bright, #f0d488); }
.fx-c-exile    { color: var(--fx-exile-bright, #d4a830); }
.fx-c-aether   { color: var(--fx-aether, #7db8ff); }
.fx-c-dominion { color: var(--fx-dominion, #b389ff); }
.fx-c-neutral  { color: var(--fx-neutral, #cdd6e0); }
