* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: #05070d;
  font-family: system-ui, sans-serif;
}

main {
  display: block;
  max-width: none;
  margin: 0;
}

a {
  color: #7fb3ff;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.skip-link:focus {
  position: static;
  z-index: 10;
  display: inline-block;
  width: auto;
  height: auto;
  margin-bottom: 0.5rem;
}

/*
  --close-danger and --far-danger each run 0..1 (set every frame; exactly one
  is non-zero at a time, both 0 at the safe band's centre) so "too close" and
  "too far" can read as genuinely different dangers -- compression/collision
  vs. thinning/breaking -- instead of one symmetric glow just getting
  stronger. --danger is simply the larger of the two, and it is the ONLY
  thing that drives colour on the tether/target: green at the safe centre,
  red approaching either threshold, nothing in between. Colour says how
  dangerous; shape/motion (compression vs. fraying) says why. Nothing below
  reads as text; it's the only feedback there is.
*/
.surface {
  position: fixed;
  inset: 0;
  overflow: hidden;
  touch-action: none;
  background:
    radial-gradient(
      ellipse at 50% 50%,
      hsl(220deg 40% 10%) 0%,
      #05070d 70%
    );
  box-shadow: inset 0 0 min(30vmin, 22rem) calc(var(--close-danger) * 7rem) hsl(4deg 75% 30% / calc(var(--close-danger) * 0.35));

  --close-danger: 0;
  --far-danger: 0;
  --danger: 0;
  --px: 50vw;
  --py: 50vh;
  --tx: 50vw;
  --ty: 50vh;
  --jitter-x: 0px;
  --jitter-y: 0px;
  --pulse-pos: 0;
}

.surface::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(circle at 50% 50%, hsl(200deg 90% 75% / 35%) 0%, transparent 65%);
}

.surface.lockon::after {
  animation: lockon-flash 380ms ease-out;
}

@keyframes lockon-flash {
  0% {
    opacity: 0;
  }

  25% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}

.surface.fail-close {
  animation: screen-shake 380ms ease-in-out;
}

@keyframes screen-shake {
  0%,
  100% {
    transform: translate(0, 0);
  }

  20% {
    transform: translate(-0.4rem, 0.15rem);
  }

  40% {
    transform: translate(0.35rem, -0.2rem);
  }

  60% {
    transform: translate(-0.3rem, 0.2rem);
  }

  80% {
    transform: translate(0.2rem, -0.1rem);
  }
}

.player,
.target {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
}

.player {
  left: calc(var(--px) - var(--jitter-x));
  top: calc(var(--py) - var(--jitter-y));
  width: 0.7rem;
  height: 0.7rem;
  background: #f4f7ff;
  box-shadow: 0 0 calc(0.6rem + var(--close-danger) * 0.5rem) calc(0.15rem + var(--close-danger) * 0.15rem)
    rgb(244 247 255 / calc(0.7 + var(--close-danger) * 0.2));
}

/* Too-close is a compression/overload metaphor, distinct from too-far's
   thinning/breaking one, and it has to read with colour removed entirely:
   the inner core contracts (shrinking gradient stop) while both shadow
   layers densify rather than simply brighten, and the whole point trembles
   (--jitter-x/--jitter-y, JS-driven, amplitude and frequency both rising
   with --close-danger). */
.target {
  left: calc(var(--tx) + var(--jitter-x));
  top: calc(var(--ty) + var(--jitter-y));
  width: 1.4rem;
  height: 1.4rem;
  background: radial-gradient(
    circle at 50% 50%,
    hsl(calc(120deg - var(--danger) * 120deg) 95% 80%) 0%,
    hsl(calc(120deg - var(--danger) * 120deg) 90% 60%) calc(70% - var(--close-danger) * 45%),
    hsl(calc(120deg - var(--danger) * 120deg) 85% 42%) 100%
  );
  box-shadow:
    0 0 calc(0.5rem + var(--close-danger) * 0.9rem) calc(0.12rem + var(--close-danger) * 0.55rem)
      hsl(calc(120deg - var(--danger) * 120deg) 95% 70% / calc(0.9 + var(--close-danger) * 0.1)),
    0 0 calc(2rem + var(--far-danger) * 0.4rem) calc(0.6rem - var(--close-danger) * 0.35rem)
      hsl(calc(120deg - var(--danger) * 120deg) 85% 55% / calc(0.55 - var(--close-danger) * 0.35));
  animation: breathe 2.6s ease-in-out infinite;
}

