/* Just Let's Play — the mode-preview clips.
 *
 * Loaded by both a game's host screen (inside the mode-button bubble, and inline under
 * the buttons where there is no hover) and its watch screen (the phone waiting on an
 * arena sees the same two clips the host saw). Those two screens share no other CSS —
 * arena-chrome.css is the host's, lobby.css is the watch page's — so the drawing
 * language lives here on its own rather than being copied into both.
 *
 * The clips themselves are per-game markup: two <template> elements in each host.html
 * and each watch.html, drawn from the parts below. */
  /* ---- mode previews -------------------------------------------------------------
     A sentence can say what a mode is called. It cannot say where your tilt goes, and
     that is the only question the two buttons pose. So each mode carries a clip of its
     own board, and the difference between the two is carried by the board rather than by
     a diagram: co-op has exactly ONE object and it is cream, which is nobody's colour;
     versus has one per player, in the hues the roster already hands out.

     Inline SVG driven by CSS keyframes and no JavaScript. The lobby is already running a
     physics loop behind this overlay on televisions that have repeatedly come up short of
     frame budget — a second animation loop is the wrong shape. Keyframes composite off the
     main thread and cost nothing at all while the bubble is closed. */
  .clip { display: block; width: 100%; height: auto; border-radius: 8px; }
  .clipCap {
    font-family: var(--display);
    font-weight: 700;
    font-size: 12.5px;
    line-height: 1.35;
    text-align: center;
    margin-top: 6px;
    opacity: 0.92;
  }
  /* the caption can run wider than the clip, and the bubble is width:max-content —
     without auto margins the clip then hangs off to the left of its own caption */
  .modeTip .clip { width: 300px; max-width: 78vw; margin-inline: auto; }

  /* the parts every clip is drawn from */
  .clip .rail { stroke: rgba(253,246,227,.22); stroke-width: 2; fill: none; }
  .clip .prop { stroke: var(--cream); stroke-width: 2.5; fill: none; }
  .clip .goal { stroke: var(--taxi); stroke-width: 3; fill: none; stroke-linecap: round; }
  /* cream is the room's: the one object in a co-op clip belongs to nobody in particular */
  .clip .ours { fill: var(--cream); stroke: rgba(34,30,58,.35); stroke-width: 2; }
  .clip .p1 { fill: var(--mint); stroke: var(--cream); stroke-width: 2.5; }
  .clip .p2 { fill: var(--cherry); stroke: var(--cream); stroke-width: 2.5; }
  .clip .p3 { fill: var(--taxi); stroke: var(--cream); stroke-width: 2.5; }

  /* Every distance and angle below is scaled by --clipAmp and timed by --clipDur, so a
     settings toggle could turn the whole set down instead of off. Motion is bespoke per
     game: a clip earns its place by acting out the one thing that game asks of you, so
     there is no shared vocabulary of drifts to reuse. Both modes of a game share the
     board and the timing; what differs is how many objects and whose they are. */
  .clip { --clipAmp: 1; --clipDur: 3s; }

  /* ---- Wobble Tower — a plank leans, and the stack arrives late ----------------------
     The whole point of the game is that the blocks are not glued down: the plank turns
     first, each block follows a beat behind the one under it, and the top of the stack
     swings furthest. Nested groups, so the rotations compound the way a real stack does. */
  .clip.wobble { --clipDur: 2.8s; }
  @keyframes wLean { 0%,100% { transform: rotate(calc(-8.5deg * var(--clipAmp))) } 50% { transform: rotate(calc(8.5deg * var(--clipAmp))) } }
  @keyframes wSway { 0%,100% { transform: rotate(calc(-3.8deg * var(--clipAmp))) } 50% { transform: rotate(calc(3.8deg * var(--clipAmp))) } }
  /* a block that has run out of plank: it slides, tips over the edge and drops out */
  @keyframes wSlip {
    0%,46% { transform: translate(0,0) rotate(0); opacity: 1 }
    62% { transform: translate(calc(13px * var(--clipAmp)), calc(4px * var(--clipAmp))) rotate(calc(30deg * var(--clipAmp))) }
    84% { transform: translate(calc(24px * var(--clipAmp)), calc(40px * var(--clipAmp))) rotate(calc(104deg * var(--clipAmp))); opacity: 1 }
    93%,100% { transform: translate(calc(28px * var(--clipAmp)), calc(58px * var(--clipAmp))) rotate(calc(132deg * var(--clipAmp))); opacity: 0 }
  }
  /* the one anvil each player gets to drop into somebody else's lane */
  @keyframes wAnvil {
    0%,20% { transform: translateY(calc(-88px * var(--clipAmp))); opacity: 0 }
    26% { opacity: 1 }
    46% { transform: translateY(0); animation-timing-function: ease-out }
    54% { transform: translateY(calc(-9px * var(--clipAmp))) }
    62% { transform: translateY(0); animation-timing-function: ease-in }
    /* the block it landed on is sliding out from under it by now, so it follows that
       block down the 17px to the top of the one below and stops there, on something */
    78%,90% { transform: translateY(calc(17px * var(--clipAmp))); opacity: 1 }
    97%,100% { transform: translateY(calc(17px * var(--clipAmp))); opacity: 0 }
  }
  .clip .wLean { transform-box: fill-box; transform-origin: center; animation: wLean var(--clipDur) ease-in-out infinite; }
  .clip .wSway { transform-box: fill-box; transform-origin: bottom center; animation: wSway var(--clipDur) ease-in-out infinite; }
  /* A fill-box origin is the bottom-centre of the group AND everything animating inside
     it, so a child that travels changes where its own parent pivots. The middle lane pins
     its pivot to a fixed point in the viewBox instead: the top of the plank, dead centre
     of the stack — which is where a stack actually turns. */
  .clip .pivotMid { transform-box: view-box; transform-origin: 160px 78px; }
  /* and the lane around them, for the same reason one level up: the anvil starts 88px
     above the frame, which dragged the lane's fill-box centre with it. Pinned to the
     middle of that lane's plank. */
  .clip .pivotLane { transform-box: view-box; transform-origin: 160px 82px; }
  /* negative delays, so each block lags without a dead beat at the start of the loop */
  .clip .wSway.d1 { animation-delay: calc(var(--clipDur) * -0.95) }
  .clip .wSway.d2 { animation-delay: calc(var(--clipDur) * -0.90) }
  .clip .wSway.d3 { animation-delay: calc(var(--clipDur) * -0.85) }
  .clip .wSlip { transform-box: fill-box; transform-origin: bottom center; animation: wSlip var(--clipDur) ease-in infinite; }
  .clip .wAnvil { transform-box: fill-box; animation: wAnvil var(--clipDur) ease-in infinite; }

  /* ---- Tipsy Table — one marble, and the maze is the whole story --------------------
     A drift says nothing about a maze. So the marble runs the maze: right along the
     bottom, up past the first wall, back down past the second, out at the top right.
     Constant speed, because a marble on a tipping table does not ease. It fades at the
     hole and fades back in at the start rather than snapping back across the board. */
  .clip.tipsy { --clipDur: 4.4s; }
  /* Waypoints, not decoration. The marble starts at (46,78); walls at x=78 and x=206 hang
     from the top and end at y=62, walls at x=142 and x=262 rise from the bottom and start
     at y=42. An 11px marble therefore has to be at cy 78 to pass under the first pair and
     cy 26 to pass over the second, which is what the two rows below are. Every crossing
     clears its wall by 5px; change a wall and these numbers are wrong. */
  @keyframes tRun {
    0% { transform: translate(0,0) }
    14% { transform: translate(calc(64px * var(--clipAmp)), 0) }
    26% { transform: translate(calc(64px * var(--clipAmp)), calc(-52px * var(--clipAmp))) }
    40% { transform: translate(calc(128px * var(--clipAmp)), calc(-52px * var(--clipAmp))) }
    52% { transform: translate(calc(128px * var(--clipAmp)), 0) }
    66% { transform: translate(calc(192px * var(--clipAmp)), 0) }
    78% { transform: translate(calc(192px * var(--clipAmp)), calc(-52px * var(--clipAmp))) }
    88%,100% { transform: translate(calc(240px * var(--clipAmp)), calc(-52px * var(--clipAmp))) }
  }
  /* visible from frame one — a clip that opens on an empty maze explains nothing if the
     page is still painting. It vanishes INTO the hole and is simply back at the start on
     the wrap, which is also what the game does between levels. */
  @keyframes tFade { 0%,88% { opacity: 1 } 95%,100% { opacity: 0 } }
  /* the table itself tips — the marble is riding a board, not sliding on a diagram */
  @keyframes tTip { 0%,100% { transform: rotate(calc(-1.7deg * var(--clipAmp))) } 50% { transform: rotate(calc(1.7deg * var(--clipAmp))) } }
  .clip .tTip { transform-box: fill-box; transform-origin: center; animation: tTip var(--clipDur) ease-in-out infinite; }
  .clip .tRun { transform-box: fill-box; animation: tRun var(--clipDur) linear infinite, tFade var(--clipDur) linear infinite; }
  /* versus puts three on the same track at once, spread a third of a lap apart */
  .clip .tRun.d1 { animation-delay: calc(var(--clipDur) * -0.34), calc(var(--clipDur) * -0.34) }
  .clip .tRun.d2 { animation-delay: calc(var(--clipDur) * -0.67), calc(var(--clipDur) * -0.67) }

  /* ---- Friction Floor — nothing here slows down on its own --------------------------
     Co-op is a transfer of momentum: the puck accelerates into a loose disc, the disc
     takes the speed and posts itself through the grate, the puck keeps gliding because
     the floor gives nothing back. Versus is four closed paths at constant speed — a puck
     on a frictionless rink never stops, it only meets a rail or somebody else. */
  .clip.friction { --clipDur: 3.2s; }
  @keyframes fPush {
    0% { transform: translateX(0); animation-timing-function: ease-in }
    38% { transform: translateX(calc(18px * var(--clipAmp))); animation-timing-function: linear }
    100% { transform: translateX(calc(34px * var(--clipAmp))) }
  }
  @keyframes fFly {
    0%,38% { transform: translateX(0); animation-timing-function: ease-out }
    86% { transform: translateX(calc(182px * var(--clipAmp))); opacity: 1 }
    93% { opacity: 0 }
    94% { transform: translateX(0) }
    100% { transform: translateX(0); opacity: 0 }
  }
  /* A and B meet head-on at 45% with their centres exactly 30 apart — two 14px pucks,
     so they touch and nothing overlaps — and leave on a snappy curve, because a hit that
     eases apart reads as two things passing through each other. C keeps to the lower
     right and never comes within a puck's width of either. Every waypoint is checked
     against the rink too: a 14px puck must keep its centre inside x 28-292, y 24-80. */
  @keyframes fLapA {
    0% { transform: translate(0,0) }
    45% { transform: translate(calc(40px * var(--clipAmp)), calc(-3px * var(--clipAmp))); animation-timing-function: cubic-bezier(.15,.85,.3,1) }
    54% { transform: translate(calc(10px * var(--clipAmp)), calc(14px * var(--clipAmp))) }
    72% { transform: translate(calc(-22px * var(--clipAmp)), calc(20px * var(--clipAmp))) }
    100% { transform: translate(0,0) }
  }
  @keyframes fLapB {
    0% { transform: translate(0,0) }
    45% { transform: translate(calc(-36px * var(--clipAmp)), calc(3px * var(--clipAmp))); animation-timing-function: cubic-bezier(.15,.85,.3,1) }
    54% { transform: translate(calc(-10px * var(--clipAmp)), calc(-12px * var(--clipAmp))) }
    72% { transform: translate(calc(16px * var(--clipAmp)), calc(-12px * var(--clipAmp))) }
    100% { transform: translate(0,0) }
  }
  @keyframes fLapC {
    0% { transform: translate(0,0) }
    25% { transform: translate(calc(40px * var(--clipAmp)), calc(-8px * var(--clipAmp))) }
    50% { transform: translate(calc(10px * var(--clipAmp)), calc(8px * var(--clipAmp))) }
    75% { transform: translate(calc(-34px * var(--clipAmp)), calc(-2px * var(--clipAmp))) }
    100% { transform: translate(0,0) }
  }
  .clip .fPush { transform-box: fill-box; animation: fPush var(--clipDur) infinite; }
  .clip .fFly  { transform-box: fill-box; animation: fFly var(--clipDur) infinite; }
  .clip .fLapA { transform-box: fill-box; animation: fLapA var(--clipDur) linear infinite; }
  .clip .fLapB { transform-box: fill-box; animation: fLapB var(--clipDur) linear infinite; }
  .clip .fLapC { transform-box: fill-box; animation: fLapC var(--clipDur) linear infinite; }

  /* ---- Shuffle Smash — you get one push, and then you are only watching --------------
     Both clips have to say the thing that makes this game not Friction Floor: the throw
     ENDS. Co-op winds back, lets go, and crosses a gap it cannot correct for. Versus is
     turns, so the board already has somebody's puck sitting on the four when yours arrives
     — the knock-off is deliberate, aimed at a target that is standing still.

     The lip is at x=286 and the drop is everything past it, so a puck knocked off does not
     stop at the edge of the rail: it keeps going, falls, and fades. Nothing bounces back. */
  .clip.shuffle { --clipDur: 3.6s; }

  /* wind back, hold, let go — and then nothing you do matters */
  @keyframes ssThrow {
    0%   { transform: translateX(0); opacity: 1; animation-timing-function: ease-in-out }
    22%  { transform: translateX(calc(-24px * var(--clipAmp))) }
    32%  { transform: translateX(calc(-24px * var(--clipAmp))); animation-timing-function: cubic-bezier(.1,.8,.3,1) }
    74%  { transform: translateX(calc(196px * var(--clipAmp))); opacity: 1 }
    92%  { transform: translateX(calc(196px * var(--clipAmp))); opacity: 1 }
    97%  { opacity: 0 }
    98%  { transform: translateX(0); opacity: 0 }
    100% { transform: translateX(0); opacity: 0 }
  }
  /* the room's wind-up, read off the same clock as the throw it produces */
  @keyframes ssMeter {
    0%   { transform: scaleX(0); animation-timing-function: ease-in-out }
    22%  { transform: scaleX(1) }
    32%  { transform: scaleX(1) }
    38%  { transform: scaleX(0) }
    100% { transform: scaleX(0) }
  }

  /* versus: your puck arrives at a board that already has somebody on the four */
  /* THE TWO PUCKS HAVE TO ACTUALLY MEET, and in the shipped version they never did. The
     sitting puck is centred at 272 and the incoming one starts at 60, both radius 13 — so
     their edges touch when the incoming has travelled 186. It stopped at 150 when the knock
     was triggered and only ever reached 178, leaving a visible gap: the mint puck flew off
     the edge as though shoved by nothing. This clip is the only place the SMASH is shown
     before you play, so it was advertising the one mechanic the game is named after and
     getting it wrong.
     186 is contact, and it happens exactly on the 52% mark where ssKnocked starts to move.
     After that it creeps to 200 — a shooter arriving nearly head-on stops close to dead,
     which is also what the real sim does. */
  @keyframes ssIncoming {
    0%   { transform: translateX(0); opacity: 0 }
    6%   { opacity: 1; animation-timing-function: cubic-bezier(.1,.8,.35,1) }
    52%  { transform: translateX(calc(186px * var(--clipAmp))); animation-timing-function: ease-out }
    64%  { transform: translateX(calc(200px * var(--clipAmp))) }
    92%  { transform: translateX(calc(200px * var(--clipAmp))); opacity: 1 }
    97%  { opacity: 0 }
    100% { transform: translateX(0); opacity: 0 }
  }
  /* struck at 52%, and past the lip there is no floor: it keeps going, drops and is gone */
  @keyframes ssKnocked {
    0%,52% { transform: translate(0,0); opacity: 1; animation-timing-function: cubic-bezier(.2,.7,.5,1) }
    64%    { transform: translate(calc(26px * var(--clipAmp)), calc(4px * var(--clipAmp))); opacity: 1; animation-timing-function: ease-in }
    80%    { transform: translate(calc(38px * var(--clipAmp)), calc(46px * var(--clipAmp))); opacity: 0 }
    100%   { transform: translate(0,0); opacity: 0 }
  }
  /* it was sitting on the four, so the four goes out with it, and comes back for the next */
  @keyframes ssLipLit {
    0%,52% { opacity: 1 }
    58%    { opacity: .28 }
    92%    { opacity: .28 }
    100%   { opacity: 1 }
  }
  .clip .ssThrow    { transform-box: fill-box; animation: ssThrow var(--clipDur) infinite; }
  .clip .ssMeter    { transform-box: fill-box; transform-origin: left center; animation: ssMeter var(--clipDur) infinite; }
  .clip .ssIncoming { transform-box: fill-box; animation: ssIncoming var(--clipDur) infinite; }
  .clip .ssKnocked  { transform-box: fill-box; animation: ssKnocked var(--clipDur) infinite; }
  .clip .ssLipLit   { animation: ssLipLit var(--clipDur) infinite; }

  /* ---- Throttle Track — a lap, and the corner that decides it -----------------------
     The clips were shipped at M1 with class names and no keyframes at all, so they sat
     perfectly still for eight milestones. What they have to say is the one thing the game
     is: the car is on rails, so nothing steers — all that happens is speed, and a corner
     that punishes carrying too much of it into it.

     Co-op is a train: several carriages moving as one, because one tilt drives all of them.
     Versus is three cars abreast, and one of them does not lift. */
  .clip.throttle { --clipDur: 3.2s; }
  @keyframes ttRun { 0% { transform: translateX(0) } 100% { transform: translateX(calc(196px * var(--clipAmp))) } }
  /* the corner ahead lights as the field arrives, and goes out once they are through */
  @keyframes ttWarn { 0%,52% { opacity: 0 } 62%,88% { opacity: 1 } 96%,100% { opacity: 0 } }
  /* the one who kept their foot in: carries on, slews, and is left behind */
  /* It does not spin, it understeers: the car carries straight on while the rail turns away
     under it, which is what the real game does now. The rail curves up at this end, so
     simply continuing in x IS leaving the track — no sideways step, and no pirouette. The
     few degrees at the finish are the last of the momentum settling, not a rotation. */
  @keyframes ttSpin {
    0% { transform: translateX(0) rotate(0) }
    58% { transform: translateX(calc(114px * var(--clipAmp))) rotate(0); animation-timing-function: ease-out }
    100% { transform: translateX(calc(198px * var(--clipAmp))) rotate(calc(7deg * var(--clipAmp))) }
  }
  .clip .ttRun  { transform-box: fill-box; animation: ttRun var(--clipDur) linear infinite; }
  .clip .ttWarn { animation: ttWarn var(--clipDur) linear infinite; }
  .clip .ttSpin { transform-box: fill-box; transform-origin: center; animation: ttSpin var(--clipDur) linear infinite; }
  /* the tail of a train leaves a beat after its nose, which is what makes it read as one
     thing rather than three cars that happen to be in a row */
  .clip .ttRun.d1 { animation-delay: calc(var(--clipDur) * -0.995) }
  .clip .ttRun.d2 { animation-delay: calc(var(--clipDur) * -0.99) }

  /* ---- Slippy Shapes — the tray slides everything at once ---------------------------
     Co-op's whole difficulty is that one tilt moves all four shapes, so they slide as a
     set; the clip earns its keep at the moment one of them finds its hole, drops in and
     goes still while the rest keep sliding. Versus is that same seat being taken away:
     a rival charges the shape sitting on a hole and knocks it clear off. */
  .clip.slippy { --clipDur: 3.4s; }
  @keyframes sSlide { 0%,100% { transform: translateX(calc(-15px * var(--clipAmp))) } 50% { transform: translateX(calc(15px * var(--clipAmp))) } }
  @keyframes sSeat {
    0% { transform: translate(calc(-15px * var(--clipAmp)), 0) }
    30% { transform: translate(calc(11px * var(--clipAmp)), 0) }
    46% { transform: translate(0, 0); animation-timing-function: ease-in }
    56%,100% { transform: translate(0, calc(-40px * var(--clipAmp))) }
  }
  @keyframes sLit { 0%,50% { stroke-opacity: .34 } 58%,100% { stroke-opacity: 1 } }
  /* the charge, the contact, and the recoil that leaves the attacker where it started */
  @keyframes sCharge {
    0% { transform: translate(calc(-70px * var(--clipAmp)), 0) }
    44% { transform: translate(calc(-12px * var(--clipAmp)), calc(-36px * var(--clipAmp))); animation-timing-function: ease-out }
    56% { transform: translate(calc(-34px * var(--clipAmp)), calc(-22px * var(--clipAmp))) }
    100% { transform: translate(calc(-70px * var(--clipAmp)), 0) }
  }
  /* holds until 44% — the same instant the charger arrives. Any earlier and the seated
     shape reads as flinching away from a hit that has not landed. */
  @keyframes sKnock {
    0%,44% { transform: translate(0,0) }
    56% { transform: translate(calc(30px * var(--clipAmp)), calc(7px * var(--clipAmp))) }
    72% { transform: translate(calc(54px * var(--clipAmp)), calc(13px * var(--clipAmp))) }
    88% { transform: translate(calc(21px * var(--clipAmp)), calc(5px * var(--clipAmp))) }
    100% { transform: translate(0,0) }
  }
  .clip .sSlide { transform-box: fill-box; animation: sSlide var(--clipDur) ease-in-out infinite; }
  .clip .sSlide.d1 { animation-delay: calc(var(--clipDur) * -0.97) }
  .clip .sSlide.d2 { animation-delay: calc(var(--clipDur) * -0.94) }
  .clip .sSeat { transform-box: fill-box; animation: sSeat var(--clipDur) ease-in-out infinite; }
  .clip .sLit { animation: sLit var(--clipDur) linear infinite; }
  .clip .sCharge { transform-box: fill-box; animation: sCharge var(--clipDur) ease-in infinite; }
  .clip .sKnock { transform-box: fill-box; animation: sKnock var(--clipDur) ease-out infinite; }

  /* NOT gated behind prefers-reduced-motion, on purpose and by the owner's decision after
     seeing it both ways. Reduce Motion is on by default on a lot of Macs, so gating here
     made the frozen frame the normal experience rather than the exception — and the clip
     IS the explanation, so freezing it costs the feature for most of the room. What these
     do is also not the motion that rule exists to stop: no parallax, no scaling, no
     scroll-coupling, largest travel 34px inside a 300px box.

     --clipAmp and --clipDur stay, so a settings toggle can turn the set down rather than
     off if that is ever wanted; .noClipMotion stops them dead. Nothing sets either yet. */
  .noClipMotion .clip * { animation: none !important; }

  /* Where there is no hover there is no bubble, so the clips stand under the buttons
     instead — side by side if there is room, stacked if there is not. This is the path a
     tablet used as the arena takes, and it is the one easiest to forget: without it the
     whole feature is invisible on one of the three ways people set this up. */
  .reel {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
    margin-top: 4px;
  }
  .reelItem { flex: 1 1 240px; max-width: 320px; }
  .reel .clip {
    background: var(--ink);
    padding: 8px;
    box-shadow: 3px 3px 0 rgba(0,0,0,.3);
  }
  /* the reel lands in two very different type scales — the host overlay and the watch
     page's big invite card — so the caption sets its own size rather than inheriting */
  .reel .clipCap { font-size: 14px; }
