/* ==========================================================================
   Christian Reversi — "Destroying the works of the devil" — styles.css
   --------------------------------------------------------------------------
   The look: a night sky lit from above. The page is deep indigo darkness with
   warm light pouring down from the top of the screen; the board is a royal
   blue sanctuary floor inside a carved olive-and-gold frame; and the two
   players are Light and Darkness rather than two arbitrary colours.

     • Light (you)      white-gold discs, each carrying a cross.
     • Darkness (them)  near-black discs, dull and unlit.

   That choice is not only thematic — light against dark is the single most
   colour-blind-safe pairing there is, because it separates the two sides by
   brightness instead of by hue.
   --------------------------------------------------------------------------
   Contents
     1.  Design tokens
     2.  Base / full-bleed background / no-scroll shell
     3.  Row 1 — title + status
     4.  Row 2 — score strip
     5.  Row 3 — toolbar
     6.  Board frame + squares
     7.  Discs — the two-sided, physically flipping kind
     8.  Legal-move markers, last move, hint
     9.  Effects layer (shockwave, floating score, pass banner, verse card)
     10. Game-over overlay + confetti
     11. Responsive (portrait, short, landscape) + reduced motion + print
   --------------------------------------------------------------------------
   Two values are written by Scripts.js and must not be hard-coded elsewhere:

     --board-px   the pixel size of the square board frame, recomputed by
                  Fx.measure() from the real size of .board-area. CSS also
                  carries an aspect-ratio fallback so the page still looks
                  right before the first measurement (or with JS disabled).
     --cell-size  the pixel size of one square, so glyphs and badges inside a
                  disc scale with the board instead of guessing from vw units.

   Re-theming: every colour lives in section 1. Changing --you-1/--you-2 and
   --cpu-1/--cpu-2 re-skins both players everywhere, including the title, the
   score strip, the discs and the confetti (which reads the tokens from JS).
   ========================================================================== */

/* -------------------------------------------------------------------------- */
/* 1. Design tokens                                                           */
/* -------------------------------------------------------------------------- */

:root {
  /* Page background — applied to <html> so it covers the whole viewport,
     the overscroll area and any region beyond the board. Deep indigo night
     with a warm glow spilling in from above: light entering the darkness. */
  --bg-deep: #08061a;
  --bg-mid: #171034;
  --bg-glow: rgba(255, 214, 138, 0.30);
  --bg-beam: rgba(255, 226, 168, 0.055);

  /* Board — carved olive wood banded with gold, around a royal blue floor. */
  --frame-1: #7a5a26;
  --frame-2: #2e2008;
  --frame-3: #c9a24f;
  --felt-1: #2f5390;
  --felt-2: #274779;
  --felt-line: rgba(6, 12, 34, 0.55);

  /* Players. Light against Darkness: the two sides differ in BRIGHTNESS, not
     in hue, which is both the point of the theme and the safest possible
     pairing for every form of colour blindness. */
  --you-1: #fffdf4;      /* Light — white-gold, carrying a cross */
  --you-2: #ffe7ab;
  --you-3: #e0b243;
  --you-edge: #a4761b;
  --cpu-1: #6a6285;      /* Darkness — dull, unlit, no shine of its own */
  --cpu-2: #2c2440;
  --cpu-3: #120c1e;
  --cpu-edge: #05030c;

  /* Text + panels */
  --text: #f4eeff;
  --text-dim: #b6a9d2;
  --panel-bg: rgba(252, 250, 255, 0.97);
  --panel-ink: #191038;
  --panel-border: rgba(255, 214, 130, 0.5);

  /* Indicators — everything that invites a move is candle-light. */
  --marker: #fff6d8;
  --marker-glow: #ffd980;
  --select: #ffd980;
  --hint: #ffe066;
  --warn: #ffab00;
  --gold-1: #fff3b0;
  --gold-2: #ffcc33;
  --gold-3: #b7860b;

  /* Buttons */
  --btn-bg: #2a2050;
  --btn-bg-hover: #3b2e6d;
  --btn-ink: #f4eeff;
  --btn-primary-1: #ffd45c;
  --btn-primary-2: #e08c14;

  /* Motion. Keep in sync with CONFIG.*_MS in Scripts.js. */
  --dur-place: 300ms;
  --dur-flip: 420ms;
  --dur-flip-stagger: 60ms;
  --dur-wave: 620ms;

  /* Written by JS; the defaults keep the page sane without scripting. */
  --board-px: min(94vw, 66vh);
  --cell-size: 44px;

  /* Chrome sizing knobs, tightened by the responsive rules in section 11. */
  --gap-row: 6px;
  --pad-x: 10px;
}

/* -------------------------------------------------------------------------- */
/* 2. Base / full-bleed background / no-scroll shell                          */
/* -------------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  height: 100%;
  background-color: var(--bg-deep);
  background-image:
    radial-gradient(ellipse 130% 70% at 50% -12%, var(--bg-glow) 0%, rgba(255, 214, 138, 0) 62%),
    linear-gradient(168deg, var(--bg-mid) 0%, var(--bg-deep) 60%, #04030d 100%);
  background-repeat: no-repeat;
  background-attachment: fixed;
  background-size: cover;
  /* Stop the page rubber-banding on iOS while a child drags a disc around. */
  overscroll-behavior: none;
}

/* Shafts of light fanning down from above the screen.

   Deliberately on its own element rather than as a third background layer on
   <html>: a browser that does not understand conic gradients would throw away
   the ENTIRE background-image property, taking the night sky with it. Here,
   an engine that cannot draw the beams simply draws nothing and the page is
   only very slightly plainer. */
body::before {
  content: '';
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 0;
  pointer-events: none;
  background: repeating-conic-gradient(
    from 208deg at 50% -18%,
    var(--bg-beam) 0deg 2.2deg,
    rgba(255, 226, 168, 0) 2.2deg 7.5deg
  );
  -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0) 78%);
  mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0) 78%);
}

