/* map-vfx.css — transient combat effects on the battle map.
   Paired with map-vfx.js; see that file for why these are edge-triggered.

   RULES THIS FILE KEEPS

   1. transform and opacity ONLY. Nothing here animates a layout property.
      The map already refuses to `transition: left/top` on tokens and
      hand-rolls FLIP instead (map.css, .bfmap-token) because that transition
      was measurably janky with a full board — an effect layer is not the
      place to reintroduce the cost.
   2. Every effect is pointer-events: none. These sit over the tokens, and a
      600ms window where a creature cannot be clicked is a worse bug than any
      of them are worth.
   3. Colours come from the app's tokens (v2/colors_and_type.css), never
      literals, so an effect can never disagree with the icon in the combat
      log about what colour fire is. */

/* The layer. Inside .bfmap-world, so pan/zoom carry it like every other
   world-space layer (the same 1px-box + overflow trick .bfmap-target-arrows
   uses).

   z-index 32 — the SAME band as the targeting arrows, and for the same
   reason map.css gives them that number: this is a record drawn over the
   art. It clears a dragged token (30, the highest any token reaches) and
   stays under the token chrome at 33-36, which map.css reserves for readouts
   with the rule that "a readout ABOUT the creature must never be obscured by
   a record drawn over the art". An effect is exactly such a record, so a hit
   flash passes UNDER the target's own health bar rather than hiding the
   number the blow just changed. */
.bfmap-vfx {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  overflow: visible;
  pointer-events: none;
  z-index: 32;
}

.bfvfx {
  position: absolute;
  pointer-events: none;
  transform-origin: 50% 50%;
  will-change: transform, opacity;
}

/* The three generic children. Each effect below decides what they mean; most
   use them as ring / core / spray. Centred and full-bleed by default so an
   effect only has to describe its own motion. */
.bfvfx > i {
  position: absolute;
  inset: 0;
  display: block;
  border-radius: 50%;
  transform-origin: 50% 50%;
  will-change: transform, opacity;
}

/* ── Impact: Blunt ──────────────────────────────────────────────────────────
   A concussive ring that punches outward and stops dead, plus a brief white
   core. No lingering particles: blunt is the one damage type with no residue,
   and the animation says so by being the shortest and hardest-stopping. */
.bfvfx.is-blunt > i:nth-child(1) {
  border: 3px solid rgba(255, 255, 255, .95);
  animation: bfvfx-ring 470ms cubic-bezier(.2, .8, .3, 1) forwards;
}
.bfvfx.is-blunt > i:nth-child(2) {
  background: radial-gradient(circle, rgba(255,255,255,.9) 0%, rgba(255,255,255,0) 65%);
  animation: bfvfx-core 300ms ease-out forwards;
}
.bfvfx.is-blunt > i:nth-child(3) {
  border: 2px solid rgba(240, 164, 92, .7);   /* --c-orange, impact */
  animation: bfvfx-ring 470ms cubic-bezier(.2,.8,.3,1) 70ms forwards;
}

/* ── Impact: Sharp ──────────────────────────────────────────────────────────
   Two crossed gashes rather than a ring — a cut has a DIRECTION, and the one
   shape no other effect here uses is a straight line. */
.bfvfx.is-sharp > i:nth-child(1),
.bfvfx.is-sharp > i:nth-child(2) {
  border-radius: 0;
  inset: 46% 4%;
  background: linear-gradient(90deg,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,.98) 30%,
    rgba(248,106,99,.95) 70%,   /* --c-red */
    rgba(255,255,255,0) 100%);
}
.bfvfx.is-sharp > i:nth-child(1) { animation: bfvfx-slash-a 450ms cubic-bezier(.15,.85,.25,1) forwards; }
.bfvfx.is-sharp > i:nth-child(2) { animation: bfvfx-slash-b 450ms cubic-bezier(.15,.85,.25,1) 60ms forwards; }
.bfvfx.is-sharp > i:nth-child(3) { display: none; }

/* ── Elemental: Fire ────────────────────────────────────────────────────────
   Rises and fades. The only effect that drifts UPWARD off its tile, because
   fire is the only one whose real-world behaviour everyone already knows. */
