@layer animations {
  /* Motion budget: three orchestrated moments and one ambient loop. Everything
     else in this interface holds still, which is what makes those three land.

       1. the operator disc clicking into its detent at the reveal
       2. a key face lighting under a finger or a physical keypress
       3. the chronograph stopping when somebody wins

     Note what is not here: no confetti. The sibling game's win is a printing
     accident and celebrates like one; this game's win is a measurement being
     recorded, and the arc freezing at the winning position with the elapsed time
     stamped beside it is a stronger idea than particles. */

  /* ---- 1. The detent -------------------------------------------------
     A quarter turn into position, overshooting by about 10% and settling hard.
     Deliberately not a slot-machine spin: the operator was chosen by the host and
     received here, so pretending to roll it would be a lie about where the
     decision happened. A click into place is honest and reads as mechanical. */

  @keyframes detent {
    from {
      transform: rotate(-90deg) scale(0.86);
      opacity: 0;
    }

    to {
      transform: rotate(0deg) scale(1);
      opacity: 1;
    }
  }

  .chrono__op[data-landing="true"] {
    animation: detent var(--dur-detent) var(--ease-detent) both;
  }

  /* ---- 2. Arrivals -----------------------------------------------------
     The operand bezels come up out of the plate rather than fading in, so the
     reveal reads as parts seating into a housing. Staggered by index, one beat
     apart, which is the whole of the reveal choreography. */

  @keyframes seat-in {
    from {
      transform: translateY(6%) scale(0.94);
      opacity: 0;
    }

    to {
      transform: none;
      opacity: 1;
    }
  }

  .bezel[data-arriving="true"] {
    animation: seat-in var(--dur-base) var(--ease-detent) both;
    animation-delay: calc(var(--index, 0) * 60ms);
  }

  .chain__row[data-arriving="true"] {
    animation: seat-in var(--dur-fast) var(--ease-out) both;
    animation-delay: calc(var(--index, 0) * 45ms);
  }

  /* ---- 3. The chronograph stopping ------------------------------------
     One pulse of the winner's hue through the disc, then it holds. The arc itself
     does not animate — it simply stops being redrawn, which is what a stopwatch
     does and costs nothing. */

  @keyframes stopped {
    0% {
      box-shadow: var(--bevel-raised), 0 0 0 0 var(--seat);
    }

    45% {
      box-shadow: var(--bevel-raised), 0 0 0 6px rgb(from var(--seat) r g b / 28%);
    }

    100% {
      box-shadow: var(--bevel-raised), 0 0 0 var(--stroke) var(--seat);
    }
  }

  .chrono[data-frozen="true"] {
    animation: stopped var(--dur-sweep) var(--ease-out) both;
  }

  /* ---- Countdown ------------------------------------------------------
     One beat per second, restarted by replacing the node so the animation cannot
     drift out of step with the deadline it is counting toward. */

  @keyframes beat {
    from {
      transform: scale(1.22);
      opacity: 0;
    }

    60% {
      opacity: 1;
    }

    to {
      transform: scale(1);
      opacity: 1;
    }
  }

  .countdown__number {
    animation: beat 620ms var(--ease-detent) both;
  }

  /* ---- Feedback -------------------------------------------------------
     A wrong answer nudges the readout sideways once. Short and small: wrong
     answers are constant in an arithmetic race, so the feedback has to be
     something you can absorb a dozen times a match without irritation. */

  @keyframes nudge {
    0%,
    100% {
      transform: translateX(0);
    }

    25% {
      transform: translateX(-3px);
    }

    75% {
      transform: translateX(3px);
    }
  }

  .readout[data-state="wrong"] {
    animation: nudge 180ms var(--ease-in-out);
  }

  /* The cooling-down ring breathes, so a player knows the lockout is running
     rather than stuck. */
  @keyframes cooling {
    from {
      opacity: 0.45;
    }

    to {
      opacity: 1;
    }
  }

  .keypad[data-locked="true"] {
    animation: cooling 700ms var(--ease-in-out) infinite alternate;
  }

  /* ---- Toasts --------------------------------------------------------- */

  @keyframes toast-in {
    from {
      transform: translateY(8px);
      opacity: 0;
    }

    to {
      transform: none;
      opacity: 1;
    }
  }

  .toast {
    animation: toast-in var(--dur-fast) var(--ease-out) both;
  }

  .toast[data-leaving="true"] {
    opacity: 0;
    transition: opacity var(--dur-fast) var(--ease-out);
  }

  /* ---- Ambient: the landing page demo --------------------------------
     The only looping animation in the project, and it is on the marketing page
     rather than the game. A dial sweeping by itself says what the game is faster
     than a sentence does. */

  @keyframes sweep {
    from {
      stroke-dashoffset: var(--arc-length);
    }

    to {
      stroke-dashoffset: 0;
    }
  }

  .demo .chrono__arc circle {
    animation: sweep 4.2s var(--ease-clock) infinite;
  }

  /* ---- Reduced motion -------------------------------------------------
     The clock is exempt by construction, not by exception: the timer arc is
     redrawn per tick from an absolute deadline, so it is data rather than
     animation and there is nothing here to switch off. Everything that IS
     animation stops. */

  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation-duration: 1ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 1ms !important;
    }

    /* The detent becomes a crossfade: the information — which operator was
       chosen — still arrives, it just does not travel to get there. */
    .chrono__op[data-landing="true"] {
      animation: none;
      opacity: 1;
    }

    .demo .chrono__arc circle {
      animation: none;
      stroke-dashoffset: calc(var(--arc-length) * 0.28);
    }
  }
}