body {
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;              /* the promise: this page never scrolls */
  background: transparent;
  color: var(--text);
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  text-align: center;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;    /* removes the 300 ms tap delay */
  user-select: none;
  -webkit-user-select: none;
}

/* The whole game is one fixed-height flex column. Only .board-area may grow
   or shrink; every other row keeps its natural height. */
.app {
  position: relative;           /* keeps the game above the light beams */
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: var(--gap-row);
  height: 100vh;
  height: 100dvh;               /* honours the iOS URL bar when supported */
  padding:
    calc(6px + env(safe-area-inset-top, 0px))
    calc(var(--pad-x) + env(safe-area-inset-right, 0px))
    calc(8px + env(safe-area-inset-bottom, 0px))
    calc(var(--pad-x) + env(safe-area-inset-left, 0px));
}

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

.noscript {
  max-width: 32rem;
  margin: 1rem auto;
  padding: 1rem;
  border-radius: 10px;
  background: var(--panel-bg);
  color: var(--panel-ink);
  font-weight: 700;
}

/* Shared focus ring — visible on every interactive control. */
button:focus-visible,
.square:focus-visible {
  outline: 3px solid var(--select);
  outline-offset: 2px;
  z-index: 5;
}

/* -------------------------------------------------------------------------- */
/* 3. Row 1 — title + status                                                  */
/* -------------------------------------------------------------------------- */

.topbar {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 4px 18px;
  margin: 0 auto;
  max-width: 1100px;
  width: 100%;
}

.title {
  display: inline-flex;
  align-items: center;
  /* Roomier than it looks like it needs to be: the lettering carries a deep
     3D shadow that reaches past the glyphs, and at 10px the flanking discs
     collided with the final "I". */
  gap: 14px;
  margin: 0;
  /* "CHRISTIAN REVERSI" is a long word-pair, so it starts smaller and grows
     more slowly than a one-word title could afford to. */
  font-size: clamp(1.02rem, 4.3vw, 2.2rem);
  font-weight: 900;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  line-height: 1;
}

/* Gradient lettering with a carved 3D drop, plus a plain-colour fallback for
   engines without background-clip:text. */
.title__text {
  position: relative;
  color: var(--gold-2);
  background-image: linear-gradient(180deg, #fff8d6 0%, var(--gold-2) 45%, #e08b00 70%, #ffe27a 100%);
  -webkit-background-clip: text;
  background-clip: text;
  text-shadow:
    0 1px 0 #6d4200,
    0 2px 0 #523100,
    0 3px 0 #3d2400,
    0 5px 12px rgba(0, 0, 0, 0.7);
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .title__text {
    -webkit-text-fill-color: transparent;
  }

  /* Re-create the 3D shadow behind the (now transparent) glyphs. */
  .title__text::before {
    content: attr(data-text);
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: -1;
    -webkit-text-fill-color: #452900;
    color: #452900;
    text-shadow:
      0 2px 0 #321d00,
      0 4px 10px rgba(0, 0, 0, 0.75);
  }
}

/* Little discs flanking the title, in the two player colours. */
.title__disc {
  width: clamp(18px, 4vw, 32px);
  height: clamp(18px, 4vw, 32px);
  border-radius: 50%;
  box-shadow:
    0 3px 6px rgba(0, 0, 0, 0.55),
    inset 0 2px 3px rgba(255, 255, 255, 0.5),
    inset 0 -3px 5px rgba(0, 0, 0, 0.4);
}

/* The Light disc glows; the Darkness disc swallows what falls on it. */
.title__disc--you {
  position: relative;
  background: radial-gradient(circle at 34% 30%, var(--you-1), var(--you-2) 62%, var(--you-3));
  box-shadow:
    0 3px 6px rgba(0, 0, 0, 0.55),
    0 0 14px rgba(255, 226, 160, 0.55),
    inset 0 2px 3px rgba(255, 255, 255, 0.6);
}

.title__disc--cpu {
  background: radial-gradient(circle at 34% 30%, var(--cpu-1), var(--cpu-2) 62%, var(--cpu-3));
  box-shadow:
    0 3px 6px rgba(0, 0, 0, 0.6),
    inset 0 2px 3px rgba(255, 255, 255, 0.14),
    inset 0 -3px 6px rgba(0, 0, 0, 0.65);
}

/* The small Light discs in the title and the score strip carry the same
   cross as the playing pieces — see .disc__face--you::after in section 7,
   which they share the rule with, so the emblem can never drift into being
   two slightly different marks on one page. */

/* The strapline the game is named for. Small, gold and calm — it explains
   the title without ever competing with the status chip for attention. */
.tagline {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0 7px;
  margin: 0;
  color: var(--gold-1);
  font-size: clamp(0.68rem, 2.3vw, 0.95rem);
  font-weight: 700;
  font-style: italic;
  letter-spacing: 0.02em;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}

.tagline__cross {
  font-style: normal;
  color: var(--gold-2);
  text-shadow: 0 0 10px rgba(255, 204, 51, 0.75);
}

.tagline__ref {
  padding: 1px 8px;
  border-radius: 999px;
  border: 1px solid rgba(255, 214, 130, 0.4);
  background: rgba(255, 214, 130, 0.1);
  color: var(--gold-2);
  font-style: normal;
  font-size: 0.85em;
  font-weight: 800;
  white-space: nowrap;
}

.status {
  margin: 0;
  padding: 5px 14px;
  border-radius: 999px;
  border: 2px solid var(--panel-border);
  background: rgba(8, 40, 29, 0.85);
  color: var(--text);
  font-size: clamp(0.85rem, 2.7vw, 1.1rem);
  font-weight: 700;
  min-height: 1.9em;
  max-width: 100%;
}

/* The computer is working — calm, not alarming. */
.status.is-thinking {
  border-color: var(--warn);
  animation: status-breathe 1.1s ease-in-out infinite;
}

/* Something needs attention right now (a pass, an illegal tap). */
.status.is-alert {
  border-color: var(--hint);
  background: rgba(90, 60, 0, 0.85);
  animation: status-pop 480ms ease-out;
}

/* Something went wrong — reported, never silent. */
.status.is-error {
  border-color: #ff6b6b;
  background: rgba(88, 16, 16, 0.9);
}

@keyframes status-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.62; }
}

