/* ── The deck ──────────────────────────────────────────────────────────────
   Every card face here is a scan of a real card — 45 jokers and one back, cut
   from the artist's own collection by the two tools in site/tools/. This file
   owns the card body and its states; js/card-art.js owns which scan goes where.
   .card, .card__inner and the two .card__side elements are the contract.

   The jokers are on the PILE. The hand is five backs until one is chosen.
   States:  face-down (b/w back)  ->  selected (turns over, red card)

   DISSOLVES ARE OPACITY AND BRIGHTNESS, NOT `mix-blend-mode`. Where two pictures
   overlap on the pile, both are lifted toward white and lose contrast while they
   do — which is what a bleached print looks like, and it reads as one exposure
   laid over another rather than as a cut.

   `mix-blend-mode: screen` is the obvious way to write this and it was tried
   first. It does not survive this structure — a blended image inside
   `.pile__card` renders as a solid black rectangle, and the same markup outside
   the pile renders correctly, so it is a compositing failure rather than a
   mistake in the rules. It is not worth chasing: brightness and opacity are
   animatable, composited, and cannot fail this way. A card is always an
   ordinary opaque image.
   ------------------------------------------------------------------------ */

.deck {
  position: relative; z-index: 3;
  padding: clamp(.8rem, 2.5vh, 1.6rem) 0 0;
  /* Landing sits the row in the middle of the screen; selecting a card moves
     the whole deck to the top and it stays there. One transform, so the move
     is a single continuous gesture rather than a relayout. */
  transform: translateY(var(--deck-y, 0));
  transition: transform .85s var(--ease-out);
  will-change: transform;
}
body[data-stage="deck"] .deck,
body[data-stage="hand"] .deck { --deck-y: calc(50vh - 50% - 1.6rem); }
/* Open: the row sits in the MIDDLE of the band between the top of the screen
   and the module, rather than hard against the top edge.

   The offset is derived, not typed. The band runs from 0 to the panel's top,
   so its centre is `(deckPadding + railHeight + panelMargin) / 2`, while the
   row's own centre is `deckPadding + railHeight / 2`. The difference reduces to
   half the panel's margin minus half the deck's padding — the rail height
   cancels out entirely, which is why this needs no measurement and holds at any
   viewport. `--lower` comes from panel.css, so pushing the module down moves
   the row with it and the two cannot drift apart. */
body[data-stage="open"] .deck {
  --deck-y: calc((clamp(1.4rem, 4vh, 2.6rem) + var(--lower, 0px)
                  - clamp(.8rem, 2.5vh, 1.6rem)) / 2);
}

.deck__rail {
  display: flex;
  gap: clamp(.4rem, 1.5vw, 1.2rem);
  justify-content: center;
  align-items: flex-start;
  margin: 0; padding: 0 clamp(1rem, 4vw, 3rem);
  list-style: none;
}

.deck__slot { flex: 0 0 auto; }
/* dealt one after another, left to right — and only once the pile is cut */
body[data-stage="hand"] .deck__slot,
body[data-stage="open"] .deck__slot {
  animation: deal .62s var(--ease-out) both;
  animation-delay: var(--deal-delay, 0ms);
}

@keyframes deal {
  from { opacity: 0; transform: translate(0, -14px) rotate(-6deg) scale(.9); }
  to   { opacity: 1; transform: translate(0, 0) rotate(0deg) scale(1); }
}


/* ── the pile ──────────────────────────────────────────────────────────────
   A spread of jokers drifting in the middle of the fogged frame. It is the whole
   of the landing: one control, no label, and cutting it deals the hand.
   ------------------------------------------------------------------------ */
.pile-wrap {
  position: fixed; inset: 0; z-index: 4;
  display: grid; place-items: center;
  pointer-events: none;                    /* only the pile itself is clickable */
}
.pile {
  position: relative;
  width: var(--card-w); aspect-ratio: 1 / var(--card-ratio);
  padding: 0; border: 0; background: none;
  cursor: pointer;
  pointer-events: auto;
  /* IT COMES UP OUT OF THE PHOTOGRAPH, slowly. Seven seconds on a symmetric
     curve — easing at BOTH ends, not --ease-out, which front-loads the change
     and makes a fade read as an appearance. It starts imperceptibly, does most
     of its work in the middle, and settles rather than arriving.

     The spread is already flowing the whole time; what fades is only whether
     you can see it. */
  opacity: 0;
  transition: opacity 7s cubic-bezier(.45,.02,.5,1), transform .5s var(--ease-out);
}
.pile--open { opacity: 1; }
/* The spread reaches well outside the card, so the pointer has to be able to
   follow it there. Without this the hover ends the moment you move onto a card
   you just fanned open, and the pile flutters shut and open again. */
.pile::before {
  content: "";
  position: absolute;
  inset: -2.5rem -11rem;
}

