/* 16:9 recording stage for animation pitches. The pitch sits in the middle
   of a dark-blue frame; a radial falloff leaves the center lit and the edges dark. */

body:has(.animation-stage-column) {
  height: 100svh;
  overflow: hidden;
}

.animation-stage-column {
  background-color: #071d49;
}

.animation-stage-frame {
  flex: 1 1 auto;
  min-height: 12rem;
  width: 100%;
  display: grid;
  place-items: center;
  container-type: size;
}

.animation-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background-color: #071d49;
  overflow: hidden;
}

@supports (width: 1cqw) {
  .animation-stage-frame .animation-stage {
    width: min(100cqw, calc(100cqh * 16 / 9));
  }
}

.animation-stage__pitch {
  position: absolute;
  left: 50%;
  top: 50%;
  height: 88%; /* Keep in sync with AnimationsHelper::ANIMATION_PITCH_HEIGHT_RATIO */
  aspect-ratio: 900 / 613;
  transform: translate(-50%, -50%);
  z-index: 1;
}

/* Fixed 1080p frame for the MP4 export. Wins over the fluid editor sizing. */
.animation-stage.animation-stage--export {
  width: 1920px;
  height: 1080px;
}

body:has(.animation-stage--export) {
  overflow: hidden;
  background-color: #071d49;
}

/* Video pitches sit on the tactical background. The reference line box is
   x=511 y=51 w=409 h=478 on a 1024×576 frame. Line width is 90% of that box,
   kept centred, so the drawing is slightly narrower at the same height. */