@keyframes status-pop {
  0%   { transform: scale(0.9); }
  55%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}

/* -------------------------------------------------------------------------- */
/* 4. Row 2 — score strip                                                     */
/* -------------------------------------------------------------------------- */

.scoreboard {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
}

.score {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
}

.score__disc {
  width: clamp(18px, 4.4vw, 26px);
  height: clamp(18px, 4.4vw, 26px);
  border-radius: 50%;
  box-shadow:
    0 2px 4px rgba(0, 0, 0, 0.5),
    inset 0 2px 3px rgba(255, 255, 255, 0.5),
    inset 0 -2px 4px rgba(0, 0, 0, 0.4);
}

.score--you .score__disc {
  position: relative;
  background: radial-gradient(circle at 34% 30%, var(--you-1), var(--you-2) 62%, var(--you-3));
  box-shadow:
    0 2px 4px rgba(0, 0, 0, 0.5),
    0 0 10px rgba(255, 226, 160, 0.5),
    inset 0 2px 3px rgba(255, 255, 255, 0.6);
}

.score--cpu .score__disc {
  background: radial-gradient(circle at 34% 30%, var(--cpu-1), var(--cpu-2) 62%, var(--cpu-3));
}

/* "Light" and "Darkness" — who the two colours actually are. First thing to
   go when the screen gets narrow; the discs and the bar still say it all. */
.score__name {
  font-size: clamp(0.7rem, 2.2vw, 0.92rem);
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}

.score--you .score__name { color: var(--you-2); }
.score--cpu .score__name { color: #a99ec6; }

.score__num {
  min-width: 1.7em;
  font-size: clamp(1.1rem, 4vw, 1.7rem);
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
}

/* The Darkness disc is genuinely dark, so its NUMBER cannot borrow the disc
   colour the way the Light's does — it would vanish into the page. A pale
   lilac keeps the reading easy while still being unmistakably the cold side. */
.score--you .score__num { color: var(--you-1); }
.score--cpu .score__num { color: #c3b8dd; }

/* The number bounces when it changes, so a child notices the score moving. */
.score__num.is-bump { animation: num-bump 460ms cubic-bezier(0.2, 1.7, 0.4, 1); }

@keyframes num-bump {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.45); }
  100% { transform: scale(1); }
}

.scorebar {
  position: relative;
  flex: 1 1 auto;
  display: flex;
  height: clamp(12px, 3vw, 18px);
  border-radius: 999px;
  overflow: hidden;
  background: rgba(0, 0, 0, 0.45);
  box-shadow:
    inset 0 2px 5px rgba(0, 0, 0, 0.6),
    0 0 0 2px rgba(255, 255, 255, 0.12);
}

.scorebar__fill {
  height: 100%;
  transition: width 520ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* The bar has to show WHERE the boundary is, so the dark side is lifted well
   clear of the black track behind it — otherwise "losing" would just look
   like an empty bar rather than like ground the darkness has taken. */
.scorebar__fill--you { background: linear-gradient(180deg, var(--you-1), var(--you-3)); }
.scorebar__fill--cpu { background: linear-gradient(180deg, #8279a0, #322a4a); }

.scorebar__gloss {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 50%;
  left: 0;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.32), rgba(255, 255, 255, 0));
  pointer-events: none;
}

/* -------------------------------------------------------------------------- */
/* 5. Row 3 — toolbar                                                         */
/* -------------------------------------------------------------------------- */

.toolbar {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  max-width: 900px;
  margin: 0 auto;
}

/* The link back to the Bible Games hub is an <a> wearing .btn, so it is the
   same object as every other control in the toolbar and needs no styling of
   its own — only an anchor's underline removed. */
a.btn {
  text-decoration: none;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  padding: 7px 13px;
  border: 2px solid rgba(255, 255, 255, 0.16);
  border-radius: 11px;
  background: linear-gradient(180deg, var(--btn-bg-hover), var(--btn-bg));
  color: var(--btn-ink);
  font: inherit;
  font-size: clamp(0.8rem, 2.5vw, 0.98rem);
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.35), 0 5px 12px rgba(0, 0, 0, 0.35);
  transition: transform 110ms ease, filter 110ms ease, box-shadow 110ms ease;
}

.btn:hover:not(:disabled) { filter: brightness(1.14); }

.btn:active:not(:disabled) {
  transform: translateY(2px);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.35);
}

.btn:disabled {
  opacity: 0.42;
  cursor: not-allowed;
  box-shadow: none;
}

.btn__icon { font-size: 1.1em; }

.btn--primary {
  background: linear-gradient(180deg, var(--btn-primary-1), var(--btn-primary-2));
  color: #3a1c00;
  border-color: rgba(255, 240, 190, 0.75);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
}

.btn--quiet {
  background: rgba(255, 255, 255, 0.09);
  box-shadow: none;
}

.btn--icon { padding: 7px 11px; }

/* The scripture button. Gold-rimmed so a child can find the cross without
   reading anything, but not so loud that it competes with New Game. */
.btn--verse {
  border-color: rgba(255, 214, 130, 0.55);
  color: var(--gold-1);
  font-size: 1.15em;
  text-shadow: 0 0 10px rgba(255, 204, 51, 0.7);
}

