/* ============================================================================
   The opponent's face-down fan — material, arrival, the beat of a draw.

   WHAT WAS WRONG. Measured on an 844x288 page box, the lane's per-index cap
   parks card 4 onward at the same `left`, so a five-card hand and a ten-card
   hand painted the SAME picture: three readable cards and an undifferentiated
   slab. The slab was not the cap's fault. Each back carried `#game-view .card`'s
   generic `1px solid rgba(0,0,0,0.15)` border and `0 3px 6px` downward drop
   shadow — and the fan overlaps LEFT over RIGHT, so a shadow thrown straight
   down lands on nothing and two abutting navy borders read as one navy bar.
   Nothing separated a card from the card it covered.

   WHAT THIS DOES. Gives the back the three things that make a stack of paper
   read as a stack of paper: a hard crease thrown onto the neighbour beneath it,
   lit top/left edges and unlit bottom/right ones that agree with the board
   art's upper-left key light, and a real tooth instead of a flat fill. The
   count becomes countable again without moving one pixel of the lane.

   AND WHAT A DRAW DOES. The pile ticks (two frames of anticipation), the card
   arcs OUT of it and banks into the fan, one narrow band of light rakes across
   it as it touches down, the same band travels dimmer down the cards behind it,
   and the two it landed against turn and slide aside before recovering. Every
   number here is a fraction of the card, never a pixel: the same gesture on a
   48px phone back and a 265px 4K one.

   THE LANE IS NOT OURS TO MOVE. The per-index `left` caps and the `rotate()`
   fan in game-board.css are FUNCTIONAL geometry: board.js targets
   `#opponent-hand .card`, cards.js builds reveals into it, and enemy-replay
   reads it as the draw-flight origin. So every animation here composes through
   the INDEPENDENT `translate`/`rotate`/`scale` properties, which apply BEFORE
   `transform` and therefore leave the fan's own transform completely intact.
   A keyframe that set `transform` would silently flatten the fan.

   AND IT IS SECONDARY. The board is the story. Everything here is short, local
   or dark — the fan should read at a glance and then be ignored.

   Contracts: tests/unit/styles/enemyFanMaterial.test.js,
              tests/unit/ui/enemyFan.test.js.
   ========================================================================== */

#game-view #opponent-hand .card-back {
    /* One scaling token for the whole material, so a 48px phone back and a
       265px 4K back wear the same card stock rather than the same pixels.
       --card-unit is cards.css's card-size scalar (1vmin, floored on phones). */
    --fan-edge: max(1px, calc(var(--card-unit, 1vmin) * 0.24));
    --fan-drop: calc(var(--card-unit, 1vmin) * 0.62);
    /* The overshoot scale: how far past its resting pose a card swings before
       it settles, and the fallback distance an arrival travels when the deck
       has not been built yet and there is no measured origin to fly from. */
    --fan-draw-in: calc(var(--card-unit, 1vmin) * 6.4);
    /* A shoulder-aside is a fraction of the CARD, not of the travel: below
       ~3% of the card's width it stops reading as one card moving out of
       another's way and starts reading as a rendering wobble, and past ~4% a
       secondary element starts shoving the row around.
       Measure this against the card's WIDTH, never its bounding box: these
       cards are rotated, so `getBoundingClientRect().width` returns the
       axis-aligned box — 101px for a 79px card at 1400x640 — and reading that
       as the card is how a 3.6% shift briefly got "corrected" to 4.5%. */
    --fan-shoulder-shift: calc(var(--card-width, 12.29vmin) * 0.036);

    /* Every layer throws right-and-down, away from the same upper-left key
       light the board art uses. Layer 1 is the CREASE — zero blur, so it reads
       as contact rather than haze, and it is the line that separates a back
       from the back it covers. */
    box-shadow:
        var(--fan-edge) var(--fan-edge) 0 rgba(4, 1, 12, 0.55),
        calc(var(--fan-edge) * 2.4) calc(var(--fan-edge) * 3) calc(var(--fan-edge) * 4.4) rgba(4, 1, 12, 0.42),
        0 var(--fan-drop) calc(var(--fan-drop) * 2) rgba(4, 1, 12, 0.34);

    /* Colour only. The border is part of the box the lane measures, so its
       WIDTH is geometry and stays exactly where game-board.css put it.
       Mid-dark, not near-black: the crease above is already doing the
       separating, and stacking a third dark layer on the same edge turned the
       back into a sticker with a printed keyline around it. */
    border-color: rgba(14, 7, 26, 0.66);
}