.bfvfx.is-fire > i:nth-child(1) {
  background: radial-gradient(circle,
    rgba(255,236,170,.98) 0%,
    rgba(240,164,92,.92) 35%,   /* --c-orange */
    rgba(248,106,99,.55) 62%,   /* --c-red */
    rgba(248,106,99,0) 78%);
  animation: bfvfx-bloom 640ms ease-out forwards;
}
.bfvfx.is-fire > i:nth-child(2) {
  inset: 18%;
  background: radial-gradient(circle, rgba(255,246,214,.95) 0%, rgba(255,214,120,0) 70%);
  animation: bfvfx-flicker 640ms ease-out forwards;
}
.bfvfx.is-fire > i:nth-child(3) {
  inset: 30%;
  background: radial-gradient(circle, rgba(255,255,255,.9) 0%, rgba(255,255,255,0) 60%);
  animation: bfvfx-rise 640ms ease-out 80ms forwards;
}

/* ── Elemental: Ice ─────────────────────────────────────────────────────────
   Crystalline: a hard-edged shard burst that CONTRACTS to a frozen core
   instead of dispersing, and a rim that stays. Ice is the one element whose
   effect should end with something still on the tile. */
.bfvfx.is-ice > i:nth-child(1) {
  border-radius: 0;
  clip-path: polygon(50% 0%, 61% 35%, 100% 50%, 61% 65%, 50% 100%, 39% 65%, 0% 50%, 39% 35%);
  background: linear-gradient(140deg, rgba(255,255,255,.98), rgba(101,212,240,.85)); /* --c-cyan */
  animation: bfvfx-shard 640ms cubic-bezier(.2,.9,.3,1) forwards;
}
.bfvfx.is-ice > i:nth-child(2) {
  border: 2px solid rgba(101, 212, 240, .9);
  animation: bfvfx-ring 640ms cubic-bezier(.2,.8,.3,1) forwards;
}
.bfvfx.is-ice > i:nth-child(3) {
  inset: 28%;
  background: radial-gradient(circle, rgba(220,248,255,.95) 0%, rgba(122,166,255,0) 70%);
  animation: bfvfx-freeze 640ms ease-out 90ms forwards;
}

/* ── Elemental: Thunder ─────────────────────────────────────────────────────
   The shortest and highest-contrast effect in the set: a flash, not a bloom.
   Thunder is instantaneous in a way fire and blight are not, and the timing
   is the main thing carrying that. */
.bfvfx.is-thunder > i:nth-child(1) {
  border-radius: 0;
  clip-path: polygon(56% 0%, 30% 52%, 47% 52%, 40% 100%, 72% 44%, 53% 44%, 68% 0%);
  background: linear-gradient(180deg, rgba(255,255,255,1), rgba(246,222,125,.95)); /* --c-yellow */
  animation: bfvfx-bolt 430ms steps(1, end) forwards;
}
.bfvfx.is-thunder > i:nth-child(2) {
  background: radial-gradient(circle, rgba(246,222,125,.75) 0%, rgba(246,222,125,0) 62%);
  animation: bfvfx-core 430ms ease-out forwards;
}
.bfvfx.is-thunder > i:nth-child(3) {
  border: 2px solid rgba(246, 222, 125, .85);
  animation: bfvfx-ring 430ms cubic-bezier(.2,.8,.3,1) forwards;
}

/* ── Elemental: Blight ──────────────────────────────────────────────────────
   The venom/poison effect. Slowest in the set and the only one that SETTLES
   downward — blight is a lingering harm (it drives the poisoned/decay
   statuses), and an effect that dissipates upward like fire would say the
   opposite. */
.bfvfx.is-blight > i:nth-child(1) {
  background: radial-gradient(circle,
    rgba(150,236,150,.9) 0%,
    rgba(108,212,140,.7) 45%,   /* --c-green */
    rgba(108,212,140,0) 72%);
  animation: bfvfx-cloud 720ms ease-out forwards;
}
.bfvfx.is-blight > i:nth-child(2) {
  inset: 22%;
  background: radial-gradient(circle, rgba(196,248,180,.85) 0%, rgba(108,212,140,0) 68%);
  animation: bfvfx-cloud 720ms ease-out 110ms forwards;
}
.bfvfx.is-blight > i:nth-child(3) {
  inset: 34%;
  background: radial-gradient(circle, rgba(108,212,140,.8) 0%, rgba(108,212,140,0) 70%);
  animation: bfvfx-settle 720ms ease-in-out 60ms forwards;
}

/* ── Elemental: Seidr ───────────────────────────────────────────────────────
   Purple, per the design system's own reading of --c-purple ("WIL / seidr
   spells / channeled magic"). Counter-rotating rings: channelled magic is the
   one thing here that should look DELIBERATE rather than violent. */