.btn--big {
  min-height: 54px;
  padding: 12px 26px;
  font-size: clamp(1rem, 3.4vw, 1.25rem);
  border-radius: 14px;
}

/* Segmented difficulty picker. */
.segmented {
  display: inline-flex;
  padding: 3px;
  border: 2px solid rgba(255, 255, 255, 0.16);
  border-radius: 12px;
  background: rgba(0, 0, 0, 0.32);
  gap: 3px;
}

.seg {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 34px;
  padding: 5px 11px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: clamp(0.75rem, 2.4vw, 0.94rem);
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease, transform 140ms ease;
}

.seg:hover:not(.is-active) { color: var(--text); background: rgba(255, 255, 255, 0.08); }

.seg.is-active {
  background: linear-gradient(180deg, var(--btn-primary-1), var(--btn-primary-2));
  color: #3a1c00;
  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.3);
}

/* -------------------------------------------------------------------------- */
/* 6. Board frame + squares                                                   */
/* -------------------------------------------------------------------------- */

.board-area {
  flex: 1 1 auto;
  min-height: 0;                /* lets the flex child actually shrink */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2px 0;
}

.board-frame {
  position: relative;
  width: var(--board-px);
  height: var(--board-px);
  max-width: 100%;
  max-height: 100%;
  aspect-ratio: 1 / 1;          /* fallback before Fx.measure() runs */
  padding: clamp(6px, 1.6vw, 13px);
  border-radius: clamp(10px, 2.4vw, 18px);
  background:
    linear-gradient(150deg, var(--frame-3) 0%, var(--frame-1) 38%, var(--frame-2) 100%);
  box-shadow:
    0 10px 30px rgba(0, 0, 0, 0.6),
    inset 0 2px 3px rgba(255, 226, 180, 0.35),
    inset 0 -4px 8px rgba(0, 0, 0, 0.5);
}

/* A big sweep by the Light: the frame itself catches fire for a moment.
   Kept to the frame so it never sits over the flipping discs.
   Duration must match CONFIG.RADIATE_MS in Scripts.js. */
.board-frame.is-radiant { animation: frame-radiate 900ms ease-out; }

@keyframes frame-radiate {
  0% {
    box-shadow:
      0 10px 30px rgba(0, 0, 0, 0.6),
      inset 0 2px 3px rgba(255, 226, 180, 0.35),
      inset 0 -4px 8px rgba(0, 0, 0, 0.5);
  }
  35% {
    box-shadow:
      0 10px 30px rgba(0, 0, 0, 0.6),
      0 0 46px 10px rgba(255, 219, 140, 0.85),
      inset 0 2px 3px rgba(255, 245, 210, 0.8),
      inset 0 -4px 8px rgba(0, 0, 0, 0.5);
  }
  100% {
    box-shadow:
      0 10px 30px rgba(0, 0, 0, 0.6),
      0 0 0 0 rgba(255, 219, 140, 0),
      inset 0 2px 3px rgba(255, 226, 180, 0.35),
      inset 0 -4px 8px rgba(0, 0, 0, 0.5);
  }
}

.board {
  position: relative;
  display: grid;
  grid-template-rows: repeat(8, 1fr);
  width: 100%;
  height: 100%;
  border-radius: clamp(4px, 1vw, 8px);
  overflow: hidden;
  background: var(--felt-2);
  box-shadow:
    inset 0 0 0 2px rgba(0, 0, 0, 0.45),
    inset 0 0 60px rgba(0, 0, 0, 0.35);
}

/* Light falling across the board from a window above it.

   This sits OVER the squares rather than under them — the same trick as
   .scorebar__gloss — because a glow painted underneath would be hidden by the
   squares' own opaque backgrounds. Kept faint enough that it reads as light
   in the room and never as a film over the discs. */
.board::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  pointer-events: none;
  background:
    radial-gradient(ellipse 62% 48% at 50% 4%, rgba(255, 236, 190, 0.13), rgba(255, 236, 190, 0) 68%),
    linear-gradient(180deg, rgba(0, 0, 0, 0) 55%, rgba(4, 2, 14, 0.22) 100%);
  z-index: 4;
}

.board__row {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
}

.square {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  perspective: 620px;           /* gives the disc flip real depth */
  background: var(--felt-1);
  box-shadow: inset 0 0 0 1px var(--felt-line);
  cursor: default;
  -webkit-tap-highlight-color: transparent;
}

/* Barely-there checker so the grid reads as a board, not a spreadsheet. */
.square.is-alt { background: var(--felt-2); }

/* The four guide marks of a real Othello board, at the 2/6 intersections —
   here they are tiny crosses inlaid into the floor rather than plain dots.
   Two overlapping gradients draw the bar and the upright in one element, so
   the pip still costs exactly one pseudo-element. */
.square.is-pip::after {
  content: '';
  position: absolute;
  right: -5px;
  bottom: -5px;
  width: 10px;
  height: 10px;
  background:
    linear-gradient(to right, rgba(0, 0, 0, 0) 40%, rgba(255, 226, 160, 0.5) 40% 60%, rgba(0, 0, 0, 0) 60%),
    linear-gradient(to bottom, rgba(0, 0, 0, 0) 32%, rgba(255, 226, 160, 0.5) 32% 52%, rgba(0, 0, 0, 0) 52%);
  z-index: 1;
}

/* A square you may play right now: it invites a tap. */
.square.is-legal { cursor: pointer; }