.surface.engaged .target,
.surface.armed .target,
.surface.fail-close .target,
.surface.fail-far .target {
  animation: none;
}

@keyframes activation-pulse {
  0% {
    filter: brightness(1);
  }

  40% {
    filter: brightness(1.9);
  }

  100% {
    filter: brightness(1);
  }
}

@keyframes breathe {
  0%,
  100% {
    transform: translate(-50%, -50%) scale(1);
  }

  50% {
    transform: translate(-50%, -50%) scale(1.2);
  }
}

/* The idle attractor: a slowly spinning, breathing ring around the target --
   the strongest visual pull on the screen until the pointer locks on. */
.target::before {
  content: "";
  position: absolute;
  inset: -0.7rem;
  border-radius: 50%;
  border: 0.12rem solid hsl(200deg 85% 72% / 55%);
  animation:
    ring-spin 4s linear infinite,
    ring-breathe 2.1s ease-in-out infinite;
}

.surface.engaged .target::before,
.surface.armed .target::before,
.surface.fail-close .target::before,
.surface.fail-far .target::before {
  opacity: 0;
  animation: none;
}

/* Armed: the ring becomes a tighter, faster concentric pulse -- an obvious
   "this is the thing to touch" cue with no text attached. */
.surface.armed .target::before {
  opacity: 1;
  animation: arm-pulse 900ms ease-out infinite;
}

@keyframes arm-pulse {
  0% {
    inset: -0.25rem;
    border-width: 0.16rem;
    opacity: 0.9;
  }

  100% {
    inset: -1.6rem;
    border-width: 0.02rem;
    opacity: 0;
  }
}

/* The compression ring: present only while a round is engaged, contracting
   inward and pulsing faster as --close-danger rises -- a shape-based,
   colour-independent read on "too close" that mirrors the tether's own
   thickening. */
.target::after {
  content: "";
  position: absolute;
  inset: 0.15rem;
  border-radius: 50%;
  border: 0.09rem solid hsl(4deg 90% 70% / 0%);
  opacity: 0;
  pointer-events: none;
}

.surface.engaged .target::after {
  opacity: calc(var(--close-danger) * 0.9);
  border-color: hsl(4deg 90% 70% / 70%);
  animation: compression-pulse calc(0.9s - var(--close-danger) * 0.6s) ease-in-out infinite;
}

@keyframes compression-pulse {
  0%,
  100% {
    inset: 0.15rem;
  }

  50% {
    inset: calc(0.42rem - var(--close-danger) * 0.32rem);
  }
}

@keyframes ring-spin {
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
}

@keyframes ring-breathe {
  0%,
  100% {
    inset: -0.9rem;
    opacity: 0.35;
  }

  50% {
    inset: -0.45rem;
    opacity: 0.8;
  }
}

/* The lock-on moment: the target snaps from its idle pulse into a bright,
   momentary flash the instant the round begins. */
.surface.lockon .target {
  animation: lockon-snap 340ms ease-out;
}

@keyframes lockon-snap {
  0% {
    transform: translate(-50%, -50%) scale(1.7);
    filter: brightness(2);
  }

  100% {
    transform: translate(-50%, -50%) scale(1);
    filter: brightness(1);
  }
}

/* Too-close failure: the two points slam together in a bright overload
   flash, then the target collapses. Reads as a collision. */
.surface.fail-close .target,
.surface.fail-close .player {
  animation: burst 420ms ease-out forwards;
}

@keyframes burst {
  0% {
    filter: brightness(1);
  }

  30% {
    filter: brightness(2.6);
    transform: translate(-50%, -50%) scale(1.7);
  }

  100% {
    filter: brightness(0.9);
    transform: translate(-50%, -50%) scale(0.6);
    opacity: 0.5;
  }
}