/* The material rides above the sharpened cardback texture and below the glint.
   Static: nothing animates this layer, so a resting fan costs no frames. */
#game-view #opponent-hand .card-back::after {
    content: "";
    position: absolute;
    inset: 0;
    /* board.js hit-tests these cards; an overlay must never eat the press. */
    pointer-events: none;
    background-image:
        /* The lit edge. Bright and THIN reads as a catchlight on the cut edge
           of the stock; wide and soft reads as haze over the art, which is
           what the first pass shipped. */
        linear-gradient(to right,
            rgba(255, 250, 236, 0.62) 0 1.1%,
            rgba(255, 250, 236, 0) 1.1%),
        linear-gradient(to bottom,
            rgba(255, 250, 236, 0.5) 0 0.7%,
            rgba(255, 250, 236, 0) 0.7%),
        /* The bottom edge is the one face of the stock the player actually
           looks along in a hanging fan, so it alone carries the unlit side.
           Left/right separation is the crease's job — doing it here as well
           put three dark layers on one edge. */
        linear-gradient(to top,
            rgba(5, 2, 14, 0.34) 0 1%,
            rgba(5, 2, 14, 0) 1%),
        /* specular rake, along the same light */
        linear-gradient(107deg,
            rgba(255, 244, 222, 0.1) 0%,
            rgba(255, 244, 222, 0.03) 18%,
            rgba(255, 244, 222, 0) 36%),
        /* and a shade away from it: form, plus the restraint that keeps a
           secondary element from out-shouting the board. Kept light — the
           cardback is the player's own chosen art and must not be muddied. */
        linear-gradient(115deg,
            rgba(9, 4, 20, 0) 58%,
            rgba(9, 4, 20, 0.17) 100%),
        /* paper tooth — a stitched fractal tile, not a repeating gradient,
           which would moire against the fan's rotation */
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cfilter id='t'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='24' height='24' filter='url(%23t)' opacity='0.07'/%3E%3C/svg%3E");
    background-repeat: no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, repeat;
    /* The grain tile is offset per card (UI.EnemyFan writes --fan-index), so
       overlapping backs never line their tooth up. Identical grain across a
       stack reads as one printed sheet, which is exactly the slab this
       material exists to break. Static offsets — nothing here animates. */
    background-position:
        0 0, 0 0, 0 0, 0 0, 0 0,
        calc(var(--fan-index, 0) * 7px) calc(var(--fan-index, 0) * -11px);
}

/* The glint: ONE narrow band of light, held above the material, that rakes
   across the card and off its right edge. `.card` is overflow:hidden, so the
   band lives outside the box until it is driven through it.

   It replaced a full-card cream wash on the arrival and an opacity ramp on the
   material for the settle. The wash read as the card being lit from in front
   rather than raked, and modulating the material's own opacity LIGHTENED the
   shadow underneath it — the one part of the layer that must never brighten.
   A band that moves says "a surface caught the light"; a fade says "an element
   changed opacity". */
#game-view #opponent-hand .card-back::before {
    content: "";
    position: absolute;
    top: -24%;
    bottom: -24%;
    left: -46%;
    width: 30%;
    /* The rake angle lives in the BOX, not in the gradient. A skewed gradient
       inside a tall narrow box is a trap: the gradient line for `102deg` is
       `0.98W + 0.21H` long, which on a card-shaped box is twice the box's own
       width — so both transparent ends fall outside it and the whole element
       lights up as a flat wash. Tilting the box and running the gradient
       straight across its width makes the stops mean what they say. */
    rotate: 9deg;
    z-index: 3;
    pointer-events: none;
    opacity: 0;
    background-image: linear-gradient(to right,
        rgba(255, 240, 206, 0) 0%,
        rgba(255, 249, 228, 0.95) 50%,
        rgba(255, 240, 206, 0) 100%);
}

/* --- The beat of a draw ---------------------------------------------------
   Each effect owns its own target, so no two can ever contend for a property:
   the arrival drives the CARD, the glint drives ::before, the material on
   ::after is never animated at all.

   Nothing here loops. An always-on idle shipped in two earlier passes and both
   measured as a WebKit regression against a paired baseline:

     sub-pixel translate/rotate tremor   14-21 dropped frames / 4s (base 0-3)
     opacity breathing on ::after         9-13 dropped frames / 4s (base 1-9)

   and the cost did NOT scale with how many cards animated — one element cost
   the same as eleven. It is the fixed price of keeping the compositor awake,
   not per-card work, so trimming it down could never have paid. WebKit is the
   engine behind every iPhone browser, and an animation that never stops is
   also battery the player never agreed to spend on a secondary element. */