.square.is-legal:hover { background: #3b66ac; }

/* Wrong tap — a shake plus the error cue, never a silent no-op. */
.square.is-bad { animation: square-shake 420ms ease; }

@keyframes square-shake {
  0%, 100% { transform: translateX(0); }
  15%      { transform: translateX(-6px); }
  30%      { transform: translateX(6px); }
  45%      { transform: translateX(-4px); }
  60%      { transform: translateX(4px); }
  80%      { transform: translateX(-2px); }
}

/* -------------------------------------------------------------------------- */
/* 7. Discs — the two-sided, physically flipping kind                         */
/* -------------------------------------------------------------------------- */
/*  A real Othello disc is one colour on one side and another on the other.
    So is this one: .disc holds two faces, and which colour you see is decided
    entirely by how far the disc has been turned.

        --turn: 0deg    → the Light side is facing the player
        --turn: 180deg  → the Darkness side is facing the player

    Which means a flip is not a repaint but a real turn: the dark face rolls
    away and the cross comes up out of it. That is the whole game in one
    gesture, and it is why the discs are built this way.

    A flip is therefore nothing more than an animation between those two
    angles, and the resting state after the animation is exactly the state
    the model already says the square is in. Nothing can drift out of sync. */

/* Sized with offsets rather than width/height so it is centred by geometry
   alone — no dependence on the parent's flex alignment. */
.disc {
  position: absolute;
  top: 8%;
  right: 8%;
  bottom: 8%;
  left: 8%;
  --turn: 0deg;
  transform: rotateY(var(--turn));
  transform-style: preserve-3d;
  -webkit-transform-style: preserve-3d;
  will-change: transform;
  z-index: 2;
}

.square:not(.has-disc) .disc { display: none; }
.square.is-cpu .disc { --turn: 180deg; }

.disc__face {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  border-radius: 50%;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  box-shadow:
    inset 0 3px 6px rgba(255, 255, 255, 0.45),
    inset 0 -5px 9px rgba(0, 0, 0, 0.45),
    0 3px 6px rgba(0, 0, 0, 0.5);
}

/* --- the Light face -------------------------------------------------------- */

.disc__face--you {
  transform: rotateY(0deg);
  background:
    radial-gradient(circle at 34% 28%, var(--you-1) 0%, var(--you-2) 55%, var(--you-3) 100%);
  border: 2px solid var(--you-edge);
  box-shadow:
    inset 0 3px 6px rgba(255, 255, 255, 0.7),
    inset 0 -5px 9px rgba(158, 106, 12, 0.45),
    0 3px 6px rgba(0, 0, 0, 0.5),
    0 0 12px rgba(255, 226, 160, 0.45);
}

/* --- the cross ------------------------------------------------------------- */
/*  The emblem the Light disc carries, and the one mark the whole game is
    built around, so it is worth getting exactly right.

    Drawn as ONE inline SVG rather than as two CSS bars. Two bars can only
    ever be two rectangles: they meet in hard square corners, they cannot be
    given a single unbroken edge, and each one rounds its own dimensions to
    the device pixel grid independently — which is why the joint looked
    slightly off-centre and the ends looked ragged at small sizes. A single
    path is one shape with one outline, so it stays true at 35px on a phone
    and at 90px on a tablet alike.

    Proportions, on the SVG's own 100-unit grid, are a real Latin cross and
    not a plus sign — this exact set was chosen by drawing several against
    the real disc at 200, 90, 46, 35 and 24 pixels and keeping the one that
    stayed both elegant when large and solid when tiny:

        thickness 14 · upper arm 20 · side arms 23 · lower arm 48

    giving  M43 6 → 88, bar 20 → 80 at y 26 → 40.

    The figure is lifted 3 units above the geometric centre. A Latin cross
    carries most of its weight below the bar, so centring it by measurement
    leaves it looking as though it has sagged; the lift puts the VISUAL
    centre where the eye expects it.

    The pale copy sitting 2.2 units lower is the engraving highlight — the
    far wall of a groove catching light from the top-left, the same
    direction the disc's own radial gradient is lit from. That is what makes
    the cross look struck into the gold rather than printed on top of it.
    `stroke-linejoin: round` at 1.6 takes the raw edge off the eight inside
    corners, at sizes where a hard corner just reads as a jaggy.            */

.disc__face--you::after,
.title__disc--you::after,
.score--you .score__disc::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 88%;
  height: 88%;
  transform: translate(-50%, -50%);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='c' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%237c5306'/%3E%3Cstop offset='1' stop-color='%23ab7719'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cg stroke-width='1.6' stroke-linejoin='round'%3E%3Cpath d='M43 8.2H57V28.2H80V42.2H57V90.2H43V42.2H20V28.2H43z' fill='%23fffaea' stroke='%23fffaea' opacity='.7'/%3E%3Cpath d='M43 6H57V26H80V40H57V88H43V40H20V26H43z' fill='url(%23c)' stroke='url(%23c)'/%3E%3C/g%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

/* --- the Darkness face ----------------------------------------------------- */
/*  No emblem and no glow. It only ever reflects what falls on it, which is
    exactly the impression wanted: it has no light of its own. */

.disc__face--cpu {
  transform: rotateY(180deg);
  background:
    radial-gradient(circle at 34% 28%, var(--cpu-1) 0%, var(--cpu-2) 55%, var(--cpu-3) 100%);
  border: 2px solid var(--cpu-edge);
  box-shadow:
    inset 0 3px 6px rgba(255, 255, 255, 0.16),
    inset 0 -5px 10px rgba(0, 0, 0, 0.7),
    0 3px 7px rgba(0, 0, 0, 0.65);
}

/* --- the disc that was just placed ---------------------------------------- */

.square.is-placed .disc {
  animation: disc-drop var(--dur-place) cubic-bezier(0.2, 1.6, 0.35, 1) both;
}

@keyframes disc-drop {
  0%   { transform: rotateY(var(--turn)) scale(0.1) translateY(-45%); opacity: 0; }
  55%  { transform: rotateY(var(--turn)) scale(1.22) translateY(0); opacity: 1; }
  78%  { transform: rotateY(var(--turn)) scale(0.94) translateY(0); }
  100% { transform: rotateY(var(--turn)) scale(1) translateY(0); }
}

/* --- the discs that get flipped ------------------------------------------- */
/*  --flip-order is set inline by View: it is the distance, in squares, from
    the disc that was just placed. Multiplying it by the stagger turns a set
    of simultaneous flips into a wave rolling outwards from the new disc. */

.square.is-flipping .disc {
  animation-duration: var(--dur-flip);
  animation-timing-function: cubic-bezier(0.45, 0, 0.25, 1);
  animation-fill-mode: both;
  animation-delay: calc(var(--flip-order, 0) * var(--dur-flip-stagger));
}

.square.is-flipping.is-you .disc { animation-name: flip-to-you; }
.square.is-flipping.is-cpu .disc { animation-name: flip-to-cpu; }

/* Always spins forward, and lifts off the felt at the halfway point. */
@keyframes flip-to-cpu {
  0%   { transform: rotateY(0deg)   scale(1); }
  50%  { transform: rotateY(90deg)  scale(1.28); }
  100% { transform: rotateY(180deg) scale(1); }
}

@keyframes flip-to-you {
  0%   { transform: rotateY(180deg) scale(1); }
  50%  { transform: rotateY(270deg) scale(1.28); }
  100% { transform: rotateY(360deg) scale(1); }
}

/* -------------------------------------------------------------------------- */
/* 8. Legal-move markers, last move, hint                                     */
/* -------------------------------------------------------------------------- */
/*  Reversi is only obvious once you can see where you are allowed to play and
    what it would win you. Both are always on screen — no hovering, no
    selecting, no reading required beyond a single number. */

.marker {
  position: absolute;
  top: 16%;
  right: 16%;
  bottom: 16%;
  left: 16%;
  display: none;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 3px dashed var(--marker);
  background: radial-gradient(circle, rgba(255, 226, 160, 0.34) 0%, rgba(255, 226, 160, 0.06) 70%);
  box-shadow:
    0 0 0 2px rgba(0, 0, 0, 0.35),
    0 0 16px var(--marker-glow);
  z-index: 3;
  pointer-events: none;
}

.square.is-legal .marker {
  display: flex;
  animation: marker-pulse 1.5s ease-in-out infinite;
}

@keyframes marker-pulse {
  0%, 100% { transform: scale(0.86); opacity: 0.78; }
  50%      { transform: scale(1.06); opacity: 1; }
}

/* "+3" — how many dark discs this move would turn to light. The single most
   useful number on the board for a beginner. */
.marker__count {
  font-size: calc(var(--cell-size) * 0.34);
  font-weight: 900;
  line-height: 1;
  color: #ffffff;
  text-shadow:
    0 0 4px rgba(0, 0, 0, 0.95),
    0 2px 3px rgba(0, 0, 0, 0.85);
  letter-spacing: -0.02em;
}

/* The best move on the board gets a warmer marker while a hint is showing. */
.square.is-hint .marker {
  border-color: var(--hint);
  border-style: solid;
  background: radial-gradient(circle, rgba(255, 224, 102, 0.5) 0%, rgba(255, 224, 102, 0.1) 70%);
  box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.4), 0 0 26px var(--hint);
  animation: marker-hint 620ms ease-in-out infinite;
}