/* Too-far failure: the target dims and shrinks away rather than bursting.
   Reads as losing the connection, not colliding. */
.surface.fail-far .target {
  animation: recede 460ms ease-in forwards;
}

@keyframes recede {
  0% {
    filter: brightness(1);
    opacity: 1;
  }

  100% {
    filter: brightness(0.5) blur(0.15rem);
    opacity: 0.3;
    transform: translate(-50%, -50%) scale(0.65);
  }
}

/* Higher-specificity than the lockon/fail-* rules above by design (armed +
   activating is a narrower, more specific state) so it has to come after
   them in source order. */
.surface.armed.activating .target {
  animation: activation-pulse 280ms ease-out;
}

.connection {
  position: absolute;
  top: 0;
  left: 0;
  height: calc(0.09rem + var(--close-danger) * 0.34rem - var(--far-danger) * 0.05rem);
  width: var(--length);
  transform-origin: left center;
  transform: translate(var(--px), var(--py)) translateY(-50%) rotate(var(--angle));
  opacity: 0;
  pointer-events: none;

  /* Two layers: a travelling pulse block (position driven every frame by
     --pulse-pos, speed rising with --close-danger, so energy visibly moves
     rapidly between player and target) laid over the existing far-side
     dash/fray pattern. */
  background-image:
    linear-gradient(
      to right,
      transparent 0%,
      hsl(4deg 90% 72% / calc(var(--close-danger) * 0.95)) 6%,
      hsl(4deg 90% 72% / calc(var(--close-danger) * 0.95)) 10%,
      transparent 16%
    ),
    repeating-linear-gradient(
      to right,
      hsl(
          calc(120deg - var(--danger) * 120deg) calc(85% - var(--far-danger) * 35%)
            calc(60% - var(--far-danger) * 20%) /
            calc(0.92 - var(--far-danger) * 0.55 + var(--close-danger) * 0.08)
        )
        0 calc(100% - var(--far-danger) * 55%),
      transparent calc(100% - var(--far-danger) * 55%) 100%
    );
  background-size:
    calc(2.6rem - var(--close-danger) * 1.4rem) 100%,
    calc(1.5rem - var(--far-danger) * 0.8rem) 100%;
  background-position: calc(var(--pulse-pos) * 100%) 0, 0 0;
  background-repeat: repeat-x, repeat-x;
  animation: tether-pulse calc(1.3s - var(--close-danger) * 0.85s - var(--far-danger) * 0.8s) ease-in-out infinite;
  transition: opacity 140ms ease-out;
}

.surface.engaged .connection {
  opacity: 1;
}

/* A compressed energy ring at the tether's midpoint -- exploits the fact
   that .connection is already positioned/rotated between the two points, so
   its own midpoint is exactly halfway along the line joining them. */
.connection::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: calc(0.5rem + var(--close-danger) * 0.8rem);
  height: calc(0.5rem + var(--close-danger) * 0.8rem);
  border-radius: 50%;
  border: 0.08rem solid hsl(4deg 90% 72% / calc(var(--close-danger) * 0.8));
  opacity: var(--close-danger);
  transform: translate(-50%, -50%) scale(calc(1 - var(--close-danger) * 0.3));
}

@keyframes tether-pulse {
  0%,
  100% {
    filter: blur(calc(var(--far-danger) * 0.16rem)) brightness(1);
  }

  50% {
    filter: blur(calc(var(--far-danger) * 0.16rem))
      brightness(calc(1 + var(--close-danger) * 0.4 + var(--far-danger) * 0.15));
  }
}

/* Too-close failure: the tether collapses inward in a bright flash rather
   than fading. */
.surface.fail-close .connection {
  animation: tether-collapse 380ms ease-in forwards;
}

@keyframes tether-collapse {
  0% {
    opacity: 1;
    filter: brightness(2.2);
  }

  50% {
    opacity: 1;
    height: 0.55rem;
    filter: brightness(2.6);
  }

  100% {
    opacity: 0;
    height: 0.03rem;
    filter: brightness(1);
  }
}