.bfvfx.is-seidr > i:nth-child(1) {
  border: 2px solid rgba(198, 156, 255, .95);   /* --c-purple */
  animation: bfvfx-ring 660ms cubic-bezier(.2,.8,.3,1) forwards;
}
.bfvfx.is-seidr > i:nth-child(2) {
  border: 2px dashed rgba(198, 156, 255, .8);
  animation: bfvfx-spin-cw 660ms ease-out forwards;
}
.bfvfx.is-seidr > i:nth-child(3) {
  inset: 26%;
  background: radial-gradient(circle, rgba(230,214,255,.9) 0%, rgba(198,156,255,0) 70%);
  animation: bfvfx-core 660ms ease-out forwards;
}

/* ── Elemental: Psychic ─────────────────────────────────────────────────────
   Concentric ripples with no core — psychic damage has no impact point, and
   leaving the middle empty is what distinguishes it from seidr at a glance. */
.bfvfx.is-psychic > i:nth-child(1),
.bfvfx.is-psychic > i:nth-child(2),
.bfvfx.is-psychic > i:nth-child(3) {
  border: 2px solid rgba(198, 156, 255, .85);
  background: none;
}
.bfvfx.is-psychic > i:nth-child(1) { animation: bfvfx-ripple 660ms ease-out forwards; }
.bfvfx.is-psychic > i:nth-child(2) { animation: bfvfx-ripple 660ms ease-out 120ms forwards; }
.bfvfx.is-psychic > i:nth-child(3) { animation: bfvfx-ripple 660ms ease-out 240ms forwards; }

/* ── Healing ────────────────────────────────────────────────────────────────
   Rises, like fire, but green and soft-edged. The shared upward motion is
   deliberate: both are "something leaving the body", and the palette is what
   separates harm from repair. */
.bfvfx.is-heal > i:nth-child(1) {
  background: radial-gradient(circle, rgba(180,248,200,.85) 0%, rgba(108,212,140,0) 68%);
  animation: bfvfx-rise 700ms ease-out forwards;
}
.bfvfx.is-heal > i:nth-child(2) {
  inset: 30% 34%;
  background: radial-gradient(circle, rgba(220,255,230,.95) 0%, rgba(108,212,140,0) 70%);
  animation: bfvfx-rise 700ms ease-out 120ms forwards;
}
.bfvfx.is-heal > i:nth-child(3) {
  inset: 40% 20%;
  background: radial-gradient(circle, rgba(108,212,140,.8) 0%, rgba(108,212,140,0) 72%);
  animation: bfvfx-rise 700ms ease-out 240ms forwards;
}

/* ── Casts ──────────────────────────────────────────────────────────────────
   Played at the CASTER, not the target, and shaped to read as the opposite of
   an impact: they gather INWARD and pulse, where every hit effect above
   expands outward. A player should be able to tell "someone is casting" from
   "someone got hit" without reading a colour. */
.bfvfx.is-galdr > i:nth-child(1) {
  border: 2px solid rgba(101, 212, 240, .9);    /* --c-cyan, galdr */
  animation: bfvfx-gather 720ms cubic-bezier(.3,.7,.2,1) forwards;
}
.bfvfx.is-galdr > i:nth-child(2) {
  border: 2px dashed rgba(101, 212, 240, .75);
  animation: bfvfx-spin-ccw 720ms ease-out forwards;
}
.bfvfx.is-galdr > i:nth-child(3) {
  background: radial-gradient(circle, rgba(200,240,255,.55) 0%, rgba(101,212,240,0) 62%);
  animation: bfvfx-pulse 720ms ease-in-out forwards;
}

.bfvfx.is-seidr-cast > i:nth-child(1) {
  border: 2px solid rgba(198, 156, 255, .9);    /* --c-purple, seidr */
  animation: bfvfx-gather 760ms cubic-bezier(.3,.7,.2,1) forwards;
}
.bfvfx.is-seidr-cast > i:nth-child(2) {
  border: 2px dashed rgba(198, 156, 255, .75);
  animation: bfvfx-spin-cw 760ms ease-out forwards;
}
.bfvfx.is-seidr-cast > i:nth-child(3) {
  background: radial-gradient(circle, rgba(230,214,255,.55) 0%, rgba(198,156,255,0) 62%);
  animation: bfvfx-pulse 760ms ease-in-out forwards;
}

.bfvfx.is-heal-cast > i:nth-child(1) {
  border: 2px solid rgba(108, 212, 140, .9);    /* --c-green */
  animation: bfvfx-gather 720ms cubic-bezier(.3,.7,.2,1) forwards;
}
.bfvfx.is-heal-cast > i:nth-child(2) { display: none; }
.bfvfx.is-heal-cast > i:nth-child(3) {
  background: radial-gradient(circle, rgba(180,248,200,.5) 0%, rgba(108,212,140,0) 62%);
  animation: bfvfx-pulse 720ms ease-in-out forwards;
}