.animation-stage--video {
  background-color: #0d1627;
  background-image: var(--video-pitch-bg);
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

.animation-stage-column:has(.animation-stage--video) {
  background-color: #0d1627;
}

.animation-stage--video .animation-stage__pitch {
  left: var(--pitch-x);
  top: var(--pitch-y);
  width: var(--pitch-w);
  height: var(--pitch-h);
  aspect-ratio: auto;
  transform: none;
}

.animation-stage--video [data-playfield-target="field"] {
  height: 100%;
}

.animation-stage--slot-right {
  --pitch-x: 51.89941406%;
  --pitch-y: 8.85416667%;
  --pitch-w: 35.94726562%;
  --pitch-h: 82.98611111%;
}

.animation-stage--slot-left {
  --pitch-x: 12.15332031%;
  --pitch-y: 8.85416667%;
  --pitch-w: 35.94726562%;
  --pitch-h: 82.98611111%;
}

.animation-stage--slot-half-right {
  --pitch-x: 51.89941406%;
  --pitch-y: 29.60069444%;
  --pitch-w: 35.94726562%;
  --pitch-h: 41.49305556%;
}

.animation-stage--slot-half-left {
  --pitch-x: 12.15332031%;
  --pitch-y: 29.60069444%;
  --pitch-w: 35.94726562%;
  --pitch-h: 41.49305556%;
}

.animation-stage--slot-dual {
  --pitch-x: 9.36848958%;
  --pitch-y: 8.85416667%;
  --pitch-w: 81.26302083%;
  --pitch-h: 82.98611111%;
}

.animation-stage--slot-half-dual {
  --pitch-x: 9.36848958%;
  --pitch-y: 29.60069444%;
  --pitch-w: 81.26302083%;
  --pitch-h: 41.49305556%;
}

.video-pitch-mark {
  position: absolute;
  top: 0;
  height: 100%;
  width: 44.235699%;
  pointer-events: none;
}

.video-pitch-mark--left {
  left: 0;
}

.video-pitch-mark--right {
  left: 55.764301%;
}

.animation-stage--video .animation-stage__spotlight {
  display: none;
}

.animation-stage__spotlight {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    radial-gradient(
      ellipse 34% 38% at 50% 50%,
      rgba(255, 255, 255, 0.07) 0%,
      rgba(255, 255, 255, 0) 72%
    ),
    radial-gradient(
      ellipse 72% 78% at 50% 50%,
      rgba(0, 0, 0, 0) 0%,
      rgba(0, 0, 0, 0) 40%,
      rgba(0, 0, 0, 0.14) 56%,
      rgba(0, 0, 0, 0.42) 72%,
      rgba(0, 0, 0, 0.76) 88%,
      rgba(0, 0, 0, 0.96) 100%
    );
}
/*
  Arrow reveal: the arrow grows out of its tail and stops at its real shape.
  Stops are mirrored in bin/export_frames.mjs (ARROW_REVEAL_STOPS) because
  export frames set the pose directly. Keep the two in sync. Timing is
  linear so the stops are the motion.

  The origin is the tail, in pitch percentages (--arrow-origin-x/y).
*/

.arrow-piece .arrow-svg {
  transform-box: border-box;
  transform-origin: var(--arrow-origin-x, 50%) var(--arrow-origin-y, 50%);
}

.arrow-reveal {
  display: none;
}

.arrow-piece.arrow--reveal-playing .arrow-svg {
  animation-name: arrow-reveal-stretch;
  animation-duration: var(--highlight-duration, 1000ms);
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-iteration-count: 1;
}

.arrow-piece.arrow--reveal-playing .arrow-handle {
  display: none;
}

.arrow-reveal-preview svg {
  transform-box: border-box;
  transform-origin: 16% 74%;
  animation: arrow-reveal-stretch 1s linear infinite;
}

@keyframes arrow-reveal-stretch {
  0% {
    opacity: 0;
    transform: scale(0.05);
  }
  10% {
    opacity: 1;
    transform: scale(0.16);
  }
  38% {
    opacity: 1;
    transform: scale(0.48);
  }
  71% {
    opacity: 1;
    transform: scale(0.78);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

[data-export-root="true"] .arrow-piece.arrow--reveal-playing .arrow-svg {
  animation: none !important;
}

.playfield--reveal-arrow-armed,
.playfield--reveal-arrow-armed .arrow-piece [data-role="arrow-shape"],
.playfield--reveal-arrow-armed .arrow-piece [data-role="arrow-hit"] {
  cursor: crosshair;
}
/*
  Opening motion for the first step of an animation pitch.
  Durations and pop stops are mirrored in playfield_controller.js
  (INTRO_MS) and bin/export_frames.mjs. Keep the three in sync.
  Pop timing is linear so the export samples the same poses.
*/

.intro-playing-fade [data-role="element"][data-kind="player"],
.intro-playing-fade [data-role="element"][data-kind="player_letter"] {
  animation: intro-player-fade 1200ms ease-out both;
  transition-property: left, top, width, height;
}

.intro-playing-pop [data-role="element"][data-kind="player"],
.intro-playing-pop [data-role="element"][data-kind="player_letter"] {
  animation-name: intro-player-pop;
  animation-duration: 520ms;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-delay: calc(var(--intro-index, 0) * 160ms);
  transition-property: left, top, width, height;
}

@keyframes intro-player-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* scale is its own property. The token is already centered with translate,
   so a transform: translate() here would shift it a second time. */
@keyframes intro-player-pop {
  0% {
    opacity: 0;
    scale: 0.12;
  }
  55% {
    opacity: 1;
    scale: 1.22;
  }
  78% {
    opacity: 1;
    scale: 0.94;
  }
  100% {
    opacity: 1;
    scale: 1;
  }
}

.intro-fade-preview {
  animation: intro-fade-preview 1.2s ease-out infinite;
}

@keyframes intro-fade-preview {
  0%, 18% { opacity: 0.15; }
  70%, 100% { opacity: 1; }
}

.intro-pop-preview {
  animation: intro-pop-preview 0.9s linear infinite;
}

@keyframes intro-pop-preview {
  0%, 8% {
    opacity: 0;
    transform: scale(0.12);
  }
  50% {
    opacity: 1;
    transform: scale(1.22);
  }
  72% {
    opacity: 1;
    transform: scale(0.94);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

.intro-pitch-preview {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.intro-pitch-preview rect,
.intro-pitch-preview line {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: intro-pitch-preview 1.8s linear infinite;
}

.intro-pitch-preview line {
  animation-delay: 0.8s;
}

@keyframes intro-pitch-preview {
  0% { stroke-dashoffset: 1; }
  70%, 100% { stroke-dashoffset: 0; }
}

.intro-tool[aria-pressed="true"] {
  border-color: rgb(5 150 105);
  background-color: rgb(236 253 245);
}

[data-export-root="true"] .intro-playing-fade [data-role="element"][data-kind="player"],
[data-export-root="true"] .intro-playing-fade [data-role="element"][data-kind="player_letter"],
[data-export-root="true"] .intro-playing-pop [data-role="element"][data-kind="player"],
[data-export-root="true"] .intro-playing-pop [data-role="element"][data-kind="player_letter"] {
  animation: none !important;
}

/* A piece that first exists on this step fades in over the same move that
   fades a departing piece out. The opening fade also writes player opacity,
   so this animation has to win while the class is on. */
@keyframes piece-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

.piece-entering[data-role="element"] {
  animation: piece-fade-in var(--piece-enter-ms, 0.8s) ease-in-out both !important;
}
/*
  Pitch pieces are sized in px for the pitch at --pitch-ref (its width in
  the 1920×1080 export). The editor draws that pitch at whatever size fits
  the window. Width is a percentage of the field so it tracks every resize.
  --piece-scale is that same ratio, used for borders and labels. Container
  query units set it on first paint; the playfield controller overwrites it
  from the field width because nested query units can stay stuck on resize.
*/

[data-playfield-target="field"] {
  container-type: inline-size;
  --piece-scale: 1;
  --piece-scale: calc(100cqw / var(--pitch-ref) / 1px);
}

[data-playfield-target="field"] .player-piece[data-kind="player"],
[data-playfield-target="field"] .player-piece[data-kind="player_letter"],
[data-playfield-target="field"] .player-piece[data-kind="coach"],
[data-playfield-target="field"] .player-piece[data-kind="hoop"] {
  width: calc(32 / var(--pitch-ref) * 100%);
  height: auto;
  aspect-ratio: 1;
}

[data-playfield-target="field"] .player-piece.player-piece--animation[data-kind="player"],
[data-playfield-target="field"] .player-piece.player-piece--animation[data-kind="player_letter"] {
  width: calc(48 / var(--pitch-ref) * 100%);
}

[data-playfield-target="field"] .player-piece[data-kind="ball"] {
  width: calc(20 / var(--pitch-ref) * 100%);
  height: auto;
  aspect-ratio: 1;
}

[data-playfield-target="field"] .player-piece[data-kind="cone"] {
  width: calc(12 / var(--pitch-ref) * 100%);
  height: auto;
  aspect-ratio: 1;
}

[data-playfield-target="field"] .player-piece[data-kind="pole"] {
  width: calc(30 / var(--pitch-ref) * 100%);
  height: auto;
  aspect-ratio: 30 / 48;
}

[data-playfield-target="field"] .player-piece[data-kind="ladder"] {
  width: calc(32 / var(--pitch-ref) * 100%);
  height: auto;
  aspect-ratio: 32 / 72;
}

[data-playfield-target="field"] .player-piece[data-kind="hurdle"] {
  width: calc(40 / var(--pitch-ref) * 100%);
  height: auto;
  aspect-ratio: 40 / 48;
}

[data-playfield-target="field"] .player-piece[data-kind="goal"] {
  width: calc(16 / var(--pitch-ref) * 100%);
  height: auto;
  aspect-ratio: 16 / 48;
}

[data-playfield-target="field"] .player-piece[data-kind="mannequin"] {
  width: calc(48 / var(--pitch-ref) * 100%);
  height: auto;
  aspect-ratio: 48 / 60;
}

[data-playfield-target="field"] .player-piece[data-kind="player"],
[data-playfield-target="field"] .player-piece[data-kind="player_letter"],
[data-playfield-target="field"] .player-piece[data-kind="ball"],
[data-playfield-target="field"] .player-piece[data-kind="coach"],
[data-playfield-target="field"] .player-piece[data-kind="hoop"],
[data-playfield-target="field"] .player-piece[data-kind="cone"] {
  border-width: calc(2px * var(--piece-scale, 1));
}

[data-playfield-target="field"] .player-piece[data-kind="player"],
[data-playfield-target="field"] .player-piece[data-kind="player_letter"] {
  --piece-border: calc(2px * var(--piece-scale, 1));
}

[data-playfield-target="field"] .player-piece.player-piece--animation[data-kind="player"]:not(.has-portrait),
[data-playfield-target="field"] .player-piece.player-piece--animation[data-kind="player_letter"]:not(.has-portrait) {
  border-width: calc(3px * var(--piece-scale, 1));
  --piece-border: calc(3px * var(--piece-scale, 1));
}

[data-playfield-target="field"] .player-piece.has-portrait[data-kind="player"],
[data-playfield-target="field"] .player-piece.has-portrait[data-kind="player_letter"] {
  border-width: 0;
  --piece-border: 0px;
}

[data-playfield-target="field"] .player-portrait-ring {
  border-width: calc(2px * var(--piece-scale, 1));
}

[data-playfield-target="field"] .player-piece--animation .player-portrait-ring {
  border-width: calc(3px * var(--piece-scale, 1));
}

[data-playfield-target="field"] .player-piece[data-kind="player_letter"] .player-letter-label,
[data-playfield-target="field"] .player-piece[data-kind="coach"] span {
  font-size: calc(14px * var(--piece-scale, 1));
}

[data-playfield-target="field"] .player-position {
  font-size: calc(8px * var(--piece-scale, 1));
  top: calc(100% - (6px * var(--piece-scale, 1)));
}

[data-playfield-target="field"] .player-piece--animation .player-position {
  font-size: calc(12px * var(--piece-scale, 1));
  top: calc(100% + (5px * var(--piece-scale, 1)));
}

[data-playfield-target="field"] .player-piece[data-kind="text"] {
  padding: calc(4px * var(--piece-scale, 1)) calc(8px * var(--piece-scale, 1));
}

[data-playfield-target="field"] .player-piece[data-kind="text"] .text-element-content {
  font-size: calc(12px * var(--piece-scale, 1));
}

[data-playfield-target="field"] .player-highlight-layer .player-highlight {
  width: calc(64 / var(--pitch-ref) * 100%);
  height: auto;
  aspect-ratio: 1;
}

[data-playfield-target="field"] .player-highlight-layer .player-highlight--token-lg {
  width: calc(96 / var(--pitch-ref) * 100%);
}

[data-playfield-target="field"] .player-highlight__ring {
  border-width: calc(3px * var(--piece-scale, 1));
}
/*
  Highlight around a player: the disc grows from the center, beats once,
  holds, then fades out. The fill is softer than the border.
  Stops are mirrored in bin/export_frames.mjs (PLAYER_PULSE_STOPS) because
  GIF frames set the pose directly. Keep the two in sync. Timing is linear
  so the stops are the motion.

  The disc is 200% of the token. On the pitch it lives in its own layer,
  under the ball (z-30) and above the equipment (z-20). The token stays
  above the ball. --piece-border matches the token border width (2px,
  3px on an animation pitch, 0 with a portrait).
*/

.player-piece[data-kind="player"],
.player-piece[data-kind="player_letter"] {
  --piece-border: 2px;
}

.player-piece.player-piece--animation[data-kind="player"]:not(.has-portrait),
.player-piece.player-piece--animation[data-kind="player_letter"]:not(.has-portrait) {
  --piece-border: 3px;
}

.player-piece.has-portrait {
  --piece-border: 0px;
}

.player-piece:has(> .player-highlight)::before {
  content: "";
  position: absolute;
  z-index: 0;
  box-sizing: border-box;
  inset: calc(var(--piece-border, 2px) * -1);
  border-radius: inherit;
  background-color: var(--el-fill, transparent);
  border: solid var(--el-stroke, transparent);
  border-width: var(--piece-border, 2px);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.85);
  pointer-events: none;
}

.player-highlight-layer {
  position: absolute;
  inset: 0;
  z-index: 25;
  pointer-events: none;
  overflow: visible;
}

.player-highlight {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 200%;
  height: 200%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  overflow: visible;
  z-index: 1;
}

/* 4rem is 200% of a 2rem token. Animation pitches use a 3rem token. */
.player-highlight-layer .player-highlight {
  width: 4rem;
  height: 4rem;
  z-index: auto;
  transition: none;
}

.player-highlight-layer .player-highlight--token-lg {
  width: 6rem;
  height: 6rem;
}

.player-piece > .player-highlight {
  z-index: -1;
}

.player-highlight__ring {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--highlight-color, #1BB57C) 10%, transparent);
  background: color-mix(in srgb, var(--highlight-color, #1BB57C) 20%, transparent);
  transform-origin: center center;
  pointer-events: none;
  -webkit-mask: radial-gradient(circle closest-side, transparent 46.5%, #000 48.5%);
  mask: radial-gradient(circle closest-side, transparent 46.5%, #000 48.5%);
}

.player-highlight--idle .player-highlight__ring {
  opacity: 1;
  transform: scale(0.18);
}

.player-highlight--playing .player-highlight__ring {
  animation-name: player-highlight-heartbeat;
  animation-duration: var(--highlight-duration, 3200ms);
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-iteration-count: 1;
}

.player-highlight--preview .player-highlight__ring {
  opacity: 1;
  animation: player-highlight-heartbeat 3.2s linear infinite;
}

/* Step thumbnails stay marked so a highlight is visible in the filmstrip. */
[data-keyframe-thumbnail] .player-highlight--idle .player-highlight__ring {
  opacity: 1;
  transform: scale(1);
}

@keyframes player-highlight-heartbeat {
  0% {
    opacity: 1;
    transform: scale(0.18);
  }
  20% {
    opacity: 1;
    transform: scale(1);
  }
  28% {
    opacity: 1;
    transform: scale(1.12);
  }
  38% {
    opacity: 1;
    transform: scale(0.94);
  }
  46% {
    opacity: 1;
    transform: scale(1.06);
  }
  56%,
  74% {
    opacity: 1;
    transform: scale(1);
  }
  88%,
  100% {
    opacity: 0;
    transform: scale(1);
  }
}

[data-export-root="true"] .player-highlight__ring {
  animation: none !important;
}

.playfield--highlight-armed,
.playfield--highlight-armed .player-piece[data-kind="player"],
.playfield--highlight-armed .player-piece[data-kind="player_letter"] {
  cursor: crosshair;
}

.highlight-tool[data-state="armed"] {
  outline: 2px solid #1BB57C;
  outline-offset: 2px;
}
/*
  Player portrait standard
  ------------------------
  Source: square cut-out, transparent background, face centered,
  top of the head at the top, shoulders at the bottom.
  On a regular pitch the photo is scaled so the head sits above the token
  and only the bottom is clipped to the circle.
  On an animation pitch the photo stays inside the circle. Transparent
  pixels show the player color chosen when the token was placed.
  Accepted uploads: JPG, PNG or WEBP, 2 MB max.
*/

.player-piece[data-kind="player"],
.player-piece[data-kind="player_letter"] {
  overflow: visible;
}

.player-piece.player-piece--animation[data-kind="player"]:not(.has-portrait),
.player-piece.player-piece--animation[data-kind="player_letter"]:not(.has-portrait) {
  border-width: 3px;
}

.player-piece--animation .player-portrait-clip {
  overflow: hidden;
  border-radius: inherit;
}

.player-piece--animation .player-portrait {
  left: 0;
  top: 5px;
  width: 100%;
  height: 100%;
  margin-left: 0;
  object-fit: cover;
  object-position: center center;
  clip-path: none;
  filter: none;
}

.player-piece--animation .player-portrait-ring {
  z-index: 4;
  border-width: 3px;
}

.player-portrait-clip {
  position: absolute;
  inset: 0;
  z-index: 3;
  overflow: visible;
  pointer-events: none;
}

.player-portrait {
  position: absolute;
  left: 50%;
  top: -30%;
  width: 178%;
  height: 178%;
  margin-left: -89%;
  max-width: none;
  max-height: none;
  object-fit: cover;
  object-position: center top;
  pointer-events: none;
  filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.4));
  /* Full photo down to the circle's midline, then the lower semicircle.
     Center y = (50 + 30) / 178, radius = 50 / 178. */
  clip-path: polygon(
    0% 0%,
    100% 0%,
    100% 44.94%,
    78.09% 44.94%,
    77.55% 50.42%,
    75.95% 55.69%,
    73.36% 60.55%,
    69.86% 64.81%,
    65.61% 68.30%,
    60.75% 70.90%,
    55.48% 72.49%,
    50% 73.03%,
    44.52% 72.49%,
    39.25% 70.90%,
    34.39% 68.30%,
    30.14% 64.81%,
    26.64% 60.55%,
    24.05% 55.69%,
    22.45% 50.42%,
    21.91% 44.94%,
    0% 44.94%
  );
}

.player-portrait-ring {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 2px solid var(--el-stroke, transparent);
  z-index: 1;
  pointer-events: none;
}

.player-piece.has-portrait {
  border-width: 0;
}

.player-piece[data-kind="player_letter"] .player-letter-label {
  position: relative;
  z-index: 4;
}

.player-piece.has-portrait .player-letter-label {
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.75);
}

.player-position {
  position: absolute;
  top: calc(100% - 6px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  display: block;
  box-sizing: border-box;
  width: max-content;
  max-width: none;
  margin: 0;
  padding: 1px 3px;
  border: none;
  border-radius: 2px;
  background: #fff;
  color: #18181b;
  font-family: var(--font-family-heading);
  font-size: 8px;
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: 0.04em;
  text-align: center;
  text-transform: uppercase;
  white-space: nowrap;
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.35);
  pointer-events: none;
}

.player-piece--animation[data-kind="player"],
.player-piece--animation[data-kind="player_letter"] {
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.85);
}

.player-piece--animation[data-kind="player"]:has(> .player-highlight),
.player-piece--animation[data-kind="player_letter"]:has(> .player-highlight) {
  box-shadow: none;
}

.player-piece--animation .player-position {
  top: calc(100% + 5px);
  padding: 0;
  border-radius: 0;
  background: transparent;
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.04em;
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.85);
  box-shadow: none;
}

input.player-position {
  width: auto;
  min-width: 1.25rem;
  field-sizing: content;
  pointer-events: auto;
  cursor: text;
  outline: none;
}

input.player-position:focus {
  box-shadow: 0 0 0 1px #059669, 0 1px 2px rgb(0 0 0 / 0.35);
}

input.player-position::placeholder {
  color: #a1a1aa;
  font-weight: 600;
  opacity: 1;
}

.player-piece--animation input.player-position:focus {
  box-shadow: none;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.player-piece--animation input.player-position::placeholder {
  color: rgb(255 255 255 / 0.55);
  font-weight: 700;
}

.player-portrait-error {
  color: #fecaca;
  font-size: 0.65rem;
  white-space: normal;
  max-width: 10rem;
}

.player-portrait-file {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}
/*
  White relation line between two players.
  Thickness matches the yellow highlight ring (see KeyframeEffect::RELATION_STROKE_PX).
  The heartbeat stops are mirrored in bin/export_frames.mjs (RELATION_STOPS).
*/

.player-relation-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 35;
  overflow: visible;
}

.player-relation {
  position: absolute;
  height: 0;
  transform-origin: 0 50%;
  pointer-events: none;
}

.player-relation__stroke {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: var(--relation-stroke, 6.624px);
  margin-top: calc(var(--relation-stroke, 6.624px) / -2);
  background: #fff;
  border-radius: 999px;
  transform-origin: center center;
}

.player-relation--idle .player-relation__stroke {
  opacity: 0;
  transform: scale(0.15, 0.35);
}

[data-keyframe-thumbnail] .player-relation--idle .player-relation__stroke,
[data-export-root="true"] .player-relation--idle .player-relation__stroke {
  opacity: 1;
  transform: none;
}

.player-relation--playing .player-relation__stroke {
  animation-name: player-relation-heartbeat;
  animation-duration: 1600ms;
  animation-timing-function: ease-in-out;
  animation-fill-mode: forwards;
  animation-iteration-count: 1;
}

@keyframes player-relation-heartbeat {
  0% {
    opacity: 0;
    transform: scale(0.15, 0.35);
  }
  10% {
    opacity: 1;
    transform: scale(1, 1);
  }
  16% {
    opacity: 1;
    transform: scale(1, 1.28);
  }
  24% {
    opacity: 1;
    transform: scale(1, 1);
  }
  30% {
    opacity: 1;
    transform: scale(1, 1.16);
  }
  40%,
  100% {
    opacity: 1;
    transform: scale(1, 1);
  }
}

[data-export-root="true"] .player-relation__stroke {
  animation: none !important;
}

.player-piece.player-relation-anchor {
  box-shadow: 0 0 0 3px #fff;
}

.playfield--relation-armed,
.playfield--relation-armed .player-piece[data-kind="player"],
.playfield--relation-armed .player-piece[data-kind="player_letter"] {
  cursor: crosshair;
}

.player-relation-preview {
  position: relative;
  display: block;
  width: 40px;
  height: 28px;
  border-radius: 4px;
  background: #14532d;
}

.player-relation-preview__dot {
  position: absolute;
  top: 50%;
  width: 10px;
  height: 10px;
  margin-top: -5px;
  border-radius: 999px;
  background: #2563eb;
  border: 2px solid #1e3a8a;
  box-sizing: border-box;
}

.player-relation-preview__dot--a {
  left: 3px;
}

.player-relation-preview__dot--b {
  right: 3px;
}

.player-relation-preview__line {
  position: absolute;
  left: 14px;
  right: 14px;
  top: 50%;
  height: 3px;
  margin-top: -1.5px;
  background: #fff;
  border-radius: 999px;
  transform-origin: center center;
  animation: player-relation-preview-beat 1.6s ease-in-out infinite;
}

@keyframes player-relation-preview-beat {
  0%,
  40%,
  100% {
    transform: scaleY(1);
  }
  16% {
    transform: scaleY(1.8);
  }
  30% {
    transform: scaleY(1.35);
  }
}
/*
  Zone highlight: the marked area grows from a small rectangle to its real
  size, beats twice, then returns to the resting zone before the end.
  Stops are mirrored in bin/export_frames.mjs (ZONE_SHAPE_STOPS /
  ZONE_RIPPLE_STOPS / ZONE_CHROME_STOPS) because GIF frames set the pose
  directly. Keep the two in sync. Timing is linear so the stops are the motion.
*/

.zone-highlight {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  z-index: 2;
}

.zone-highlight__shape,
.zone-highlight__ripple {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-sizing: border-box;
  transform-origin: center center;
  pointer-events: none;
}

.zone-highlight__shape {
  border: 1px solid color-mix(in srgb, var(--el-stroke, #0f172a) 80%, transparent);
  background: color-mix(in srgb, var(--el-fill, #ffffff) 20%, transparent);
  opacity: 0;
  transform: scale(0.16);
}

.zone-highlight__ripple {
  border: 2px solid color-mix(in srgb, var(--el-stroke, #0f172a) 70%, transparent);
  background: transparent;
  opacity: 0;
  transform: scale(1);
}

.zone-highlight--idle .zone-highlight__shape,
.zone-highlight--idle .zone-highlight__ripple {
  animation: none;
}

/* While the reveal plays, the real zone chrome steps aside so only the
   growing shape is visible. It comes back before the end, the same way the
   player ring is gone again by the end of its pulse. */
.player-piece.zone--highlight-playing {
  animation-name: zone-highlight-chrome;
  animation-duration: var(--highlight-duration, 2800ms);
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-iteration-count: 1;
}

.player-piece.zone--highlight-playing::before {
  animation-name: zone-highlight-chrome-fill;
  animation-duration: var(--highlight-duration, 2800ms);
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-iteration-count: 1;
}

.zone-highlight--playing .zone-highlight__shape {
  animation-name: zone-highlight-reveal;
  animation-duration: var(--highlight-duration, 2800ms);
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  animation-iteration-count: 1;
}

.zone-highlight--playing .zone-highlight__ripple {
  animation-name: zone-highlight-ripple;
  animation-duration: var(--highlight-duration, 2800ms);
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  animation-iteration-count: 1;
}

.zone-highlight--preview {
  inset: 0;
}

.zone-highlight--preview .zone-highlight__shape {
  animation: zone-highlight-reveal 2.8s linear infinite;
}

.zone-highlight--preview .zone-highlight__ripple {
  animation: zone-highlight-ripple 2.8s linear infinite;
}

/* Filmstrip: the zone stays at full size, with a halo so the effect is marked. */
[data-keyframe-thumbnail] .zone-highlight--idle .zone-highlight__shape {
  opacity: 1;
  transform: scale(1);
  background: transparent;
  border-color: transparent;
  box-shadow:
    0 0 0 10px color-mix(in srgb, var(--el-fill, #ffffff) 42%, transparent),
    inset 0 0 0 8px color-mix(in srgb, var(--el-fill, #ffffff) 42%, transparent);
}

[data-keyframe-thumbnail] .zone-highlight--idle .zone-highlight__ripple {
  opacity: 0;
}

@keyframes zone-highlight-reveal {
  0% {
    opacity: 0;
    transform: scale(0.16);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
  5% {
    opacity: 1;
    transform: scale(0.24);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
  16% {
    opacity: 1;
    transform: scale(0.55);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
  30% {
    opacity: 1;
    transform: scale(0.84);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
  42% {
    opacity: 1;
    transform: scale(1.012);
    box-shadow: 0 0 6px 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
  48% {
    opacity: 1;
    transform: scale(1);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
  54% {
    opacity: 1;
    transform: scale(1.045);
    box-shadow: 0 0 22px 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
  60% {
    opacity: 1;
    transform: scale(0.988);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
  66% {
    opacity: 1;
    transform: scale(1.022);
    box-shadow: 0 0 14px 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
  74% {
    opacity: 1;
    transform: scale(1);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
  88% {
    opacity: 0;
    transform: scale(1);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
  94%,
  100% {
    opacity: 0;
    transform: scale(0.16);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--el-fill, #ffffff) 50%, transparent);
  }
}

@keyframes zone-highlight-chrome {
  0%,
  74% {
    border-color: transparent;
  }
  88%,
  100% {
    border-color: color-mix(in srgb, var(--el-stroke, #0f172a) 80%, transparent);
  }
}

@keyframes zone-highlight-chrome-fill {
  0%,
  74% {
    opacity: 0;
  }
  88%,
  100% {
    opacity: 1;
  }
}

@keyframes zone-highlight-ripple {
  0%,
  48% {
    opacity: 0;
    transform: scale(0.98);
  }
  54% {
    opacity: 0.5;
    transform: scale(1);
  }
  62% {
    opacity: 0;
    transform: scale(1.18);
  }
  66% {
    opacity: 0.35;
    transform: scale(1);
  }
  76%,
  100% {
    opacity: 0;
    transform: scale(0.98);
  }
}

/* Animation pitches only: no stroke, and the highlight fades in at full
   size instead of growing and beating. Stops match ZONE_FADE_STOPS in
   bin/export_frames.mjs. The fill still hands back to the zone at 88%. */

.player-piece.player-piece--animation[data-kind="zone"] {
  border: none;
}

.player-piece--animation .zone-highlight__shape,
.player-piece--animation .zone-highlight__ripple {
  border: none;
}

.player-piece.player-piece--animation.zone--highlight-playing {
  animation: none;
  border: none;
}

.player-piece--animation .zone-highlight--playing .zone-highlight__shape,
.player-piece--animation .zone-highlight--preview .zone-highlight__shape {
  animation-name: zone-highlight-fade;
  animation-fill-mode: both;
}

.player-piece--animation .zone-highlight--playing .zone-highlight__ripple,
.player-piece--animation .zone-highlight--preview .zone-highlight__ripple {
  animation: none;
  opacity: 0;
}

@keyframes zone-highlight-fade {
  0% {
    opacity: 0;
    transform: scale(1);
  }
  22% {
    opacity: 1;
    transform: scale(1);
  }
  74% {
    opacity: 1;
    transform: scale(1);
  }
  88%,
  100% {
    opacity: 0;
    transform: scale(1);
  }
}

[data-export-root="true"] .zone-highlight__shape,
[data-export-root="true"] .zone-highlight__ripple,
[data-export-root="true"] .player-piece.zone--highlight-playing,
[data-export-root="true"] .player-piece.zone--highlight-playing::before {
  animation: none !important;
}

[data-export-root="true"] .player-piece.zone--highlight-playing::before {
  opacity: var(--zone-chrome, 0);
}

.playfield--highlight-zone-armed,
.playfield--highlight-zone-armed .player-piece[data-kind="zone"] {
  cursor: crosshair;
}

/* The zone interior normally lets clicks through. While this tool is armed,
   the whole rectangle can be chosen. */
.playfield--highlight-zone-armed .player-piece[data-kind="zone"][data-role="element"] {
  pointer-events: auto;
}
/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *


 */