/* ── the spread ────────────────────────────────────────────────────────────
   A seat holds a card's place in the fan; the card inside it does the drifting.
   Two elements because they animate the same property from different clocks —
   the fan is a transition the pointer drives, the drift is an endless keyframe,
   and one `transform` cannot be both.

   AT REST THE PILE IS A PILE. The fan opens by 3px and a third of a degree a
   card — enough that it is visibly made of cards, and no more. A wide resting
   spread was tried and it was wrong: eighteen cards fanned apart with two of the
   three layers blurred stopped being a composition and became a smear with
   nothing to hold on to. The idle image wants the earlier balance — a compact
   stack with a haze on it — and gets its life from the drift and the dissolves
   underneath instead of from being spread out.

   The spread is what HOVERING is for, and cards.js starts the cards swapping
   places along it once it is open.
   ------------------------------------------------------------------------ */
.pile__seat {
  position: absolute; inset: 0;
  /* A PYRAMID, not a row. --oa is the card's distance from the middle, so the
     further out a card sits the lower and the smaller it is. At rest --rise and
     --shrink are zero and this collapses back to a flat stack. Shaping the open
     fan like the conveyor's path is what lets the animation take over from the
     transition without a jump. */
  transform: translateX(calc(var(--o) * var(--fan-gap, 3px)))
             translateY(calc(var(--oa, 0) * var(--rise, 0px)))
             rotate(calc(var(--o) * var(--fan-rot, .34deg)))
             scale(calc(1 - var(--oa, 0) * var(--shrink, 0)));

  /* DEPTH READS AS OPACITY.
     --depth is 1 for the card at the front of the stack and 0 for the one at
     the back; cards.js keeps it in step with z-index. The nearer a card is to
     the top, the more solid it is, and the deeper it lies the more it gives way
     to what is over it. Without this every card was equally solid and a card
     coming forward in a swap simply appeared in front of the others, which is
     the jump. Now it arrives by becoming solid.

     --dim sets how far back the deepest card goes. Nearly closed at rest, since
     the cards are almost on top of each other and there is little to see
     through; wide open once the spread is. */
  opacity: calc(var(--dim, .74) + (1 - var(--dim, .74)) * var(--depth, 1));
  z-index: var(--z, 1);

  /* Slow, and slower than the swaps that use it: a card takes 2.6s to cross to
     its new place while a swap starts every 1.7–2.4s, so their travels overlap
     and the spread is always in motion rather than stepping between poses. The
     opacity runs on the same clock, so coming forward and becoming solid are
     one movement. */
  transition: transform 2.6s cubic-bezier(.42,0,.3,1),
              opacity 2.6s cubic-bezier(.42,0,.3,1),
              filter 2.6s cubic-bezier(.42,0,.3,1);
}
/* ── the spread ────────────────────────────────────────────────────────────
   THERE IS NO OPENING SEQUENCE. The landing is the flowing spread, faded up on
   arrival; the conveyor is running before anything is visible. Two earlier
   versions had a stack that opened first — one deck that started to flow when
   it landed, then two decks dissolving into each other — and both were work
   spent hiding a changeover that does not need to exist.

   These values still matter when the animation does not run: under
   prefers-reduced-motion the seats hold this static pyramid instead.
   ------------------------------------------------------------------------ */
.pile--open .pile__seat {
  --fan-gap: 46px;
  --fan-rot: 4.2deg;
  --rise: 14px;
  --shrink: .1;
  --dim: .28;
}

/* ── THE CONVEYOR ──────────────────────────────────────────────────────────
   A PYRAMID, TRACED BY MOVEMENT. Every card runs the same straight path — out
   at the base of its own side and low, in to the apex and high — so at any
   instant the cards are strung along two converging lines and the spread reads
   as a triangle.

   Nothing here steps. The previous version moved one card at a time into the
   middle and shifted everything between by a place; that is a DISCRETE EVENT,
   the whole spread twitches on it, and two streams of events beat against each
   other into a tick-tock. No spacing fixes that, because the spacing is the
   fault. A card on a continuous loop cannot tick.

   Gapless comes from the phases: each card starts at the point of its journey
   matching where it sits in the open fan, so the slope is occupied evenly. The
   journey times differ per card (7.5–13s), so the two sides drift against each
   other and never pair up. Both sides run the whole time — there is no
   alternation to hear.
   ------------------------------------------------------------------------ */
.pile__seat--flow {
  animation: flow var(--flow-dur, 10s) linear infinite;
  animation-delay: var(--flow-delay, 0s);
}