@keyframes marker-hint {
  0%, 100% { transform: scale(0.92); }
  50%      { transform: scale(1.2); }
}

/* Where the last disc landed — helps a child follow the darkness's reply. */
.square.is-last::before {
  content: '';
  position: absolute;
  top: 6%;
  right: 6%;
  bottom: 6%;
  left: 6%;
  border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 0 12px rgba(255, 255, 255, 0.6);
  z-index: 1;
  animation: last-fade 2.4s ease-out forwards;
}

@keyframes last-fade {
  0%   { opacity: 1; transform: scale(1.15); }
  70%  { opacity: 0.55; transform: scale(1); }
  100% { opacity: 0.28; transform: scale(1); }
}

/* -------------------------------------------------------------------------- */
/* 9. Effects layer                                                           */
/* -------------------------------------------------------------------------- */
/*  A sibling of the grid, inset to match it exactly, so repainting squares
    never interrupts an effect that is mid-flight. */

.fx-layer {
  position: absolute;
  top: clamp(6px, 1.6vw, 13px);
  right: clamp(6px, 1.6vw, 13px);
  bottom: clamp(6px, 1.6vw, 13px);
  left: clamp(6px, 1.6vw, 13px);
  pointer-events: none;
  overflow: visible;
  z-index: 6;
}

/* Shockwave ring bursting from a newly placed disc. */
.fx-wave {
  position: absolute;
  border-radius: 50%;
  border: 4px solid var(--marker);
  transform: translate(-50%, -50%) scale(0.2);
  opacity: 0.95;
  animation: fx-wave var(--dur-wave) cubic-bezier(0.15, 0.7, 0.3, 1) forwards;
}

/* The dark side's own colours are far too dim to read as a ring against the
   board, so its effects borrow a cold violet instead: still unmistakably the
   darkness, but actually visible. */