/* Too-far failure: the tether flickers and thins before snapping, reading as
   a breaking connection rather than a collision. */
.surface.fail-far .connection {
  animation: tether-fray 480ms steps(2, jump-end) forwards;
}

@keyframes tether-fray {
  0% {
    opacity: 1;
  }

  20% {
    opacity: 0.35;
  }

  40% {
    opacity: 0.85;
  }

  60% {
    opacity: 0.15;
  }

  80% {
    opacity: 0.6;
  }

  100% {
    opacity: 0;
    filter: blur(0.3rem);
  }
}

/* A restrained early-digital-instrument HUD: fixed top-centre at all times
   so a frozen final time never grows large enough to sit over the play
   area -- it gets a brief in-place pulse and a warmer accent on a new best,
   nothing more, then the board takes over the "remember this run" job. */
.timer {
  position: absolute;
  top: 1.1rem;
  left: 50%;
  transform: translate(-50%, 0);
  margin: 0;
  padding: 0.1rem 0.6rem;
  color: hsl(190deg 55% 78% / 85%);
  font:
    700 clamp(1rem, 2.6vmin, 1.4rem) / 1 SFMono-Regular, Consolas, "Liberation Mono", Menlo,
    monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  text-align: center;
  text-shadow: 0 0 0.5rem hsl(190deg 90% 65% / 55%);
  pointer-events: none;
}

.timer.frozen {
  animation: timer-pulse 500ms ease-out;
}

.timer.new-best {
  color: hsl(45deg 90% 74%);
  text-shadow: 0 0 0.65rem hsl(45deg 95% 65% / 70%);
}

@keyframes timer-pulse {
  0% {
    transform: translate(-50%, 0) scale(1.4);
  }

  100% {
    transform: translate(-50%, 0) scale(1);
  }
}

/* The record board: a small, integrated HUD panel rather than a website
   sidebar -- desktop keeps it fixed to the right, out of the target's
   wandering range; mobile collapses it to a compact strip pinned to the
   bottom so it never eats into horizontal play space. */
.board {
  position: absolute;
  top: 50%;
  right: 1.1rem;
  transform: translateY(-50%);
  margin: 0;
  padding: 0.6rem 0.75rem;
  min-width: 4.5rem;
  border-radius: 0.4rem;
  background: hsl(220deg 40% 9% / 55%);
  pointer-events: none;
}

.board-heading {
  margin: 0 0 0.35rem;
  color: hsl(200deg 30% 70% / 60%);
  font: 700 0.65rem / 1 system-ui, sans-serif;
  letter-spacing: 0.16em;
  text-align: center;
}

.board-list {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  margin: 0;
  padding: 0;
  color: hsl(200deg 50% 82% / 75%);
  font:
    600 0.8rem / 1.3 SFMono-Regular, Consolas, "Liberation Mono", Menlo,
    monospace;
  font-variant-numeric: tabular-nums;
  list-style: none;
  text-align: center;
}

.board-list li:first-child {
  color: hsl(45deg 90% 72%);
}

@media (width <= 640px) {
  .board {
    top: auto;
    right: 50%;
    bottom: 0.6rem;
    padding: 0.35rem 0.6rem;
    transform: translate(50%, 0);
  }

  .board-heading {
    display: none;
  }

  .board-list {
    flex-direction: row;
    gap: 0.5rem;
    font-size: 0.7rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .target,
  .target::before,
  .target::after,
  .connection,
  .timer.frozen,
  .surface.armed .target::before,
  .surface.lockon .target,
  .surface.fail-close .target,
  .surface.fail-close .player,
  .surface.fail-close .connection,
  .surface.fail-far .target,
  .surface.fail-far .connection,
  .surface.engaged .target::after {
    animation: none;
  }

  .surface.lockon::after {
    animation: none;
  }

  .surface.fail-close {
    animation: none;
  }

  .surface.fail-close .target,
  .surface.fail-close .player {
    opacity: 0.5;
  }

  .surface.fail-far .target {
    opacity: 0.3;
  }

  .timer.frozen {
    font-weight: 900;
  }

  .surface.armed.activating .target {
    animation: none;
  }
}