/* ── THE CONVEYOR ──────────────────────────────────────────────────────────
   A PYRAMID, TRACED BY MOVEMENT. Every card runs the same straight path — out
   at the base of its own side and low, in to the apex and high — so at any
   instant the cards are strung along two converging lines and the spread reads
   as a triangle.

   Nothing here steps. The previous version moved one card at a time into the
   middle and shifted everything between by a place; that is a DISCRETE EVENT,
   the whole spread twitches on it, and two streams of events beat against each
   other into a tick-tock. No spacing fixes that, because the spacing is the
   fault. A card on a continuous loop cannot tick.

   Gapless comes from the phases: each card starts at the point of its journey
   matching where it sits in the open fan, so the slope is occupied evenly. The
   journey times differ per card (7.5–13s), so the two sides drift against each
   other and never pair up. Both sides run the whole time — there is no
   alternation to hear.
   ------------------------------------------------------------------------ */
.pile__seat--flow {
  animation: flow var(--flow-dur, 10s) linear infinite;
  animation-delay: var(--flow-delay, 0s);
}

/* IT ENTERS SMALL AND GROWS INWARD, from .48 at the far edge to 1.08 at the
   centre. The two ends of the slope are therefore not the same thing: out
   there the cards are small, low and crowded, and by the middle they are full
   size and overlapping. That is what gives the spread its depth — the edges
   read as far away rather than merely as further along.

   The path also DECELERATES. Evenly-spaced stops would keep the cards evenly
   spaced the whole way and they would arrive and vanish one at a time.
   Compressing the last third crowds several into the same place at the apex,
   overlapping and showing through each other — which, with the stationary top
   card gone, is the whole point of the centre. */
@keyframes flow {
  0%   { transform: translate(calc(var(--side, -1) * 252px), 74px)
                    rotate(calc(var(--side, -1) * 9deg)) scale(.48);
         opacity: 0; }
  10%  { opacity: .95; }
  30%  { transform: translate(calc(var(--side, -1) * 150px), 44px)
                    rotate(calc(var(--side, -1) * 5.4deg)) scale(.68); }
  55%  { transform: translate(calc(var(--side, -1) * 82px), 23px)
                    rotate(calc(var(--side, -1) * 3deg)) scale(.86); }
  75%  { transform: translate(calc(var(--side, -1) * 39px), 9px)
                    rotate(calc(var(--side, -1) * 1.4deg)) scale(.98); }
  90%  { transform: translate(calc(var(--side, -1) * 15px), 0px)
                    rotate(0deg) scale(1.04);
         opacity: .9; }
  100% { transform: translate(0, -6px) rotate(0deg) scale(1.08);
         opacity: 0; }
}

/* Three drifts at once, stacked. The sharp one is the deck; the two blurred ones
   are brightened over it and read as the smear a slow movement leaves behind. A
   single sharp layer was too stark to watch — but the smear has to stay a smear.
   Held close and kept faint, it is a halo on a stack of cards; opened up it is
   just fog. */
.pile__layer {
  position: absolute; inset: 0;
  display: block;
}
.pile__layer--haze,
.pile__layer--ghost {
  transition: filter 2.6s var(--ease-out), opacity 2.6s var(--ease-out);
}
.pile__layer--haze {
  filter: blur(2.5px) brightness(1.12);
  opacity: .34;
  transform: translate(-1px, 1px);
}
.pile__layer--ghost {
  filter: blur(7px) brightness(1.2);
  opacity: .2;
  transform: translate(2px, -1px) scale(1.025);
}

/* Once the spread is open the blurred layers come up and sharpen part-way, so
   the three stop being a picture and a haze around it and become one spread of
   cards at different removes — some solid, some barely there, moving through
   each other. */
@media (hover: hover) {
  .pile--open .pile__layer--haze {
    filter: blur(1.5px) brightness(1.06);
    opacity: .56;
  }
  .pile--open .pile__layer--ghost {
    filter: blur(5px) brightness(1.14);
    opacity: .4;
  }
}

.pile__card {
  position: absolute; inset: 0;
  border-radius: 5px; overflow: hidden;
  /* pure black, not bone: see the compositing note at the top of this file */
  background: #000;
  box-shadow: 0 8px 22px rgba(0,0,0,.55), 0 1px 0 rgba(255,255,255,.25) inset;
  /* Every card gets its OWN duration, delay, travel and tilt, seeded per load in
     cards.js. A single shared cycle with even delays is what made the first
     version look mechanical: identical arcs, evenly spaced, forever. */
  animation: drift var(--dur, 90s) ease-in-out infinite;
  animation-delay: var(--delay, 0s);
  z-index: 1;
}

/* The two faces of the film dissolve. cards.js drives their opacity and their
   brightness; the resting values live in inline styles rather than in a
   finished animation, so a card interrupted mid-dissolve still settles showing
   a card. The markup ships face A opaque and face A alone. */
.pile__face {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 0;
}