/* A crit widens the whole effect rather than adding a new shape to learn.
   Scaling the container means every damage type gets its crit tell for free,
   including ones added later. */
.bfvfx.is-crit { transform: scale(1.35); }

/* ── Keyframes ──────────────────────────────────────────────────────────────
   transform + opacity only, throughout. */

@keyframes bfvfx-ring {
  from { transform: scale(.25); opacity: .95; }
  to   { transform: scale(1.5); opacity: 0; }
}
@keyframes bfvfx-core {
  from { transform: scale(.4); opacity: 1; }
  to   { transform: scale(1.15); opacity: 0; }
}
@keyframes bfvfx-bloom {
  0%   { transform: scale(.3); opacity: 1; }
  55%  { transform: scale(1.05); opacity: .9; }
  100% { transform: scale(1.25) translateY(-14%); opacity: 0; }
}
@keyframes bfvfx-flicker {
  0%   { transform: scale(.5) translateY(0); opacity: 1; }
  40%  { transform: scale(.95) translateY(-8%); opacity: .85; }
  70%  { transform: scale(.8) translateY(-16%); opacity: .6; }
  100% { transform: scale(1) translateY(-28%); opacity: 0; }
}
@keyframes bfvfx-rise {
  from { transform: scale(.6) translateY(10%); opacity: .95; }
  to   { transform: scale(1.05) translateY(-46%); opacity: 0; }
}
@keyframes bfvfx-slash-a {
  0%   { transform: rotate(-32deg) scaleX(0); opacity: 0; }
  25%  { transform: rotate(-32deg) scaleX(1); opacity: 1; }
  100% { transform: rotate(-32deg) scaleX(1.12); opacity: 0; }
}
@keyframes bfvfx-slash-b {
  0%   { transform: rotate(38deg) scaleX(0); opacity: 0; }
  25%  { transform: rotate(38deg) scaleX(1); opacity: 1; }
  100% { transform: rotate(38deg) scaleX(1.12); opacity: 0; }
}
@keyframes bfvfx-shard {
  0%   { transform: scale(.2) rotate(-25deg); opacity: 1; }
  60%  { transform: scale(1.1) rotate(6deg); opacity: .9; }
  100% { transform: scale(.85) rotate(0deg); opacity: 0; }
}
@keyframes bfvfx-freeze {
  from { transform: scale(1.3); opacity: 0; }
  40%  { opacity: .9; }
  to   { transform: scale(.7); opacity: 0; }
}
@keyframes bfvfx-bolt {
  0%   { transform: scale(1.05); opacity: 1; }
  35%  { transform: scale(1.05); opacity: 0; }
  55%  { transform: scale(1.05); opacity: .95; }
  100% { transform: scale(1.05); opacity: 0; }
}
@keyframes bfvfx-cloud {
  0%   { transform: scale(.35); opacity: 0; }
  35%  { transform: scale(.95); opacity: .9; }
  100% { transform: scale(1.3); opacity: 0; }
}
@keyframes bfvfx-settle {
  from { transform: scale(.5) translateY(-8%); opacity: .9; }
  to   { transform: scale(1.1) translateY(18%); opacity: 0; }
}
@keyframes bfvfx-ripple {
  from { transform: scale(.2); opacity: .85; }
  to   { transform: scale(1.45); opacity: 0; }
}
@keyframes bfvfx-spin-cw {
  from { transform: scale(.3) rotate(0deg); opacity: .9; }
  to   { transform: scale(1.35) rotate(150deg); opacity: 0; }
}
@keyframes bfvfx-spin-ccw {
  from { transform: scale(1.35) rotate(0deg); opacity: 0; }
  35%  { opacity: .9; }
  to   { transform: scale(.45) rotate(-150deg); opacity: 0; }
}
@keyframes bfvfx-gather {
  from { transform: scale(1.6); opacity: 0; }
  45%  { opacity: .95; }
  to   { transform: scale(.35); opacity: 0; }
}
@keyframes bfvfx-pulse {
  0%   { transform: scale(.4); opacity: 0; }
  50%  { transform: scale(1.05); opacity: .9; }
  100% { transform: scale(.8); opacity: 0; }
}

/* Reduced motion. map-vfx.js already declines to spawn anything when this is
   set, so this block is the backstop for a preference that flips mid-session
   while nodes are already on the board: collapse them instead of leaving a
   frozen shape sitting on a tile forever. */
@media (prefers-reduced-motion: reduce) {
  .bfvfx, .bfvfx > i {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
  }
}