/* `will-change` is safe here precisely BECAUSE these classes are one-shot: the
   hint arrives with the class and leaves with it, so nothing holds a layer
   open at rest. A back is a rotated, overflow-hidden box carrying two
   pseudo-element layers (the material and the band), and without the hint
   every frame of the arrival re-rasterizes the SVG-turbulence material
   underneath it. */
#game-view #opponent-hand .card-back.enemy-fan-arrive {
    animation: enemy-fan-arrive 420ms linear var(--fan-arrive-delay, 0ms) backwards;
    will-change: translate, rotate, scale, opacity;

    /* THE CARD IN FLIGHT MUST NOT BE HIT-TESTABLE — and this is a deliberate
       trade, not a tidy-up.

       The arriving card is the fan's own `.card-back`: `pointer-events: auto`,
       `z-index: 10`, and for ~620ms its box travels across the play area. On
       the 844x288 page box that put it over opponent board slot 0 for ~67ms of
       every draw, in Chromium AND Firefox, measured with elementFromPoint at
       each slot centre through a frozen flight. A tap aimed at a board slot
       landed on the opponent's flying card.

       WHAT THIS COSTS: `#opponent-hand .card` is in board.js's targeting
       selector (board.js:111), so for the duration of the flight the arriving
       card stops being a legitimate spell target. That is defensible — a card
       mid-draw-flight is not in its authoritative resting place, and the
       alternative is the player's board-slot tap being stolen by a decoration.
       It is NOT free, and it is scoped as tightly as it can be: this class
       only, for this animation only. The lane keeps its hit targets, every
       settled back keeps its own, and the card is hit-testable again the
       moment the class expires. Locked both ways by the flight/at-rest pair in
       enemy-fan.spec.js. */
    pointer-events: none;
}

#game-view #opponent-hand .card-back.enemy-fan-glint::before {
    animation: enemy-fan-glint 130ms linear var(--fan-glint-delay, 0ms) backwards;
    will-change: translate, opacity;
}

#game-view #opponent-hand .card-back.enemy-fan-shoulder {
    animation: enemy-fan-shoulder 300ms cubic-bezier(0.33, 0, 0.2, 1) var(--fan-shoulder-delay, 0ms) backwards;
    will-change: translate, rotate;
}

/* --- Departure ----------------------------------------------------------
   enemy-replay clones the back and flies it to the board, but the original
   stayed put, so a summon briefly showed the card twice. This fades the one
   left behind. Presentation only: the box stays in the layout and keeps its
   hit target, and the authoritative snapshot is what actually removes it. */
#game-view #opponent-hand .card-back.enemy-fan-departing {
    animation: enemy-fan-depart 280ms cubic-bezier(0.36, 0, 0.9, 0.42) forwards;
}

/* --- The pile the card comes out of --------------------------------------
   Two frames of anticipation before the flick. The deck carries a `transform`
   hover of its own (indicators.css), so this lifts through the independent
   properties and cannot fight it. */
#game-view #opponent-deck.enemy-fan-deck-tick {
    animation: enemy-fan-deck-tick 190ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
}

@keyframes enemy-fan-deck-tick {
    0% {
        translate: 0 0;
        rotate: 0deg;
    }

    /* Up and over in two frames, held for one, then let down. A shove rather
       than a bounce — the pile has weight and does not spring. */
    18% {
        translate: 0 -6%;
        rotate: -1.6deg;
    }

    34% {
        translate: 0 -6%;
        rotate: -1.6deg;
    }

    100% {
        translate: 0 0;
        rotate: 0deg;
    }
}

/* The card leaves the pile, bows down through the empty board space, banks
   over, and settles past its resting pose before coming to rest. --fan-from-*
   and --fan-mid-* are measured against the live deck by UI.EnemyFan; the
   fallbacks are card-relative and only used before the deck exists.

   The per-key timing is the shape of the throw: it accelerates out of the
   pile, decelerates into contact, then two decaying swings. One easing across
   the whole animation would ease into and out of the arc's own apex, which
   reads as the card pausing in mid-air. */