.fx-wave--you { border-color: var(--you-1); box-shadow: 0 0 26px var(--you-1); }
.fx-wave--cpu { border-color: #9c8fc4; box-shadow: 0 0 22px #6b5f96; }

@keyframes fx-wave {
  0%   { transform: translate(-50%, -50%) scale(0.2); opacity: 0.95; }
  100% { transform: translate(-50%, -50%) scale(2.6); opacity: 0; }
}

/* "+5" floating up from the move that earned it. */
.fx-float {
  position: absolute;
  transform: translate(-50%, -50%);
  font-size: calc(var(--cell-size) * 0.8);
  font-weight: 900;
  line-height: 1;
  white-space: nowrap;
  text-shadow:
    0 0 6px rgba(0, 0, 0, 0.95),
    0 3px 6px rgba(0, 0, 0, 0.8);
  animation: fx-float 1150ms cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
}

.fx-float--you { color: var(--you-1); }
.fx-float--cpu { color: #b7abd8; }

@keyframes fx-float {
  0%   { transform: translate(-50%, -50%) scale(0.4); opacity: 0; }
  22%  { transform: translate(-50%, -80%) scale(1.25); opacity: 1; }
  70%  { transform: translate(-50%, -135%) scale(1); opacity: 1; }
  100% { transform: translate(-50%, -190%) scale(0.9); opacity: 0; }
}

/* Full-width banner for "nobody could move" moments. */
.fx-banner {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  max-width: 92%;
  padding: 14px 22px;
  border-radius: 16px;
  border: 3px solid var(--hint);
  background: rgba(20, 14, 46, 0.95);
  color: var(--text);
  font-size: clamp(0.95rem, 3.6vw, 1.4rem);
  font-weight: 900;
  text-align: center;
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.7);
  animation: fx-banner 1900ms cubic-bezier(0.2, 1.4, 0.4, 1) forwards;
}

/* A verse shown over the board by the ✝ button. Scripture is set apart from
   the game's own announcements: quieter weight, italic, and a gold rule down
   the side, so it reads as something quoted rather than something shouted. */
.fx-banner--verse {
  max-width: 86%;
  padding: 16px 24px;
  border-color: rgba(255, 214, 130, 0.7);
  border-left-width: 8px;
  background: rgba(16, 11, 38, 0.96);
  color: var(--gold-1);
  font-size: clamp(0.88rem, 3.1vw, 1.2rem);
  font-weight: 700;
  font-style: italic;
  line-height: 1.42;
}

.fx-banner__ref {
  display: block;
  margin-top: 8px;
  color: var(--gold-2);
  font-size: 0.82em;
  font-style: normal;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

@keyframes fx-banner {
  0%   { transform: translate(-50%, -50%) scale(0.5) rotate(-6deg); opacity: 0; }
  14%  { transform: translate(-50%, -50%) scale(1.08) rotate(2deg); opacity: 1; }
  24%  { transform: translate(-50%, -50%) scale(1) rotate(0deg); opacity: 1; }
  78%  { transform: translate(-50%, -50%) scale(1) rotate(0deg); opacity: 1; }
  100% { transform: translate(-50%, -90%) scale(0.9); opacity: 0; }
}

/* Big friendly pointer used by the Hint button. */
.fx-point {
  position: absolute;
  transform: translate(-50%, -50%);
  font-size: calc(var(--cell-size) * 0.9);
  line-height: 1;
  filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.8));
  animation: fx-point 700ms ease-in-out infinite;
}

@keyframes fx-point {
  0%, 100% { transform: translate(-50%, -105%); }
  50%      { transform: translate(-50%, -135%); }
}

/* -------------------------------------------------------------------------- */
/* 10. Game-over overlay + confetti                                           */
/* -------------------------------------------------------------------------- */

.overlay {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(5, 3, 18, 0.8);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: overlay-in 260ms ease-out;
}

.overlay[hidden] { display: none; }