/* THE DRIFT — not a riffle.
   The first version dealt a card off the top every few seconds: a fast flick
   with a snap at the end of it, repeating forever in the middle of the screen.
   Read against a backdrop that takes a minute and a half to cross itself, it
   was disruptive — a nervous tic on a still photograph.

   So the pile now moves on the backdrop's own rhythm: 48–110 seconds a cycle, a
   quarter faster than the four backdrop layers (59/67/79/97s), and it grows
   rather than snaps. A card swells to 1.12, leans, travels, and settles back,
   easing at both ends so there is no moment where a movement starts or stops.
   The z-index churn is gone with it — cards changing depth is a hand dealing,
   and there is no hand here.

   TWO SWELLS, NOT ONE. The cycle rises around 28% and again, further, around
   55%, with the travel and the lean peaking at different moments from the scale.
   One swell per cycle is a breath and you can feel where you are in it; two of
   different size, out of phase with each other, is weather. This is where the
   idle pile gets its morphing from — not from spreading the cards apart, which
   only cost it its composition. Eighteen of these at eighteen rates means the
   stack is always changing shape without any one card doing anything sudden.

   The speed is unchanged: still 48–110 seconds a cycle. What moves more is the
   amount, not the rate. */
@keyframes drift {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
  14%  { transform: translate3d(calc(var(--dx) * .38), -4px, 0)
                    rotate(calc(var(--rot) * .30)) scale(1.035); }
  28%  { transform: translate3d(calc(var(--dx) * .72), 1px, 0)
                    rotate(calc(var(--rot) * .66)) scale(1.09); }
  41%  { transform: translate3d(calc(var(--dx) * .50), 5px, 0)
                    rotate(calc(var(--rot) * .42)) scale(1.048); }
  55%  { transform: translate3d(var(--dx), 2px, 0)
                    rotate(var(--rot)) scale(1.135); }
  68%  { transform: translate3d(calc(var(--dx) * .64), 6px, 0)
                    rotate(calc(var(--rot) * .70)) scale(1.07); }
  81%  { transform: translate3d(calc(var(--dx) * .26), 3px, 0)
                    rotate(calc(var(--rot) * .22)) scale(1.022); }
  92%  { transform: translate3d(calc(var(--dx) * .09), -2px, 0)
                    rotate(calc(var(--rot) * .06)) scale(1.006); }
  100% { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
}

@media (hover: hover) {
  .pile:hover { transform: scale(1.04); }
}
.pile:focus-visible { outline: 2px solid var(--red); outline-offset: 8px; }

/* once the hand is dealt the pile is spent — it lifts away and stops listening */
body:not([data-stage="deck"]) .pile-wrap { pointer-events: none; }
body:not([data-stage="deck"]) .pile {
  opacity: 0; transform: translateY(-26px) scale(.92); pointer-events: none;
  transition: opacity .5s var(--ease-out), transform .5s var(--ease-out);
}

/* the hand only exists once it has been dealt */
body[data-stage="deck"] .deck { opacity: 0; pointer-events: none; }
.deck { transition: transform .85s var(--ease-out), opacity .6s var(--ease-out); }

/* ── instability ───────────────────────────────────────────────────────────
   The drift properties are REGISTERED so they can be transitioned. That is the
   whole trick behind "you never see it move": the JS sets a new offset every
   few seconds and CSS eases the transform there over the same few seconds, so
   nothing is animating per frame and nothing has a period. Unregistered custom
   properties cannot transition — they would jump — which is exactly why the
   shiver properties below are deliberately left unregistered.

   `.deck__jit` owns this transform alone. The slot above it owns the deal, the
   card below it owns hover/flip/scale. No two of them touch the same property,
   so none of them can cancel another. */
@property --dft-x { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --dft-y { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --dft-r { syntax: "<angle>";  inherits: false; initial-value: 0deg; }
@property --dft-s { syntax: "<number>"; inherits: false; initial-value: 1; }

.deck__jit {
  display: block;
  /* shiver is intentionally NOT registered: it must jump, not ease */
  --shv-x: 0px; --shv-y: 0px; --shv-r: 0deg;
  transform:
    translate(calc(var(--dft-x) + var(--shv-x)), calc(var(--dft-y) + var(--shv-y)))
    rotate(calc(var(--dft-r) + var(--shv-r)))
    scale(var(--dft-s));
  transition: --dft-x 4s linear, --dft-y 4s linear,
              --dft-r 4s linear, --dft-s 4s linear;
  will-change: transform;
}

/* ── the second reality ────────────────────────────────────────────────────
   A clone of the card's own face, laid over it and drifting out of
   registration. Inert in every sense that matters: no pointer, no focus, not
   announced. It is created when it appears and removed when it goes, so an
   undoubled card carries no extra DOM at all. */
.card__ghost,
.card__frags,
.card__frag {
  pointer-events: none;
  user-select: none;
}
.card__ghost {
  position: absolute; inset: 0;
  z-index: 2;
  filter: saturate(.85) contrast(1.06);
}
/* Every copy is a WRAPPER holding a cloned `.card__inner`. The wrapper takes the
   movement; the clone keeps its own transform, which is the flip — so a copy of
   the chosen card stays red instead of turning back to its chequer side. The 3D
   context has to survive the extra element or the flip stops hiding backfaces. */
.card__ghost, .card__frag { transform-style: preserve-3d; }
.card__ghost > .card__inner,
.card__frag > .card__inner { position: absolute; inset: 0; }

/* ── coming apart ──────────────────────────────────────────────────────────
   Fragments are clipped clones stacked over the card while its own face is at
   zero opacity. The BUTTON never moves and never stops being clickable — only
   the picture on it is briefly in pieces. */
.card__frags { position: absolute; inset: 0; z-index: 3; }
.card__frag {
  position: absolute; inset: 0;
  transition: none;
}



/* ── the aura ──────────────────────────────────────────────────────────────
   The chosen card's own luminous presence: a soft red field bleeding out past
   its edges, and a red bloom pushed into the card's own shadow. Continuous —
   this belongs to the card, not to any event, and it is the thing that makes it
   read as charged rather than merely marked.

   Its INTENSITY is not: `--aura` is driven from js/glitch.js on the same
   unscheduled cadence as the ring's brightness, but drawn separately, so the
   two never swell together and neither can be read as a breathing loop.

   Painted first in the markup, so it sits behind the card without a z-index —
   the flip creates its own stacking context and a z-index here would have to
   fight it. */
.card__aura {
  position: absolute; inset: -26%;
  pointer-events: none;
  border-radius: 22%;
  opacity: 0;
  background: radial-gradient(ellipse at center,
    rgba(255,72,78,.60) 0%,
    rgba(216,31,38,.42) 30%,
    rgba(216,31,38,.18) 54%,
    rgba(216,31,38,0) 76%);
  filter: blur(7px);
  transition: opacity .45s var(--ease-out);
}
body[data-stage="open"] .card[aria-current="true"] .card__aura {
  opacity: var(--aura, .55);
}
/* and the same charge pushed into the card's own edge */
body[data-stage="open"] .card[aria-current="true"] .card__side--face {
  box-shadow:
    inset 0 0 0 7px var(--red),
    inset 0 0 0 8px var(--red-d),
    0 14px 32px rgba(0,0,0,.72),
    0 0 calc(18px + var(--aura, .55) * 42px) rgba(216,31,38,calc(.25 + var(--aura, .55) * .55));
}

/* ── the selection short-circuit ───────────────────────────────────────────
   Full-viewport red, let out of the card that was chosen. Decorative, inert,
   and gone in about a third of a second. */
.sel-flash {
  position: fixed; inset: 0; z-index: 90;
  pointer-events: none;
  background:
    radial-gradient(circle at var(--fx, 50%) var(--fy, 50%),
      #ff2a30 0%, var(--red) 34%, var(--red-d) 78%, #4a070a 100%);
  mix-blend-mode: screen;
}

/* ── the card control ──────────────────────────────────────────────────── */
.card {
  position: relative;
  display: block;
  width: var(--card-w); aspect-ratio: 1 / var(--card-ratio);
  padding: 0; border: 0; background: none;
  cursor: pointer;
  font: inherit;
  perspective: 900px;
  -webkit-tap-highlight-color: transparent;
  /* width is animatable and the height follows it through aspect-ratio, so the
     shrink from the landing size rides along with the move to the top instead
     of snapping to it. .85s matches the deck's own travel. */
  transition: transform .4s var(--ease-out), width .85s var(--ease-out);
}

.card__inner {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transition: transform .62s var(--ease-out);
  border-radius: 5px;
}

/* the turn */
.card[aria-current="true"] .card__inner { transform: rotateY(180deg); }

.card__side {
  position: absolute; inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  border-radius: 5px;
  overflow: hidden;
  /* Named so the attended state has something exact to hand back to — see the
     stand-down rule under hover/focus. Restating these three by hand there
     would be two copies of one fact. */
  box-shadow: var(--card-shadow);
}

/* toner speckle, shared by both sides */
.card__side::after {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  opacity: .2;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' width='90' height='90'>\
<filter id='s'><feTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='2'/>\
<feColorMatrix type='saturate' values='0'/></filter>\
<rect width='90' height='90' filter='url(%23s)'/></svg>");
}

/* ── back: the resting face ────────────────────────────────────────────────
   A CHEQUER, DRAWN, and drawn with no elements at all — a chequerboard is what
   `repeating-conic-gradient` is for. It replaced a scanned back (a grey
   engraving of two birds, which sat badly against the joker scans) and then an
   841-rect SVG version, which turned out not to cover the card's corners once
   the grid was rotated. A gradient tiles by definition; it cannot fail to fill.

   The field is sized in PERCENT, so it scales with the card. The cards run
   176px on the pile, 138px dealt and 104px in the top menu, and a chequer fixed
   in pixels would visibly re-tile itself at every stage — a card's print does
   not change when the card changes size.

   Nothing happens to it on hover. An earlier pass bleached a joker up through
   it under the pointer, and that was a misunderstanding: the jokers belong to
   the pile. A card in the hand shows its back until it is chosen.
   ------------------------------------------------------------------------ */
.card__side img { width: 100%; height: 100%; object-fit: cover; display: block; }

.card__side--back {
  background: var(--bone);
  /* the printed border, as concentric inset rules rather than a nested box */
  box-shadow:
    inset 0 0 0 1px rgba(0,0,0,.4),
    inset 0 0 0 4px var(--bone),
    inset 0 0 0 5.5px var(--ink),
    inset 0 0 0 8px var(--bone),
    inset 0 0 0 8.8px rgba(11,11,13,.45);
}

/* The field is its own element so the printed border can sit OVER it. An inset
   box-shadow paints below an element's children, so a chequer drawn on
   ::before covers the border rules; this clips the chequer to the field and
   leaves the border alone. */
.card__field {
  position: absolute; inset: 8px;
  overflow: hidden;
  border-radius: 2px;
}
/* Oversized and turned 45°, so the chequer reads as diamonds and the field's
   own clip puts a whole square in every corner — where a rotated grid sized to
   the card leaves the corners bare. */
.card__field::before {
  content: "";
  position: absolute;
  inset: -45%;
  background-image: repeating-conic-gradient(
    var(--ink) 0deg 90deg, transparent 0deg 180deg);
  background-size: 15% 15%;
  transform: rotate(45deg);
  opacity: .9;
}

/* ── face: the chosen card ─────────────────────────────────────────────────
   PURELY RED. It carried a Moroccan-blue frame, an inner rule and a lozenge at
   the centre; all of it is gone. A chosen card should read as a mark, and a
   mark does not need a device on it — a plain red field turning up in a hand of
   chequered backs is louder than any drawing that could have been put on it.

   The only thing left is a hairline of a darker red, inset the way a printed
   border is, so the face is a card and not a rectangle of colour.
   ------------------------------------------------------------------------ */
.card__side--face {
  background: var(--red);
  transform: rotateY(180deg);
  box-shadow:
    inset 0 0 0 1px rgba(0,0,0,.28),
    inset 0 0 0 7px var(--red),
    inset 0 0 0 8px var(--red-d),
    0 14px 32px rgba(0,0,0,.72),
    0 0 30px rgba(216,31,38,.3);
}

/* ── hover / focus ─────────────────────────────────────────────────────────
   SCALE, NOT LIFT. Raising the card broke the row's centre line: the hovered
   card left the line its neighbours still sat on, so the hand read as
   misaligned rather than as one card answering. Scaling about the card's own
   middle keeps every card on that line and still answers the pointer. Small on
   purpose — this is a hand of cards responding, not a card being chosen.

   ONE ATTENDED STATE, TWO WAYS IN. Hover used to be the scale alone while
   keyboard focus also lit the aura and the red edge — so closing a section,
   which returns focus to the card that opened it, put a focused card and a
   hovered card side by side wearing different clothes. Pointer and keyboard are
   the same intention and now say so: both get the aura, the red edge and the
   scale. The values live in `--attend-*` so the two rules cannot drift.
   ------------------------------------------------------------------------ */
.card {
  --card-shadow:
    0 1px 0 rgba(255,255,255,.3) inset,
    0 10px 26px rgba(0,0,0,.6),
    0 2px 6px rgba(0,0,0,.45);
  --attend-scale: 1.045;
  --attend-aura: .46;
  --attend-shadow: 0 10px 26px rgba(0,0,0,.6), 0 0 16px rgba(216,31,38,.5);
}

@media (hover: hover) {
  .card:hover { transform: scale(var(--attend-scale)); }
  .card:hover .card__aura { opacity: var(--attend-aura); }
  .card:hover .card__side { box-shadow: var(--attend-shadow); }

  /* THE POINTER OVERRULES A LEFT-OVER FOCUS, and it has to.
     Closing a section returns focus to the card that opened it — deliberately,
     so a keyboard visitor does not lose their place — and if that close came
     from Escape then `:focus-visible` matches and the card keeps the attended
     look after the hand returns. Once hover and focus were given the SAME
     appearance, that lingering focus became indistinguishable from a selection:
     a card sat lit in the spread, and pointing at another lit that one too
     without ever putting the first one down. Two cards attended, neither
     answering the pointer.

     Only one card can be meant at a time, and it is the one under the pointer.
     So while any card is hovered, every OTHER card hands back its attended
     values — which neutralises the focus rules without touching them, because
     they resolve through these same variables. The focus ring itself is not
     removed and returns the moment the pointer leaves the deck: this changes
     which card looks meant, not whether focus is visible. */
  .deck__rail:has(.card:hover) .card:not(:hover) {
    --attend-scale: 1;
    --attend-aura: 0;
    --attend-shadow: var(--card-shadow);
  }
}
/* ── the focus indicator, in the site's own language ──────────────────────
   Keyboard focus has to be visible — closing a section with Escape returns
   focus to the card that opened it, and a keyboard visitor has to be able to
   see where it went. But it was showing as a hard 2px red rectangle from the
   global `:focus-visible` rule, and that frame then rode the card through the
   escape flight and into the docked row: a browser artefact sitting on top of a
   design whose entire selected state is already red.

   So the rectangle is dropped and the card's OWN aura carries the job instead —
   the same red glow the chosen card uses, at a lower strength and available at
   any stage. It is still an unmistakable indicator, it is still only ever shown
   to `:focus-visible` (so a pointer never triggers it), and it belongs to the
   picture rather than interrupting it.

   Not an accessibility regression: the indicator moved, it did not go. Removing
   it outright would leave Escape returning focus somewhere invisible, which is
   the one thing a keyboard visitor cannot recover from. */
.card:focus-visible { outline: none; transform: scale(var(--attend-scale)); }
.card:focus-visible .card__aura { opacity: var(--attend-aura); }
.card:focus-visible .card__side {
  box-shadow: var(--attend-shadow);
}

/* The selected card stands slightly proud of the row — in SIZE, not height, for
   the same reason as the hover above: it is already the only red card in a row
   of chequers, so it does not also need to leave the line the others sit on. */
.card[aria-current="true"] { transform: scale(1.06); }

/* ── the chosen card, docked ───────────────────────────────────────────────
   Above an open module the red card is 15% larger and stays on the row's
   centre line. It scales about its own middle and the rail centres its items,
   so the extra height is taken equally above and below — the row does not
   shift and the neighbouring cards do not move. The lift is dropped here: a
   card that is both larger and raised stops reading as one of the hand and
   starts reading as a tab. */
.deck__rail { align-items: center; }
body[data-stage="open"] .card[aria-current="true"] {
  transform: scale(1.15);
  z-index: 3;
}
body[data-stage="open"] .card[aria-current="true"]::after { bottom: -9px; }

/* ── the circling glow ─────────────────────────────────────────────────────
   A light travelling round the chosen card's edge. It is a conic gradient
   masked down to the border itself — `mask-composite: exclude` punches the
   middle out, so this is a ring of light and not a coloured card behind a
   coloured card, and it costs one pseudo-element rather than a stack of them.

   The travelling core is BLACK, not white: a gap running round a red edge
   rather than a light running round it. Black reads as the ring failing where
   the highlight passes, which is nearer to inconsistent current than a shine is.

   THE PERIOD IS 0.85ms — about twenty full turns per frame. Rotation is not
   being displayed at all at that rate and is not meant to be: what is left is
   the aliasing itself, an edge that will not hold a value, which is the point.
   It is refresh-rate dependent by construction and that is accepted — an edge
   that behaves differently on different machines is closer to the intent than
   one identical everywhere.

   Its brightness is separately irregular: `--ring-o` is driven from
   js/glitch.js on an unscheduled cadence and drops to nothing for a frame or
   two now and then. */
@property --ring-a { syntax: "<angle>"; inherits: false; initial-value: 0deg; }

body[data-stage="open"] .card[aria-current="true"]::before {
  content: "";
  position: absolute; inset: -3px;
  border-radius: 8px;
  padding: 2px;
  background: conic-gradient(from var(--ring-a),
    transparent 0 54%,
    rgba(216,31,38,.55) 66%,
    #ff3b40 76%,
    #000 81%,
    #ff3b40 86%,
    rgba(216,31,38,.5) 93%,
    transparent 100%);
  /* keep the fill, drop the middle — the ring is what is left */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  opacity: var(--ring-o, .9);
  filter: drop-shadow(0 0 5px rgba(216,31,38,.85));
  pointer-events: none;
  z-index: 4;
  animation: ringSpin .85ms linear infinite;
}
@keyframes ringSpin { to { --ring-a: 360deg; } }

/* mark under the face-up card */
.card[aria-current="true"]::after {
  content: "";
  position: absolute; left: 24%; right: 24%; bottom: -12px; height: 2px;
  background: var(--red);
  box-shadow: 0 0 12px var(--red);
}

/* THE REST OF THE DECK NO LONGER RECEDES. It used to dim to `brightness(.66)`
   while one card was up, on the reasoning that the chosen card should be the
   only lit thing. But the chosen card already announces itself three times over
   — it is red, it is 15% larger, and it carries the aura and the travelling
   ring — so the dimming was not doing the work of distinguishing it. What it
   did instead was grey out the navigation: five cards that had just been dealt
   at full strength went dull the moment one was picked, and the row read as
   disabled rather than as waiting. They keep their dealt colour now. */

/* ── mobile: two rows of three ──────────────────────────────────────────────
   THREE AND THREE, NOT A SCROLLING RAIL. The hand used to run off the side on a
   phone and be swiped through. Six cards is a hand you should be able to see at
   once — scrolling hid half the sections behind a gesture nobody is told about.

   AND IT WAS CLIPPING THE CHOSEN CARD. `overflow-x: auto` makes the rail a
   scroll container, and a scroll container clips BOTH axes — with one axis auto
   the other can never be `visible`. So the selected card, which scales to 1.15
   about its own centre, had its top cut off by the rail's edge. The two bugs
   were one bug: there is no scroll container here now, so nothing clips. */
@media (max-width: 700px) {
  .deck__rail {
    display: grid;
    grid-template-columns: repeat(3, auto);
    justify-content: center;
    align-items: center;
    row-gap: clamp(.5rem, 2.5vw, 1.1rem);
  }
  :root { --card-w: 92px; }
}

@media (prefers-reduced-motion: reduce) {
  .pile__card { animation: none; }
  .pile__seat { transition: none; }
  .pile { transition: none; }
  .deck { transition: none; }
  .deck__slot { animation: none; }
  .card, .card__inner { transition: none; }
  .card:hover, .card:focus-visible, .card[aria-current="true"] { transform: none; }
  /* No involuntary movement at all. The JS declines to start any of it; this
     is the belt to that braces — nothing can drift, tremble or double even if a
     property were left set. The chosen card keeps its size, because that is
     state rather than motion. */
  .deck__jit { transform: none; transition: none; }
  .card__ghost, .card__frags { display: none; }
  /* the aura is light, not movement — it stays, at a fixed strength */
  .card__aura { transition: none; }
  body[data-stage="open"] .card[aria-current="true"] { transform: scale(1.15); }
  /* one layer, not three: the blurred pair are motion, and there is none */
  .pile__layer--haze, .pile__layer--ghost { display: none; }
}

/* ── EXPERIMENT: two fields, two clocks (?pulse=c) ─────────────────────────
   DISTORTIONS, not graphics. Everything here sits at z-index 1 — above the film
   (0), below the panel (2), the cards (3) and the pile (4) — so neither system
   can reach the interface. A `backdrop-filter` only ever sees what is painted
   behind its own element, which is why the module's text is safe by
   construction rather than by restraint.

   THE WRAPPER MUST STAY PLAIN. No opacity, no filter, no isolation on
   `.pulse-layer`: any of those would make it a backdrop root, and the giants
   would then filter only what is inside the wrapper instead of the photograph.

   Inside it, the sonar paints UNDER the giants, so a giant's blur softens the
   red web where they cross.

   Experimental. Delete this block and js/pulse.js and nothing else changes. */
.pulse-layer {
  position: fixed; inset: 0; z-index: 1;
  pointer-events: none;
}
.pulse-sonar {                        /* the fast red manifold — painted first */
  position: absolute; inset: 0; z-index: 1;
  width: 100%; height: 100%;
  overflow: visible;
  pointer-events: none;
}
.pulse-giants {                       /* the slow fields — painted over it */
  position: absolute; inset: 0; z-index: 2;
  pointer-events: none;
}
.pulse-band {
  position: absolute; inset: 0;
  pointer-events: none;
  will-change: clip-path, opacity;
}
/* the grain a field carries — static noise, moving only because its band does */
.pulse-grain {
  position: absolute; inset: 0;
  pointer-events: none;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'>\
<filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/>\
<feColorMatrix type='saturate' values='0'/></filter>\
<rect width='160' height='160' filter='url(%23n)'/></svg>");
}
/* The panel's coupling is an inline blur written from js/pulse.js each frame.
   Deliberately NOT given a transition: one would fight the per-frame value and
   leave the text easing back into focus after a field had already passed. */
@media (prefers-reduced-motion: reduce) { .pulse-layer { display: none; } }

/* ── DIAGNOSTIC: modulation readout (?mod=readout) ─────────────────────────
   An instrument, not a design. Deliberately plain, and deleted with the
   experiment. */
.modreadout {
  position: fixed; left: 8px; bottom: 8px; z-index: 95;
  margin: 0; padding: 8px 10px;
  max-height: 92vh; overflow: hidden;
  font: 11px/1.35 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  color: #b9f0c0; background: rgba(0,0,0,.82);
  border: 1px solid rgba(185,240,192,.35);
  white-space: pre; pointer-events: none;
  text-shadow: 0 0 6px rgba(120,255,150,.25);
}
