@layer animations {
  /* Feedback is defined twice over: a static state that holds for as long as
     JS leaves the class on, and — only for players who have not asked for
     less motion — a movement layered on top.

     Doing it the other way round is the usual mistake. If the shake *is* the
     feedback, then honouring prefers-reduced-motion by shortening it to
     nothing removes the feedback along with the motion, and a missed invoke
     becomes silent. Here the colour change is the signal and the shake is
     decoration. */

  .spell-card.is-miss .spell-card__art {
    border-color: var(--miss);
    box-shadow: 0 0 0 3px color-mix(in oklab, var(--miss), transparent 62%), var(--shadow);
  }

  .spell-card.is-hit .spell-card__art {
    border-color: var(--hit);
    box-shadow: 0 0 0 3px color-mix(in oklab, var(--hit), transparent 62%), var(--shadow);
  }

  @media (prefers-reduced-motion: no-preference) {
    .spell-card.is-miss .spell-card__art {
      animation: shake var(--tempo-slow) var(--ease);
    }

    .spell-card.is-hit .spell-card__art {
      animation: pulse var(--tempo) var(--ease);
    }

    /* The new icon arriving. Deliberately the shortest animation in the game:
       it plays in the gap the player is already using to recognise the spell,
       and anything longer would be time they cannot act in. */
    .spell-card.is-arriving .spell-card__art {
      animation: arrive var(--tempo) var(--ease);
    }

    .orb-slot[data-filled] {
      animation: orb-pop var(--tempo-fast) var(--ease);
    }

    .countdown__value {
      animation: countdown-tick var(--tempo-slow) var(--ease);
    }

    .rank-badge {
      animation: rank-reveal 480ms var(--ease) both;
    }

    .split {
      animation: split-in 260ms var(--ease) both;
      /* Rows cascade in reading order; capped so the tenth row is not still
         arriving a second after the ninth. */
      animation-delay: calc(var(--row, 0) * 34ms);
    }
  }

  @keyframes shake {
    0%,
    100% {
      transform: translateX(0);
    }
    18% {
      transform: translateX(-9px) rotate(-1.4deg);
    }
    38% {
      transform: translateX(8px) rotate(1.2deg);
    }
    58% {
      transform: translateX(-5px) rotate(-0.7deg);
    }
    80% {
      transform: translateX(3px);
    }
  }

  @keyframes pulse {
    50% {
      transform: scale(1.045);
    }
  }

  @keyframes arrive {
    from {
      opacity: 0.35;
      transform: scale(0.94);
    }
  }

  @keyframes orb-pop {
    from {
      transform: scale(0.7);
    }
  }

  @keyframes countdown-tick {
    from {
      opacity: 0;
      transform: scale(1.5);
    }
  }

  @keyframes rank-reveal {
    from {
      opacity: 0;
      transform: translateY(10px) scale(0.97);
    }
  }

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