@keyframes overlay-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.overlay__confetti {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.overlay__card {
  position: relative;
  z-index: 1;
  width: min(94vw, 430px);
  max-height: 92vh;
  overflow: auto;
  padding: clamp(18px, 5vw, 30px);
  border-radius: 22px;
  border: 3px solid var(--panel-border);
  background:
    radial-gradient(ellipse 90% 50% at 50% 0%, rgba(255, 220, 150, 0.16), rgba(255, 220, 150, 0) 70%),
    linear-gradient(180deg, #2a1f5c, #150e33);
  box-shadow:
    0 22px 60px rgba(0, 0, 0, 0.8),
    0 0 40px rgba(255, 204, 51, 0.12);
  animation: card-in 420ms cubic-bezier(0.2, 1.5, 0.4, 1);
}

@keyframes card-in {
  0%   { transform: scale(0.72) translateY(24px); opacity: 0; }
  100% { transform: scale(1) translateY(0); opacity: 1; }
}

.overlay__icon {
  font-size: clamp(3rem, 14vw, 5rem);
  line-height: 1;
  animation: icon-bob 1.6s ease-in-out infinite;
}

@keyframes icon-bob {
  0%, 100% { transform: translateY(0) rotate(-4deg); }
  50%      { transform: translateY(-10px) rotate(4deg); }
}

.overlay__title {
  margin: 8px 0 4px;
  font-size: clamp(1.5rem, 7vw, 2.3rem);
  font-weight: 900;
  letter-spacing: 0.03em;
  color: var(--gold-2);
  text-shadow: 0 3px 0 #6d4200, 0 6px 16px rgba(0, 0, 0, 0.7);
}

.overlay__score {
  margin: 0 0 6px;
  font-size: clamp(1.05rem, 4.4vw, 1.45rem);
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.overlay__msg {
  margin: 0 0 14px;
  font-size: clamp(0.9rem, 3.4vw, 1.05rem);
  color: var(--text-dim);
  line-height: 1.45;
}

/* Every game ends on a verse — the thing worth taking away from it. */
.overlay__verse {
  margin: 0 0 18px;
  padding: 12px 14px;
  border-left: 5px solid var(--gold-2);
  border-radius: 4px 12px 12px 4px;
  background: rgba(255, 214, 130, 0.09);
  text-align: left;
}

.overlay__verse-text {
  margin: 0;
  color: var(--gold-1);
  font-size: clamp(0.86rem, 3.2vw, 1rem);
  font-style: italic;
  font-weight: 600;
  line-height: 1.45;
}

.overlay__verse-ref {
  display: block;
  margin-top: 6px;
  color: var(--gold-2);
  font-size: 0.8rem;
  font-style: normal;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* An empty verse block (should not happen, but never leave a bare gold bar
   floating in the card if a message ever ships without one). */
.overlay__verse:empty,
.overlay__verse.is-empty { display: none; }

.overlay__close {
  display: block;
  margin: 12px auto 0;
  font-size: 0.86rem;
}

/* -------------------------------------------------------------------------- */
/* 11. Responsive / reduced motion / print                                    */
/* -------------------------------------------------------------------------- */

/* Phones in portrait: trim the chrome so the board keeps the space. */
@media (max-width: 560px) {
  :root {
    --gap-row: 4px;
    --pad-x: 6px;
  }

  .topbar { gap: 2px 10px; }
  .title { gap: 6px; }

  /* Tight enough that New Game + difficulty + Undo + Hint + mute fit on ONE
     row from about 380px up. That is worth roughly 46px of board height on
     the commonest phone sizes, which is why the padding is this specific. */
  .toolbar { gap: 4px; }
  .btn { padding: 7px 10px; }
  .btn--icon { padding: 7px 8px; }
  #undo, #hint { padding: 7px 9px; }

  /* The score strip keeps the discs, the bar and the numbers; the words
     "Light" and "Darkness" are the first thing it can afford to drop. */
  .score__name { display: none; }

  /* Secondary controls become icons and the difficulty picker becomes three
     faces (😊 🙂 😤). Each keeps its aria-label, and the active one is still
     highlighted, so nothing is lost but the width.

     These drop together, at the same breakpoint, on purpose: it is the width
     at which the whole toolbar collapses from two rows to one, which is worth
     about 45px of board. Dropping them one at a time would cost that for no
     benefit and would leave the mute button stranded on a row of its own.

     New Game always keeps its words — it is the one control a child must be
     able to find unaided. */
  #undo .btn__label,
  #hint .btn__label,
  .seg__label { display: none; }

  .seg { padding: 5px 9px; }
}

/* Very small phones: shrink the title furniture too. */
@media (max-width: 400px) {
  .title { font-size: 1.05rem; }
  .title__disc { width: 16px; height: 16px; }
  .tagline { font-size: 0.66rem; }
  .tagline__ref { padding: 1px 6px; }
}

/* Short screens (small phones, or a browser with lots of chrome). */
@media (max-height: 620px) {
  :root { --gap-row: 3px; }
  .status { padding: 3px 10px; font-size: 0.82rem; }
  .btn { min-height: 36px; padding: 5px 10px; }
  .seg { min-height: 30px; }

  /* The strapline is the game's name-badge, not part of playing it, so on a
     short screen the board takes its row back. The ✝ button still brings
     scripture up on demand, and the title still says what the game is. */
  .tagline { display: none; }
}

@media (max-height: 500px) {
  .title__text { display: none; }   /* the light and dark discs still say it */
}

/* Landscape phones: put the chrome in a column beside the board so the board
   can use the full height instead of the full (short) width. */
@media (orientation: landscape) and (max-height: 560px) {
  /* The board is height-limited in landscape, so widening the chrome column
     costs it nothing and gives the score bar room to actually read. */
  .app {
    display: grid;
    grid-template-columns: minmax(180px, 34%) 1fr;
    grid-template-rows: auto auto 1fr;
    column-gap: 8px;
    row-gap: 4px;
  }

  .score__num { min-width: 1.4em; }

  .topbar     { grid-column: 1; grid-row: 1; }
  .scoreboard { grid-column: 1; grid-row: 2; }
  .toolbar    { grid-column: 1; grid-row: 3; align-content: start; }
  .board-area { grid-column: 2; grid-row: 1 / -1; }

  .title { font-size: 1rem; }
  .title__text { display: inline; }
  .tagline { display: none; }
  .score__name { display: none; }

  /* Same rule as everywhere else: everything sheds its words except the one
     button a child has to be able to find on their own. */
  .toolbar .btn:not(#new-game) .btn__label,
  .seg__label { display: none; }
}

/* Pointer-less devices get no hover affordances (they would stick on tap). */
@media (hover: none) {
  .square.is-legal:hover { background: var(--felt-1); }
  .square.is-legal.is-alt:hover { background: var(--felt-2); }
  .btn:hover:not(:disabled) { filter: none; }
}

/* The beams are decoration made of motion-free gradients, but they are also
   the one thing on the page that a light-sensitive visitor cannot dim. */
@media (prefers-contrast: more) {
  body::before { display: none; }
  .board::after { display: none; }
}

/* Respect the visitor's motion preference: every effect still *happens*, it
   just happens instantly. The game never depends on an animation completing —
   Scripts.js shortens its own timers to match. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }

  .square.is-legal .marker { animation: none; opacity: 1; transform: scale(1); }
  .square.is-hint .marker { animation: none; transform: scale(1.1); }
  .square.is-last::before { animation: none; opacity: 0.5; }
  .fx-point { animation: none; transform: translate(-50%, -120%); }
  .overlay__icon { animation: none; }

  /* The pass banner must NOT vanish. The blanket 1ms rule above would run its
     keyframes straight to opacity 0 and leave the announcement invisible —
     taking the game's clearest explanation away from precisely the people who
     depend on reading it. It stays put instead, for its full duration; only
     the sliding and the rotation are given up.
     Scripts.js keeps the matching timer at full length (Util.hold). */
  .fx-banner {
    animation: none;
    opacity: 1;
    transform: translate(-50%, -50%);
  }
}

@media print {
  html { background: #ffffff; }
  body { color: #000000; overflow: visible; height: auto; }
  body::before { display: none; }
  .toolbar,
  .overlay,
  .fx-layer { display: none !important; }
  .app { height: auto; }
}