@keyframes enemy-fan-arrive {
    0% {
        translate: var(--fan-from-x, var(--fan-draw-in)) var(--fan-from-y, calc(var(--fan-draw-in) * -0.5));
        rotate: -7deg;
        scale: 0.94;
        opacity: 0;
        animation-timing-function: cubic-bezier(0.36, 0, 0.62, 0.42);
    }

    /* Opaque almost immediately. A card that fades in over the first third of
       its flight is a second wash of light on top of the glint, and the two
       together were the "double-wash" this pass exists to kill. */
    15% {
        opacity: 1;
    }

    46% {
        translate: var(--fan-mid-x, calc(var(--fan-draw-in) * 0.46)) var(--fan-mid-y, calc(var(--fan-draw-in) * 0.12));
        rotate: -3.6deg;
        scale: 1.03;
        animation-timing-function: cubic-bezier(0.18, 0.62, 0.32, 1);
    }

    76% {
        translate: calc(var(--fan-draw-in) * -0.1) calc(var(--fan-draw-in) * 0.06);
        rotate: 1.5deg;
        scale: 1;
        animation-timing-function: ease-out;
    }

    90% {
        translate: calc(var(--fan-draw-in) * 0.035) calc(var(--fan-draw-in) * -0.02);
        rotate: -0.5deg;
        animation-timing-function: ease-out;
    }

    100% {
        translate: 0 0;
        rotate: 0deg;
        scale: 1;
        opacity: 1;
    }
}

/* One pass of the band, left to right, at the intensity the caller asked for:
   bright on the card that arrived, a hint on the stack it landed against. The
   band is 30% of the card wide and parked 46% off its left edge, so 490% of
   its OWN width carries it clean off the right. */
@keyframes enemy-fan-glint {
    0% {
        translate: 0 0;
        opacity: 0;
    }

    18% {
        opacity: var(--fan-glint-power, 0.36);
    }

    74% {
        opacity: var(--fan-glint-power, 0.36);
    }

    100% {
        translate: 490% 0;
        opacity: 0;
    }
}

/* A card nudged at one corner TURNS before it slides, so the rotation peaks at
   26% and the displacement at 44%. --fan-shoulder-power decays it: the card the
   arrival landed against moves fully, the one behind that half as far. */
@keyframes enemy-fan-shoulder {
    0% {
        translate: 0 0;
        rotate: 0deg;
    }

    26% {
        translate: calc(var(--fan-shoulder-shift) * -0.16 * var(--fan-shoulder-power, 1)) 0;
        rotate: calc(-1.4deg * var(--fan-shoulder-power, 1));
    }

    44% {
        translate: calc(var(--fan-shoulder-shift) * -1 * var(--fan-shoulder-power, 1)) calc(var(--fan-shoulder-shift) * 0.3 * var(--fan-shoulder-power, 1));
        rotate: calc(-0.85deg * var(--fan-shoulder-power, 1));
    }

    100% {
        translate: 0 0;
        rotate: 0deg;
    }
}

@keyframes enemy-fan-depart {
    0% {
        translate: 0 0;
        scale: 1;
        opacity: 1;
    }

    100% {
        translate: 0 calc(var(--fan-draw-in) * -0.28);
        scale: 0.94;
        opacity: 0;
    }
}

/* Nothing here carries information the still fan does not: how many cards the
   opponent holds is the count, and the count is unaffected. So a reduced-motion
   player loses decoration only — but must never be left holding a card frozen
   at opacity 0 by a `backwards`/`forwards` fill. */
@media (prefers-reduced-motion: reduce) {

    #game-view #opponent-hand .card-back.enemy-fan-arrive,
    #game-view #opponent-hand .card-back.enemy-fan-glint::before,
    #game-view #opponent-hand .card-back.enemy-fan-shoulder,
    #game-view #opponent-hand .card-back.enemy-fan-departing {
        animation: none;
        /* The classes are still applied — only the motion is off — so without
           this the compositor holds a layer per card for the life of a flight
           that is never going to draw a frame. */
        will-change: auto;
    }

    /* Kept out of the group above: every selector that mentions the hand must
       end at `.card-back`, so a revealed FACE in the same row can never be
       reached. The pile is a different element and gets its own rule. */
    #game-view #opponent-deck.enemy-fan-deck-tick {
        animation: none;
    }

    /* The arrival and the departure both fill, so switching them off has to
       restore the pose they would otherwise have frozen. The glint's ::before
       rests at opacity 0 and needs no restoring — with the animation gone it
       simply never appears. */
    #game-view #opponent-hand .card-back.enemy-fan-arrive,
    #game-view #opponent-hand .card-back.enemy-fan-departing {
        opacity: 1;
    }
}
